File Coverage

blib/lib/Algorithm/SetCovering.pm
Criterion Covered Total %
statement 94 100 94.0
branch 21 24 87.5
condition 6 6 100.0
subroutine 10 11 90.9
pod 3 8 37.5
total 134 149 89.9


line stmt bran cond sub pod time code
1             package Algorithm::SetCovering;
2              
3 2     2   47115 use strict;
  2         5  
  2         81  
4 2     2   13 use warnings;
  2         4  
  2         181  
5 2     2   2844 use Log::Log4perl qw(:easy);
  2         294136  
  2         12  
6              
7             our $VERSION = '0.05';
8              
9             ##################################################
10             sub new {
11             ##################################################
12 3     3 1 603 my($class, @options) = @_;
13              
14 3         15 my %options = @options;
15              
16 3 50       14 die "No value given for mandatory parameter 'columns'"
17             unless exists $options{columns};
18              
19 3         23 my $self = {
20             mode => "greedy",
21             @options,
22             rows => [],
23             prepared => 0,
24             combos => [],
25             };
26              
27 3         19 bless $self, $class;
28             }
29              
30             ##############################################
31             sub add_row {
32             ##############################################
33 15     15 1 90 my($self, @columns) = @_;
34              
35 15 50       66 if($self->{columns} != scalar @columns) {
36 0         0 die "add_row expects $self->{columns} columns" .
37             "but received " . scalar @columns . "\n";
38             }
39            
40 15         85 DEBUG "Adding row @columns";
41              
42 15         126 push @{$self->{rows}}, [@columns];
  15         61  
43              
44 15         52 $self->{prepared} = 0;
45             }
46              
47             ##############################################
48             sub row {
49             ##############################################
50 0     0 0 0 my($self, $idx) = @_;
51              
52 0         0 return @{$self->{rows}->[$idx]};
  0         0  
53             }
54              
55             ##############################################
56             sub min_row_set {
57             ##############################################
58 11     11 1 5186 my($self, @columns_to_cover) = @_;
59              
60 11 100       49 if($self->{mode} eq "brute_force") {
    50          
61 5         14 return brute_force_run(@_);
62             } elsif($self->{mode} eq "greedy") {
63 6         17 return greedy_run(@_);
64             } else {
65 0         0 die "$self->{mode} not implemented\n";
66             }
67             }
68              
69             ##############################################
70             sub brute_force_run {
71             ##############################################
72 5     5 0 11 my($self, @columns_to_cover) = @_;
73              
74 5 100       17 $self->brute_force_prepare() unless $self->{prepared};
75              
76 5         13 COMBO:
77 5         7 for my $combo (@{$self->{combos}}) {
78              
79 28         80 for(my $idx = 0; $idx < @columns_to_cover; $idx++) {
80             # Check if the combo covers it, [0] is a ref
81             # to a hash for quick lookups.
82 95 100       231 next unless $columns_to_cover[$idx];
83 60 100       195 next COMBO unless $combo->[0]->[$idx];
84             }
85             # We found a minimal set, return all of its elements
86             # (which are idx numbers into the @rows array)
87 5         7 return @{$combo->[1]};
  5         32  
88             }
89              
90             # Can't find a minimal set
91 0         0 return ();
92             }
93              
94             ##############################################
95             sub brute_force_prepare {
96             ##############################################
97             # Create data structures for fast lookups
98             ##############################################
99 1     1 0 3 my($self) = @_;
100            
101             # Delete old combos;
102 1         3 $self->{combos} = [];
103              
104 1         2 my $nrows = scalar @{$self->{rows}};
  1         3  
105              
106             # Create all possible permutations of keys.
107             # (TODO: To optimize, we should get rid of
108             # keys which are subsets of other
109             # keys)
110             # Sort combos ascending by the number of keys
111             # they contain, i.e. combos with fewer keys
112             # come first.
113 101         149 my @combos =
114 1         13 sort { bitcount($a) <=> bitcount($b) }
115             (1..2**$nrows-1);
116            
117 1         17 DEBUG "Combos are: @combos";
118              
119             # A bunch of bitmasks to easily determine
120             # if a combo contains a certain key or not.
121 1         11 my @masks = map { 2**$_ } (0..$nrows-1);
  5         10  
122              
123 1         4 for my $combo (@combos) {
124             # The key values of the combo as (1,0,...)
125 31         46 my @keys = ();
126 31         35 my @covered = ();
127              
128 31         82 for(my $key_idx = 0; $key_idx < @masks; $key_idx++) {
129 155 100       373 if($combo & $masks[$key_idx]) {
130             # Key combo contains the current key. Iterate
131             # over all locks and store in @covered if
132             # the current key opens them.
133 80         150 for(0..$self->{columns}-1) {
134 320   100     1109 $covered[$_] ||= $self->{rows}->[$key_idx]->[$_];
135             }
136 80         231 push @keys, $key_idx;
137             }
138             }
139              
140 31         165 DEBUG "Combo '@keys' covers '@covered'";
141              
142             # Push hash ref and combo fields to 'combos'
143             # array
144 31         217 push @{$self->{combos}}, [\@covered, \@keys];
  31         139  
145             }
146              
147 1         6 $self->{prepared} = 1;
148             }
149              
150             ##############################################
151             sub bitcount {
152             ##############################################
153             # Count the number of '1' bits in a number
154             ##############################################
155 202     202 0 210 my($num) = @_;
156              
157 202         175 my $count = 0;
158              
159 202         299 while ($num) {
160 850         788 $count += ($num & 0x1) ;
161 850         1223 $num >>= 1 ;
162             }
163              
164 202         323 return $count ;
165             }
166              
167             ##############################################
168             sub greedy_run {
169             ##############################################
170 6     6 0 14 my($self, @columns_to_cover) = @_;
171              
172 6         7 my @hashed_rows = ();
173 6         12 my %column_hash = ();
174 6         7 my @result = ();
175              
176 6         24 for(my $i=0; $i<@columns_to_cover; $i++) {
177 24 100       96 $column_hash{$i} = 1 if $columns_to_cover[$i];
178             }
179              
180 6         9 for my $row (@{$self->{rows}}) {
  6         15  
181 30         195 my $rowhash = {};
182 30         67 for(my $i=0; $i<@columns_to_cover; $i++) {
183 120 100 100     529 $rowhash->{$i}++ if $columns_to_cover[$i] and $row->[$i];
184             }
185 30         52 push @hashed_rows, $rowhash;
186 30         127 DEBUG("Hash of idx (", join('-', keys %$rowhash), ")");
187             }
188              
189 6         66 my %not_covered = %column_hash;
190              
191 6         9 do {
192             # Get the longest list
193 9         34 my $max_len = 0;
194 9         14 my @max_keys = ();
195 9         11 my $max_idx = 0;
196 9         24 for my $idx (0..$#hashed_rows) {
197 45         58 my $row = $hashed_rows[$idx];
198 45         80 my @keys = keys %$row;
199 45 100       116 if(scalar @keys > $max_len) {
200 11         18 @max_keys = @keys;
201 11         15 $max_len = scalar @keys;
202 11         22 $max_idx = $idx;
203             }
204             }
205              
206             # Return empty solution if rows can't cover columns_to_cover
207 9 100       36 return () unless $max_len;
208            
209 7         31 DEBUG("Removing max_keys: @max_keys");
210              
211 7         68 delete $not_covered{$_} for @max_keys;
212 7         13 push @result, $max_idx;
213              
214             # Remove max_keys columns from all keys
215 7         11 foreach my $row (@hashed_rows) {
216 35         234 delete $row->{$_} for @max_keys;
217 35         115 DEBUG("Remain (", join('-', keys %$row), ")");
218             }
219            
220 7         62 DEBUG("Not covered: (", join('-', keys %not_covered), ")");
221            
222             } while(scalar keys %not_covered);
223              
224 4         83 return @result;
225             }
226            
227             1;
228              
229             __END__