This question is inspired by a similar question first asked on StackOverflow by Florian Margaine, then asked in variant forms on CS by Gilles and on Programmers by GlenH7.
This time, it's a code golf, with specific metrics for success!
In an office of ten people, it's someone's turn to buy croissants every morning. The goal is to produce a schedule that lists whose turn it is on any given day, subject to the following constraints:
- Presence. Each person is away from the office on business or vacation on average ~20% of the time. Each time you schedule a person who is away, lose one point per person who didn't get to eat croissants. For simplicity, generate the away-schedule with the random number generator
seed = (seed*3877 + 29573) % 139968
, starting with42*(10*i + 1)
as the initial (unused) seed, wherei
is the zero-indexed number of the employee; wheneverseed
is below 27994, that person will be away. Note that if everyone is away, you can assign anyone and not lose points. - Fairness. Each person will keep track of whether they have bought more or fewer croissants than the average. A person who never buys more than 10 more than the average will give you one point (they understand that someone has to buy first, and they'll of course go above the average). Otherwise, let
k
be the farthest above the average that a person has gotten inn
days of buying; that person will give you1 - (k-10)/100
points. (Yes, this may be negative.) - Randomness. The employees get bored if they can predict the sequence. Gain ten times the number of bits of information revealed a typical choice; assume that the employees can search for repetitive patterns and know the order of how long it's been since each of them bought croissants.
- Non-repetitiveness. Employees hate buying croissants two days in a row. Each time this happens, lose a point.
- Brevity. if
b
is the number of bytes of the gzipped* version of your solution, reduce your score byb/50
.
The solution should be an output of letters or numbers (0-9 or A-J) indicating who should buy croissants on each day. Run the algorithm for five hundred days (two years of work); give yourself a point if the output is in 10 rows of 50.
You only need to produce the sequence, not calculate the score. Here is a reference program in Matlab (or Octave) that picks people completely at random except for avoiding people who are away:
function b = away(i)
b=zeros(500,1); seed=42*(10*i+1);
for j=1:500
seed=mod((seed*3877+29573),139968);
b(j)=seed<27994;
end
end
A=zeros(500,10);
for i=1:10
A(:,i)=away(i-1);
end
for i=1:10
who = zeros(1,50);
for j=1:50
k = 50*(i-1)+j;
who(j)=floor(rand(1,1)*10);
if (sum(A(k,:)) < 10)
while (A(k,who(j)+1)==1)
who(j)=floor(rand(1,1)*10);
end
end
end
fprintf(1,'%s\n',char(who+'A'));
end
Here is a sample run:
HHEADIBJHCBFBGBIDHFFFHHGGHBEGCDGCIDIAEHEHAJBDFIHFI
JFADGHCGIFHBDFHHJCFDDAEDACJJEDCGCBHGCHGADCHBFICAGA
AJAFBFACJHHBCHDDEBEEDDGJHCCAFDCHBBCGACACFBEJHBEJCI
JJJJJBCCAHBIBDJFFFHCCABIEAGIIDADBEGIEBIIJCHAEFDHEG
HBFDABDEFEBCICFGAGEDBDAIDDJJBAJFFHCGFBGEAICEEEBHAB
AEHAAEEAJDDJHAHGBEGFHJDBEGFJJFBDJGIAJCDCBACHJIJCDF
DIJBHJDADJCFFBGBCCEBEJJBAHFEDABIHHHBDGDBDGDDBDIJEG
GABJCEFIFGJHGAFEBHCADHDHJCBHIFFHHFCBAHCGGCBEJDHFAG
IIGHGCAGDJIGGJFIJGFBAHFCBHCHAJGAJHADEABAABHFEADHJC
CFCEHEDDJCHIHGBDAFEBEJHBIJDEGGDGJFHFGFJAIJHIJHGEBC
which gives a total score of -15.7:
presence: 0 (Nobody scheduled while away)
fairness: 6.6 (~340 croissants bought "unfairly")
randomness: 33.2 (maximum score--uniformly random)
no-repeat: -51.0 (people had to bring twice 51 times)
size: -5.5 (275 bytes gzipped)
format: 1 (50 wide output)
Here is the reference Scala method I'm using to compute the score
def score(output: String, sz: Either[Int,String], days: Int = 500) = {
val time = (0 until days)
def away(i: Int) =
Iterator.iterate(42*(10*i+1))(x => (x*3877+29573)%139968).map(_<27994)
val everyone = (0 to 9).toSet
val aways =
(0 to 9).map(i => away(i).drop(1).take(days).toArray)
val here = time.map(j => everyone.filter(i => !aways(i)(j)))
val who = output.collect{
case x if x>='0' && x<='9' => x-'0'
case x if x>='A' && x<='J' => x-'A'
}.padTo(days,0).to[Vector]
val bought =
time.map(j => if (here(j) contains who(j)) here(j).size else 0)
val last = Iterator.iterate((0 to 10).map(_ -> -1).toMap){ m =>
val j = 1 + m.map(_._2).max
if (bought(j)==0) m + (10 -> j) else m + (who(j) -> j)
}.take(days).toArray
def log2(d: Double) = math.log(d)/math.log(2)
def delayEntropy(j0: Int) = {
val n = (j0 until days).count(j => who(j)==who(j-j0))
val p = n.toDouble/(days-j0)
val q = (1-p)/9
-p*log2(p) - 9*q*log2(q)
}
def oldestEntropy = {
val ages = last.map{ m =>
val order = m.filter(_._1<10).toList.sortBy(- _._2)
order.zipWithIndex.map{ case ((w,_),i) => w -> i }.toMap
}
val k = last.indexWhere(_.forall{ case (k,v) => k == 10 || v >= 0 })
(k until days).map{ j => ages(j)(who(j)) }.
groupBy(identity).values.map(_.length / (days-k).toDouble).
map(p => - p * log2(p)).sum
}
def presence =
time.map(j => if (here(j) contains who(j)) 0 else -here(j).size).sum
def fairness = {
val ibought = (0 to 9).map{ i =>
time.map(j => if (i==who(j)) bought(j) else 0).
scan(0)(_ + _).drop(1)
}
val avg = bought.scan(0)(_ + _).drop(1).map(_*0.1)
val relative = ibought.map(x => (x,avg).zipped.map(_ - _))
val worst = relative.map(x => math.max(0, x.max-10))
worst.map(k => 1- k*0.01).sum
}
def randomness =
((1 to 20).map(delayEntropy) :+ oldestEntropy).min*10
def norepeat = {
val actual = (who zip bought).collect{ case (w,b) if b > 0 => w }
(actual, actual.tail).zipped.map{ (a,b) => if (a==b) -1.0 else 0 }.sum
}
def brevity = -(1/50.0)*sz.fold(n => n, { code =>
val b = code.getBytes
val ba = new java.io.ByteArrayOutputStream
val gz = new java.util.zip.GZIPOutputStream(ba)
gz.write(b, 0, b.length); gz.close
ba.toByteArray.length + 9 // Very close approximation to gzip size
})
def columns =
if (output.split("\\s+").filter(_.length==50).length == 10) 1.0 else 0
val s = Seq(presence, fairness, randomness, norepeat, brevity, columns)
(s.sum, s)
}
(there is a usage note^).
The winner is the one with the highest score! Actually, there will be two winners: a globally optimizing winner and a short-notice winner for algorithms where earlier selections do not depend vacations that will happen in the future. (Real office situations are in between these two.) One entry may of course win both (i.e. it is short-notice but still outperforms other entries). I'll mark the short-notice winner as the "correct" winner, but will list the globally optimizing winner first on the scoreboard.
Scoreboard:
Who What Score Short-notice?
============== ====================== ======= ===
primo python-miniRNG 37.640 Y
Peter Golfscript-precomputed 36.656 N
Rex Scala-lottery 36.002 Y
Rex Matlab-reference -15.7 Y
*I'm using gzip 1.3.12 with no options; if there are multiple files, I cat them together first.
**A list prepared in advance is not considered short-notice unless it would be swapped out for a better list if an away-day appeared the day before; if this is a major issue (e.g. an unanticipated away day causes reversion to an algorithm that performs horribly) I'll define precise criteria.
^ If you use gzip, pass in the size as Left(287)
; otherwise if your code is in code
(val code="""<paste>"""
is useful here), use Right(code)
)
gzip
is deterministic, the size of the gzipped source will depend on the options and possibly on the version. \$\endgroup\$gzip
version (i.e. what I have on the machine I use most). \$\endgroup\$