1

I am trying to solve the following problem, which I am quite sure has a solution. I am not able to disclose too much information,so I formulated it general term and came up with the following allegory. I present you here the sleeping monks problem.

Imagine a monastery where 12 monks sleep, eat, pray, or work outside the monastery. Each monk, needs to sleep 8 or 6 hours per night. Each monk has X amount where he has to work in the garden, the stall or herd the sheep (e.g, activities outside the monastery) and Y hours hangs around within the walls of the monastery during these Y hours each monk is either hanging around (e.g. praying, eating, consuming beer) or he is sleeping S hours (Y is always bigger the S).

The head of this monastery asks if there is a possible way to find a best possible sleeping arrangement such that the number of beds can be reduced, and that each monks get its required sleeping.

The input data I have for the problem is given in the following form:

time_slots_in_monastery = [
#  time of the day
#14 16 18 20 22 24 2  4  6  8 10  12
(0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0),
(0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0),
(0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0),
(0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0),
(0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0),
(0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
(0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0),
(0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0),
(0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0),
(0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0),
(0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0),
(0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0),
]

Each row represents 1 monk. The digit 1 represents a time slot inside the monastery, and 0 represents a time where the monk is outside.

In addition I have a vector containing the sleeping requirements of each monk.

required_sleeping = [4, # number of sleeping slots , each one is 2 hours.
                     3,
                     3,
                     4,
                     4,
                     4,
                     4,
                     3,
                     3,
                     3,
                     3,
                     3]

The current, and really not satisfying sleeping, solution is:

solution = [
    (0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
    ]

# total beds:
#    0, 0, 12, 12, 12, 5, 0, 0 , 0, 0, 0, 0

All monks go to sleep at 6PM. But if we make monks 11 and 12 use the same bed we can reduce the number of beds to 11. And even better, monks 1 and 2 can also share their bed, then we reduce the number of beds to 10.

better_solution = [
        (0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
        (0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0),
        (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
        (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
        (0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
        (0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
        (0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
        (0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
        (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
        (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
        (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
        (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
        ]

Right now with 12 monks, the problem can be brute forced, but in reality I have about 50 monks and the time resolution is 5 minutes, and not 2 hours. Hence, I am looking for a way to find a way to solve the problem in an f-minimum-search style or any other way which is not brute force.

I present here my brute force solution in Python:

 import pprint
 time_slots_in_monastery = [
 #  time of the day
 #14 16 18 20 22 24 2  4  6  8 10  12
 (0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0),
 (0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0),
 (0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0),
 (0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0),
 (0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0),
 (0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
 (0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0),
 (0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0),
 (0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0),
 (0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0),
 (0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0),
 (0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0),
 ]
 required_sleeping = [4,
                      3,
                      3,
                      4,
                      4,
                      4,
                      4,
                      3,
                      3,
                      3,
                      3,
                      3]

 def make_new_bed(rl):
     bed = '0' * rl
     return bed

 def search_in_beds(beds, rl, sleep_time, first, last):
     sleep_slots = '0' * sleep_time
     sleep = '1' * sleep_time
     index = False
     for cn, bed in enumerate(beds):
         try:
             index = bed[first:last+1].index(sleep_slots)
             index += first
             bed = bed[:first] + bed[first:last+1].replace(sleep_slots, sleep, 1) + bed[last+1:]
             beds[cn] = bed
             print()
             print('monastery time from: %s to: %s' % (first, last))
             print('this monk found a place in bed(%s) from (%s) to (%s)' % (cn, index, index + sleep_time-1))
             print('bed(%s) time: %s ' % (cn, bed))
         except:
             """
             I did not found a free time in this bed
             """
             pass
     if index:
         return index, index + sleep_time - 1
     #adding a bed and searching again
     beds.append(make_new_bed(rl))
     return search_in_beds(beds, rl, sleep_time, first, last)

 def monks_beds(t, rsleep, rl=12):
     beds = []
     output = []
     for cn, i in enumerate(t):
         sleep_time = rsleep[cn]
         first = i.index(1)
         last = len(i) - 1 - i[::-1].index(1)
         first, last = search_in_beds(beds, rl, sleep_time, first, last)
         out = rl * '0'
         out = out[:first] + sleep_time * '1' + out[last:]
         output.append([int(x) for x in out])
     return beds

 print('time_slots_in_monastery:')
 pprint.pprint(time_slots_in_monastery)
 print('required_sleeping')
 pprint.pprint(required_sleeping)
 pprint.pprint(monks_beds(time_slots_in_monastery, required_sleeping))
oz123
  • 27,559
  • 27
  • 125
  • 187

2 Answers2

1

Naïve solution

One solution I could come up with that doesn't take into account "time at monastery" was always making sure that sleep times of two neighbouring monks never overlap, modulo the length of our day:

monks = numpy.array([
    (1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0),
    (0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0),
    (0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0),
    (1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1),  # Pattern wraps around at midnight
    ...
])

I believe (but can't formally prove right now) this solution is optimal, as:

  1. For a sufficiently long day, you would only need one bed.
  2. For a day exactly one hour too short you would need a second bed, for the first time slot. For the rest of the day the bed would be vacant.
  3. For a day two hours short you would need a second bed, for the first two time slots. For the rest of the day the bed would be vacant.
  4. The number of beds per timeslot would fill up all the way to the last time slot before you would need a third bed for the first slot again.
  5. Continue.

So all we need to solve is how to programmatically shift our monk sleep times that two neighbouring times don't overlap:

import numpy

monks = numpy.array([
    (0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0),
])

# Hours of sleep required is sum along columns
hours = numpy.sum(monks, axis=1)

# Reset all sleeping times to first column
monks = numpy.sort(monks, axis=1)[:, ::-1]

# Generate sleeping pattern without overlap of two neighboring monks. When
# one monk rises, the next one goes to bed.
hours = numpy.cumsum(hours)

# Insert 0 shift for first monk
hours = numpy.insert(hours, 0, 0)

for s, i in zip(hours, range(monks.shape[0])):
    monks[i, :] = numpy.roll(monks[i, :], s)

beds = numpy.max(numpy.sum(monks, axis=0))

print monks
# [[1 1 1 1 0 0 0 0 0 0 0 0]
#  [0 0 0 0 1 1 1 0 0 0 0 0]
#  [0 0 0 0 0 0 0 1 1 1 0 0]
#  [1 0 0 0 0 0 0 0 0 0 1 1]
#  [0 1 1 1 1 0 0 0 0 0 0 0]
#  [0 0 0 0 0 1 1 1 1 0 0 0]
#  [1 0 0 0 0 0 0 0 0 1 1 1]
#  [0 1 1 1 1 0 0 0 0 0 0 0]
#  [0 0 0 0 0 1 1 1 0 0 0 0]
#  [0 0 0 0 0 0 0 0 1 1 1 0]
#  [1 1 0 0 0 0 0 0 0 0 0 1]
#  [0 0 1 1 1 0 0 0 0 0 0 0]
#  [0 0 0 0 0 1 1 1 0 0 0 0]]

print beds
# 4

Possible solution for contiguous "time at monastery"

The second solution I "can imagine working" (but definitely cannot prove to be optimal) is to use the same algorithm, restricted to the "time at monastery" times, after sorting the "time at monastery" matrix.

  1. First I would sort the matrix so that the monk entering the earliest comes first:

    time_slots_in_monastery = numpy.array([
        (0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),  # monk 0 enters earlier than 1
        (0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0),
        (0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0),
        (0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0),
        (0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0),
        (0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0),
        (0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0),
        (0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0),
        (0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0),
        (0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0),
        (0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0),
        (0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0),
    ])
    
  2. Then sort the tied entries (monks entering at same time) so the monk leaving earliest comes first:

    time_slots_in_monastery = numpy.array([
        (0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
        (0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0),
        (0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0),  # monk 2 and 3 enter at same time, but 2 leaves earlier than 3
        (0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0),
        (0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0),
        (0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0),
        (0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0),
        (0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0),
        (0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0),
        (0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0),
        (0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0),
        (0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0),
    ])
    
  3. Apply previous algorithm, but restricted to the time_slots_in_monastery binary mask:

    1. Monk 0 starts sleeping at Monk 0 mask index 0
    2. Monk 1 starts sleeping when Monk 0 stops. Overlap beyond mask is wrapped around, starting at Monk 1 mask index 0
    3. Continue for all monks.

The idea is to have two neighboring monks have as much overlap as possible and filling the first slots as early as possible.

def rangemod(val, start, stop):
    """ Modulo in range between start and stop

    Adjusted from
    http://stackoverflow.com/questions/3057640/math-looping-between-min-and-max-using-mod
    """
    p = stop - start
    mod = (val - start) % p
    if mod < 0:
        mod += p
    return start + mod;


timeslots = numpy.array([
    (0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0),
    (0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0),
    (0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0),
    (0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0),
    (0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0),
    (0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0),
    (0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0),
    (0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0),
    (0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0),
    (0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0),
    (0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0),
    (0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0),
])

sleephours = numpy.array([4, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 3])

# Sort timeslots
startidx = numpy.array([list(row).index(1) for row in timeslots])
endidx = numpy.array([list(row)[::-1].index(1) for row in timeslots])

sortidx = numpy.array([
    x for _, _, x in sorted(
        zip(startidx, endidx, range(len(startidx))),
        key=lambda (x, y, z): (x, -y)
    )
])

# Put all indices and sleephours in new order
startidx = startidx[sortidx]
endidx = endidx[sortidx]
endidx = timeslots.shape[1] - endidx
sleephours = sleephours[sortidx]
timeslots = timeslots[sortidx, :]

print timeslots
# [[0 1 1 1 1 1 0 0 0 0 0 0]
#  [0 0 1 1 1 1 1 1 0 0 0 0]
#  [0 0 1 1 1 1 1 1 0 0 0 0]
#  [0 0 1 1 1 1 1 1 1 0 0 0]
#  [0 0 1 1 1 1 1 1 1 0 0 0]
#  [0 0 1 1 1 1 1 1 1 0 0 0]
#  [0 0 1 1 1 1 1 1 1 1 0 0]
#  [0 0 0 1 1 1 1 1 1 0 0 0]
#  [0 0 0 1 1 1 1 1 1 0 0 0]
#  [0 0 0 1 1 1 1 1 1 0 0 0]
#  [0 0 0 1 1 1 1 1 1 1 0 0]
#  [0 0 0 0 0 1 1 1 1 1 0 0]]

out = numpy.zeros_like(timeslots)

# Fill sleep schedule
prevstop = startidx[0]
for r, s, e, t, row in zip(sleephours, startidx, endidx, timeslots, out):
    sleeprange = numpy.arange(s, e)
    sleeppattern = numpy.zeros(e - s)
    sleeppattern[:r] = 1
    sleeppattern = numpy.roll(sleeppattern, prevstop - s)
    row[sleeprange] = sleeppattern
    prevstop += r
    prevstop = rangemod2(prevstop, s, e)

print out
# [[0 1 1 1 1 0 0 0 0 0 0 0]
#  [0 0 1 0 0 1 1 1 0 0 0 0]
#  [0 0 0 1 1 1 1 0 0 0 0 0]
#  [0 0 1 0 0 0 0 1 1 0 0 0]
#  [0 0 0 1 1 1 0 0 0 0 0 0]
#  [0 0 0 0 0 0 1 1 1 0 0 0]
#  [0 0 1 1 1 0 0 0 0 0 0 0]
#  [0 0 0 0 0 1 1 1 1 0 0 0]
#  [0 0 0 1 1 1 0 0 0 0 0 0]
#  [0 0 0 0 0 0 1 1 1 0 0 0]
#  [0 0 0 1 1 1 1 0 0 0 0 0]
#  [0 0 0 0 0 0 0 1 1 1 0 0]]

print numpy.max(numpy.sum(sleeping_slots, axis=0))
# 6
Nils Werner
  • 34,832
  • 7
  • 76
  • 98
  • This is a really cool way of solving this problem. I knew my solution would involve numpy somehow. What I don't understand here is: `monks` is the array where monks are in the monastery, but where are the boundary conditions, e.g. the sleeping requirements? – oz123 Mar 17 '17 at 08:22
  • You created hours, in such a way that hours are always the times in the monestary, but originally for each monk the time in monastery is longer than the time in bed. – oz123 Mar 17 '17 at 08:24
  • This solution does not take into account "time in monastery" conditions – Nils Werner Mar 17 '17 at 08:25
  • Thank you very much. Glad to see it is of some interest to others too. – oz123 Mar 21 '17 at 11:51
1

Here is is solution that is a little different from the one above. I have shuffled the indexes of the monks, bit I am sure reindexing will not be a problem. I believe that the solution is optimal.

Here is the algorithm:

First, we group monks into units if 4 and 3.

  • 3 monks who sleep 4 units, can be grouped into a unit who will take just one bed. This is the optimal solution for this group of 3 monks.
  • Similarly 4 monks who sleep for 3 units can be thought of as a unit that will take one bed each.

Now we need to count the number of such units, and assign a single bed to each of these units.

Now about the remaining monks:

We will definitely take the remaining monks who consume the 4 units of time, and keep adding monks (who take 3 units of time) to this unit until we reach 12 units of time. We assign this group one bed.

We now have to assign one bed to the remaining monks who are not part of the previous unit.

The program is shown below:

if __name__ == '__main__':

    reqSleep = [4, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 3]
    num4     = len([r for r in reqSleep if r==4])
    num3     = len([r for r in reqSleep if r==3])

    group4, left4 = num4/3, num4%3
    group3, left3 = num3/4, num3%4

    groups = {}
    groups['group3'] = [
        (1,1,1,0,0,0,0,0,0,0,0,0),
        (0,0,0,1,1,1,0,0,0,0,0,0),
        (0,0,0,0,0,0,1,1,1,0,0,0),
        (0,0,0,0,0,0,0,0,0,1,1,1),
    ]

    groups['group4'] = [
        (1,1,1,1,0,0,0,0,0,0,0,0),
        (0,0,0,0,1,1,1,1,0,0,0,0),
        (0,0,0,0,0,0,0,0,1,1,1,1),
    ]

    print num4, '-->', (group4, left4)
    print num3, '-->', (group3, left3)

    alreadyIn = []
    for i in range(group4):
        alreadyIn += groups['group4']

    for i in range(group3):
        alreadyIn += groups['group3']

    # we add the number of monks who
    # sleep for 4 hours
    alreadyIn += groups['group4'][:left4]

    # Now we are left with `left3`. How many can 
    # we add to this ? We take until they consume 
    # 12 time units. 
    num3ToFill = 0
    currTotal  = left4*4
    for i in range(left3):
        if currTotal + 3 > 12: break
        num3ToFill += 1
        currTotal  += 3

    # Fill in the number of groups
    alreadyIn += groups['group3'][-num3ToFill:]

    # Finally add the leftover monks at the end
    if left3 > num3ToFill:
        alreadyIn += groups['group3'][: (left3-num3ToFill)]

    print 'Monk sleep specs ...'
    for x in  alreadyIn:
        print x

    print 'Beds used ...'
    numBeds = map(sum, zip(*alreadyIn))
    print numBeds

    print 'done'   

Here is the result:

5 --> (1, 2)
7 --> (1, 3)
Monk sleep specs ...
(1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0)
(0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0)
(0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1)
(1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0)
(0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0)
(0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0)
(0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1)
(1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0)
(0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0)
(0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1)
(1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0)
(0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0)
Beds used ...
[4, 4, 4, 4, 4, 4, 3, 3, 2, 3, 3, 3]

Adding more monks and different units of time can be optimised by looking at the "required sleeping" times more carefully. All algorithms will probably resolve around optimising that array.

ssm
  • 5,277
  • 1
  • 24
  • 42