给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
(这题也是字节提前批的一道面试题)刚开始想的是求两个字符串的最长公共子序列,这些是不会变的,然后用较长的一个去减,就是需要改动的最少操作次数,这个方法在想的时候就注意到会有错位的问题,但是没思考好,比如 a**b和*a*b,我的答案是4-2=2,但是实际上a**b必须经过3次操作才能变成*a*b。所以这道题还是直接记录需要修改的次数是最简单直接的。
转移方程跟 最长公共子序列 差不多,倒是可以一起思考。
f[i][j]=min(min(f[i-1][j]+1,f[i][j-1]+1),word1[i-1]==word2[j-1]?f[i-1][j-1]:f[i-1][j-1]+1);
class Solution {
public:
int minDistance(string word1, string word2) {
int len1=word1.length(),len2=word2.length();
int f[len1+10][len2+10];
memset(f,0,sizeof(f));
for (int j=1;j<=len2;++j)
{
f[0][j]=j;
}
for (int i=1;i<=len1;++i)
{
f[i][0]=i;
}
for (int j=1;j<=len2;++j)
for (int i=1;i<=len1;++i)
{
f[i][j]=min(min(f[i-1][j]+1,f[i][j-1]+1),word1[i-1]==word2[j-1]?f[i-1][j-1]:f[i-1][j-1]+1);
}
return f[len1][len2];
}
};
这边插播一道相同类型的字符串匹配处理的题目,也是直接定义f[i][j]为s1[0..i-1]和s2[0..j-1]是否能够交错组成s3[0..i+j-1],最后f[lens][lenp]即为答案。
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
int len1=s1.length(),len2=s2.length(),len3=s3.length();
if (len1+len2!=len3) return false;
vector<vector<bool>> f(len1+10,vector<bool>(len2+10,false));
f[0][0]=true;
for (int i=1;i<=len1;i++)
if (s1[i-1]==s3[i-1]) f[i][0]=f[i-1][0];
for (int j=1;j<=len2;j++)
if (s2[j-1]==s3[j-1]) f[0][j]=f[0][j-1];
for (int i=1;i<=len1;i++)
for (int j=1;j<=len2;j++)
{
if (s1[i-1]==s3[i+j-1]) f[i][j]=f[i-1][j];
if (s2[j-1]==s3[i+j-1]) f[i][j]=f[i][j] || f[i][j-1];
}
return f[len1][len2];
}
};
这题的思路其实跟上面这道题一样,f[i][j]就是直接记录s[0..i-1]和p[0..j-1]能否匹配,最后输出f[lens][lenp]即可。
上面这道距离编辑也是,f[i][j]就是直接记录s[0..i-1]和p[0..j-1]的编辑距离,最后输出f[lens][lenp]即可,一模一样可以说是。
当然这两题的初始化边界条件是不一样的,需要自己摸索调试。
class Solution {
public:
bool isMatch(string s, string p) {
int lens=s.length(),lenp=p.length();
if (lens==0 && lenp==0) return true;
if (lenp==0) return false;
bool f[lens+10][lenp+10];
memset(f,0,sizeof(f));
f[0][0]=true;
int index=0;
while (index<lenp && p[index]=='*') {f[0][++index]=true;}
for (int j=1;j<=lenp;j++)
for (int i=1;i<=lens;i++)
{
if (s[i-1]==p[j-1] || '?'==p[j-1]) f[i][j]=f[i-1][j-1];
else
{
if ('*'==p[j-1]) f[i][j]=f[i-1][j] || f[i][j-1];
else
f[i][j]=false;
}
}
return f[lens][lenp];
}
};
另外附个官方题解:https://leetcode-cn.com/problems/wildcard-matching/solution/tong-pei-fu-pi-pei-by-leetcode-solution/
里面的方法二讲述了 贪心算法 ,相比动态规划节省了空间的开销,而且后面有关于AC自动机的讨论,有时间看看。
下面再来一道进阶版的吧,难度提升。
模式 p 中的任意一个字符都不是独立的,会和前(后)的字符互相关联,形成一个新的匹配模式。因此,本题的状态转移方程需要考虑的情况会多一些。简单思考了一下,".*"居然可以匹配ab,相当于0个或多个".",那".**"的效果其实也是这样,即多个"*"没有意义。这时候我们发现,即单个的"."相当于上面的"?",而".*'相当于上面的"*",多个“*”其实可以简化为一个“*”。现在最后一个问题是“a*”怎么处理,然后其实我们发现,但凡出现‘*’我们肯定要去考虑*前面的那个,题解是遇到*处理,我们这里是判断i+1是否为*处理当前的,是一样的效果。
但是标准答案有一个没处理好:就是她它默认正则表达式不以“*”开头(以*开头正则表达式就不合法了,即不存在以*开头的正则表达式),不然就会发生数组越界。这里我还处理完了好久,基本是跟 通配符 的处理方式是一样的。
class Solution {
public:
bool isMatch(string s, string p) {
int lens=s.length(),lenp=p.length();
if (lens==0 && lenp==0) return true;
if (lenp==0) return false;
bool f[lens+10][lenp+10];
memset(f,0,sizeof(f));
f[0][0]=true;
//int index=0;
//while (index<lenp && p[index]=='*') {f[0][++index]=true;}
//if (index==lenp) return false;
for (int j=1;j<=lenp;j++)
for (int i=0;i<=lens;i++)
{
if (p[j-1]=='*')
{
f[i][j] = f[i][j-2];
if (i>0 && (p[j-2]==s[i-1] || p[j-2]=='.'))
f[i][j] |=f[i-1][j];
}
else
{
if (i>0 && (p[j-1]==s[i-1] || p[j-1]=='.'))
f[i][j] = f[i-1][j-1];
else
f[i][j]=false;
}
}
return f[lens][lenp];
}
};
这个写法比较好,因为是从*开始判断的,1.比较简洁,2.能处理i=0的初始化问题,3.单独处理a*的a时,虽然进行了无效转化,但是处理a*的*时,是从j-2转移过来的,所以不影响。这里也不考虑**的问题,所以其实初始化可以把p串的开头的*去掉,并且把连续的*去掉。
这里记录c++11新特新,即lamda表达式,不用在函数外再写一个大函数了,,,
auto matches = [&](int i, int j)
{
if (i == 0)
return false;
if (p[j - 1] == '.')
return true;
return s[i - 1] == p[j - 1];
};
使用引用捕获一切的Lambda表达式,Lambda表达式的结构 [ capture list ] (parameters) -> return-type { method definition}内部会用到一些外部的变量,这个叫做捕获,可以显式写在[]内,也可以隐式捕获它们。 []内有不同的情况,空的表示不捕获,[=]表示采取copy的方式捕获一切,[&]表示采取引用的方式捕获一切。 详情可以参考
https://www.cnblogs.com/kekec/p/10904802.html
to be continued......