字符串dp+区间dp 核心[i,j] (1)枚举长度 左边端点为i 右边端点为j(区间合并) 有时候for k切割区间 (2)表示i为第一个串第i个位置 j为第二个串第j个位置 (3).
串问题
最短编辑距离https://www.acwing.com/problem/content/904/
f[i][j] i表示a的第i个位置 j表示b的第j个位置 让从原序列 a[i]和b[j]开始 让他们相等的最小操作
输入两个字符串 求让a和b相等的最小操作
1)删除操作:把a[i]删掉之后 a[1i]和b[1j]匹配
说明之前已经让a[1(i-1)]和b[1j]匹配 情况+1
f[i-1][j] + 1
2)插入操作:对a插入一个 之后a[i]与b[j]完全匹配,所以插入的就是b[j]
那说明之前a[1i]和b[1(j-1)]匹配
f[i][j-1] + 1
3)替换操作:把a[i]改成b[j]之后想要a[1i]与b[1j]匹配
那么修改这一位之前,a[1(i-1)]应该与b[1(j-1)]匹配
f[i-1][j-1] + 1
但是如果本来a[i]与b[j]这一位上就相等,那么不用改,即
f[i-1][j-1] + 0
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main()
{
scanf("%d%s", &n, a + 1);
scanf("%d%s", &m, b + 1);
for (int i = 0; i <= m; i ++ ) f[0][i] = i;//a初始长度0 b长度i i多长增加多少次
for (int i = 0; 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);//先写必然满足的情况 在a[i-1]和b[j]相等的时候 只需要 a[i-1]补一个就行了
if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);//这里就要求 i-1 和 b[j-1]相等的情况下 这里i和j相等就是继承之前的情况
else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);// [i]和[j]不相等的情况下 需要在[i-1]和[j-1]相等的情况下 将i修改为和j相等
}
printf("%d\n", f[n][m]);
return 0;
}
最长公共子序列https://www.acwing.com/problem/content/899/
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];//表示i,j 对到达 a[i]和b[i]最长公共子序列的长度 f[i][j]
int main() {
cin >> n >> m >> a + 1 >> b + 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i] == b[j]) {
f[i][j] = f[i - 1][j - 1] + 1;//如果a[i]和b[i]字符相等 那么就在他们前面情况基础上a[i-1] b[j-1] +1
} else {
f[i][j] = max(f[i - 1][j], f[i][j - 1]);//不相等的话 是不用+1 的只需要继承之前的情况就行因为两个字符一定有一个可以+1 表示遍历到这个字符和之前情况一样
}
}
}
cout << f[n][m] << '\n';
return 0;
}
区间合并 i j 表示区间【i,j】
先枚举区间len为2的区间
石子合并https://www.acwing.com/problem/content/284/
i和j 表示i个石头到j个石头合并的最小价值
合并但这能选择相邻两个合并
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310;
int n;
int s[N];
int f[N][N];
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &s[i]);
for (int i = 1; i <= n; i ++ ) s[i] += s[i - 1];//用于快速求前缀和
for (int len = 2; len <= n; len ++ )//长度从2开始
for (int i = 1; i + len - 1 <= n; i ++ )
{
int l = i, r = i + len - 1;//r是长度
f[l][r] = 1e8;//因为求最小值 先设置个较大值
//到上面都是模板
for (int k = l; k < r; k ++ )//k是分割每个区间从中间切开 为两部分 i~k k+1~j
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);//l到k 和 k+1到r区间的最小值 +合并的消耗
}
printf("%d\n", f[1][n]);
return 0;
}
回文串
由四种情况衍生出来
- 当i-1~j-1是回文串 并且i和j是相等的时候 f[i][j]=f[i+1][j-1]
2.当i-1~j是回文串 因为多出了个i所以 f[i][j]=f[i-1][j]+1
3.当i-1~j是回文串 因为多出了个j所以 f[i][j]=f[i][j-1]+1
4.当i-1和j-1是回文串 因为ij不相等 多出了 i和j 的情况 所以应该是由 f[i][j]=f[i+1][j-1]+2 但是由于上面2和3的情况是也由f[i-1][j-1]衍生出来的 所以实际上计算的时候这一类是不显示的
密码脱落:最长回文子序列
i表示原来字符串的左端点 j表示原来字符串的右边端点
for 枚举长度
for 枚举左端点
f[i][j] i到j长度的 最长回文字符串长度
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1005;
int f[N][N];
char s[N];
int main()
{
scanf("%s", &s);
int n;n=strlen(s);
for (int len = 1; len <= n; len ++ ){//枚举长度
for (int i = 0; i+len-1 < n; i ++ ){//枚举左端点
int j=i+len-1;
if(len==1) f[i][j]=1;
else {
f[i][j]=max(f[i+1][j],f[i][j-1]);//左边少一个的状况 和 右边少一个字母的状况
if(s[i]==s[j]){
f[i][j]=max(f[i][j],f[i+1][j-1]+2);
}
}
}
}
cout << n-f[0][n-1];
return 0;
}
括号配对 回文+区间匹配
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110,inf=1e9+10;
int f[N][N];
bool ismatch(char l,char r){
if(l=='['&&r==']') return true;
if(l=='('&&r==')') return true;
return false;
}
int main()
{
string s;
cin >> s;
int n=s.size();
for (int i = 0; i < n; i ++ ) f[i][i]=1;//这里特别注意 因为len从2开始所以这里只能跟据实际意义 f[][]=1;
for (int len = 2; len <= n; len ++ ){
for (int i = 0; i+len-1 < n; i ++ ){
//左右端点回文
int l=i,j=i+len-1;
f[i][j]=inf;
if(ismatch( s[i],s[j] )) f[i][j]=f[i+1][j-1];
f[i][j]=min(f[i][j],min(f[i][j-1],f[i+1][j])+1);
//下面是区间
for (int k = l; k < j; k ++ ){
f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
}
}
}
cout << f[0][n-1];
return 0;
}
区间dp
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 410, INF = 0x3f3f3f3f;
int n;
int w[N], s[N];
int f[N][N], g[N][N];
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++ )
{
cin >> w[i];
w[i + n] = w[i];
}
for (int i = 1; i <= n * 2; i ++ ) s[i] = s[i - 1] + w[i];//用于快速求前缀和
memset(f, 0x3f, sizeof f);//f最小是
memset(g, -0x3f, sizeof g);//分别需要求最大值和最小值需要两个dp方程
for (int len = 1; len <= n; len ++ )//枚举长度
for (int l = 1; l + len - 1 <= n * 2; l ++ )
{
int r = l + len - 1;
if (l == r) f[l][r] = g[l][r] = 0;//相等不能合并
else
{
for (int k = l; k < r; k ++ )
{
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + s[r] - s[l - 1]);
}
}
}
int minv = INF, maxv = -INF;
for (int i = 1; i <= n; i ++ )
{
minv = min(minv, f[i][i + n - 1]);//左端点是i 右端点n-1
maxv = max(maxv, g[i][i + n - 1]);
}
cout << minv << endl << maxv << endl;
return 0;
}