princepawn has asked for the wisdom of the Perl Monks concerning the following question:

My interest in a solution to this problem was stimulated by my recent hiring at a company. I learned that my benefits rep handled people whose last name starts with A, B, and L. And of course, I thought, "why those letters?" And the answer is obvious: they have N benefits managers who have to deal with Z people who are binned into groups based on the initial of their last name. In order to have each benefit manager deal with as near to Z/N people, they ran some sort of optimization search to come up with the ideal assignments of letter sets to benefits managers. And hence my problem spec below. Any solution (partial or otherwise), welcome.

Problem Spec

let's say a number is associated with each letter of the alphabet, representing the number of students enrolled with that letter of the first of their last name. let's say we have a list of account managers, the number of which is less than or equal to 20. the goal of the program is to figure out the letters to assign to each account manager such that the sum of students dealt with by each manager is as close as possible to the amount dealt with by all others. example, just using 5 letters and 3 acct managers:
A - 5 B - 1 C - 2 D - 5 E - 4 acct mgr 1 => A, (5) acct mgr 2 => D,B (6) acct mgr 3 => E,C (5)

Replies are listed 'Best First'.
Re: Constraint Satisfaction Problem
by mr.nick (Chaplain) on May 03, 2001 at 17:22 UTC
    Neat problem. I love things like this.

    Okay, so my thought was the utilize the following logic: walk forward through the list of managers, assigning each one in order the letter group with the highest population. After walking the list once, the first manager has the most people while the last one has the fewest. So, walk the list agian, but in reverse, assign the highest population group as you go. If there is more left after this walk, reverse direction and do it again.

    It seems to work for my test groups; though I know it won't work in all situations :) Given the input data above, I get:

    1: A 5 2: DB 6 3: EC 6
    Doubling the size of the list (F-J with A-E's numbers) gets me:
    1: AJC 11 2: FEH 11 3: DIBG 12
    Which also seems to work.

    Now below is the code. I want to apologize for the while loop: I couldn't (quickly) think of a clever way of running a list backwards and forwards multiple times until a condition is met. Suggests are welcome!

    #!/usr/bin/perl -w use strict; ## my %letters=( A => 5, B => 1, C => 2, D => 5, E => 4, ); my $reps=3; my %reps; ## hackarific! my $pos=1; my $dir=1; ## keep going until we run out while (keys %letters) { ## get highest my($h,undef)=sort { $letters{$b} <=> $letters{$a} } keys %letters; ## note the grouping $reps{$pos}{letter}.=$h; $reps{$pos}{size}+=$letters{$h}; delete $letters{$h}; ## sorry folks, couldn't think of a better way fast enough :) if ($dir==1) { $pos++; if ($pos>$reps) { $pos--; $dir=0; } } else { $pos--; if ($pos==0) { $pos++; $dir=1; } } } for my $r (keys %reps) { print "$r: $reps{$r}{letter} $reps{$r}{size}\n"; }
    For fun, replace the %letters declaration at the top with this
    my %letters; for my $x ('A'..'Z') { $letters{$x}=int(rand(10)+1); }

    UPDATE!!

    I thought of a better way. Instead of walking the list backwards and fowards (which even distributes the NUMBER of letters each rep has), instead pick the rep with the LOWEST size of it's letter groups. Programmatically, this requires an initialization of the %reps hash so that we can do a simple sort to get the lowest.

    #!/usr/bin/perl -w use strict; ## my %letters; for my $x ('A'..'Z') { $letters{$x}=int(rand(10)+1); } my $reps=5; my %reps; for my $x (1..$reps) { $reps{$x}{size}=0; } my $pos=1; ## keep going until we run out while (keys %letters) { ## get highest my($h,undef)=sort { $letters{$b} <=> $letters{$a} } keys %letters; ## note the grouping $reps{$pos}{letter}.="$h($letters{$h}) "; $reps{$pos}{size}+=$letters{$h}; delete $letters{$h}; ## find the lowest position ($pos,undef)=sort { $reps{$a}{size} <=> $reps{$b}{size} } keys %reps +; } for my $r (keys %reps) { print "$r: $reps{$r}{letter} $reps{$r}{size}\n"; }
    which yields the following as output:
    1: M(10) U(10) T(6) L(3) B(3) 32 2: R(10) O(9) J(7) D(3) K(3) 32 3: V(10) Z(9) S(6) F(6) P(2) 33 4: W(10) E(8) H(7) G(6) C(1) 32 5: N(10) X(8) I(7) Q(4) A(2) Y(1) 32
Re: Constraint Satisfaction Problem
by Masem (Monsignor) on May 03, 2001 at 17:19 UTC
    This pretty much sounds like the NP-problem "filling sacks with loot", so obviously the best answer can't be solved in poly-time.

    However, an alternate solution that should get you close is to sort the list by the number of items in that list. Also calculate the Z/N ratio to know what to aim for (eg 100 people with 5 benefits managers should have Z/N = 20).

    Take the letter with teh most frequent distribution, then add letters from the bottom of the list items until the sum total is just greater than Z/N (or within some range of it). Put that list aside, and repeat for all N managers. Assuming a sufficiently good distribution of last names, this really shouldn't be a problem.

    If any item on the distribution is greater than Z/N by itself, (say, the S's or M's) break it up and go from there.


    Dr. Michael K. Neylon - mneylon-pm@masemware.com || "You've left the lens cap of your mind on again, Pinky" - The Brain
Re: Constraint Satisfaction Problem
by MeowChow (Vicar) on May 03, 2001 at 21:23 UTC
    As Masem has already said, this is equivalent to the NP-complete bin-packing problem. Here's an implementation of the first-fit-decreasing greedy heuristic (similar to the one that Masem describes).
       MeowChow                                   
                   s aamecha.s a..a\u$&owag.print