Longest repeating and non-overlapping substring
Given a string, find the length of the longest substring that occurs at least twice within it, where the two occurrences do not overlap each other in position.
Note. Listed under DP on Trees in the course's source list, but the recurrence compares two positions within one string, making it a stringPair2d problem rather than a tree DP.
Do this lesson first: longest common subsequenceExample input
s = "abababab"
Expected output
4
Break it down
Answer each question out loud before you open it. Getting it wrong here is the useful part. A revealed answer you never guessed at teaches you nothing.
Fill the table
The table pauses before each cell you have to supply. Type the value the recurrence gives, and the write animation confirms it.
Press start. The animation stops at every cell YOUR recurrence must fill.
Two kinds of base, both 0. Row 0, across columns 0 to n: the earlier prefix is empty, so it has no letter to end a run on. The main diagonal, dp[i][i] for i from 1 to n: a letter set against itself, where the gap j - i is 0, so the cap leaves no room for a run of even one letter. At the drive size that is 9 bases in row 0 and 8 on the diagonal, 17 in all. The cells below the diagonal are left out, which is why column 0 holds only dp[0][0] and row n holds only dp[n][n]. No computed cell reads a diagonal base: a cell (i, j) with j > i reads at most dp[i - 1][j - 1], whose column is still ahead of its row, so that cell sits in row 0 or above the diagonal. The diagonal bases are in the table to say what the pair (i, i) is, not to feed anything. Row 0 is read in only a few places: dp[0][j] is read only by dp[1][j + 1], and only when letter 1 matches letter j + 1, which at the drive size means dp[0][2], dp[0][4] and dp[0][6]. A row-major sweep, left to right within each row, finishes the previous row before it starts the next, so the diagonal neighbour is final whenever a match needs it, and a mismatch needs nothing. As on longest-common-substring, the answer is the largest cell anywhere, not the last one; here the last cell, dp[n][n], is a diagonal base and holds 0 at every size.
Cols →
Rows ↓s, the first n letters of abababab: the earlier copy ends at letter i
Cols →s again: the later copy ends at letter j
- 1
function longestRepeatingNonOverlapping(s) { - 2
const n = s.length; - 3
const dp = Array.from({ length: n + 1 }, () => new Array(n + 1).fill(0)); - 4
let best = 0; - 5
for (let i = 1; i <= n; i++) { - 6
for (let j = i + 1; j <= n; j++) { - 7
dp[i][j] = s[i - 1] === s[j - 1] - 8
? Math.min(dp[i - 1][j - 1] + 1, j - i) - 9
: 0; - 10
best = Math.max(best, dp[i][j]); - 11
} - 12
} - 13
return best; - 14
}
The code, the trap, the variations
- 1
function longestRepeatingNonOverlapping(s) { - 2
const n = s.length; - 3
const dp = Array.from({ length: n + 1 }, () => new Array(n + 1).fill(0)); - 4
let best = 0; - 5
for (let i = 1; i <= n; i++) { - 6
for (let j = i + 1; j <= n; j++) { - 7
dp[i][j] = s[i - 1] === s[j - 1] - 8
? Math.min(dp[i - 1][j - 1] + 1, j - i) - 9
: 0; - 10
best = Math.max(best, dp[i][j]); - 11
} - 12
} - 13
return best; - 14
}
Where people go wrong
Dropping the cap: writing dp[i - 1][j - 1] + 1 on a match, as longest-common-substring does. That table measures the longest substring that occurs twice with overlap allowed, and on this string overlapping repeats are common: it returns 2, 3, 4, 5, 6 across lengths 4 to 8 against the correct 2, 2, 2, 3, 4, right only at length 4. At length 8 it returns 6, from dp[6][8], for ababab at letters 1 to 6 and again at letters 3 to 8, two copies that share letters 3 to 6. The cap and the j > i bound each keep a letter from counting as a repeat of itself, and either one alone is enough: with the cap, dp[i][i] = min(dp[i - 1][i - 1] + 1, 0) = 0, and with the bound the main diagonal is never computed. Drop both, letting j start at i rather than i + 1 with no cap, and the main diagonal climbs dp[i][i] = i, reporting the whole string, 8 letters at length 8, as its own repeat. The third trap is longest-common-subsequence's habit of returning the last cell; here that is dp[n][n], a diagonal base, which gives 0 at every length.
Write 0 instead of capping when the extended run would overlap, a form common in published solutions: dp[i][j] = dp[i - 1][j - 1] + 1 if the letters match and dp[i - 1][j - 1] < j - i, else 0.
Only the match branch changes; the bases, the left-out lower triangle, the single diagonal read and the row-major sweep stay as they are. Individual cells do come out differently. At the drive size three of them do: along the gap-2 diagonal this page writes 1, 2, 2, 2, 2, 2 where the reset form writes 1, 2, 0, 1, 2, 0, counting up to the gap, dropping to 0 rather than going past it, and starting over. The answer does not change: measured at each slider length, both forms give 2, 2, 2, 3, 4. That is not luck of this string. Both tables hold 0 on the cell just before a run of matching pairs starts, a mismatch or a row-0 base. From there the reset form never writes more than this page does: it extends only while its diagonal neighbour is below the gap, so what it writes stays within the gap, and that neighbour is no larger than this page's, so it stays within this page's extended run as well. On a run of matching pairs it counts 1, 2, 3 and so on until it reaches the gap or the run ends, which is exactly the largest value this page writes on that run. So each diagonal's largest cell is the same number in both tables.
Return the repeated substring itself, not just its length.
The table, its recurrence and its bases do not change; the running best also records the cell (i, j) where it was found. A run is contiguous, so its end and its length pin it down: the substring is letters i - best + 1 to i, and its second copy is letters j - best + 1 to j. At the drive size that is dp[4][8] = 4, abab at letters 1 to 4 and 5 to 8. A tie needs a rule, and this string produces ties: at length 6 four cells hold the maximum, 2, namely dp[2][4], dp[2][6], dp[3][5] and dp[4][6]. Keeping the first one in row-major order, as this page's answer cell does, reports ab at letters 1 to 2 and 3 to 4; dp[3][5] would report ba, at letters 2 to 3 and 4 to 5.