File Coverage

blib/lib/Algorithm/Pair/Best2.pm
Criterion Covered Total %
statement 91 101 90.1
branch 25 34 73.5
condition 6 14 42.8
subroutine 9 12 75.0
pod 3 5 60.0
total 134 166 80.7


line stmt bran cond sub pod time code
1             # Algorithm::Pair::Best2.pm
2             #
3             # Copyright (C) 2004-2011 Reid Augustin reid@HelloSix.com
4             #
5             # This library is free software; you can redistribute it and/or modify it
6             # under the same terms as Perl itself, either Perl version 5.8.5 or, at your
7             # option, any later version of Perl 5 you may have available.
8             #
9             # This program is distributed in the hope that it will be useful, but
10             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11             # or FITNESS FOR A PARTICULAR PURPOSE.
12             #
13              
14 1     1   37144 use 5.002;
  1         3  
  1         60  
15 1     1   6 use strict;
  1         2  
  1         33  
16 1     1   5 use warnings;
  1         6  
  1         72  
17              
18             package Algorithm::Pair::Best2;
19              
20             our $VERSION = '2.035'; # VERSION
21              
22             # ABSTRACT: select pairings (designed for Go tournaments, but can be used for anything).
23              
24 1     1   5 use Carp;
  1         2  
  1         1205  
