--Apple-Mail-2-25062225
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=US-ASCII;
delsp=yes;
format=flowed
I apologize for the 11th hour solution; this week was busy. It took
me a while to come up with an algorithm that would actually produce
fewer keystrokes than the naive just write-out-every-code-so-long-as-
it-isn't-already-tested solution in every case. After trying a few
(I believe 3 is the number) poor solutions, I was getting frustrated,
so I figured I'd just try to work through it backwards. This worked
out rather well, I think (it beats the naive solution by over 70,000
strokes on the 5-digit, 3-stop case). It was pretty fun, too: I
actually had a reason to use lambda and shift (which I've never used
before), as well as inject (which I've only used once before).
Of course, it takes a while to run (the 5 digit, 3 stop code case
took nearly 40 minutes on a dual 2.7GHz PowerMac G5), so I'm
providing the output as well as the code.
So, here're my results:
user system total real
One stop digit:
2 digits 0.010000 0.000000 0.010000 ( 0.006755)
Search space exhausted.
Tested 100 of 100 codes in 271 keystrokes.
0 codes were tested more than once.
================================================== ==========
3 digits 0.110000 0.000000 0.110000 ( 0.132552)
Search space exhausted.
Tested 1000 of 1000 codes in 3439 keystrokes.
0 codes were tested more than once.
================================================== ==========
4 digits 9.580000 0.260000 9.840000 ( 11.005426)
Search space exhausted.
Tested 10000 of 10000 codes in 40951 keystrokes.
0 codes were tested more than once.
================================================== ==========
5 digits 1208.900000 23.850000 1232.750000 (1439.590285)
Search space exhausted.
Tested 100000 of 100000 codes in 468682 keystrokes.
46 codes were tested more than once.
================================================== ==========
Three stop digits:
2 digits 0.010000 0.000000 0.010000 ( 0.005834)
Search space exhausted.
Tested 100 of 100 codes in 219 keystrokes.
0 codes were tested more than once.
================================================== ==========
3 digits 0.170000 0.000000 0.170000 ( 0.196276)
Search space exhausted.
Tested 1000 of 1000 codes in 2545 keystrokes.
4 codes were tested more than once.
================================================== ==========
4 digits 16.580000 0.300000 16.880000 ( 19.149404)
Search space exhausted.
Tested 10000 of 10000 codes in 28105 keystrokes.
96 codes were tested more than once.
================================================== ==========
5 digits 1910.240000 27.430000 1937.670000 (2240.60393

Search space exhausted.
Tested 100000 of 100000 codes in 299559 keystrokes.
1517 codes were tested more than once.
================================================== ==========
My code is attached, along with alarm_keypad.rb (which contains the
AlarmKeypad class).
Tim
--Apple-Mail-2-25062225
Content-Transfer-Encoding: 7bit
Content-Type: text/x-ruby-script;
x-unix-mode=0755;
x-mac-creator=454D4178;
name="mirror_bne.rb"
Content-Disposition: attachment;
filename=mirror_bne.rb
#!/usr/bin/env ruby -w
require 'alarm_keypad'
require 'benchmark'
def crack_code (code_length, stop_digits)
# This is the weak part of the algorithm (in the case of there being more than 1 stop digit)
# Unfortunately, I don't have the time right now to write something better than a random
# selection.
stop_digit = lambda { stop_digits[rand(stop_digits.size)].to_s }
pad = AlarmKeypad.new code_length, stop_digits
# Work with codes with lots of stop digits in them first.
all_codes = ("0"*code_length.."9"*code_length).to_a.sort_by{|c | num_of_stop_digs c, stop_digits }.reverse
# This algorithm works backwards, so we start with a stop digit, followed by a code.
# You may notice that this doesn't reverse the codes themselves, which means that
# the code that will actually get input into the pad is the reverse of the code used.
solution = stop_digits[0].to_s + all_codes.shift
while !all_codes.empty?
match = /[#{stop_digits.join}](.{0,#{code_length-1}})$/.match(solution[-code_length..-1])
string = (match ? match[1] : nil)
if match.nil?
solution << stop_digit.call + all_codes.shift
elsif string.nil? || string.empty?
solution << all_codes.shift
else
overlap = match ? string.size : 0
code = all_codes.find { |c| c.index(string) == 0 }
if code.nil?
code = all_codes.shift
solution << stop_digit.call + code
else
all_codes.delete code
solution << code[overlap..-1]
end
end
end
solution.reverse.split(//).each{ |d| pad.press d.to_i }
pad
end
def num_of_stop_digs (code, stop_digits)
stop_digits.inject(0) { |num_stop_digs, dig| num_stop_digs + code.count(dig.to_s) }
end
if __FILE__ == $PROGRAM_NAME
include Benchmark
bm(10) do |x|
hacked_pad = nil
puts "One stop digit:\n"
puts
for i in (2..5)
x.report("#{i} digits") { hacked_pad = crack_code(i, [1]) }
hacked_pad.summarize
puts "="*60
end
puts
puts "Three stop digits:"
puts
for i in (2..5)
x.report("#{i} digits") { hacked_pad = crack_code(i, [1,2,3]) }
hacked_pad.summarize
puts "="*60
end
end
end
--Apple-Mail-2-25062225
Content-Transfer-Encoding: 7bit
Content-Type: text/x-ruby-script;
x-unix-mode=0644;
x-mac-creator=454D4178;
name="alarm_keypad.rb"
Content-Disposition: attachment;
filename=alarm_keypad.rb
#!/usr/bin/env ruby -w
#
# Stephen Waits <>
#
class AlarmKeypad
# init a keypad, with length of security code, and the code's
# stop digits
def initialize(code_length = 4, stop_digits = [1,2,3])
# remember the length of the security code
@code_length = code_length
# and which digits cause a code to be checked
@stop_digits = stop_digits
# and reset our data structures to 0
clear
end
# reset keypad to initial state
def clear
# an array of each code and how many times it's been entered
@codes = Array.new(10**@code_length,0)
# last N+1 keypad button presses
@key_history = []
# total number of keypad button presses
@key_presses = 0
end
# press a single digit
def press(digit)
# add digit to key history
@key_history.shift while @key_history.size > @code_length
@key_history << digit
@key_presses += 1
# see if we just tested a code
if @stop_digits.include?(@key_history.last) and
@key_history.length > @code_length
@codes[@key_history[0,@code_length].join.to_i] += 1
end
end
# find out if every code had been tested
def fully_tested?
not @codes.include?(0)
end
# find out if an individual code has been tested
# NOTE: an actual keypad obviously doesn't offer this functionality;
# but, it's useful and convenient (and might save duplication)
def tested?(code)
@codes[code] > 0
end
# output a summary
def summarize
tested = @codes.select { |c| c > 0 }.size
tested_multiple = @codes.select { |c| c > 1 }.size
puts "Search space exhausted." if fully_tested?
puts "Tested #{tested} of #{@codes.size} codes " +
"in #{@key_presses} keystrokes."
puts "#{tested_multiple} codes were tested more than once."
end
end
if $0 == __FILE__
hacked_pads = []
for i in (1..5)
a = AlarmKeypad.new(i, [1, 2, 3])
("0"*i.."9"*i).each do |c|
next if a.tested?(c.to_i)
c.split(//).each { |d| a.press(d.to_i) }
a.press(rand(3) + 1)
end
a.summarize
end
for i in (1..5)
a = AlarmKeypad.new(i, [1])
("0"*i.."9"*i).each do |c|
next if a.tested?(c.to_i)
c.split(//).each { |d| a.press(d.to_i) }
a.press(1)
end
a.summarize
end
end
--Apple-Mail-2-25062225--