动态规划归纳(基础篇)
概要
对于对动态规划不是特别精通的我,写的一篇大佬看了掉头离开的杂文。
主要归纳一些常见的、基础的动态规划的模型。
求最大连续子序列和
Description
有一个整数数列,求一个连续子序列,使得子序列的和最大。
Input
第一行,n {表示该数列有n个整数,n <= 10000 }
第二行,n个整数(integer类型),每个整数之间有一个空格。
Output
一行,一个值,最大连续子序列和(结果保证在正负 2^31 之间)。
Sample Input
6 3 2 -20 12 15 -100
Sample Output
27
代码及注释
1 #include<iostream> 2 #include<cstdio> 3 #define MAXN 10005 4 #define INF 0x3f3f3f3f 5 using namespace std; 6 7 int n; 8 int a[MAXN], maxsum[MAXN]; 9 int ans = -INF; 10 11 int main() { 12 13 scanf("%d", &n); 14 for (int i = 1; i <= n; i++) 15 scanf("%d", &a[i]); 16 17 for (int i = 1; i <= n; i++) { 18 maxsum[i] = maxsum[i - 1] + a[i]; //求子序列的和 19 ans = max(maxsum[i], ans); //更新答案 20 if (maxsum[i] < 0) maxsum[i] = 0; //若该子序列对于答案的最大化没有贡献了,舍弃之 21 } 22 23 printf("%d", ans); 24 25 return 0; 26 }
求最长不下降序列
Description
设有一个正整数的序列:b1,b2,…,bn,若对于下标 i1 < i2 < i3 < … < ik(注:下标 i1 < i2 < i3 < … < ik,不一定是连续的!!),有 bi1≤bi2≤…≤bik,则称存在一个长度为K的不下降序列。如:数列23,17,19,26,48 对于下标 i1=1, i2=4, i3=5, 且满足23<26<48,则存在长度为3的不下降序列。问题为:当给定一列数时,求出其最长的不下降序列。
Input
第一行是数据的个数N,第二行是N个整数( N <= 1000 )。
Output
一行,最长不下降序列的长度(即最长不下降序列的数据个数)。
Sample Input
5 7 13 9 16 28
Sample Output
4
{样例的结果为:7 9 16 28,此部分无须输出}
代码及注释
1 #include<iostream> 2 #include<cstdio> 3 #define MAXN 1005 4 #define INF 0x3f3f3f3f 5 using namespace std; 6 7 int n, a[MAXN]; 8 int maxlen[MAXN]; 9 int ans = -INF; 10 11 int main() { 12 13 scanf("%d", &n); 14 for (int i = 1; i <= n; i++) { 15 scanf("%d", &a[i]); 16 maxlen[i] = 1; //初始化单个元素一个长度为1的子序列 17 } 18 19 for (int i = 1; i <= n; i++) //阶段:到第i个元素时最优解 20 for (int j = 1; j < i; j++) //寻找最优继承状态 21 if (a[j] <= a[i] && maxlen[j] + 1 > maxlen[i]) { //满足最长不下降且可优化阶段解 22 maxlen[i] = maxlen[j] + 1; 23 ans = max(ans, maxlen[i]); 24 } 25 26 printf("%d", ans); 27 28 return 0; 29 }
最长公共子序列
Description
输入2个字符串A和B,要求找出A和B共同的最长子序列,可以不连续,但顺序不能起颠倒。例如:A=‘abdcef’ , B=‘jakfdaca’, 此时存在下列子序列:‘adc’,长度为3,在A和B中都存在,且顺序相同,所以是符合要求的子序列。
[要求]从文件substr.in中读入两个字符串A和B(文件中有二行,第一行为字符串A,第二行为字符串B,字符串的最大长度不超过200),找出A和B中最长公共子序列,并输出最长公共子序列的长度,结果输出到substr.out。
Input
文件中有二行,第一行为字符串A,第二行为字符串B,字符串的最大长度不超过200。
Output
输出最长公共子序列的长度。
Sample Input
abdcef jakfdaca
Sample Output
3
思想
设 f [ i ] [ j ] 为到 str1 的 i 位,到 str2 的 j 位的最长公共子序列。
1.当 str1[ i ] != str2[ j ] 时, f [ i ] [ j ] = max { f [ i - 1 ] [ j ] , f[ i ] [ j - 1 ] } ,即在前一位找相同字母
2.当 str1[ i ] == str2[ j ]时,f [ i ] [ j ] = f [ i - 1 ] [ j - 1 ] + 1 ,可增加子序列长度
代码
1 #include<iostream> 2 #include<cstdio> 3 using namespace std; 4 5 char str1[205], str2[205]; 6 int f[205][205]; 7 8 int main() { 9 10 scanf("%s%s", &str1, &str2); 11 for (int i = 0; i < strlen(str1); i++) 12 for (int j = 0; j < strlen(str2); j++) { 13 if (str1[i] != str2[j]) f[i][j] = max(f[i - 1][j], f[i][j - 1]); 14 else f[i][j] = f[i - 1][j - 1] + 1; 15 } 16 printf("%d", f[strlen(str1) - 1][strlen(str2) - 1]); 17 18 return 0; 19 }
背包问题模型
(不贴代码了…)
01背包 : 采药
https://www.luogu.org/problemnew/show/P1048
状态转移方程:F[ v ] = max { F[ v ] , F[ v - c [ i ] ] + w[ i ] }
完全背包:疯狂的采药
https://www.luogu.org/problemnew/show/P1616
完全背包的方程与01背包的方程相同,在动态规划的时候枚举背包容量时逆推即可
有关背包的计数问题:小A点菜
https://www.luogu.org/problemnew/show/P1164
状态转移方程:F[ j ] = F[ j ] + F[ j - a[ i ] ]
动归顺序按照完全背包,注意逆推
区间DP模型
经典问题:合并沙子
Description
设有N沙子排成一排,其编号为1,2,……,N(N小于500),每堆子有一定的数量,用a[k]表示第K堆沙子的数量值,现在要将N堆沙子归并成为一堆,归并的过程为每次只能将相邻的两堆沙子堆成一堆,合并后的这堆沙子的代价为这两堆沙子的数值和,这样经过N-1次归并之后,最后成为一堆。不同的归并方案的总代价值是不同的。现给出N堆沙子的数量后,找出一种合理的归并方法,使总的归并代价为最小。
Input
有 n+1 行,第一行为n的值,从第2行开始,为n个正整数,表示各堆沙子的数量值。
Output
只有一行,表示最小的总代价,结果保证小于2^31。
Sample Input
10 { n 值 } 12 { 以下,每行一个数值,共n行 } 3 13 7 8 23 14 6 9 34
Sample Output
398
代码及注释
1 #include<iostream> 2 #include<cstdio> 3 #define INF 0x7fffffff 4 using namespace std; 5 6 int n, a[505], sum[505], f[505][505]; 7 8 int main() { 9 10 scanf("%d", &n); 11 for (int i = 1; i <= n; i++) { 12 scanf("%d", &a[i]); 13 sum[i] = sum[i - 1] + a[i]; //计算前缀和 14 } 15 for (int k = 2; k <= n; k++) //阶段:区间长度 16 for (int i = 1; i <= n - k + 1; i++) { //区间起点为位置i 17 int j = i + k - 1; //定义区间终点 18 if (j > n) break; //区间超出范围则返回 19 f[i][j] = INF; //求最小值,赋初始值为+INF 20 for (int l = i; l <= j; l++) //枚举中断点 21 f[i][j] = min(f[i][j], f[i][l] + f[l + 1][j] + sum[j] - sum[i - 1]); //DP 22 } 23 printf("%d", f[1][n]); 24 25 return 0; 26 }
归纳
区间DP的方式比较固定,首先阶段为区间的长度,把一个大区间化为长度很小的子区间,问题分解,之后枚举区间位置和中断点位置。因此可以得出一个区间DP模板
for (int k = 2; k <= /*区间长度*/; k++) for (int i = 1; i <= /*区间长度*/; i++) { //子区间起点 int j = i + k - 1; //子区间终点 /*f[i][j] = INF 若求最小值 */ for (int l = i; l <= j; l++) //枚举中转点 f[i][j] = max /* or min */ (f[i][j], f[i][l] + f[l + 1][j] + val[i][j] /* i到j的value */; }
进阶:环形区间DP
https://www.luogu.org/problemnew/show/P1063
拿能量项链这一道题来示例。
环形DP的方法主要有两种,第一种:对于每一个枚举元素下标的变量取%,但是比较难实现。所以第二种:延伸。将原区间伸长一倍进行DP,最终答案在1到2*n中的1到n的区间里(具体看下列代码解释)
1 #include<iostream> 2 #include<cstdio> 3 using namespace std; 4 5 int a[500], n, x; 6 long long f[500][500]; 7 long long ans = 0; 8 9 int main() { 10 11 scanf("%d", &n); 12 for (int i = 1; i <= n; i++) { 13 scanf("%d", &a[i]); 14 a[i + n] = a[i]; //延伸一倍 15 } 16 for (int i = 1; i <= 2 * n; i++) 17 f[i - 1][i] = a[i - 1] * a[i] * a[i + 1]; //预处理:将相邻两个能量项链合并所生成的能量 18 19 for (int k = 2; k <= n; k++) 20 for (int i = 1; i <= 2 * n; i++) { 21 int j = i + k - 1; 22 for (int l = i; l < j; l++) 23 f[i][j] = max(f[i][j], f[i][l] + f[l + 1][j] + a[i] * a[l + 1] * a[j + 1]); 24 } 25 for (int i = 1; i <= n; i++) 26 if (f[i][i + n - 1] > ans) ans = f[i][i + n - 1]; //答案不在f[1][n]中,在1-2*n的1-n中 27 printf("%lld", ans); 28 29 return 0; 30 }
尾声
主要都是一些很常见基础的模型,至于其他的一些奇奇怪怪的DP,ummm我更相信我的暴力!!!暴力出奇迹