DP问题从入门到精通2.2(线性DP,最短编辑距离)

Posted 芜湖之肌肉金轮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DP问题从入门到精通2.2(线性DP,最短编辑距离)相关的知识,希望对你有一定的参考价值。

DP入门到精通系列

dp问题的难点到底是什么呢?

 

我认为是如何写出dp的更新方式,还有如何去对dp方程初始化。

 

相信很多人都有这样的做题经历,dp方程是对的,但就是少了个dp[0]=1,或者dp[0][i]=1(i取值从0到n)。

 

而这两个问题方法去解决,首先如何初始化,对应的就是状态表示,也就是我们如何去思考这道题的方式,我们可以通过状态表示去推导我们将要如何初始化,然后就是状态计算,状态计算对应的就是集合的划分——你是如何对给你所表示的状态分类的?而这个分类在线性dp中通常都是针对最后一个选与不选。通过对不同的分类取max,min等操作,就可以得到答案。

 

那么利用下面的题目,我就和大家分享一下我是怎么去思考的(思路来源acwing):

最短编辑距离


给定两个字符串 A 和 B,现在要将 A 经过若干操作变为 B,可进行的操作有:

  1. 删除–将字符串 A 中的某个字符删除。
  2. 插入–在字符串 A 的某个位置插入某个字符。
  3. 替换–将字符串 A 中的某个字符替换为另一个字符。

现在请你求出,将 A变为 B 至少需要进行多少次操作。


这道题也是比较经典的线性dp的问题,由之前对最长公共子序列(DP问题从入门到精通2.1(线性DP))的求解我们可以知道面对两个子序列的时候,我们可以选择从二维的角度去分析,所以我们可以很容易的想到我们的状态表示:以 i 结尾的子序列变成以 j 结尾的子序列所需要的最少的操作。这就是我们的状态方程f[ i ][ j ]的含义,那么状态计算呢?

由图我们看出来,我们总共由三个状态,分别对应删增改:
状态1删除,对应i-1,j 意思就是想让长度为 i 的子序列变为 j 的,同时我们发现长度i - 1 和 j 已经匹配了,那么我们只需要把长度为i 的第 i 个删除就可以了同时操作数+1。

 

状态2我们可以发现长度i 和 j-1 已经匹配了,那么想让 i 和 j 匹配只要在 i 后面加上 j 就可以了同时操作数+1。


状态3,这个状态分为两种情况,就是以 i 结尾的和以 j 结尾的子序列,前 i -1和 j-1 都已经匹配了,但 i 和 j 不相等,那么就要把 i 改掉,同时操作数+1。第二种情况是前 i -1和 j-1 都已经匹配了,但 i 和 j 相等,那么就不需要+1了。

 

现在状态计算对应的集合划分已经完成了,已经可以对dp方程做更新了,那么怎么初始化呢?开头我们说过,初始化其实就跟我们的状态表示有关,我们的状态表示是:

 

以 i 结尾的子序列变成以 j 结尾的子序列所需要的最少的操作。 

那么如果把以 0 结尾第一个序列的变成以 j 结尾的第二个序列就只能用增,且最少的操作数与 j 的长度有关 :

 for(int j=1;j<=m;j++)f[0][j]=j;

同理,如果把以 i 结尾第一个序列的变成以0结尾的第二个序列就只能用删,且最少的操作数与 i 的长度有关 :

for(int i=1;i<=n;i++)f[i][0]=i;

好了那么万事俱备,我们开始写最短编辑距离的代码:

const int N = 1010;
string a,b;

int n,m;
int f[N][N];

int main()
{
    cin>>n>>a>>m>>b;
    
    for(int i=1;i<=m;i++)f[0][i]=i;//初始化
    for(int i=1;i<=n;i++)f[i][0]=i;
    
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
        {
            f[i][j]=min(f[i-1][j]+1,f[i][j-1]+1);//前两个状态取min
            if(a[i-1]==b[j-1])f[i][j]=min(f[i][j],f[i-1][j-1]);//第三个状态取和两种情况分别取min
            else f[i][j]=min(f[i][j],f[i-1][j-1]+1);
        }
    cout<<f[n][m];
    return 0;
}

编辑距离 


给定 n 个长度不超过 10 的字符串以及 m 次询问,每次询问给出一个字符串和一个操作次数上限。

对于每次询问,请你求出给定的 n 个字符串中有多少个字符串可以在上限操作次数内经过操作变成询问给出的字符串。

每个对字符串进行的单个字符的插入、删除或替换算作一次操作。

输入格式

第一行包含两个整数 n 和 m。

接下来 n 行,每行包含一个字符串,表示给定的字符串。

再接下来 m 行,每行包含一个字符串和一个整数,表示一次询问。

字符串中只包含小写字母,且长度均不超过 10。

输出格式

输出共 m 行,每行输出一个整数作为结果,表示一次询问中满足条件的字符串个数。 


由题意可知,编辑距离就是一道最短编辑距离的多次应用,所以直接上代码:


const int N =11;
int f[N][N];
string b;
int n,m;

int cmp(string a,string b)
{
    int k=a.size(),l=b.size();
    for(int i=1;i<=l;i++)f[0][i]=i;
    for(int i=1;i<=k;i++)f[i][0]=i;
    
    for(int i=1;i<=k;i++)
        for(int j=1;j<=l;j++)
        {
            f[i][j]=min(f[i-1][j]+1,f[i][j-1]+1);
            if(a[i-1]==b[j-1])f[i][j]=min(f[i][j],f[i-1][j-1]);
            else f[i][j]=min(f[i][j],f[i-1][j-1]+1);
        }
    return f[k][l];
}

int main()
{
    cin>>n>>m;
    vector<string> a(n,string(n,0));
    for(int i=0;i<n;i++)cin>>a[i];
    while(m--)
    {   
        int limt,res=0;;
        cin>>b;
        cin>>limt;
        for(int i=0;i<n;i++)
            if(cmp(a[i],b)<=limt) res++;
        cout<<res<<endl;
    }
    return 0;
}

 

线性DP完结(可能)

由这个最短编辑距离可以说是体现了绝大部分dp问题的本质,只要写出状态表示和状态计算,这道dp题目就离ac不远了,以上就是我从acwing中学习的一点心得把,真心希望可以帮到大家

以上是关于DP问题从入门到精通2.2(线性DP,最短编辑距离)的主要内容,如果未能解决你的问题,请参考以下文章

DP问题从入门到精通2.2(线性DP,最短编辑距离)

DP问题从入门到精通4(状态压缩dp,蒙德里安,最短Hamilton路径)

DP问题从入门到精通4(状态压缩dp,蒙德里安,最短Hamilton路径)

DP问题从入门到精通4(状态压缩dp,蒙德里安,最短Hamilton路径)

DP从入门到精通2.1(线性DP,上升子序列,公共子序列)

DP从入门到精通2.1(线性DP,上升子序列,公共子序列)