动态规划——数字三角形、最长上升子序列、最长公共子序列、最短编辑距离

数字三角形

题目背景

给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。

7
3 8
8 1 0
2 7 4 4
4 5 2 6 5

算法描述

数字三角形问题在DP里面不会太难吼。
首先,我们从动态规划的性质入手:

动态规划问题要素

1.最优子结构性质

我们用序列(v1,v2,v3……vn)来表示从第一层到最后一层的路径选择。对于第n层上的结点vn,到达该结点路径最大对应的序列为:(v1,v2,v3……vn),那么序列(v1,v2,v3……vn-1)到达结点vn-1对应的路径长度也是最大的。这个利用反证法很容易证明。

2.子问题重叠性质

我们考虑第i层的第j个数a[i][j],到达该结点会有两个选择:a[i-1][j-1]+a[i][j]以及a[i-1][j]+a[i][j](j≠1且j≠i),这样由最优子结构性质我们知道,若我们用f[i][j]表示到达a[i][j]结点最大路径,这样我们需要计算f[i-1][j-1]以及f[i-1][j]。而同样的,我们在考虑a[i][j+1]时,也需要计算f[i-1][j],这样我们就会有重复去计算某一个子问题。

3.递归关系

这里的递归关系就很清晰了:

1≤j≤ii≥2f[i][j]=max(f[i-1][j-1]+a[i][j]+f[i-1][j]+a[i][j]).
j=1f[i][j]=f[i-1][j-1]+a[i][j].(第一个数)
j=if[i][j]=f[i-1][j]+a[i][j].(最后一个数)
最后结束递归的条件是f[i][j]=a[i][j],i=1.

闫式DP

状态表示:f[i][j]

①集合

在这里,状态的这个集合表示到该结点所有路径的集合

②属性

f[i][j]表示路径长度的最大值→max

状态计算

我们会把到结点的所有路径分为两个部分:一个部分为从该结点的左上结点过来的,另一个部分从该节点的右上结点过来的,那么很明显:

计算式为:f[i][j]=max(f[i-1][j-1]+a[i][j]+f[i-1][j]+a[i][j]).

代码实现

如果将每一行第一个结点和最后一个结点单独拎出来的话,可以这么写:

for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++){
            if(j==1) f[i][j]=f[i-1][j]+a[i][j];
            else
                if(j==i) f[i][j]=f[i-1][j-1]+a[i][j];
                else f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];
        }
    }

那如果你不想介么写的话:

如果每个结点的权值都是≥0的,那可以直接写:

    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++){
            f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];
        }
    }

因为默认的初值是0:

对于每一行第一个数,f[i][1]=max(f[i-1][0],f[i-1][1])+a[i][1]=f[i-1][1]+a[i][1]

对于每一行最后一个数,f[i][i]=max(f[i-1][i],f[i-1][i+1])+a[i][i]=f[i-1][i]+a[i][i]。

但是如果不能保证每一个结点的权值非负,就要把数组整个初始化为负无穷

    for(int i=1;i<=n;i++){
        for(int j=0;j<=i+1;j++){
            f[i][j]=-1e9;
        }
    }
    f[1][1]=a[1][1];
    for(int i=2;i<=n;i++){
        for(int j=1;j<=i;j++){
            f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];
        }
    }

完整代码:

#include<iostream>
#include<algorithm>
using namespace std;
const int N=510;
int a[N][N];//a[i][j]表示第i行的第j个数,其中1≤j≤i
int f[N][N];//f[i][j]表示到达点(i,j)路径的最大值
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++){
            scanf("%d",&a[i][j]);
        }
    }
    //开始dp
    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++){
            if(j==1) f[i][j]=f[i-1][j]+a[i][j];
            else
                if(j==i) f[i][j]=f[i-1][j-1]+a[i][j];
                else f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];
        }
    }
    //求第n行
    int res=f[n][1];
    for(int i=2;i<=n;i++){
        if(f[n][i]>res)  res=f[n][i];
    }
    cout<<res;
}

最长上升子序列Ⅰ

题目背景

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

3 1 2 1 8 5 6

算法描述

思路一

动态规划基本要素

