Packing students into dorm using simulated annealing

Given, say, 27 students. And they all have various interests/hobbies in their life, like hiking, clubbing, dancing, swimming, maybe hanging out with girls, etc.

A dormitory has 9 rooms. Three students can be accommodated in each room.

The problem to pack them all in such a way, so that all roommates would share as many interests/hobbies with each other, as possible. To make them happy and tight-knit.

This is how it can be solved using simannealing package:

from __future__ import print_function
import sys
import math, os
import random
from simanneal import Annealer

init_state = [[0,1,2],[3,4,5],[6,7,8],[9,10,11],[12,13,14],[15,16,17],[18,19,20],[21,22,23],[24,25,26]]

GROUPS=len(init_state)

# interests/hobbies for each student
interests=[
0b10101110, 0b01000000, 0b11111100, 0b11001001, 0b00111010, 0b10100000, 0b11100001, 0b01000010, 0b00111101,
0b01011000, 0b10110101, 0b00010011, 0b11100001, 0b10010010, 0b00101011, 0b10101101, 0b10010111, 0b11100110,
0b00001000, 0b11111101, 0b10000110, 0b01101000, 0b11001111, 0b01001101, 0b00110010, 0b01000111, 0b11000100]

# https://stackoverflow.com/questions/407587/python-set-bits-count-popcount
def POPCNT(i):
    assert 0 <= i < 0x100000000
    i = i - ((i >> 1) & 0x55555555)
    i = (i & 0x33333333) + ((i >> 2) & 0x33333333)
    return (((i + (i >> 4) & 0xF0F0F0F) * 0x1010101) & 0xffffffff) >> 24

def fitness(state, dump_info):
    f=0
    for group in state:
        s1=group[0]
        s2=group[1]
        s3=group[2]
        i1=POPCNT(interests[s1] & interests[s2])
        i2=POPCNT(interests[s2] & interests[s3])
        i3=POPCNT(interests[s3] & interests[s1])
        if dump_info:
            total=i1+i2+i3
            print ("* group")
            print ("students: ", s1, s2, s3)
            print ("common interests between 1 and 2: ", i1)
            print ("common interests between 2 and 3: ", i2)
            print ("common interests between 3 and 1: ", i3)
            print ("total" , total)
        f=f+i1
        f=f+i2
        f=f+i3
    if dump_info:
        print ("total across all groups:" , f)
    return f

def random_two_non_eq(max_):
    while True:
        v1=random.randint(0, max_-1)
        v2=random.randint(0, max_-1)
        if v1 != v2:
            return v1, v2

class TestProblem(Annealer):

    # pass extra data (the distance matrix) into the constructor
    def __init__(self, state):
        super(TestProblem, self).__init__(state)  # important!

    def move(self):
        # swap two random students from distinct groups:
        g1, g2 = random_two_non_eq(GROUPS)
        s1=random.randint(0, 2)
        s2=random.randint(0, 2)
        self.state[g1][s1], self.state[g2][s2] = self.state[g2][s2], self.state[g1][s1]

    def energy(self):
        return -fitness(self.state, False)


test = TestProblem(init_state)
test.steps = 5000000
# since our state is just a list, slice is the fastest way to copy
test.copy_strategy = "slice"
state, e = test.anneal()

print()
print("%i energy:" % e)
for e in state:
    print("\t", e)

fitness(state, True)



-67 energy:
	 [20, 13, 2]
	 [25, 21, 17]
	 [26, 15, 1]
	 [11, 5, 24]
	 [18, 3, 12]
	 [9, 19, 22]
	 [14, 16, 10]
	 [4, 8, 0]
	 [7, 23, 6]
* group
students:  20 13 2
common interests between 1 and 2:  2
common interests between 2 and 3:  2
common interests between 3 and 1:  2
total 6
* group
students:  25 21 17
common interests between 1 and 2:  1
common interests between 2 and 3:  2
common interests between 3 and 1:  3
total 6
* group
students:  26 15 1
common interests between 1 and 2:  2
common interests between 2 and 3:  0
common interests between 3 and 1:  1
total 3
* group
students:  11 5 24
common interests between 1 and 2:  0
common interests between 2 and 3:  1
common interests between 3 and 1:  2
total 3
* group
students:  18 3 12
common interests between 1 and 2:  1
common interests between 2 and 3:  3
common interests between 3 and 1:  0
total 4
* group
students:  9 19 22
common interests between 1 and 2:  3
common interests between 2 and 3:  5
common interests between 3 and 1:  2
total 10
* group
students:  14 16 10
common interests between 1 and 2:  2
common interests between 2 and 3:  4
common interests between 3 and 1:  2
total 8
* group
students:  4 8 0
common interests between 1 and 2:  3
common interests between 2 and 3:  3
common interests between 3 and 1:  3
total 9
* group
students:  7 23 6
common interests between 1 and 2:  1
common interests between 2 and 3:  2
common interests between 3 and 1:  1
total 4
total across all groups: 53

It has to be executed several times to get better result. This is not the best result, but it may be good enough to be used in practice.

On contrary, my MaxSAT solution chokes on bigger problems, but gives you a best result possible.

Also, simulated annealing algorithm is so simple and primitive in comparison to SAT/MaxSAT solvers.


List of my other blog posts. Subscribe to my news feed,
If you enjoy my work, you can support it on patreon.
Some time ago (before 24-Mar-2025) there was Disqus JS script for comments. I dropped it --- it was so motley, distracting, animated, with too much ads. I never liked it. Also, comments din't appeared correctly (Disqus was buggy). Also, my blog is too chamberlike --- not many people write comments here. So I decided to switch to the model I once had at least in 2020 --- send me your comments by email (don't forget to include URL to this blog post) and I will copy&paste it here manually.
Let's party like it's ~1993-1996, in this ultimate, radical and uncompromisingly primitive pre-web1.0-style blog and website. This website is best viewed under lynx/links/elinks/w3m.