Skip to content

Commit 82c460f

Browse files
committed
bug fix: compare g on a per-length-l basis. #158
1 parent 890ecc5 commit 82c460f

File tree

1 file changed

+28
-17
lines changed

1 file changed

+28
-17
lines changed

src/scoring.coffee

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ scoring =
4949
# the optimal "minimum guesses" sequence is here defined to be the sequence that
5050
# minimizes the following function:
5151
#
52-
# l! * Product(m.guesses for m in sequence) + D^(l - 1)
52+
# g = l! * Product(m.guesses for m in sequence) + D^(l - 1)
5353
#
5454
# where l is the length of the sequence.
5555
#
@@ -77,6 +77,9 @@ scoring =
7777
matches_by_j = ([] for _ in [0...n])
7878
for m in matches
7979
matches_by_j[m.j].push m
80+
# small detail: for deterministic output, sort each sublist by i.
81+
for lst in matches_by_j
82+
lst.sort (m1, m2) -> m1.i - m2.i
8083

8184
optimal =
8285
# optimal.m[k][l] holds final match in the best length-l match sequence covering the
@@ -85,16 +88,12 @@ scoring =
8588
# a shorter match sequence spanning the same prefix, optimal.m[k][l] is undefined.
8689
m: ({} for _ in [0...n])
8790

88-
# same structure as optimal.m, except holds the product term Prod(m.guesses for m in sequence).
91+
# same structure as optimal.m -- holds the product term Prod(m.guesses for m in sequence).
8992
# optimal.pi allows for fast (non-looping) updates to the minimization function.
9093
pi: ({} for _ in [0...n])
9194

92-
# optimal.g[k] holds the lowest guesses up to k according to the minimization function.
93-
g: (Infinity for _ in [0...n])
94-
95-
# optimal.l[k] holds the length, l, of the optimal sequence covering up to k.
96-
# (this is also the largest key in optimal.m[k] and optimal.pi[k] objects)
97-
l: (0 for _ in [0...n])
95+
# same structure as optimal.m -- holds the overall metric.
96+
g: ({} for _ in [0...n])
9897

9998
# helper: considers whether a length-l sequence ending at match m is better (fewer guesses)
10099
# than previously encountered sequences, updating state if so.
@@ -110,12 +109,16 @@ scoring =
110109
g = @factorial(l) * pi
111110
unless _exclude_additive
112111
g += Math.pow(MIN_GUESSES_BEFORE_GROWING_SEQUENCE, l - 1)
113-
# update state if new best
114-
if g < optimal.g[k]
115-
optimal.g[k] = g
116-
optimal.l[k] = l
117-
optimal.m[k][l] = m
118-
optimal.pi[k][l] = pi
112+
# update state if new best.
113+
# first see if any competing sequences covering this prefix, with l or fewer matches,
114+
# fare better than this sequence. if so, skip it and return.
115+
for competing_l, competing_g of optimal.g[k]
116+
continue if competing_l > l
117+
return if competing_g <= g
118+
# this sequence might be part of the final optimal sequence.
119+
optimal.g[k][l] = g
120+
optimal.m[k][l] = m
121+
optimal.pi[k][l] = pi
119122

120123
# helper: considers whether bruteforce matches ending at position k are optimal.
121124
# three cases to consider...
@@ -151,7 +154,14 @@ scoring =
151154
unwind = (n) =>
152155
optimal_match_sequence = []
153156
k = n - 1
154-
l = optimal.l[k]
157+
# find the final best sequence length and score
158+
l = undefined
159+
g = Infinity
160+
for candidate_l, candidate_g of optimal.g[k]
161+
if candidate_g < g
162+
l = candidate_l
163+
g = candidate_g
164+
155165
while k >= 0
156166
m = optimal.m[k][l]
157167
optimal_match_sequence.unshift m
@@ -169,12 +179,13 @@ scoring =
169179
update(m, 1)
170180
bruteforce_update(k)
171181
optimal_match_sequence = unwind(n)
182+
optimal_l = optimal_match_sequence.length
172183

173184
# corner: empty password
174185
if password.length == 0
175186
guesses = 1
176187
else
177-
guesses = optimal.g[n - 1]
188+
guesses = optimal.g[n - 1][optimal_l]
178189

179190
# final result object
180191
password: password
@@ -210,7 +221,7 @@ scoring =
210221
bruteforce_guesses: (match) ->
211222
guesses = Math.pow BRUTEFORCE_CARDINALITY, match.token.length
212223
# small detail: make bruteforce matches at minimum one guess bigger than smallest allowed
213-
# submatch guesses, such that non-bruteforce submatches over the same [i..j] take precidence.
224+
# submatch guesses, such that non-bruteforce submatches over the same [i..j] take precedence.
214225
min_guesses = if match.token.length == 1
215226
MIN_SUBMATCH_GUESSES_SINGLE_CHAR + 1
216227
else

0 commit comments

Comments
 (0)