对于我们给出的样例,我们考虑序列(3,1,2,1,8),它的最长子序列是1,2,8.我们再考虑下一个元素5,由于我们要求子序列是递增的,所以我们不把5加进去时,(3,1,2,1,8,5)最长上升子序列长度仍然为3.但事实上,我们可以考虑把5作为最大的元素,看是否会有更长的上升序列,只是说在这个例子中,加入5,序列为1,2,5,仍然为3.但这告诉我们,我们定义这个集合,不能只是说定义f[i]=j,表示前i个数最长上升子序列的长度为j。我们要定义二维:f[i][j]=k表示前i个数最大值≤j的最长上升子序列长度为k。这样才方便我们计算。

最优子结构性质

对于序列(a1,a2,a3……,an),若它的最长上升子序列的长度为(b1,b2,……bk),且max(b1,b2,……bk)=j。若an>j,那么(b1,b2,……bk-1)一定是(a1,a2,a3……,an-1)的拥有最大长度为k-1的子序列。若an≤j,那么(b1,b2,……bk-1)一定是(a1,a2,a3……,an-1)的拥有最大长度为k的子序列。

子问题重叠性质

我觉得类似于0-1背包问题吼,不是那么的明显

递归关系

对于j<a[i],这个时候我都不能把a[i]放到我的序列中,f[i][j]=f[i-1][j];

对于j≥a[i],这个时候我可以选择把a[i]放进去,但是我要想呀,我放进去会不会使得我的最长上升子序列更长呢?如果我不放进去,仍然是f[i][j]=f[i-1][j];但是我一旦放进去,那此时要求我i-1个数对应的子序列的最大值要小于a[i]f[i][j]=f[i-1][a[i]-1]+1。所以f[i][j]=max(f[i-1][j],f[i-1][a[i]-1]+1).

那我们j到什么时候结束捏?其实我们…像到最后求解,就是j判断到a[n],但是可能我下一个元素要用到a[i]-1,所以我认为只需要修改j=a[i+1]-1

但是我们一看数据:

−1e9≤数列中的数≤1e9

那其实我们发现这种思路并不可行,数字上限又太高,还有可能为负数,数组下标怎么可以为负数呢。

思路二

我们会想,用f[i]=j表示前i个元素的最长子序列为k不够,因为我没有办法递归更新;可是二维也不行。

因而,我们考虑对于f[i]=j换一种含义,将前i个换成第i个

这也是马后炮啦,但是可以看到,对于背包问题,i表示的就是前i个物品;而数字三角形问题i表示的就是“第i行”的含义。

也就是,我们用f[i]=j表示以第i个元素结尾的子序列长度的最大值为j

最优子结构性质

f[n]=j,令这个子序列为(a1,a2,……a(j-1),aj),且aj=an;那么序列(a1,a2,……a(j-1))一定是子问题:以a(j-1)结尾的拥有最大长度对应的上升子序列。这个用反证法很容易证明。

子问题重叠性质

递归关系

我们考虑第i个元素:在所有以ai结尾的序列中,长度最短为1。对于1≤j≤i-1,若a[j]<a[i],那么我就可以把所有以ai结尾的序列中,划分成上一个元素是a(k1),a(k2),……,这些元素都会小于ai.

如果ai>a1,加上ai,就可以得到一个长度+1的子序列.

同样的,一直到ai-1,如果ai>ai-1,就把ai加上.

