@@ -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