7/2, 字符串类
字符串 DP 类的很多典型问题,用这一张图都能说明白:

Longest Common Subsequence (CLRS)
这个版本的做法非常适合存储和输出 optimal path,也可以应用到 Longest Increasing Subsequence 中,用于 follow-up 情况下输出 sequence.
比如输入数组为 X = [x1, x2 .. xn],其排序后的数组为 X' = [x'1, x'2 .. x'n]
最大值肯定在 dp[][] 的最右下角。
核心思想是,既然有且只有 1 个位置 mismatch,我们可以直接在找到 mismatch 位置上判断:
在字符串 DP 中,原始字符串上的 substring,等价于原始区间内的 subproblem.
关于这个问题最好的 slides, by Stanford
然而相同的是,这道题思考并寻找 optimal substructure 的思路是非常接近的,都是直接从最终答案 String Z 的结构 top-down 往回看。因为对于每个 string,我们对其任意操作都会构造出来一个只与它自己相差一个字符的新 string.
于是;
来自 Stanford 课件,bottom - up 的 String 构造法。我们定义 D(i, j - 1) 为 insertion 是因为 i 是外循环位置,j 是内循环位置;因此当 i 固定,相对于 j 取了前一个位置 + 新字符的时候是 j 的 insertion; 而 D(i - 1, j) 代表 j 在新循环中并没有增加长度,反而从 i 里删除了。
考虑到所有操作都在双循环内完成,调换循环位置并不会影响算法正确性,但是会赋予每次操作相反的意义,即调换等价互补操作 "S + 1" 和 "T - 1".
其实和算法导论的图是一个意思,方向不同而已。
如果这题每种操作附带了 cost ,要求最小的 cost 怎么办?
不难看出,add 和 delete 作为互补操作,其 cost 是一样的;即使不一样,我们也可以在两个 String 的构造过程中,总是选择更小的那个。
(Google, Snapchat面经)
和 Edit distance 非常像,考虑到 add/delete 在构造 string 上的等价性质,问题的 optimal substructure 即为
注意这题不支持 replace,如果支持的话,dp[i][j] 还要看一个新状态,dp[i + 1][j - 1]

s[i] , p[j] 为 s , p 的第 i / j 个字符,略去 off-by-one 的 index 问题
最后要注意一下 dp[0][0], dp[0][i] 的初始化。

以这两道题看,dp[i - 1][j] 都代表着 “以 p 当前 * 字符,匹配 s 的[1,n]长度字符”
这题dp[0][i]初始化和 Wildcard 不太一样,因为会有 c*a*b 这种情况,多个星号跳着出现,不要立刻 break 掉,而要扫到底,dp[0][i] 要看 dp[0][i - 2];
optimal substructure 是,如果 [0, j] 可以 break 并且 [j + i , i] 在字典里,那么 [0 , j] 就可以 break.
这题有简单暴力的 dfs + backtracking,然而为了优化计算时间,也要利用备忘录法和记忆化搜索。
然而单独靠以上两种优化都还不够。。
事实证明在这题里,直接用 HashSet + substring 检查字典,要比预处理计算 boolean[][] 快。 (8ms vs. 22ms)
O(len(wordDict) ^ len(s / minWordLenInDict))
Because there're len(wordDict) possibilities for each cut,同时空间占用可能会比较大。
首先从题目结构来看,和这章的前几道字符串 DP 非常相似,都是一个字符串 “构造问题”,即用小的 substring 通过生长和拼接,构造出更大的目标 string. 一般这类问题有天然的 bottom-up 思路,当然,top-bottom 的递归思路也是完全可行的。
简单观察之后发现正确答案的限制条件: k = i + j,换句话说,k 可以用 i + j 表示出来,因此不需要以 k 作为单独维度遍历。
同时我们也要处理好 i = 0 和 j = 0 的初始化条件。
时间空间复杂度 O(s1.len * s2.len)
Last updated