25              
26             sub new {
27 2     2 1 3745 my ($proto, %args) = @_;
28              
29 2         3 my $self = {};
30             $self->{scoreSub} = delete $args{scoreSub}
31 2   50 0   10 || sub { croak "No scoreSub() callback" };
  0         0  
32 2   50     13 $self->{items} = delete $args{items} || [];
33 2   50 0   9 $self->{progress} = delete $args{progress} || sub { };
  0         0  
34 2   50     8 $self->{window} = delete $args{window} || 5;
35 2 50       8 if (keys %args) {
36 0 0       0 croak sprintf "Unknown option%s to %s->new: %s",
37             scalar(keys %args) > 1 ? 's' : '',
38             __PACKAGE__,
39             join(', ', keys %args);
40             }
41 2   33     14 return bless($self, ref($proto) || $proto);
42             }
43              
44             sub add {
45              
46 9     9 1 956 push @{shift->{items}}, @_;
  9         28  
47             }
48              
49             sub get_score {
50 2813     2813 0 4217 my ($self, $idx0, $idx1) = @_;
51              
52 2813         3460 my $cache_key = "$idx0,$idx1";
53 2813 100       6697 if (not exists $self->{score_cache}{$cache_key}) {
54 105         122 my $items = $self->{items};
55 105         124 my $score = &{$self->{scoreSub}}($items->[$idx0], $items->[$idx1]);
  105         242  
56 105 50       754 croak "Negative score: $score" if ($score < 0);
57 105         265 $self->{score_cache}{$cache_key} = $score;
58             # $self->{cache_misses}{$cache_key}++;
59             # $self->{cache_misses_total}++;
60             }
61             else {
62             # $self->{cache_hits}{$cache_key}++;
63             # $self->{cache_hits_total}++;
64             }
65 2813         5343 return $self->{score_cache}{$cache_key};
66             }
67              
68             sub pick {
69 2     2 1 13 my ($self, $window) = @_;
70              
71 2   33     5 $window ||= $self->{window}; # size of sliding window
72              
73 2         2 my %paired; # for marking off pairs
74             my @results;
75              
76 2         4 my $items = $self->{items};
77 2 50       3 if (scalar(@{$items}) <= 0) {
  2         8  
78 0         0 croak sprintf "No items";
79             }
80 2 50       2 if (scalar(@{$items}) & 1) {
  2         6  
81 0         0 croak sprintf "Odd number of items (%d)", scalar @{$items};
  0         0  
82             }
83 2         3 my $progress = $self->{progress};
84              
85             # Sliding window:
86 2         3 while (1) {
87             # create new list containing only a windows-worth of items
88 4         19 my @w_idxs; # items for this window
89 4         5 for my $idx (0 .. $#{$items}) {
  4         8  
90 32 100       51 if (not exists $paired{$idx}) {
91 26         28 push @w_idxs, $idx;
92 26 100       52 last if (@w_idxs >= $window * 2) # window filled
93             }
94             }
95 4         4 my $score = 0; # need an initial score, might as well count
96             # initial items as passed to us
97 4         11 foreach (my $idx = 0; $idx < @w_idxs; $idx += 2) {
98 13         26 $score += $self->get_score($w_idxs[$idx], $w_idxs[$idx + 1]);
99             }
100             # pair this window
101 4         11 $self->_r_best(0, $score, \@w_idxs);
102              
103 4 100       7 if (scalar keys %paired < (scalar(@{$items}) - (2 * $window))) {
  4         12  
104             # keep top pair
105 2         5 $paired{$w_idxs[0]} = 1;
106 2         3 $paired{$w_idxs[1]} = 1;
107 2         3 push @results, $items->[$w_idxs[0]], $items->[$w_idxs[1]];
108 2         6 &$progress($items->[$w_idxs[0]], $items->[$w_idxs[1]]);
109             }
110             else {
111             # keep all the results, we are done
112 2         6 push @results, map {$items->[$_]} @w_idxs;
  18         35  
113 2         9 foreach (my $idx = 0; $idx < @w_idxs; $idx += 2) {
114 9         60 &$progress($items->[$w_idxs[$idx]], $items->[$w_idxs[$idx + 1]]);
115             }
116 2         18 last;
117             }
118             }
119             # print "cache hits:\n";
120             # map { print " $_:$self->{cache_hits}{$_}\n" } sort keys %{$self->{cache_hits}};
121             # print "total misses: $self->{cache_misses_total}, hits: $self->{cache_hits_total}\n";
122 2 50       22 return wantarray ? @results : \@results;
123             }
124              
125             sub _r_best {
126 1000     1000   1291 my ($self, $depth, $best_score, $idxs) = @_;
127              
128 1000         1177 my $items = $self->{items};
129 1000 100       949 if (@{$idxs} <= 2) {
  1000         1882  
130 494 50       408 croak sprintf("%d items left", scalar @{$idxs}) if (@{$idxs} <= 1);
  0         0  
  494         822  
131 494         1073 return $self->get_score($idxs->[0], $idxs->[1]);
132             }
133 506         444 my $best_trial;
134             # starting at idx=1, move item at idx to slot 1, calculate scores
135 506         566 for (my $idx = 0; $idx <= @{$idxs} - 2; $idx++) {
  2812         5460  
136 2306         2165 my ($trial_0, $trial_1, @tail) = @{$idxs}; # copy of original
  2306         5698  
137 2306 100       3780 if ($idx > 0) { # first is same as original
138             # put second item at head of tail
139 1800         2468 unshift @tail, $trial_1;
140             # put the item from $idx in $tail into second item
141 1800         2585 $trial_1 = splice @tail, $idx, 1;
142             }
143             # print "$idx trial [", join (', ', $trial_0, $trial_1, @tail), "]\n" if ($depth == 0);
144 2306         4132 my $trial_score = $self->get_score($trial_0, $trial_1); # first pair
145 2306 50       4036 croak "Negative score: $trial_score" if ($trial_score < 0);
146 2306 100       5291 next if ($trial_score >= $best_score); # worse? abandon branch
147             # get best pairing for tail and add to score
148 996         1955 $trial_score += $self->_r_best($depth + 1, $best_score, \@tail);
149 996 100       2525 next if ($trial_score >= $best_score); # worse? abandon branch
150             # aha, a potential candidate, save it
151 437         691 $best_score = $trial_score;
152 437         2859 unshift @tail, $trial_0, $trial_1;
153 437         951 $best_trial = \@tail;
154             # print "$depth $idx Best score=$best_score, idxs=[", $self->print_items(@{$best_trial}), "]\n" if ($depth == 0);
155             }
156 506 100       878 if ($best_trial) {
157 355         316 @{$idxs} = @{$best_trial};
  355         1264  
  355         486  
158             # print "$depth Improve score=$best_score, idxs=[", $self->print_items(@{$best_trial}), "]\n";
159             }
160 506         1144 return $best_score;
161             }
162              
163             sub print_items {
164 0     0 0   my ($self, @idxs) = @_;
165              
166 0           return join ', ', map { $self->{items}[$_] } @idxs;
  0            
167             }
168              
169             1;
170              
171              
172              
173             =pod
174              
175             =head1 NAME
176              
177             Algorithm::Pair::Best2 - select pairings (designed for Go tournaments, but can be used for anything).
178              
179             =head1 VERSION
180              
181             version 2.035
182              
183             =head1 SYNOPSIS
184              
185             use Algorithm::Pair::Best2;
186              
187             my $pair = Algorithm::Pair::Best2->new( [ options ] );
188              
189             $pair->add( item, [ item, ... ] );
190              
191             @new_pairs = $pair->pick( [ window ] );
192              
193             =head1 DESCRIPTION
194              
195             This is a re-write of Algorithm::Pair::Best. The interface is simplified
196             and the implementation is significantly streamlined.
197              
198             After creating an Algorithm::Pair::Best2 object (with -EB), B
199             items to the list of items (i.e: players) to be paired. The final list
200             must contain an even number of items or Bing the pairs will throw an
201             exception.
202              
203             Algorithm::Pair::Best2-EB explores all combinations of items and
204             returns the pairing list with the best (lowest) score. This can be an
205             expensive proposition - the number of combinations goes up very fast with
206             respect to the number of items:
207              
208             items combinations
209             2 1 (1)
210             4 3 (1 * 3)
211             6 15 (1 * 3 * 5)
212             8 105 (1 * 3 * 5 * 7)
213             10 945 (1 * 3 * 5 * 7 * 9
214             12 10395 (1 * 3 * 5 * 7 * 9 * 11)
215             14 135135 (1 * 3 * 5 * 7 * 9 * 11 * 13)
216              
217             It is clearly unreasonable to try to pair a significant number of items.
218             Trying to completely pair even 30 items would take too long.
219              
220             Fortunately, there is a way to get pretty good results for big lists,
221             even if they're not perfect. Instead of trying to pair the whole list
222             at once, Algorithm::Pair::Best2 pairs a series of smaller groups in a
223             sliding window to get good 'local' results.
224              
225             The B<-Enew> method accepts a B option to limit the number
226             of pairs in the sliding window. The B option can also be
227             overridden by calling B with an explicit window argument:
228              
229             $pair->pick($window);
230              
231             The list should be at least partially sorted so that reasonable
232             pairing candidates are within the 'sliding window' of each other.
233             Otherwise the final results may not be globally 'best', but only
234             locally good. For (e.g.) a tournament, sorting by rank is sufficient.
235              
236             Here's how a window value of 5 works: the best list for items 1
237             through 10 (5 pairs) is found. Save the pairing for the top two items
238             and then slide the window down to pair items 2 through 12. Save the
239             top pairing from this result and slide down again to items 4 through
240             14. Keep sliding the window down until we reach the last 10 items
241             (which are completed in one iteration). In this way, a large number
242             of pairings can be completed without taking factorial time.
243              
244             =head1 METHODS
245              
246             =over
247              
248             =item my $pair = Bnew>( options )
249              
250             Creates a B Algorithm::Pair::Best2 object.
251              
252             =item $pair-EB ( item, [ item, ...] )
253              
254             Add an item (or several items) to be paired. Item(s) can be any scalar
255             or reference. They will be passed (a pair at a time) to the B
256             callback.
257              
258             =item @new_pairs = $pair-EB ( ?$window? )
259              
260             Returns the best pairing found using the sliding window technique as
261             discussed in DESCRIPTION above. B is the number of pairs in the
262             sliding window. If no B argument is passed, the B selected
263             in the B, or the default value is used.
264              
265             B returns the list (or a reference to the list in scalar context) of
266             items in pairing order: new_pair[0] is paired to new_pair[1], new_pair[2]
267             to new_pair[3], etc.
268              
269             If the number of items in the list (from B) is not even, an exception
270             is thrown.
271              
272             =back
273              
274             =head1 OPTIONS
275              
276             The B<-Enew> method accepts the following options:
277              
278             =over 4
279              
280             =item B => number of pairs
281              
282             Sets the default number of pairs in the sliding window during B. Can
283             also be set by passing a B argument to B.
284              
285             Default: 5
286              
287             =item B => reference to scoring callback
288              
289             The callback is called as B(item_0, item_1), where item_0 and item_1
290             are members of the list created by Bing items. The callback must
291             return a positive number representing the 'badness' of this pairing. A
292             good pairing should have a number closer to 0 than a worse pairing. If
293             B returns a negative number, an exception is thrown.
294              
295             B(A, B) should be equal to B(B, A). B(A, B)
296             is called only one time (for any particular A and B), and the result is
297             cached. B(B, A) is never called.
298              
299             Note that scores are always positive (Algorithm::Pair::Best2 searches for
300             the lowest combined score).
301              
302             Default: a subroutine that throws an exception.
303              
304             =item B => reference to progress callback
305              
306             Each time a pair is finalized in the B routine, the
307             B($item_0, $item_1) callback is called where $item_0 and
308             $item_1 are the most recently finalized pair:
309              
310             progress => sub {
311             my ($item_0, $item_1) = @_;
312             # assuming $items have a 'name' method that returns a string:
313             print $item_0->name, " paired with ", $item_1->name, "\n";
314             },
315              
316             Default: a subroutine that does nothing.
317              
318             =back
319              
320             =head1 EXAMPLE
321              
322             use Scalar::Util qw( refaddr );
323             use Algorithm::Pair::Best2;
324              
325             my @players = (
326             Player->new( # Player object defined elsewhere
327             name => "Player 1",
328             rank => 3.5, # Player also has a 'rank' method
329             ),
330             Player->new( ... ), # more players
331             ...
332             );
333              
334             # some extra information not provided by Player methods:
335             my %already_been_paired = (
336             refaddr($player_0) => {
337             refaddr($player_1) => 1, # player_0 played player_1
338             refaddr($player_4) => 1, # and player_4
339             },
340             ...
341             );
342              
343             my $pair = Algorithm::Pair::Best2->new(
344             scoreSub => sub { # score callback
345             my ($player_A, $player_B) = @_;
346              
347             # Compare using the 'rating' method defined for Players.
348             # Closer in rating is a better match:
349             my $score = abs($player_A->rating - $player_B->rating);
350              
351             ...
352              
353             # but if they have already been matched, increase the 'badness' of
354             # this pair by a lot:
355             if ($already_been_paired{refaddr($player_A)}{refaddr($player_B)}) {
356             $score += 50;
357             }
358              
359             ... # other criterion that can increase $score
360              
361             return $score; # always positive
362             }
363             );
364              
365             $pair->add(sort { $a->rank <=> $b->rank } @players);
366              
367             my @pairs = $pair->pick;
368              
369             ...
370              
371             =head1 SEE ALSO
372              
373             =over
374              
375             =item Games::Go::W3Gtd::Paring.pm
376              
377             =back
378              
379             =head1 AUTHOR
380              
381             Reid Augustin
382              
383             =head1 COPYRIGHT AND LICENSE
384              
385             This software is copyright (c) 2011 by Reid Augustin.
386              
387             This is free software; you can redistribute it and/or modify it under
388             the same terms as the Perl 5 programming language system itself.
389              
390             =cut
391              
392              
393             __END__