所以递归关系为:f[i]=max(1,valid(f[1]+1),valid(f[2]+1),……vaild(f[i-1]+1).这个可以由最优子结构性质得到,比如我前面一个元素是aj,aj前面是什么元素我不管,我只知道以aj结尾的最长子序列长度为f[j],并且因为我a[i]>a[j],我就得到了一个长度为f[j]+1的,以ai结尾的子序列。

其中valid(f[j]+1)是值如果a[i]>a[j],我们就取f[j]+1.否则的话,我从a[j]跳不过来呀,只能是从我开始,变成1了

闫式DP

事实上,在上述分析过程中,已经包括用闫式DP的所有内容了。

状态表示:f[i]

①集合:f[i]对应的集合为以ai为结尾的上升子序列的集合。
②属性:f[i]表示以ai为结尾的上升子序列长度的最大值→max

状态计算

我们将集合划分成,倒数第二个数为a1,a2……ai-1以及只有ai,共i个区域。很显然,最后一个元素是ai,倒数第二个元素是aj,它的最长上升子序列的长度就是f[j]+1,这样,f[i]=max(f[j]+1),j=1,2……i-1. 当然了,若a[j]>a[i],这样的子序列是不存在的,我们只是这样划分。

代码实现

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1100;
int f[N];
int a[N];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    f[1]=1;
    for(int i=2;i<=n;i++){
        int l=1;
        for(int j=1;j<=i-1;j++){
            if(a[i]>a[j]) l=max(l,f[j]+1);
        }
        f[i]=l;
    }
    int res=0;
    for(int i=1;i<=n;i++){
        res=max(res,f[i]);
    }
    cout<<res;
}

输出最长上升子序列

我们在求f[i]的时候,无非就是两种情况:

①由于a[i]比前面任何数都大,因而只能让a[i]自己单独作为一个子序列。
②我们求得f[i]=f[j]+1,是从a[j]跳转过来的。

所以当我们从a[j]跳转到a[i]的时候,我们可以用g[i]来记录,a[i]是从哪个元素跳转过来的,是由哪个元素状态更新得到的。

①我们会说,中间与前面所有的元素比较,怎么知道是哪一个跳转过来的呢?

我们用l记录了当前最长的一个子序列,当我们遍历到j时,发现f[j]+1>l,这时候我们要更新l,同时我们也去更新g[i]=j,表示当前以a[i]结尾长度为l的子序列,a[i]是从a[j]跳转过来的,后面发现比l更大的情况,我们再次进行替换。最后退出循环j:1→i-1的时候,g[i]就是正确的了。

②如何输出呢?

首先我们要记录最长上升子序列最后一个元素是什么。我们通过遍历一遍所有元素,比较他们的f[i].我们这里只需要输出下标即可:

int res=1;//默认下标是1
for(int i=2;i<=n;i++){
	if(f[i]>f[res]) res=i;
}

这样最后的res就是最长子序列的下标。

然后我们倒序输出即可。我们要注意,我们第一个元素是没有来源的,所以没有更新g[i]的值,即g[i]=0,这就是我们停止while循环的条件。

int t=res;//获得最后一个元素的下标
while(t!=0){//只要不是0
	cout<<a[t]<<" ";
	t=g[t];//找到这个元素的前一个元素的下标
}

最长上升子序列 Ⅱ

我们现在分析最长上升子序列Ⅰ求解的时间复杂度:

事实上,我们二重循环:

for(int i=2;i<=n;i++)
………
for(int j=1;j<=i-1;j++)

就决定了时间复杂度是O(n²).当n比较大的时候,时间复杂度就很高了,我们接下来考虑对算法进行优化:

我们用样例来分析:

3 1 2 1 8 5 6

f[1]=1,f[2]=1,而1<3,这样我还有必要让后面的元素与a[1]=3比较吗?

很显然,是没有必要的。这是因为,对于后面的任何元素,我通过a[1]进行了更新,使得a[i]=a[1]+1,我一定满足a[i]>3,这样我就一定会满足a[i]>1,而f[1]=f[2],所以在往后的每一个元素,我实质上,只需要与a[1]比较。

换句话说,若f[x1]=f[x2]=……=f[xn],我只需要保留最小的xi即可。这是因为既然我们的f值相同,我用更大的元素更新,也一定能用更小的元素更新。

因而,对于所有的f[xij]=j,我们对于每一个j都只需要保存最小的xij

下面我们来证明,对于{xi|f[xi]=i}的集合中,若f[xi]<f[xj],一定有xi<xj.

假设xi≥xj,那么f[xi]=f[xj]+1,即可以通过xj跳转到xi完成更新。那么f[xi]>f[xj]。这与f[xi]<f[xj]矛盾。

