LC 1977. 划分数字的方案数
https://leetcode-cn.com/problems/number-of-ways-to-separate-numbers/
这题目有两个DP需要解决,第一个DP就是解决字面上的问题,第二个DP则是需要在实现中细化得到的。
状态方程是 `dp[i][j]` 表示结尾为 `s[i:j+1]` 的组合数,其中
- dp[i][j] += dp[i][j-1] 这个比较好理解
- p=2*i-j, 如果 num[p:i] > num[i:j]. 考虑上个状态中等长字符串的情况,那么这次需要考虑 dp[p][i-1]
- p=2*i-j, 如果 num[p-1:i] > num[i:j+1]. 考虑这个状态中等长字符串的情况,那么这次需要考虑 dp[p-1][i-1]
class Solution: def numberOfCombinations(self, num: str) -> int: if not num or num[0] == '0': return 0 MOD = 10 ** 9 + 7 n = len(num) dp = [[0] * n for _ in range(n)] for i in range(n): dp[0][i] = 1 for i in range(1, n): if num[i] == '0': continue for j in range(i, n): if (j - 1) >= i: dp[i][j] += dp[i][j - 1] # check some cases not counted before. # num[p..i-1] and num[i..j-1] p = 2 * i - j if p >= 0 and num[p] != '0': if num[p:i] > num[i:j]: dp[i][j] += dp[p][i - 1] p = 2 * i - j # num[p-1..i-1] and num[i..j] if p >= 1 and num[p - 1] != '0': if num[p - 1:i] <= num[i:j + 1]: dp[i][j] += dp[p - 1][i - 1] dp[i][j] %= MOD ans = 0 for i in range(n): # print(dp[i][n - 1]) ans += dp[i][n - 1] ans = ans % MOD return ans
但是实测的时候就会发现, `num[p:i] > num[i:j]` 这个字符串比较代价太高了,而且测试集合中的超长case都是相同的字符,所以一个办法就是预先计算好子字符串的hash。先比较hash是否相同,这样可以节省不少开销。
hash = [[0] * n for _ in range(n)] for i in range(n): acc = 0 for j in range(i, n): x = ord(num[j]) - ord('0') acc = acc * 11 + x acc = acc % MOD2 hash[i][j] = acc
不过这题还有个更好的解法,就是针对两个子字符串,我们可以预先计算LCP(longest common prefix). 可以首先计算出 s[i..] 和 s[j..] 的最大公共长度 d, 之后比较 s[i:k] 和 s[j:k] 的话可以直接比较 s[i+d:k] 和 s[j+d:k]. 虽然这里写的是子串,但是第一个字符肯定是不同的,所以比较也会非常迅速。
lcp = [[0] * n for _ in range(n)] for i in reversed(range(n)): for j in reversed(range(n)): if num[i] == num[j]: if (i + 1) < n and (j + 1) < n: lcp[i][j] = lcp[i + 1][j + 1] else: lcp[i][j] = 0 lcp[i][j] += 1 ### p = 2 * i - j d = lcp[p][i] if p >= 0 and num[p] != '0' and num[p + d:i] > num[i + d:j]: dp[i][j] += dp[p][i - 1] ### p = 2 * i - j d = lcp[p - 1][i] # num[p-1..i-1] and num[i..j] if p >= 1 and num[p - 1] != '0' and num[p - 1 + d:i] <= num[i + d:j + 1]: dp[i][j] += dp[p - 1][i - 1]