【学习笔记】区间dp

NCC79601

2019-05-29 22:54:53

Personal

## 例题 [P1880](https://www.luogu.org/problemnew/show/P1880) # 分析 一堆石子在环上,需要合并成一堆,合并时得到的分数为合并后石子的重量,求得到的最大/最小分数。 这很明显就是个区间$\text{dp}$,只是相对来说如何进行状态转移比较蛋疼。 # 转移方程 用$f[i][j]$表示将$[i,j]$区间内的石子合并之后得到的最大分数,那么就可以枚举断点$k\in[i,j)$,进行状态转移: $$f[i][j]=\max\ f[i][k]+f[k+1][j]+sum(i,j)$$ 然后就可以很明显地注意到,虽然转移方程好写,但是以什么样的顺序来进行转移就比较蛋疼了。如果单纯地枚举$i,j$,然后枚举$k$,那么很可能$f[i][k]$和$f[k+1][j]$是根本还没有计算的状态,那么就没有转移的意义了。 所以换个思想,我们希望的是**从简单到复杂**地考虑,比如第一次转移肯定希望是最开始的两堆石子合并,然后第二次转移就是第一次合并之后的石子和相邻石子合并$\cdots$以此类推。这样一想,很明显地发现我们需要的是$d=(j-i)$这个式子**递增**,也就是应该先枚举$d$,然后枚举$i$,最后根据定义,$j=i+d$,就可以保证每次转移的$f[i][k]$和$f[k+1][j]$都是已经计算过的状态。 --- # 总结 这里强调的就是:**枚举顺序非常重要**!可以按照这个思路考虑: 1. 能否覆盖全状态? 2. 用于转移的状态是否已经确定? 3. 是否修改了已经计算好的状态? --- # Add 2019-7-11复习了一次区间$\text{dp}$,然后我觉得我的理解更加深刻了。 首先,既然是区间$\text{dp}$,一定要找的就是**区间**。$f[i][j]$描述的是$\left[ i,j\right]$区间,还是两个序列的$[1,i]$和$[1,j]$的对应关系?初始的区间有哪些,它们的值分别是多少? 其次,以什么样的顺序枚举?如果是类似分治的思想,需要用小区间合并得到大区间,那么一定是从$len$开始枚举。否则,两层枚举顺序就没有限制。 最后,怎么样合并信息?有没有额外的变动?最后获得的答案在哪一个区间? --- ## [P2758](https://www.luogu.org/problemnew/show/P2758) 这道题说的是给出替换、插入、删除三种操作,将字符串A变为字符串B,询问需要进行的最少操作次数。(A,B字符串长度不一定相等) **想不到吧!** 这是个区间$\text{dp}$呢! 考虑之后,我建立的模型是$f[i][j]$表示用A串的$[1,i]$区间变为B串的$[1,j]$区间所需要的最少操作次数。 1. 不需要进行操作,直接赋值。 2. 插入,$f[i][j]=f[i][j-1]+1$。 3. 删除,$f[i][j]=f[i-1][j]+1$。 4. 替换,$f[i][j]=f[i-1][j-1]+1$。 最后,对于上述四种情况取最小值即可。 ```cpp #include<bits/stdc++.h> using namespace std; const int MAXN = 2010; char s1[MAXN], s2[MAXN]; int f[MAXN][MAXN], len1, len2; int main() { scanf("%s", s1); len1 = strlen(s1); getchar(); scanf("%s", s2); len2 = strlen(s2); for(int i = 1; i <= len1; i++) f[i][0] = i; //全部删除 for(int i = 1; i <= len2; i++) f[0][i] = i; //全部插入 for(int i = 1; i <= len1; i++) for(int j = 1; j <= len2; j++) { if(s1[i - 1] == s2[j - 1]) f[i][j] = f[i - 1][j - 1]; else f[i][j] = min(min(f[i - 1][j], f[i][j - 1]), f[i - 1][j - 1]) + 1; } printf("%d", f[len1][len2]); return 0; } ``` --- ## [POJ 2955](poj.org/problem?id=2955) 找出最长的匹配括号子序列,**想不到吧!** 这又是个区间$\text{dp}$! 假设有一个待考虑的区间$[i,j]$,获得它的匹配括号子序列有两种办法: 1. 利用 a => (a) 首尾匹配,那么$f[i][j]=f[i+1][j-1]+1$。 2. 利用 a, b => ab 把它从中间砍断,前后分别考虑,那么$f[i][j]=f[i][k]+f[k+1][j]$。 ```cpp #include<iostream> #include<stdio.h> #include<stdlib.h> #include<algorithm> #include<cstring> using namespace std; const int MAXN = 110; char s[MAXN]; int T, n, f[MAXN][MAXN]; bool match(char a, char b) { return (a == '(' && b == ')') || (a == '[' && b == ']'); } int main() { while(1) { memset(f, 0, sizeof(f)); scanf("%s", s); getchar(); if(s[0] == 'e') return 0; n = strlen(s); for(int len = 2; len <= n; len++) for(int i = 0, j = i + len - 1; j < n; i++, j++) { if(match(s[i], s[j])) f[i][j] = f[i + 1][j - 1] + 2; for(int k = i; k < j; k++) //得充分考虑!! f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j]); } printf("%d\n", f[0][n - 1]); } return 0; } ``` --- ## [POJ 1651](poj.org/problem?id=1651) 给定一个序列,要从中一直拿数直到只剩两个数,每次拿出去的时候获得一个分数,分数等于拿走的数与它相邻两数之积。使获得分数最小。 实际上这道题难在**建模**。用$f[i][j]$表示$[i,j]$区间拿到**只剩首尾两数**获得的最小分数,考虑一个区间$[i,j]$,如果我把这个区间拿到只剩$a[i],a[k],a[j]\ (i<k<j)$三个数,再把$a[k]$拿掉,获得的分数就很显然了。那么拿到只剩三数这个过程就可以继续分治,最后很轻松就获得了答案。 ```cpp #include<iostream> #include<stdio.h> #include<algorithm> #include<stdlib.h> #include<string.h> using namespace std; const int MAXN = 110; // f[i][j] = min(f[i][k] + f[k][j] + a[k] * a[i] * a[j]) int n, a[MAXN]; int f[MAXN][MAXN]; int main() { memset(f, 0x3f, sizeof(f)); scanf("%d", &n); for(int i = 1; i <= n; i++) scanf("%d", &a[i]); for(int i = 1; i <= n; i++) f[i][i] = 0; for(int i = 1; i < n; i++) f[i][i + 1] = 0; for(int len = 3; len <= n; len++) for(int i = 1, j = i + len - 1; j <= n; i++, j++) for(int k = i + 1; k < j; k++) f[i][j] = min(f[i][j], f[i][k] + f[k][j] + a[i] * a[k] * a[j]); printf("%d", f[1][n]); return 0; } ``` --- ## [LightOJ 1422](lightoj.com/volume_showproblem.php?problem=1422) 有一些衣服,可以套着穿,但是脱了就报废,求最少要多少件衣服才够穿。 这题简单粗暴,只要发现一个区间首尾需要穿的衣服一样,那就可以套在里面穿其他衣服。剩余的常规操作即可。这道题难在如何用莽夫思维简化问题。 ```cpp #include<iostream> #include<stdlib.h> #include<stdio.h> #include<algorithm> #include<cstring> using namespace std; const int MAXN = 110; int n, c[MAXN]; int f[MAXN][MAXN]; int main() { int T, cas = 0; scanf("%d", &T); while(T--) { cas++; memset(f, 0x3f, sizeof(f)); scanf("%d", &n); for(int i = 1; i <= n; i++) scanf("%d", &c[i]); for(int i = 1; i <= n; i++) f[i][i] = 1; for(int len = 2; len <= n; len++) for(int i = 1, j = i + len - 1; j <= n; i++, j++) { if(c[i] == c[j]) f[i][j] = f[i][j - 1]; for(int k = i; k < j; k++) if(c[i] == c[k]) f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j]); } printf("Case %d: %d\n", cas, f[1][n]); } return 0; } ```