这代表了什么呢?这代表,我们把f[x]=1对应的最小的元素x1,f[x]=2对应的最小元素x2,f[x]=i对应的最小元素xi选出来,一定有x1<x2<……xn.由前面分析我们知道,对于遍历到了a,我们只需要让ax1,x2,……xn比较即可。若xi<a≤xi+1,那么我f[a]=i+1.非常的巧妙~~

那我们如何去存储每一个长度的子序列最小的那个结尾元素?
我们还是以样例为举例:

3 1 2 1 8 5 6

我们用q[i]=j来维护,长度为i的子序列最小的结尾元素是j,现在我们只求到了q[i],我们就令right=i。那我们现在把q[0:i]划分成两部分:左半边是q<a,右半边是q≥a,我们就要找到左半边部分的临界点,然后让q[i+1]=a,这是因为,若我的确发现q[i]<a<q[i+1],那这样q[i+1]是不是要更新成更小的那个值,所以q[i+1]=a;那如果都比我小,q[i+1]压根没有,那我也是作为第一个填充进去的值。为了方便第一个元素求值,我们会定义q[0]=-2e9;

①对于第一个元素3,我们的left=0,right=0,这样其实都不用求,最后退出就是q[1]=a1;

②对于第二个元素1,我们的left=0,right=1,那我q[0]确实是作为小于1的最大值了,也就是左半边集合的分界点,这样q[1]=a[2]=1.

③对于第三个元素2,我们的left=0,right=1,而q[1]是小于2的最大值了,q[2]=2.
……

我们用len来维护现在数组q求到第几个了

所以将我们到目前为止求到的q[len]进行二分,左半边区域是<a[i],我们就是找左半边的临界点。因而若q[middle]<a[i],那么left=middle,因为它有可能是我的临界点;否则,right=middle-1,一定不会是我的临界点。所以这种情况,对应的middle=(left+right+1)/2.

我们在更新len时,我们只需要比较一下就好了:len=max(right+1,len);

最后输出结果的时候,注意我们是输出len,而不是q[len],因为q[len]表示的是最大长度为len的子序列的最小结尾的值。

具体代码:

#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010;
int a[N];
int q[N];
int len;
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    q[0]=-2e9;
    for(int i=1;i<=n;i++){
        int left=0,right=len;
        while(left!=right){
            int middle=(left+right+1)/2;
            if(q[middle]<a[i]){//说明左边可以移动
                left=middle;
            }
            else{
                right=middle-1;//如果≥a[i],这个点绝对不是我想要的点,可以减1
            }
        }
        q[right+1]=a[i];//就是说现在我更新了q[middle+1]呀
        len=max(right+1,len);
    }
    cout<<len;
}

最长公共子序列

题目背景

给定两个长度分别为 N 和 M 的字符串 X和 Y,求既是 X 的子序列又是 Y 的子序列的字符串长度最长是多少。

4 5
acbd
abedc

动态规划基本要素

最优子结构性质

X={x1,x2,……xn}Y={y1,y2,……,ym}有最长公共子序列Z={z1,z2,……,zk}

①若xn=ym,那么有xn=ym=zk,因而X={x1,x2,……x(n-1)}Y={y1,y2,……,y(m-1)}有最长公共子序列Z={z1,z2,……,z(k-1)}.

②若xn≠ymxn=zk,那么X={x1,x2,……xn}Y={y1,y2,……,y(m-1)}有最长公共子序列Z={z1,z2,……,zk}

③若xn≠ymym=zk,那么X={x1,x2,……x(n-1)}Y={y1,y2,……,ym}有最长公共子序列Z={z1,z2,……,zk}

④若xn≠ymym≠zkxn≠zk,那么X={x1,x2,……x(n-1)}Y={y1,y2,……,y(m-1)}有最长公共子序列Z={z1,z2,……,zk}

所以我们会想能不能用二维数组c[i][j]来表示X前i位和Y前j位最长公共子序列的长度

①若xi=yj,那么c[i][j]=c[i-1][j-1]+1.

②若xi≠yj,我们可以发现,xi和yi肯定不是在我的最长公共子序列中了。

但是可以为case②:c[i][j]=c[i][j-1];也可以为case③:c[i][j]=c[i-1][j];还可以为case④:c[i][j]=c[i-1][j-1]

