Michael and the candy, my solution
[email protected] (Ton Hospel)
| Newsgroups | gmane.comp.lang.perl.golf |
|---|---|
| Organization | lunix confusion services |
| Message-ID | <[email protected]> |
The basic problem is given a set of bags (3 to 99) with in each 1 to 99
objects of 3 possible colors, move them all to 3 bags so that in each
bag is only one color. Do this in as few as possible moves. If multiple
solutions exist, order all possibilities lexical and report the first
one.
Each bag is represented as a line with 3 numbers (one for each color).
Moving all colors to one bag involves removing it from all of them and
subtracting from that the number there already where in the target bag,
since these don't have to be moved. The first part is a constant for all
solutions, so we can ignore that and the problem becomes:
For each color, select a bag and see how many of that color there are
Maximize the sum.
(no repetitions of bags, lexical first if there are multiple solutions).
The plan is to basically try all combinations and see which one is best.
Since the value to be optimised is a relatively small number, you can
do that by assigning to to an array and then popping the highest value,
basically like:
$array[$value] = $solution for all_combinations; print pop @array;
To get the lexical first, you must work through combinations in reverse
lexical order. That also means that the $solution part is evaluated
before the $value part, which turns out to be awkward. So instead I
went for:
$array[$value] ||= $solution for all_combinations; print pop @array;
$value is now evaluated before $solution (if $solution is evaluated at all)
and only the first time $value is seen is it put in the @array, so here you
must run through the combinations in lexical order.
The next step is generating all combinations without repeats. All ways of
generating a complete set and 3 times removing one element from that
turned out long, so instead I go for generating all combinations with
repeats and then filtering out the repeats. There are two basic ways to
get all combinations with repeats quickly:
map...,0 x6..$~
generates all strings from "000000" to "999999". Still needs to be chopped
in groups of 2 characters
and
...for 1..2e6
generates all numbers up to 2e6. Still needs the first char removed and
chopping in groups of 2 chars, and elimination of 2000000 and the
1..999999 range since they correspond to selecting less than 3 bags.
In most solutions you don't have to do anything for that, since if you
select less than 3 bags, there will be a solution with selecting one
more bag which gives a higher sum (there is at least one of every color
in each bag). The 2000000 also gives no problem since it's a repeat of
00 00 00 which will already have been seen as 1000000, so it's already
in the @array, and the ||= will not trigger.
Next is splitting up in groups of 2 and filtering. If you do them together,
for the map case that can be done with :
/(..)(?!..\1|\1)/g
and for the for case with:
/(\B..)(?!..\1|\1)/g
so combined they work out to exactly the same length. Or you can do the
filtering seperate from the chopping, and using a hash you get:
%c=();check $c{$_}++ for each /../g in the map case
%c=();check $c{$_}++ for each /\B../g in the for case
Again the lengths will be the same.
The chopping up process gives us strings like "00" where in the output we
must have 0. So we need to numify the results, which will involve something
like $_+=0 or $_*=1 at some point.
The last remaining step is taking the sum. For small positive numbers in
a loop that's easiest done with map in scalar context, since it needs
no initialization of a counter and directly returns the sum:
map{(1)x$_}@array
In fact, you can do better with
map 1..$_,@array
something I missed and why my solution is not optimal. In the case of the
hash filtering this becomes:
map{($c{$_--}++&&())x$_}@array
which basically removes the contribution of repeated entries, so it
basically selects less than 3 bags in these cases. And since then there
is a bigger sum by indeed selecting an extra different bag, it will never
become the result of pop@array, so repeated solutions are indeed effectively
eliminated.
Next we need to get all input in an array so we can repeatedly select from
it. We could put each bag line in an element, but we later need to select
the specific colors, which gets awkward. It's easier to have each value
as element, and then use $array[3*$line+$color] to select line and color.
That works out perfectly for the -a option of perl which makes the @F
array exactly like we want it.
For $color, we want to select the next color each time, so something
like $b++ seems natural, but after each combination we want it to start
at 0 again, so it either needs a reset or a %3 if the selection is always
per 3 in the 3-string region.
Working through these possible combination, and using the fact that -p gives
a free ; at the end, we get:
-p0a map%c=$;[map{($c{$_+=0}++&&())x$F[3*$_+$b++%3]}@a=/../g]||="@a\n",$^Cx6..$~;$_=pop@ (86)
-p0a %c=$;[map{($c{$_+=0}++&&())x$F[3*$_+$b++%3]}@a=/\B../g]||="@a\n"for 1..2e6;$_=pop@ (86)
%c here is either empty the very first time, or initialized with something
like "0 1 2\n" => undef, which doesn't screw up the relevant entries
$c{a number}, so it's effectively a reset of %c.
$b happens to be a multiple of 3 by the time we get to $_ = "1000000", so
the candy counting starts at the first color.
That $_+=0 bugged me however. I'd love to make that into $_--, but if $_
would be 0, that would become -1, which will select from the last bag in @F
and lead to false solutions. It also means the result will never have
bag 99, but that's ok, the challenge happens to be such that bags only go
from 0 to 98.
The $_-- would work if somehow we could pre-sabotage %c so that it starts
with e.g. %c = (0 => 1), but that's pretty expensive. But..for an array
it's easier to put something on index 0, so i started considering the
following 85:
-p0a map@c=$;[map{($c[$_--]++&&())x$F[3*$_+$b++%3]}@a=/../g]||="@a\n",$^Cx6..$~;$_=pop@
The very first time @c will be (), but the rest of the time it will start
out as "x y z\n". So it looks like we have something that's true on index 0
always ! But no !
What if x is 0 ? That one is no problem actually. if $_ is 0, the
$c[$_--]++ will be "0 y z\n", which is actually true, only AFTER that is
$_ numified and $c[0] becomes 1.
The problem is actually if x is -1 and the selected combination has two
zeros. In that case the first zero gives "-1 y z\n", which causes it to
be ignored for the sum, and $c[0] is set to 0. The SECOND zero now
picks up that $c[0]=0 and IS counted in the sum as "-1", so selecting
the last bag. The non-zero number now also gets counted. It's no problem
if that refers to any but the last bag, since it then still corresponds
to selecting 2 bags, so there will be some better 3 bag solution. If
however the last number ALSO refers to the last bag, we're in trouble
if the counts are high enough that it gives a solution. We will effectively
have chosen the same bag twice. And this DOES happen:
1 2 1
1 1 2
99 99 99
gives
-1 -1 2
Bummer !
causes -1 -1 2
But interesting enough, this works:
-p0a @c=$;[map{($c[$_--]++&&())x$F[3*$_+$b++%3]}@a=/\B../g]||="@a\n"for 1..2e6;$_=pop@ (85)
Why ? What's different ?
The reason lies in the failure mode of the previous example. The -1 -1 2
solution succeeded in getting into the system because @c was "-1 y z\n",
which was the kind of solution we didn't really want anyways. and the -1's
got into @; at the very first "000000".
In the last variant however @; will never contain -1, which we can
prove by induction:
- The very first time @c=(), and $_ runs over the empty set, so
we end up with @; = ("\n")
- Now we are considering some @a just before being processed in the map
(consisting of 2 digit strings) and currently @; has no entries with -1,
and therefore @c only has one value which does not contain -1. It's also
not "", since the result of the ||= in the previous round can't be ""
- suppose @a has no zeros => a new entry will get added to @; or not,
but there will be no new -1
- suppose @a has at least one zero. In that case when the zeros get
processed, $c[0] will be true (remember that "0 y z\n" is still true,
and all the next times it will be x+1, x+2 etc), so the 0 entries
don't get counted in the sum. So an @a = (0, a, 0) becomes
effectively (a), except that in the first case we selected color 2 from
the bag. So the actual cases are:
- @a = (0), @a= (0,0), @a = (0,0,0)
All of them get ignored in the sum, so they index @;[0], which is
already "\n" => No new -1 is entered
- @a = (x, 0) (x!=0)
The zero's get ignored, so the sum only consists of bag x, color 1
We already selected bag x only before as
"1x", "2x", "3x"..."9x" , all giving (x)
if at "1x" $b%3 didn't happen to be 0 (so we select color 1), it will
be in one of the later ones, since their distance is 10, and 10%3 = 1,
so $b%3 will cycle through 0, 1 and 2
So whatever sum we get, we've seen it before and @; remains unchanged.
- @a = (0, x), @a=(x, 0, 0), @a = (0, x, 0), @a = (0, 0, x) x != 0
give exactly the same story.
- @a = (x, y, 0) x!=0!=y
Here we get color 1 for bag x and color 2 for bag y. Now consider
"1xy", "2xy", ...."9xy", all giving (x, y)
the distance between these is 10000, and 10000%3 is 1, so
$b%3 for x an y will cycle like (0, 1), (1, 2), (2, 0)
So x with color 1 and y with color 2 will be one of these
- @a = (0, x, y) x!=0!=y gives the same story, now with (1, 2)
- @a = (x, 0, y) x!=0!=y now selects x with color 1 and y with color 3
Here we must consider
"1yx", "2yx", ...."9yx", all giving (y, x)
so $b%3 for y and x will cycle like (0, 1), (1, 2), (2, 0), or,
for x and y will cycle like (1,0), (2,1), (0,2).
So again one of these will match (0,2), and we get x with color 1 and
y with color 2 => seen before => no new entry in @;
Which covers all possibilities and finishes the proof.
Notice this all this works only because 3 is relative prime to 100 and
small enough so that with 9 tries $b%3 cycles through all possible
combinations. Which makes 3 (and 0 or 1) actually the ONLY number of bags
for which this method works. It also needs the fact that there can be only
99 instead of 100 bags, and that no bag has none of any color. So it
actually uses every random detail of the challenge.
Going back to using 1..$_ instead of (1)x$_, in the post-mortem this was
improved to:
-p0a @c=$;[map 1..!$c[$_--]++*$F[3*$_+$b++%3],@a=/\B../g]||="@a\n"for 1..2e6;$_=pop@ (83)
and it turns out we don't even need the tricky proof above by going back to
a hash:
-p0a %c=$;[map$c{$_+=0}++*$^T..$F[3*$_+$b++%3],@a=/\B../g]||="@a\n"for 1..2e6;$_=pop@ (83)
A pity, since i really liked my twisted solution.
--
In modern mathematics, algebra has become so important that
numbers will soon only have symbolic meaning.