[SPOILER] Solution to Perl 'Hard' Quiz of the Week #2005-03-22

Frank Fischer <frank.fischer-JJ2xi2hz/[email protected]> Fri, 25 Mar 2005 11:58:37 +0100
Newsgroups gmane.comp.lang.perl.qotw.discuss
Message-ID <[email protected]>
At first: sorry for my bad english and my bad perl ;)

I solved the problem for the LSB-FSM and I must say it was really a hard
task. The main problem was not to write the generator-function but to 
design the FSM itself. 

My first solution was an FSM with about O(n^2) ("n" is that FSM-number) 
states. It's a straightforward solution from the theory. Then i tried to 
minimize the FSM with a standard minimization-algorithm (i.e. I wrote an 
algorithm replacing equivalent states by a new one). This leads to my 
first real solution. This one is written in ruby because it was too hard 
for me to translate it into good perl. The main problem of this solution 
is that it has a complexity of O(n^4) because the minimization algorithm
has a complexity of O(n^2), too.

But I made an observation leading to my final solution which is pretty
small. The minimized FSM for "n" has alwasy n states. So I tried to find
a way to create the equivalent states and their connections directly
without using the minimization algorithm. 

Although that solution is really nice, it was a long way to get it ;). I
don't know if someone is interested in me explaining my thoughts in a
more detailed way, but this would be a long mail. If so, let me know.

I also created a simple third solution that is an extension to my second
one. This one also works for even n by checking for 2^k prime-factors
first and then do the standard algorithm on the odd rest (in fact, it's
essential for the standard FSM that n is not even).



Greetings

	Frank Fischer



=== first solution: O(n^2)-states with minimization (ruby) ===
# simulate the FSM
def run_fsm(states, n)
  n = n.to_i

  cur = 0
  while n > 0
    cur = states[cur]['next_states'][n & 0x1]
    n >>= 1
  end

  return states[cur]['ret']
end

# create the FSM
def gen_is_divisible_fsm(n)
  n = n.to_i
  # no even numbers till now
  raise "n must not be even" if n % 2 == 0

  states = [] # the states
  states_by_pq = { } # the (p,q) => state-number map
  # create the (p,q)=>number map for all possible values of (p,q)
  for p in 0...n do
    for q in 1...n do
      states_by_pq[[p,q]] = states_by_pq.size
    end
  end

  # now create the arcs for each state from state (p,q)
  # if we read a "0", go to (p,q*2)
  # if we read a "1", got to (p+q,q*2)
  # if we reach an accept-state (0,x), always use (0,1)
  states_by_pq.each_pair do |pq, idx|
    p,q = pq
    next_0 = [p, (q*2)%n]
    next_1 = [(p+q)%n, (q*2)%n]
    new_state = { 'next_states' => [ states_by_pq[next_0], 
	                             states_by_pq[next_1] ],
                  'ret' => p }
    states[idx] = new_state
  end

  # return the states
  return states
end

# minimize the FSM by replacing all equivalent states by one
def minimize(states)
  n_old_states = states.size

  # first we remove unreachable states
  # this is not really necessary for our FSM, but it's good in general
  remove_unreachable!( states ) 
  clear_states!( states ) # clean the states-array

  # the "marks"-map has a value of true for all pairs of states that are not
  # equivalent, false otherwise (i.e. if two states are equivalent,
  # marks[ [a,b] ] == false
  # at the beginning, two states are equivalent if and only if both
  # are accepting states or both are no accepting states
  marks = {}
  for p in 0...states.size do
    if states[p]
      for q in p...states.size do
	if states[q]
	  marks[[p,q]] = (states[p]['ret'] == 0) != (states[q]['ret'] == 0)
	end
      end
    end
  end

  # now we loop as long as we found two states that are not equivalent
  # i.e. as long as we can reduce the number of states
  begin
    new_mark = false
    marks.each_pair do |pair, mark|
      unless mark # only check if the may be equivalent
	s1 = states[pair[0]] 
	s2 = states[pair[1]]
	
	# two states are not equivalent if at least "0" or "1" leads to
	# two not-equivalent states (i.e. marked states)
	if marks[ [s1['next_states'][0], s2['next_states'][0]].sort ] or
           marks[ [s1['next_states'][1], s2['next_states'][1]].sort ] then
	  marks[pair] = true # mark that pair as not equivalent
	  new_mark = true # we reduced the number of stated
	end
      end
    end
  end while new_mark

  # now remove all equivalent states but one (of each equivalence class)
  map = Hash.new { |h,k| k }
  marks.delete_if { |k,v| v }
  marks.keys.sort.each { |pair| map[pair[1]] = map[pair[0]] }

  for idx in 0...states.size
    if states[idx]
      if map[idx] == idx then # repoint the first
	states[idx]['next_states'].map! { |n| map[n] }
      else
	states[idx] = nil # delete the others
      end
    end
  end

  clear_states!( states ) # clean up
  return states, n_old_states-states.size