所以c[i][j]=max(c[i][j-1],c[i-1][j],c[i-1][j-1]).但事实上,c[i][j-1]或者c[i-1][j]一定为大于c[i-1][j-1],这是因为我都包括了X的前i-1项和Y的前j-1项,并且我还多了一位,若这一位没有让我的最长子序列长度增加,那也是相等,但是一定不会让我的最长子序列长度减小。

所以,c[i][j]=max(c[i][j-1],c[i-1][j]).

递归关系

由上述关系,递归关系就很清晰了:

xi=yj,c[i][j]=c[i-1][j-1]+1;

xi≠yj,c[i][j]=max(c[i][j-1],c[i-1][j]).

结束递归的条件是c[i][j]=0,i=0或j=0.

子问题重叠性质

xi≠yj时,我们要去计算比较c[i][j-1]c[i-1][j],而这两个值的计算都需要用到c[i-1][j-1].

我们回过头反思,如何确定我们的状态表示?

我认为关键还是在于最优子结构性质的分析。

我们自顶向下分析:1.若两个串最后一位相同的,那我们去求X(n-1)以及Y(m-1)的最长公共子序列。
2.若两个串最后一位不同,但是X最后一位在最长公共子序列中,那我们就去求X(n)以及Y(m-1)的最长公共子序列。
3,若两个串最后一位不同,但是Y最后一位我认为在最长公共子序列中,那我们就去求X(n-1)以及Ym的最长公共子序列。
4.若两个串最后一位不同,并且我认为这两位都不在我的最长公共子序列当中,那我们就去求X(n-1)以及Y(m-1)的最长公共子序列。

所以我们可以发现,状态转移的关键是X和Y的下标,那我们的状态表示,就肯定要包括X串和Y串的下标,表示这两个串的前k位→用二维数组c[i][j]来表示状态。

代码实现

我们要让字符串从下标1开始存储,当然可以是:

string A,B;
    cin>>A>>B;
    for(int i=n;i>=1;i--) A[i]=A[i-1];
    for(int i=m;i>=1;i--) B[i]=B[i-1];

但是一般我们会直接定义一个字符型数组存储:char a[N],b[N];,然后输入字符串:scanf("%s%s",a+1,b+1);

完整代码:

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int c[N][N];
char a[N],b[N];
int main(){
    int n,m;
    cin>>n>>m;
    scanf("%s%s",a+1,b+1);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(a[i]==b[j]) c[i][j]=c[i-1][j-1]+1;
            else c[i][j]=max(c[i-1][j],c[i][j-1]);
        }
    }
    cout<<c[n][m];
}

最短编辑问题

题目背景

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

删除–将字符串 A 中的某个字符删除。
插入–在字符串 A 的某个位置插入某个字符。
替换–将字符串 A 中的某个字符替换为另一个字符。
现在请你求出,将 A 变为 B 至少需要进行多少次操作。

10
AGTCTGACGC
11
AGTAAGTAGGC

动态规划基本要素

最优子结构性质

现在令A={a1,a2,……an},B={b1,b2,……bm}.

①若a1=b1,那么我们只需要把A={a2,a3,……a(n)}经过操作变换成B={b2,b3,……b(m)}

②若a1≠b1,并且我们采用替换操作a1=b1后,同样的我们只需要把A={a2,a3,……a(n)}经过操作变换成B={b2,b3,……b(m)}

③若a1≠b1,并且我们在A的最前面添加b1,那么我们需要把A={a1,a2,……a(n)}经过操作变换成B={b2,b3,……b(m)}

④若a1≠b1,并且我们采用删除操作,把a1给删除咯,那么需要把A={a2,a3,……a(n)}经过操作变换成B={b1,b2,……b(m)}

因此我们可以看到, 我们的状态转移的关键又是下标,所以我们考虑:可不可以用c[i][j]来表示字符串A[i:n]变换到字符串B[j:m]需要经过的最少的操作次数。

递归关系

①对于case①,我们有c[i][j]=c[i+1][j+1];
②对于case②,我们有c[i][j]=c[i+1][j+1]+1;
③对于case③,我们有c[i][j]=c[i][j+1]+1;
④对于case④,我们有c[i][j]=c[i+1][j]+1;

