1 3 2,4 #### 1 3 2,4 5,6 #### #!/usr/bin/perl use strict; use warnings; use autodie; use Data::Dumper; use List::Util qw(shuffle); use List::MoreUtils qw(firstidx); use Getopt::Long; sub del_ballots{ for my $f(glob("*.bt")){ unlink $f; } } sub make_ballots{ my ($nb,$nbc) = @_; my @candidates; my $same_rank = ""; my $j = 0; for my $i (1..$nbc){ $candidates[$j] = $i; $j++; } my $more_to_come = 0; for my $i (0 .. $nb){ my @ballot = shuffle(@candidates); open(my $fh, '>',$i.".bt"); foreach my $i ( 0..$#ballot){ my $c = $ballot[$i]; my $line_end = "\n"; if(int(rand(6)) == 0 && !$more_to_come){#leave all other candidates out last; } elsif(int(rand(2)) == 0 && $i < $#ballot){#put one or more candidate on this row $line_end = ","; $more_to_come = 1; print $fh $c.$line_end; } else{ $more_to_come = 0; print $fh $c.$line_end; } } close($fh); } } sub rand_tiebreak_set{ our @tiebreak; my ($cand_ref,$votes) = @_; my @ballot = shuffle(@{$cand_ref}); update_tiebreak(\@ballot,$votes); } sub enumerate{ my $candidates = shift; my $votehash = {}; foreach my $c(@$candidates){ foreach my $d(@$candidates){ if($c != $d){ $votehash->{$c}->{$d} = 0; } } } return $votehash; } sub vote_against_below{ my ($startidx,$cv,$votes,$candidate) = @_; my $c = $candidate; my @curvote = @{$cv}; for my $j($startidx .. $#curvote){ #if on the lines below I encounter multiple candidates #I count a vote against all of them if($curvote[$j] =~ /^\d+\s?,\s?\d+/){ my @losers = split /,/,$curvote[$j]; foreach my $l (@losers){ $votes->{$c}->{$l}++; } } #if it is a normal case then I deal with it else{ $votes->{$c}->{$curvote[$j]}++; } } } sub readfile{ my ($votes,$fh) = @_; my $i = 0; my @curvote; #prepare a hash to check for left out candidates my %seen; my @keys = keys %{$votes}; foreach my $k (@keys){ $seen{$k} = 0; } while(<$fh>){ chomp $_; $curvote[$i] = $_; if($curvote[$i] =~ /\A\d+\s?,\s?/){ my @votes = split /\s?,\s?/, $curvote[$i]; foreach my $v(@votes){ $seen{$v} = 1; } } else{ $seen{$curvote[$i]} = 1; } $i++; } #all candidates left out of the ballot are accounted for as if they #were added together at the lowest possible position my $leftovers = ""; foreach my $k (@keys){ if(!$seen{$k}){ if($leftovers =~ /\d+\z/){ $leftovers = $leftovers . ",$k"; } else{#leftovers is empty $leftovers = $k; } } } if($leftovers ne ""){ $curvote[$i] = $leftovers; } for my $i (0 .. $#curvote){ my @candidates; if($curvote[$i] =~ /^\d+\s?,\s?\d+/){#more than one candidate on this line #I split the candidate list into an array @candidates = split /\s?,\s?/,$curvote[$i]; #foreach candidate in the list I count one vote for him #against everyone below him foreach my $c (@candidates){ vote_against_below($i+1,\@curvote,$votes,$c); } } else{ vote_against_below($i+1,\@curvote,$votes,$curvote[$i]); } our $tiebreak_ready; if(!$tiebreak_ready){ update_tiebreak(\@curvote,$votes); } } } sub update_tiebreak{ my ($cv,$votes)= @_; my @curvote = @{$cv}; our( @tiebreak,$tiebreak_ready); if(!@tiebreak){#empty tiebreak foreach my $v (@curvote){ push @tiebreak,$v; } } elsif(!$tiebreak_ready){ foreach my $v (@curvote){ if($v =~ /\d+\s?,\s?\d+/){ next; #curvote alreay has a tie } else{ foreach my $t (@tiebreak){ if($t =~ /\d+\s?,\s?\d+/){ my @candidates = split /\s?,\s?/, $t; my $chosen_idx = firstidx {$_ == $v} @candidates; if($chosen_idx == -1){ #can not solve, untied vote no in tie next; } my $curpos = firstidx {$_ =~ /$t/} @tiebreak; #current position of the tie inside the tiebreak #I get the index of the one not in the tie my $split_candidate = $candidates[$chosen_idx]; my $idx = firstidx {$_ =~ /$split_candidate/} @curvote; #remove split candidate splice @candidates, $chosen_idx,1; my $new_tie = join(',',@candidates); if(is_updown($split_candidate,\@candidates,\@curvote,0,1)){ #our split is before every other tied candidate in #curvote tiebreak_replace(\@tiebreak,$split_candidate,$new_tie,$curpos); } elsif(is_updown($split_candidate,\@candidates,\@curvote,0,0)){ tiebreak_replace(\@tiebreak,$new_tie,$split_candidate,$curpos); } } } } } } my @cdt = keys %{$votes}; $tiebreak_ready = $#tiebreak == $#cdt;#ready when we have #one candidate per line } sub is_updown{ my ($split_candidate,$candidates,$curvote,$idx,$dir) = @_; if($idx > $#$candidates){ return 1; } my $split_idx = firstidx {$_ =~ /$split_candidate/} @$curvote; my $nextidx = firstidx {$_ =~ /$candidates->[$idx]/} @$curvote; if($dir == 1){#check above in the order if($split_idx < $nextidx){ return is_updown($split_candidate,$candidates,$curvote,$idx+1,$dir); } else{ return 0; } } else{#check below if($split_idx < $nextidx){ return is_updown($split_candidate,$candidates,$curvote,$idx+1,$dir); } else{ return 0; } } } sub tiebreak_replace{ my ($tiebreak, $cd1,$cd2,$curpos) = @_; splice @{$tiebreak},$curpos,1,($cd1,$cd2); } sub calculate_majorities{ my $votes = shift; my @candidates = keys %$votes; my @majorities; my %done; my $n = 0; for my $i (0 .. $#candidates){ for my $j (0 .. $#candidates){ my $c1 = $candidates[$i]; my $c2 = $candidates[$j]; if($c1 != $c2 && !exists($done{$c1}{$c2}) && !exists($done{$c2}{$c1})){ $done{$c1}{$c2} = 1; my $fori = $votes->{$c1}->{$c2}; my $forj = $votes->{$c2}->{$c1}; if($fori > $forj){ $majorities[$n] = {$c1=>{$c2 => $fori,'min'=>$forj}}; } elsif($fori < $forj){ $majorities[$n] = {$c2=>{$c1 => $forj,'min'=>$fori}}; } else{#record both and let the tiebreak do his #job $majorities[$n] = {$c2=>{$c1 => $forj,'min'=>$fori}}; $n++; $majorities[$n] = {$c1=>{$c2 => $fori,'min'=>$forj}}; } $n++; } } } return \@majorities; } sub get_loser{ my $hash = shift; foreach my $k (keys %$hash){ if ($k ne 'min'){ return $k; } } } sub affirm{ my($winner,$loser,$finishOver) = @_; $finishOver->{$winner}->{$loser} = 1; my @candidates = keys %$finishOver; foreach my $c (@candidates){ if($c == $winner || $c == $loser){ next; } if($finishOver->{$c}->{$winner} == 1 && $finishOver->{$c}->{$loser} == 0){ affirm($c,$loser,$finishOver); } if($finishOver->{$loser}->{$c} == 1 && $finishOver->{$winner}->{$c} == 0){ affirm($winner,$c,$finishOver); } } } sub win_order{ my ($majorities,$candidates) = @_; my @maj = @{$majorities}; my $k = 0; my $finishOver = enumerate($candidates); foreach my $m (@$majorities){ my $winner = (keys %$m)[0]; my $loser = get_loser($m->{$winner});; if($finishOver->{$winner}->{$loser} == 0 && $finishOver->{$loser}->{$winner} == 0){ affirm($winner,$loser,$finishOver); } } return $finishOver; } sub getsubkeys{ my($a,$b) = @_;#get one keyed/subkeyed hashs my ($a_key,$a_subkey,$b_key,$b_subkey); if(defined($a)){ my @keys = keys %{$a}; $a_key = $keys[firstidx {$_ ne 'min'} @keys]; @keys = keys %{$a->{$a_key}}; $a_subkey = $keys[firstidx {$_ ne 'min'} @keys]; if(defined($b)){ @keys = keys %{$b}; $b_key = $keys[firstidx {$_ ne 'min'} @keys]; @keys = keys %{$b->{$b_key}}; $b_subkey = $keys[firstidx {$_ ne 'min'} @keys]; return ($a_key,$a_subkey,$b_key,$b_subkey); } return ($a_key,$a_subkey); } elsif(!defined($a) && !defined($b)){ die("get subkeys takes at least one arg\n"); } } sub majsort{ my ($a_key,$a_subkey,$b_key,$b_subkey) = getsubkeys($a,$b); my $aval = $a->{$a_key}->{$a_subkey}; my $bval = $b->{$b_key}->{$b_subkey}; if($aval < $bval){ return 1; } elsif($aval == $bval){ my $amin = $a->{$a_key}->{min}; my $bmin = $b->{$b_key}->{min}; #here check for the minority size rule in case of equality #the majority opposed by the smallest minority has precedence print STDERR "solving $a_key -> $a_subkey vs $b_key -> $b_subkey using minority rules\n"; if($amin > $bmin) { return 1; } elsif($amin < $bmin){ return -1; } else{ #use tiebreak our @tiebreak; my $indb = firstidx {$_ == $a_key} @tiebreak; my $inda =firstidx {$_ == $b_key} @tiebreak; print STDERR "solving using tiebreak for $a_key vs $a_subkey and $b_key vs $b_subkey\n"; if($inda < $indb){ return 1; } elsif($inda == $indb){ my $indb = firstidx {$_ == $a_subkey} @tiebreak; my $inda =firstidx {$_ == $b_subkey} @tiebreak; if($inda < $indb){ return 1; } else{ return -1; } } else{ return -1; } } } else{ return -1; } } sub scoresort{ my $a_score = (keys %$a)[0]; my $b_score = (keys %$b)[0]; if( $b->{$b_score}->{score} < $a->{$a_score}->{score}){ return -1; } elsif($b->{$b_score}->{score} == $a->{$a_score}->{score}){ our @tiebreak; my $idx_a = firstidx {$_ == $a->{$a_score}->{self}} @tiebreak; my $idx_b = firstidx {$_ == $b->{$b_score}->{self}} @tiebreak; if($idx_a < $idx_b){ return -1; } else{ return 1; } } else{ return 1; } } sub relook{ my $finishOrder = shift; my @candidates = keys %$finishOrder; my @tmp_results; foreach my $c (@candidates){ my $score = 0; foreach my $adv (keys %{$finishOrder->{$c}}){ $score += $finishOrder->{$c}->{$adv}; } push @tmp_results, {$c => {score=>$score,self=>$c}}; } @tmp_results = sort scoresort @tmp_results; my @results; foreach my $r (@tmp_results){ my $candidate = (keys %$r)[0]; push @results, $candidate; } return \@results; } sub getdistance{ my $votes = shift; my @candidates = keys %$votes; my @distances; foreach my $c (@candidates){ foreach my $a(@candidates){ if($a != $c){ $distances[$c][$a] = $votes->{$c}->{$a}; $distances[$a][$c] = $votes->{$a}->{$c}; } } } return \@distances; } sub max{ my($x,$y) = @_; if($x > $y){ return $x; } else{ return $y; } } sub min{ my($x,$y) = @_; if($x < $y){ return $x; } else{ return $y; } } sub compute_path{ my $distances = shift; print STDERR "I have $#$distances candidates\n"; my @path; for my $i (1 .. $#$distances){ for my $j (1 .. $#$distances){ if($i != $j){ print STDERR "distance for $i and $j = $distances->[$i][$j]\n reverse $j $i = $distances->[$j][$i]\n"; if($distances->[$i][$j] > $distances->[$j][$i]){ $path[$i][$j]= $distances->[$i][$j]; } else{ $path[$i][$j]=0; } } } } print STDERR "paths state before compute:\n".Dumper(\@path); for my $i (1..$#$distances){ for my $j (1..$#$distances){ for my $k (1..$#$distances){ if($i != $j){ if($i != $k && $j != $k){ $path[$j][$k] = max($path[$j][$k],min($path[$j][$i],$path[$i][$k])); } } } } } return \@path; } sub shulze_winner{ my $path = shift; my %results; for my $i(1 .. $#$path){ for my $j (1 .. $#$path){ if($i != $j){ $results{$i}->{$j} = $path->[$i][$j]; } } } print STDERR "schulze winner hash = \n".Dumper(\%results); return \%results; } sub main{ my ($nbc,$schulze) = @_; my @candidates; my $j = 0; my $finish_table; for my $i (1..$nbc){ $candidates[$j] = $i; $j++; print "adding candidate $i\n"; } my $hash = enumerate(\@candidates); my @files = shuffle(glob("*.bt")); foreach my $f (@files){ open(my $fh,'<',$f); readfile($hash,$fh); close($fh); } our $tiebreak_ready; if(!$tiebreak_ready){ print STDERR "autocompleting tiebreaker with strict random ballot\n"; rand_tiebreak_set(\@candidates,$hash); } if(!defined($schulze)){ my @maj = @{calculate_majorities($hash)}; @maj = sort majsort @maj; print STDERR "majorities list:\n".Dumper(\@maj); my $finish_order = win_order(\@maj,\@candidates); $finish_table = relook($finish_order); print STDERR "Finish order:\n".Dumper($finish_order); } else{ my $distances = getdistance($hash); print STDERR "distances:\n\n".Dumper($distances); my $strongest_path = compute_path($distances); print STDERR "paths:\n".Dumper($strongest_path); $finish_table = relook(shulze_winner($strongest_path)); } our @tiebreak; print STDERR "Tiebreak:\n"; foreach my $t (@tiebreak){ print STDERR "$t\n"; } print "Here is the win order:\n"; for my $i (0 .. $#$finish_table){ print "$finish_table->[$i]\n"; } } Getopt::Long::Configure ("bundling"); our @tiebreak; our $tiebreak_ready = 0; my($autogen,$delete,$schulze,$candidates,$help); GetOptions("autogen|a=i" => \$autogen, "delete|d" => \$delete, "candidates|c=i" =>\$candidates, "help|h" =>\$help, "schulze|s"=>\$schulze) or exit; if(defined($help)||!defined($candidates)){ print "to run a voting simulation with randomly generated ballots:\n"; print "./mam.pl --(autogen|a) number_of_generated_ballots --(candidates|c) number_of_candidates\n\n"; print "--delete option will remove all .bt files in the same directory after computation\n"; print "--schulze|s will use the schulze method to compute the winning order\n\n\n"; print "candidate option is obligatory\n"; exit; } if(defined($autogen)){ make_ballots($autogen,$candidates); } main($candidates,$schulze); if(defined($delete)){ del_ballots; }