public static Result MostGuessableMatchSequence(string password, IEnumerable <Match> matches, bool _exclude_additive = false) { int n = password.Length; List <List <Match> > matchesByJ = new List <List <Match> >(); foreach (int i in EnumerableUtility.Range(0, n)) { matchesByJ.Add(new List <Match>()); } foreach (Match m in matches) { matchesByJ[m.j].Add(m); } foreach (List <Match> lst in matchesByJ) { lst.Sort(delegate(Match m1, Match m2) { return(m1.i - m2.i); }); } Optimal optimal = new Optimal(n); /* # helper: considers whether a length-l sequence ending at match m is better (fewer guesses) # than previously encountered sequences, updating state if so. */ void update(Match m, int l) { // helper: considers whether a length-l sequence ending at match m is better (fewer guesses) // than previously encountered sequences, updating state if so. int k = m.j; double pi = EstimateGuesses(m, password); if (l > 1) { /* # we're considering a length-l sequence ending with match m: # obtain the product term in the minimization function by multiplying m's guesses # by the product of the length-(l-1) sequence ending just before m, at m.i - 1. */ pi *= optimal.pi[m.i - 1][l - 1]; } // calculate the minimization func double g = factorial(l) * pi; if (!_exclude_additive) { g += Math.Pow(MIN_GUESSES_BEFORE_GROWING_SEQUENCE, l - 1); } // update state if new best // first see if any competing sequences covering this prefix, with l or fewer matches, // fare better than this sequence. if so, skip it and return foreach (KeyValuePair <int, double> val in optimal.g[k]) { int competingL = val.Key; double competingG = val.Value; if (competingL > l) { continue; } if (competingG <= g) { return; } } optimal.g[k][l] = g; optimal.m[k][l] = m; optimal.pi[k][l] = pi; } // helper: make bruteforce match objects spanning i to j, inclusive. Match makeBruteForceMatch(int i, int j) { Match m = new Match(); m.Token = password.Substring(i, j - i + 1); m.Pattern = "bruteforce"; m.i = i; m.j = j; return(m); } // helper: evaluate bruteforce matches ending at k. void bruteForceUpdate(int k) { // see eif a single bruteforce match spanning the k-prefix is optimal Match m = makeBruteForceMatch(0, k); update(m, 1); foreach (int i in EnumerableUtility.Range(1, k + 1)) { // generate k bruteforce matches, spanning from (i=1, j=k) up to (i=k, j=k) // see if adding these new matches to any of the sequences in optimal[i-1] // leads to new bests m = makeBruteForceMatch(i, k); foreach (KeyValuePair <int, Match> val in optimal.m[i - 1]) { int l = val.Key; // corner: an optimal sequence will never have two adjacent bruteforce matches // it is strictly better to have a single bruteforce match spanning the same region: // same contribution to the guess product with a lower length // --> safe to skip those cases if (val.Value.Pattern != null && val.Value.Pattern.Equals("bruteforce")) { continue; } // try adding m to this length-l sequence update(m, l + 1); } } } // helper: step backwards through optimal.m starting at the end, // constructing the final optimal match sequence List <Match> unwind(int nN) { List <Match> optimalMatchSequenceList = new List <Match>(); int k = nN - 1; // find the final best seqence length and score int l = 1; double g = Double.MaxValue; foreach (KeyValuePair <int, double> val in optimal.g[k]) { int candidateL = val.Key; double candidateG = val.Value; if (candidateG < g) { l = candidateL; g = candidateG; } } while (k >= 0) { Match m = optimal.m[k][l]; optimalMatchSequenceList.Insert(0, m); k = m.i - 1; l--; } return(optimalMatchSequenceList); } foreach (int k in EnumerableUtility.Range(0, n)) { foreach (Match m in matchesByJ[k]) { if (m.i > 0) { foreach (int l in optimal.m[m.i - 1].Keys) { update(m, l + 1); } } else { update(m, 1); } } bruteForceUpdate(k); } List <Match> optimalMatchSequence = unwind(n); int optimalL = optimalMatchSequence.Count; double guesses = (password.Length == 0) ? 1 : optimal.g[n - 1][optimalL]; double seconds = guesses / Math.Pow(10, 4); double crackTime = PasswordScoring.GuessesToCrackTime(guesses); // UnityEngine.Debug.Log($"Seconds: {seconds}"); // Debug.Log($"Score: {PasswordScoring.GuessesToScore(guesses)}"); int score = PasswordScoring.GuessesToScore(guesses); return(new Result { Password = password, Guesses = guesses, GuessesLog10 = Math.Log10(guesses), MatchSequence = optimalMatchSequence, CrackTime = Math.Round(crackTime, 3), CrackTimeDisplay = Utility.DisplayTime(crackTime), Score = score }); }