这样递归关系为:

a1=b1,那么c[i][j]=c[i+1][j+1];
a1≠b1,那么c[i][j]=min(c[i+1][j+1],c[i][j+1],c[i+1][j])+1;

那么结束递归的条件又是什么呢?

可以想象,到最后肯定会有i到了n或者是j到了m,那我们看,c[n+1][j]表示将A后面没有了,就是要把一个空的东西变换到B[j:m],这其中会有m-j+1个元素;c[i][m+1]表示要删去n-i+1个元素;当然c[n+1][m+1]表示0,结果也是对的。
所以:if(i==n+1) c[i][j]=m-j+1; if(j==m+1) c[i][j]=n-i+1;

子问题重叠性质

子问题重叠性质是显然的,比如对于c[i][j+1]c[i+1][j]都需要计算到子问题c[i+1][j+1].

具体代码

这里我们采用的是左对齐,递归结束的条件是在右端。

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int c[N][N];
char A[N],B[N];
int main(){
    int n,m;
    cin>>n;
    scanf("%s",A+1);
    cin>>m;
    scanf("%s",B+1);
    for(int i=1;i<=n;i++){
        c[i][m+1]=n-i+1;
    }
    for(int j=1;j<=m;j++){
        c[n+1][j]=m-j+1;
    }
    for(int i=n;i>=1;i--){
        for(int j=m;j>=1;j--){
           if(A[i]==B[j]) c[i][j]=c[i+1][j+1];
           else{
               c[i][j]=min(c[i+1][j+1]+1,c[i+1][j]+1);
               c[i][j]=min(c[i][j],c[i][j+1]+1);
           }
        }
    }
    cout<<c[1][1];
}

编辑距离

题目背景

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

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

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

3 2
abc
acd
bcd
ab 1
acbd 2

算法描述

这个…其实只是最短编辑距离的变形啦,这是说原先是两个串比较一次,这里要比较n×m次,然后输出最小的操作次数,与它限定的操作次数呢进行比较。

有几个地方注意一下就好:

1.我们还是要从下标1开始存,并且我们也得同时知道源串和目的串的长度,所以我们这么读:canf("%s",sstring[i].s+1);以及scanf("%s",d+1);然后呢,求他的长度我们就int lens=strlen(sstring[k].s+1);以及int lend=strlen(d+1);

2.另外呢,我们在最短编辑距离中是这么写的:

    for(int i=1;i<=n;i++){
        c[i][m+1]=n-i+1;
    }
    for(int j=1;j<=m;j++){
        c[n+1][j]=m-j+1;
    }

我们并没有求c[n+1][m+1],这是因为本身默认值是0,所以也没有必要求。但是在这题中,必须求出c[n+1][m+1]或者是给它赋值成0,这是因为我上一个串长度可能更长呀,c[n+1][m+1]对于它来说不一定是最后一个位置,所以你就不能保证c[n+1][m+1]是0了,答案就会出错。

具体代码

#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int N=20;
int c[N][N];
char d[N];//目的串
struct strings{
    char s[N];
};
strings sstring[1010];
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        scanf("%s",sstring[i].s+1);
    }
    for(int l=1;l<=m;l++){
        int res=0;
        int maxop=0;
        scanf("%s",d+1);
        cin>>maxop;
        int lend=strlen(d+1);
        for(int k=1;k<=n;k++)
        {
            int lens=strlen(sstring[k].s+1);
            for(int i=1;i<=lens+1;i++) c[i][lend+1]=lens+1-i;
            for(int j=1;j<=lend+1;j++) c[lens+1][j]=lend+1-j;
            for(int i=lens;i>=1;i--){
                for(int j=lend;j>=1;j--){
                    if(sstring[k].s[i]==d[j]) c[i][j]=c[i+1][j+1];
                    else{
                        c[i][j]=min(c[i+1][j],c[i][j+1])+1;
                        c[i][j]=min(c[i][j],c[i+1][j+1]+1);
                    }
                }   
            }
            if(c[1][1]<=maxop){
                res++;
            }
        }
        cout<<res<<endl;
    }
}

Over~感谢AcWing平台

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值