赞
踩
拓展:
| 题号 | 题解 | 难度 |
| 本题:正则化匹配 | 状态转移推导+滚动优化+提前结束 | 困难 |
| 剑指offer 19.正则表达式匹配 | 状态转移推导+滚动优化+提前结束 | 困难 |
| 44.通配符匹配 | 状态转移推导+滚动优化+提前结束 | 困难 |
| 72.编辑距离 | 字符串DP解决一众字符匹配问题 | 困难 |
| 115.不同的子序列 | 记忆化搜索、动态规划+滚动数组优化 | 困难 |
| 583.两个字符串的删除操作 | 字符串DP解决一众字符匹配问题 | 困难 |
| 1143.最长公共子序列 | 字符串DP解决一众字符匹配问题 | 困难 |
动态规划,可解决一众字符串匹配问题:
解题思路:动态规划
一. 状态定义
定义 dp[i][j] 表示 s 的前 i个字符和 p的前 j 个字符能否匹配。
二.状态转移
在进行状态转移时,s中的字符是固定不变的,我们考虑p的第j个字符与s的匹配情况:
1. 是一个小写字母a-z,则
必须也为同样的小写字母方能完成匹配:
2. ,则
一定可以与
匹配成功,此时有
3. ,则表示可对
的前一个字符
匹配(或理解为复制)任意次(包括 00 次)。为便于阐述,以下图
,
,
,
和
为例进行说明:

》匹配 0次,意味着和
不起作用,相当于在中删去了
和
,此时有:
》匹配 1 次,意味着和
组成了'a',若
,则
可由
转移而来,此时有:
》匹配 2 次,意味着和
组成了'aa',若
,则
可由
转移而来,此时有:
》匹配 3 次,意味着和
组成了'aaa',若
,则
可由
转移而来,此时有:
.......
》匹配 k次,意味着和
组成了k个'a',若
,则
可由
转移而来,此时有:
上述过程对应的状态更新过程如下图所示:

状态转移的优化:总的来看,当时,对于匹配0~k次,我们有:
同时,对于我们有:
观察发现,(2)式与(1)式 中的后k项刚好相差了一个条件\(\mathbf{{\color{Red} s[i] = p[j-1]}\\),将(2)式代入(1)式可得简化后的 状态转移方程 为:
时,简化后对应的状态更新过程如下图所示:

需要特别注意的是:若,此时
可表示任意字符,显然也就满足
以上状态转移的空间优化与「完全背包问题」的极为相似,感兴趣的读者可参考题目 322. 零钱兑换 以及对应的状态转移公式的优化推导 题解:从0-1背包到完全背包,逐层深入+推导。
总结以上各种情况来看,最终的状态转移方程如下:
由于空间限制,上述判断中未展示的情况。事实上,当
时我们认为
。
从坐标的角度来看,对应的状态更新过程如下图所示:

三.初始化
记s的长度为m,p的长度为n。为便于状态更新,减少对边界的判断,初始二维dp数组维度为(m + 1)*(n + 1),其中第一行和第一列的状态分别表示字符串s和p为空时的情况。
显然dp[0][0] = true。对于其他,当
时,
无法与空字符匹配,因此有
;而当
时,则有
。
以p= "c * a *b"为例,
需要特别注意的是:由于dp数组维度为(m + 1)*(n + 1),在具体的代码实现时,和
才是分别表示 s 和 p 中的第 i 和第 j 个字符。
代码:
1. 二维DP
动态规划的基础代码如下:
- class Solution {
- public:
- bool isMatch(string s, string p) {
- int m = s.size();
- int n = p.size();
- vector< vector<int> > dp(m+1, vector<int>(n+1));
-
- //初始化
- dp[0][0] = true;
- for(int j = 1; j <= n+1; j++){
- if(p[j-1] == '*'){
- dp[0][j] = dp[0][j-2];
- }
- }
-
- //状态更新
- for(int i = 1; i <= m; i++){
- for(int j = 1; j <= n; j++){
- if(s[i-1] == p[j-1] || p[j-1] == '.'){
- dp[i][j] = dp[i-1][j-1];
- }
- else if(p[j-1] == '*'){
- if(s[i-1] != p[j-2] && p[j-2] != '.'){
- dp[i][j] = dp[i][j-2];
- }
- else{
- dp[i][j] = dp[i][j-2] | dp[i-1][j];
- }
- }
- }
- }
- return dp[m][n];
- }
- };

2.一维DP: 动态规划的滚动数组优化
在上面的状态转移方程中,每一行的状态值都只与上一行(正上方)的
和本行的
状态值有关,因此可基于滚动数组的思想对状态空间dp进行优化而省去第一维度。

代码:
- class Solution {
- public:
- bool isMatch(string s, string p) {
- int m = s.size();
- int n = p.size();
- vector<int> dp(n+1);
-
- //初始化
- dp[0] = true;
- for(int j = 1; j <= n; j++){
- if(p[j-1] == '*'){
- dp[j] = dp[j-2];
- }
- }
-
- //状态更新
- for(int i = 1; i <= m; i++){
- vector<int> dp2(n+1);
- for(int j = 1; j <= n; j++){
- if(s[i-1] == p[j-1] || p[j-1] == '.'){
- dp2[j] = dp[j-1];
- }
- else if(p[j-1] == '*'){
- if(s[i-1] != p[j-2] && p[j-2] != '.'){
- dp2[j] = dp2[j-2];
- }
- else{
- dp2[j] = dp2[j-2] | dp[j];
- }
- }
- }
- dp = dp2;
- }
- return dp[n];
- }
- };

3.一维DP:动态规划的滚动数组优化 + 提前结束
注意到,在状态转移过程中,每一行的的状态值都只与上一行(正上方)的
和本行的
状态值有关,因此上一行的
值均为 false,即对于 s 中的某一个字符无法在 p 中得到匹配时,整个 s 字符串也就无法得到匹配,可直接返回 false 而提前终止程序。
- class Solution {
- public:
- bool isMatch(string s, string p) {
- int m = s.size();
- int n = p.size();
- vector<int> dp(n+1);
-
- //初始化
- dp[0] = true;
- for(int j = 1; j <= n; j++){
- if(p[j-1] == '*'){
- dp[j] = dp[j-2];
- }
- }
-
- //状态更新
- for(int i = 1; i <= m; i++){
- vector<int> dp2(n+1);
- int sum = 0;
- for(int j = 1; j <= n; j++){
- if(s[i-1] == p[j-1] || p[j-1] == '.'){
- dp2[j] = dp[j-1];
- }
- else if(p[j-1] == '*'){
- if(s[i-1] != p[j-2] && p[j-2] != '.'){
- dp2[j] = dp2[j-2];
- }
- else{
- dp2[j] = dp2[j-2] | dp[j];
- }
- }
- sum = sum + dp2[j];
- }
- dp = dp2; //滚动数组
- if(sum == 0){ //提前结束
- return false;
- }
- }
- return dp[n];
- }
- };

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。