end

# do a simple depth-first-search to find unreachable states (which are removed)
def remove_unreachable!(states)
  reached = Hash.new(false)

  stack = [0]
  while !stack.empty? do
    idx = stack.pop
    reached[idx] = true
    states[idx]['next_states'].each do |next_state|
      stack.push next_state unless reached[next_state]
    end
  end

  states.each_index do |idx|
    states[idx] = nil unless reached[idx]
  end

  return states 
end

# remove nil-states from the array and fix the numbering
def clear_states!(states)
  i = 0
  map = {}
  states.each_index do |idx|
    s = states[idx]
    unless s.nil?
      map[idx] = i
      i += 1
    end
  end
  states.delete_if { |s| s.nil? }
  states.each do |s|
    s['next_states'].map! { |idx| map[idx] }
  end

  return states
end

# create FSM
m = gen_is_divisible_fsm( ARGV[0] )
# minimize FSM
m, n_removed = minimize( m )
# show it
m.each do |s|
  puts s.inspect
end

puts "States: #{m.size}, removed: #{n_removed}"

# a simple test: check the FSM for some numbers
n = ARGV[0].to_i
for i in 0..10000 
  rest = i%n
  result = run_fsm( m, i )
  if (rest == 0 and result != 0) or (rest != 0 and result == 0)
     raise "Error: #{i}"
  end
end

=== second solution: O(n)-states and really nice ===
use Carp;

sub gen_is_divisible_fsm {
  my $n = $_[0];
  my @states = ();

  croak "Number must not be even\n" if $n % 2 == 0;
  
  for (my $i = 0; $i < $n; $i++) {
    push(@states, { "next_states" => [ ($i*($n+1)/2) % $n, 
	                               (($i+1)*($n+1)/2) % $n ],
	            "ret" => $i });
  }

  return \@states
}

sub run_fsm {
  my ($states, $n) = @_;
  my $cur = 0;
  while ($n > 0) {
    $cur = $$states[$cur]->{next_states}[$n & 0x1];
    $n >>= 1;
  }
  return $$states[$cur]->{ret}
}

my $n = $ARGV[0];
my $m = gen_is_divisible_fsm($n);

print "States: ", $#$m+1, "\n";

for (my $i = 0; $i < 10000; $i++) {
  my $rest = $i % $n;
  my $result = run_fsm( $m, $i );
  if ( ($rest == 0 && $result != 0) or ($rest != 0 && $result == 0) ) {
    print $i, " ", $rest, " ", $result, "\n";
    croak "Invalid FSM\n";
  }
}
  
print "Test passed\n";

=== third solution: n states with even numbers ===
sub gen_is_divisible_fsm {
  my $n = $_[0];
  my @states = ();
  my $epow = 0;

  while ($n > 0 && $n % 2 == 0) {
    $epow++;
    $n >>= 1;
  }

  for (my $i = 0; $i < $epow; $i++) {
    push(@states, { "next_states" => [ $i+1, $epow + $n ],
		    "ret" => 0 });
  }

  for (my $i = 0; $i < $n; $i++) {
    push(@states, { "next_states" => [ ($i*($n+1)/2) % $n + $epow, 
	                               (($i+1)*($n+1)/2) % $n + $epow ],
	            "ret" => $i });
  }

  push(@states, { "next_states" => [ $n + $epow, $n + $epow ], "ret" => 1 })
    if ($epow > 0);

  return \@states
}
==================================================

-- 
eMail: frank.fischer-JJ2xi2hz/[email protected] 
Jabber: lyro-/[email protected]
ICQ: 49470926
freebits.de - Linux, Unix && OpenSource