对于这道题,我发现好多网上的题解对dp的物理意义讲的并不是十分透彻,但是代码又是正确的,这样初学者会感到十分难以接收dp的正确性
在这篇题解,我会讲解我的dp方程的物理意义(其实是对网上代码的理解之后的自己的想法),这题我觉得思维难度还是挺大的,如果是第一次做的话
首先我们看到什么改变字符串,又是最小,很容易想到dp,并且是区间dp
所以我们自然而然地会想到f[l][r],表示将a转移成b的最小代价,其实这样也能做,但是我们下次遇到这种题目的话,可以先转换成空串转移到b串,之后再与a串进行对比
下面我们来讲解代码中的难点
1.首先我们要相信这个状态定义的正确性,如果我们在l-r范围是最小的,那么显然在大区间中使用这个答案是没有问题的,不可能说我整个大区间的最小值不是由小区间转移过来的
因为区间dp是按照长度来一层层递增的,我们在更新每个大1区间的时候,都是根据小区间的最小值转移过来,这样这个大区间就是最小的,同理可以一直递推上去。
2.为了更好理解,我们可以先对长度为1的区间进行初始化,所有f[i][i]=1,这是很合理的。
3.核心代码讲解
for(len=2;len<=n;len++){ for(l=1;l+len-1<=n;l++){ r=l+len-1; f[l][r]=f[l][r-1]+1; for(k=l;k<r;k++){ if(b[k]==b[r]){ f[l][r]=min(f[l][r],f[l][k-1]+f[k][r-1]); } } } }
首先f[l[][r]=f[l][r-1]+1,因为我们最坏的情况就是直接对这个点进行改变成和b相等的情况
之后我们枚举每个断点,这也是区间dp的常见手法,这里的意义就是如果我们在这段区间内有一个点b[k]的值和b[r]的值是相同的,那么就有可能可以优化
因为k和r相等,那么我们在涂k的时候,完全可以先把k-j都涂成相同的值,这样我们涂k-r就相当于涂k--r-1,因为我们肯定要涂k,在这个时候顺便把k-r涂成同一个值,所以涂r就可以不用额外算
这样改变的最小值就成为f[l][k-1](前k-1的最小值)+f[k][r-1];
4.之后我们再考虑a串的情况
最坏的情况就是跟空串一样,所以先赋初值。
显然如果a[i]==b[i],ans[i]=ans[i-1],ans[i]代表的是从1-i的最小代价
这样的话就可以直接continue了,不用进行下面的循环,我在网上看到他们没有continue,让我一度无法理解下面的ans的转移方程的正确性,我当时一直在想为什么相等了还会有更小的情况
实际如果相等,就不用进行下面的for循环了。
那么下面的for循环的物理意义就是,也相当于如果两个位置不相等,我们不知道从之前哪个位置涂到这个位置才会使前i个最小,所以进行暴力枚举。
#include<iostream> #include<cstdio> #include<cstring> #include<string> #include<algorithm> using namespace std; const int N=1e5+10; const int inf=0x3f3f3f3f; int f[102][102]; int ans[N]; int main(){ int i; string s,b; while(cin>>s>>b){ memset(f,0x3f,sizeof f); memset(ans,0,sizeof ans); s=" "+s; b=" "+b; int j; int n=s.size(); for(i=1;i<=n;i++){ f[i][i-1]=0; } for(i=1;i<=n;i++){ f[i][i]=1; } int len,l,r; int k; for(len=2;len<=n;len++){ for(l=1;l+len-1<=n;l++){ r=l+len-1; f[l][r]=f[l][r-1]+1; for(k=l;k<r;k++){ if(b[k]==b[r]){ f[l][r]=min(f[l][r],f[l][k-1]+f[k][r-1]); } } } } for(i=1;i<=n;i++){ ans[i]=f[1][i]; if(s[i]==b[i]){ ans[i]=ans[i-1]; continue; } for(int j=1;j<=i;j++) ans[i]=min(ans[i],ans[j-1]+f[j][i]); } cout<<ans[n]<<endl; } return 0; }