[dp16_两个数组] 通配符匹配 | 交错字符串 | 两个字符串的最小ASCII删除和
目录
1.通配符匹配
题解
2.交错字符串
题解
3.两个字符串的最小ASCII删除和
1.通配符匹配
链接:44. 通配符匹配
给你一个输入字符串 (s
) 和一个字符模式 (p
) ,请你实现一个支持 '?'
和 '*'
匹配规则的通配符匹配:
'?'
可以匹配任何单个字符。'*'
可以匹配任意字符序列(包括空字符序列)。
判定匹配成功的充要条件是:字符模式必须能够 完全匹配 输入字符串(而不是部分匹配)。
示例 1:
输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:s = "aa", p = "*"
输出:true
解释:'*' 可以匹配任意字符串。
题解
1.状态表示
- 经验 + 题目要求
- dp[i][j] 表示: s[0, i] 区间内的子串能否 被 p[0, j] 区间内的子串匹配
2.状态转移方程
- 根据最后一个位置的状况,分情况讨论
- p[j]是普通字符,如果 s[i]==p[j],并且 s [0, i - 1]能被 p[0, j -1] 匹配上,那 [0, i] 就能被 [0, j] 匹配上
特殊处理
- p[j] == ‘?’ ,'?'可以匹配任何单个字符,因此只用去看s [0, i - 1]能否被 p[0, j -1] 匹配上
关于*号的处理:
- p[j] == ’ * ',( ’ * ’ 可以匹配任意字符序列(包括空字符序列)。)
如果p[j]匹配空串,我们就看dp[i][j-1]
如果p[j]匹配1个字符,我们就看dp[i-1][j-1]
如果p[j]匹配2个字符,我们就看dp[i-2][j-1]
…
- 多种情况之间 是 || 的关系
dp[i][j] 时间复杂度就是O(N^2),在加上p[j] == ’ * ',时间复杂度就是O(N^3)了。
想个办法看看p[j] == ’ * ',能否由若干个有限的状态表示。
优化:
第一种方法:数学
- p[j] == ‘*’,有这么多种情况,只要有一种情况为真就可以了
- 所以我们可以得到下面的等式。发现 j -1是不变的,让等式所有 i 都减1。
- 然后就可以进行替换了。
第二种方法:根据状态表示以及实际情况,优化状态转移方程
- p[j] == ‘*’,找到dp[i][j]。
- 当j位置是 ‘*’,第一种情况还是去匹配空串。
- 第二种情况,去匹配一个字符 ,但是匹配完,不把 j 位置丢掉,继续去看 s[0, i -1] 能否被 p[0, j]匹配
可能会有疑问,为什么’ * ‘就匹配一个,明明可以匹配多个,并且匹配完为什么不把’ * '丢掉。
- 利用 * 号的 传递性
我们这里是对 p[j] == ’ * ’ 好多情况的优化,我们让p[j] == ’ * ‘只匹配一个,但不舍去得到的 dp[i-1][j],也就是说在dp[i-1][j] 这一个状态里面,j位置 依旧是’ * ',它依旧有两种情况,要么去匹配空串,要么i-1匹配上,然后去dp[i-2][j]考虑匹配2个字符的情况。
- 同理在dp[i-3][j]考虑匹配3个字符的情况。
- 也就是说这个 j 会向下传递直到把s[0,i]所有字符匹配。
- 也就是说当匹配一个,不丢弃的话,进入dp[i-1][j],’ * ’ 继续会把 i -1匹配掉。
同理进入dp[i-2][j],’ * ’ 继续会把 i -2匹配掉。所有仅需匹配一个不丢弃就可以把所有情况都考虑掉。
3.初始化
- 引入空串
- 里面的值要保证后序的填表是正确的
- 下标的映射关系
dp表上面多加一行表示s是空串,左边多加一列表示p是空串。接下来看看里面值应该填什么。
- 当 s 是空串,p也是空串,肯定能匹配上,所以第一行第一个格子是true
- 当s 是空串,但是 p 不是空串的话,注意 ’ * ‘ 可以匹配空串。现在就要对p的字符串就行讨论
- 然后初始化第一行后面的位置,如果s是空字符串,p字符串 前面出现’ * ‘ 可以匹配空串,但是 ’ * ‘ 之后出现普通字符了,后面不管有多少个’ * '都不能匹配了。
现在考虑第一列,如果p是空串,s也是空串,肯定能匹配,所以第一列第一个空格还是true。
后续如果 p 是空串,s不是空串,肯定匹配不上,所以第一列后续都是false
下标映射有两种方法:
- dp表多加一行一列整体向右下移动一位,如果要回原表需要 i -1,j -1 才行
- s = ’ ‘ + s,字符串前面加一个辅助字符,这样字符串字符就和dp表一一对应了。
4.填表顺序
- 从上往下填写每一行,每一行从左往右
- 左上角 开始
5.返回值
- dp[i][j] 表示: s[0, i] 区间内的子串能否 被 p[0, j] 区间内的子串匹配
- 题目要求是整个字符串,因此返回dp[m][n],m是s的长度,n是p的长度
class Solution {
public:bool isMatch(string s, string p) {int m=s.size(),n=p.size();vector<vector<bool>> dp(m+1,vector<bool>(n+1,false));dp[0][0]=true; //都为空串时//!!!初始化字符串s=' '+s;p=' '+p;for(int i=1;i<=n;i++){if(p[i]!='*'){dp[0][i]=false;}else dp[0][i]=dp[0][i-1];}for(int i=1;i<=m;i++){for(int j=1;j<=n;j++){if(p[j]>='a' && p[j]<='z'){if(s[i]==p[j] && dp[i-1][j-1])dp[i][j]=true;}if(p[j]=='?' && dp[i-1][j-1])dp[i][j]=true;if(p[j]=='*')//!!!dp[i][j]=dp[i][j-1] || dp[i-1][j];//空串 或者 抵消一个//填表的巧妙设计}}return dp[m][n];}
};
注意
一定要记得↓
2.交错字符串
链接:97. 交错字符串
给定三个字符串 s1
、s2
、s3
,请你帮忙验证 s3
是否是由 s1
和 s2
交错 组成的。
两个字符串 s
和 t
交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:
s = s1 + s2 + ... + sn
t = t1 + t2 + ... + tm
|n - m| <= 1
- 交错 是
s1 + t1 + s2 + t2 + s3 + t3 + ...
或者t1 + s1 + t2 + s2 + t3 + s3 + ...
注意:a + b
意味着字符串 a
和 b
连接。
示例 1:
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true
示例 2:
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false
示例 3:
输入:s1 = "", s2 = "", s3 = ""
输出:true
单看叙述很难理解,接下来我们借用例子理解一下。
- 这道题的意思是我们可以把字符串拆成若干份,然后把若干份拼接成s3。
- 比如上面先把s1拆分为aa,s2拆分成dbbc,在把s1剩余拆分为bc,在把s2剩余拆分a,在把s1剩余拆分为c。就能拼接成s3.
注意空串是有意义的。
题解
- 之前做的字符串问题最多也就是两个字符串,但这道题出现了三个字符串,分别是s1、s2、s3。
- 那之前的经验还能用吗?我们之前的经验是在两个字符串里面各选一段区间作为研究对象来研究问题。
这三个字符串怎么办呢?其实我们依旧可以这个经验。
我们选取s1、s2区间之后我们想把两个区间拼接形成s3。
- 我们其实已经知道s1字符串长度,s2字符串长度了,那s3字符串长度其实也就固定了。
- 也就是说当我们选取s1区间、s2区间,s3区间就已经被确定了。
- 因此也可以用之前的经验 + 题目要求,来定义状态表示。
这里我们预处理一下(s=' '+s),让字符串下标从1开始,这样等下下标对应就会非常简单。
1.状态表示
经验 + 题目要求
dp[i][j] 表示:s1中 [1,i] 区间内字符串以及 s2中 [1,j] 区间内字符串,能否拼接凑成 s3 [1,i+j]区间内的字符串
2. 状态转移方程
- 根据最后一个位置的状况,分情况讨论
- 最后一个位置我们来看看这个s3,如果s1、s2能够交错拼成s3,那s3最后 i+j 位置元素, 要么等于 s1 最后 i 位置元素,要么等于 s2 最后 j 位置元素。要么都不相等。如果都不相等那就不用管了,创建dp时就已经处理了 。我们只研究能匹配的情况。
- 如果s1[i] == s3[i+j],我们还需要去看 s1[1,i-1] 和 s2[1,j] 区间能否拼接s3[1, i+j-1],正好是dp[i-1][j]
如果s2[j] == s3[i+j],和上面一样
- 其实对于S3,就是要么选择S1要么选择S2
- 动态规划 实现选不选
3.初始化
- 引入空串
- 里面的值要保证后序的填表是正确的
- 下标的映射关系
dp表上面多加一行表示s1是空串,左边多加一列表示s2是空串。接下来看看里面值应该填什么。
s1是空串,s2是空串,i+j = 0,s3也是空串。空串和空串是可以拼接凑成空串的。所以第一行第一个位置填true
- s1是空串,s2不是空串,s3也不是空串,如果s2字符串能够对应上s3,填ture,如果有一个字符不能对应,后面都填false
s2是空串,s1不是空串,s3也不是空串,同理如果s1字符串能够对应上s3,填ture,如果有一个字符不能对应,后面都填false
4.填表顺序
- 从上往下填写每一行,每一行从左往右
- 从左上 开始填
5.返回值
- dp[m][n]
class Solution {
public:bool isInterleave(string s1, string s2, string s3) {int m=s1.size(),n=s2.size();if(m+n!=s3.size()) return false;
//预处理s1=' '+s1;s2=' '+s2;s3=' '+s3;vector<vector<bool>> dp(m+1,vector<bool>(n+1,false));dp[0][0]=true;//空串
//初始化//s1 空for(int k=1;k<=n;k++){if(s2[k]!=s3[k])dp[0][k]=false;elsedp[0][k]=dp[0][k-1];//!! 否则的话就继承前值}//s2 空 和s1比对for(int k=1;k<=m;k++){if(s1[k]!=s3[k])dp[k][0]=false;elsedp[k][0]=dp[k-1][0];}for(int i=1;i<=m;i++){for(int j=1;j<=n;j++){if(s1[i]==s3[i+j] && dp[i-1][j])dp[i][j]=true;if(s2[j]==s3[i+j] && dp[i][j-1])dp[i][j]=true;}}return dp[m][n];}
};
一定要加else才能生效,否则就会继承前值,一直为 true
3.两个字符串的最小ASCII删除和
链接: 712. 两个字符串的最小ASCII删除和
给定两个字符串s1
和 s2
,返回 使两个字符串相等所需删除字符的 ASCII 值的最小和 。
示例 1:
输入: s1 = "sea", s2 = "eat"
输出: 231
解释: 在 "sea" 中删除 "s" 并将 "s" 的值(115)加入总和。
在 "eat" 中删除 "t" 并将 116 加入总和。
结束时,两个字符串相等,115 + 116 = 231 就是符合条件的最小和。
示例 2:
输入: s1 = "delete", s2 = "leet"
输出: 403
解释: 在 "delete" 中删除 "dee" 字符串变成 "let",
将 100[d]+101[e]+101[e] 加入总和。在 "leet" 中删除 "e" 将 101[e] 加入总和。
结束时,两个字符串都等于 "let",结果即为 100+101+101+101 = 403 。
如果改为将两个字符串转换为 "lee" 或 "eet",我们会得到 433 或 417 的结果,比答案更大。
题解
返回 使两个字符串相等 所需删除字符的 ASCII 值的最小和。
- 在正式解决这道题之前,我们先看一下示例2,第二种删法Ascll码和最小,所以选择第二种。
- 然后看一下删完之后留下来的字符串。是不是第二种删法留下来的字符串和是最大的。
那现在能不能把上面问题转化一下,你让我找删除的最小和,是不是就是变相去找这两个字符串公共子序列里面Ascll码最大的那个。
- 这两个字符串Ascll码总和是不变的,你让我找最小
- 其实就是变相去找公共子序列里面Ascll码和最大的。
正难则反:找最小删除,其实就是保留剩余最大
就又回归到了 保留最大子序列的问题~
1.状态表示
- 经验 + 题目要求
- dp[i][j] 表示:s1 的 [0, i] 区间以及 s2 的 [0, j] 区间内的所有子序列里,公共子序列的 ASCll 最大和
2.状态转移方程
根据最后一个位置的状况,分情况讨论
- 我们要找公共子序列,是不是要从s1拿出来一个子序列,再从s2拿出来一个子序列
- 判断它们是不是公共子序列
- 然后把它们Ascll码求一下。然后继续在s1里拿一个,s2里拿一个。。。
- 把所有情况都找到然后求里面的最大值。
所有从s1拿一个子序列,在从s2那一个子序列有如下情况
- 有s1[i],有s2[j]
- 有s1[i],没有s2[j]
- 没有s1[i],有s2[j]
- 没有s1[i],没有s2[j]
然后求出这四种情况的最大值。
有s1[i],有s2[j],子序列是以它们俩为结尾的。
- 如果是公共子序列是不是这里两个位置要相等啊 s1[i] == s2[j],然后去s1[0, i - 1],s2 [0, j - 1]
- 找公共子序列Ascll码最大值就是dp[i-1][j-1],然后加上s1[i]的Ascll码或者s2[j]的Ascll码因为它俩Ascll码是一样的。
有s1[i],没有s2[j]
- 可能会直接想用 dp[i][j - 1] 公共子序列最大Ascll码和。
- 但是要注意一下这个状态表示, 表示的是[0, i] 区间内所有的子序列,[0, i]既有s[i]也可以没有s[i],也就是说 情况2包含情况4。
- 所以 s1[i],没有s2[j] 不能直接和 dp[i][j - 1] 划等号。但是没关系,我们求得是一个最大和。管dp[i][j - 1]里面包不包含情况4,你只要包含情况2就可以了。因为我们最后求得是最大值,所有这里可以用dp[i][j - 1]。
同理情况3也是这样。
没有s1[i],没有s2[j],就是dp[i-1][j-1],
- 考虑变化的情况,每一步都取最大
3.初始化
- 引入空串(方便填表)
- 里面的值要保证后序的填表是正确的
- 下标的映射关系 s=' '+s
4.填表顺序
- 从上往下填写每一行,每一行从左往右
- 左上角
5.返回值 s
状态表示求的是两个字符串的区间里面公共子序列的Ascll 最大和
- 题目要求两个字符串相等所需删除字符的 ASCII 值的最小和 。所以可以这样做
- 找到两个字符串中公共子序列的Ascll 最大和,dp[m][n]
- 统计两个字符串中Ascll和 -> sum
- sum - dp[m][n] * 2