动态规划是为了求解一种包含大量重叠子问题的最优化问题的方法。基本思想是,将原问题分解为若干相似的子问题,在求解的过程中通过子问题的解求出原问题的解。听起来和分治法很相似,但是,分治法只是不断地将问题分解成小问题求解,而动规之所以优秀是它会进行类似于记忆化搜索的过程,在求解的过程中把每一个子问题的解保存下来(不管后面会不会用到),然后在求解更大的问题时,从这些子问题的解里面寻求当前问题的最优解。所以,从这里也可以看出动规解法的限定条件是,该问题有一个最优子结构,就是由它分解出的子问题的解也是最优的。
这样看起来是很抽象的,我们接下来一个个模型看下去吧。感觉对 dp 不太好分类,个人就做简单的整理,代码不对或是分类有问题请大家指出,谢谢!
首先是线性动规。
单调递增最长子序列
这个问题的基本模型是,给一个序列,要求它的最长递增子序列,子序列的值是递增的,它的坐标在原序列中也是递增的,但不要求连续。
如,给一串数字, 0 2 3 4 3 4 6 9 13 24 11 26,那么它的单调递增最长子序列就是 0 2 3 4 6 9 13 24 26
给一串字母,a f r e f j l m o d, 单调递增最长子序列就是 a e f j l m o
下面是求一个字母序列的单调递增子序列
原题:http://acm.nyist.net/JudgeOnline/problem.php?pid=17
解1:
输入字符串为 str, 用一个数组 dp 记录问题的解, dp[i] 表示以 str[i] 结束的单调递增最长子序列的长度。
状态转移方程: dp[i] = max(1, dp[j] + 1) , str[j] < str[i]
复杂度为 O(n^2)
代码:
#include <iostream> #include <string> #include <cstring> using namespace std; #define max(a,b) (a)>(b)?(a):(b) int main() { int dp[10001],ans; int T; cin>>T; while(T--) { ans = 1; string str; cin>>str; int n = str.size(); for(int i = 0;i < n;++i) { dp[i] = 1; for(int j = 0;j < i;++j) { if(str[i] > str[j]) { dp[i] = max(dp[i],dp[j] + 1); if(dp[i] > ans) ans = dp[i]; } } } cout<<ans<<endl; } return 0; }
解二:
数组 dp 存不是答案子序列,但它和子序列长度是一样的,因为它的不断更新,所以总能确保答案正确,多看几遍。复杂度大概只有 O(100*N),因为最长单调递增子序列的长度是不可能超过127的。
#include<iostream> #include<string> #include<cstring> using namespace std; //求最长单调递增子序列长度 int length(string str) { int dp[128], ans = 1;; memset(dp,0,sizeof(dp)); int n = str.size(); dp[0] = str[0]; //初始化 for(int i = 0;i < n;++i) { for(int j = ans - 1;j >= 0; --j) { if(j == 0 && dp[0] > str[i]) dp[0] = str[i]; if(dp[j] < str[i]) { dp[j+1] = str[i]; if(j == ans - 1) ans++; break; } } } return ans; } int main() { int T; string str; cin>>T; while(T--) { cin>>str; cout<<length(str)<<endl; } return 0; }
单调递增最长子序列应用:
拦截导弹:
原题:http://acm.nyist.net/JudgeOnline/problem.php?pid=79
题意:就是给一个序列然后求它的单调递减最长子序列的长度。
思路:按照最长单调递增子序列的思路,只要把递增的比较改为递减的比较就好,length1 函数为上面解法一的修改解法, length2 为上面解法二的修改解法,而且运用二分查找存放位置。
代码:
/* * http://acm.nyist.net/JudgeOnline/problem.php?pid=79 * * 求最长单调递减子序列长度 */ #include<iostream> #include<cstring> #include <cstdio> using namespace std; #define MAXN 20 + 1 #define max(a,b) (a)>(b)?(a):(b) int num[MAXN]; int dp[MAXN]; //直接 dp ,O(n^2) int length1(int *num,int n,int *dp) { int ans = 1; memset(dp,0,sizeof(dp)); for(int i = 0;i < n;++i) { dp[i] = 1; for(int j = 0;j < i;++j) { if(num[j] > num[i]) { dp[i] = max(dp[i],dp[j] + 1); ans = max(ans,dp[i]); } } } return ans; } //求最长单调递减子序列长度,运用二分 int length2(int *num, int n, int *dp) { int ans = 1;; memset(dp,0,sizeof(dp)); dp[0] = num[0]; //初始化 for(int i = 0;i < n;++i) { //二分查找 num[i] 应放的位置 int st = 0,ed = ans - 1; while(st <= ed) { int mid = (st + ed)/2; if(dp[mid] == num[i]) break; if(dp[mid] > num[i]) { st = mid + 1; } else { ed = mid - 1; } } // st > ed 的时候才要将 num[i] 放进去,用纸演算一下,要放在 dp[st] 的位置 if(st > ed) { dp[st] = num[i]; if(st == ans) ans++; } } return ans; } int main() { int N,M; scanf("%d",&N); while(N--) { scanf("%d",&M); for(int i = 0;i < M;++i) scanf("%d",&num[i]); cout<<length1(num, M, dp)<<endl; } return 0; }
原题: http://www.rqnoj.cn/problem/26
题意:给 n 个整数,求一个子序列,有一个中间数 i, 使得 i 的左边递增,右边递减,要这个子序列最长。输出 n - length, length 为该子序列的长度。
思路:这还是单调递增最长子序列的问题,只是多了一步,要一个左边递增右边递减加起来最长的序列,数据较小,所以我们可以枚举。对队列里的每个数 i ,计算以它为中心的最长递增和最长递减,长度加起来和答案比较,更新 ans. 复杂度为 O(n^3) 。
代码:
/* * http://www.rqnoj.cn/problem/26 */ #include <iostream> #include <cstring> #include <cstdio> #include <string> using namespace std; #define maxn 103 int n,p[maxn],dp[maxn]; int ans; int solve() { for(int i = 1;i <= n;++i) { for(int j = 1;j <= n;++j) { dp[j] = 1; for(int k = 1;k < j;++k) { if(p[k] < p[j]) dp[j] = max(dp[j],dp[k]+1); } } int l = dp[i]; for(int j = n;j >= i;--j) { dp[j] = 1; for(int k = n;k > j;--k) { if(p[k] < p[j]) dp[j] = max(dp[j],dp[k]+1); } } int r = dp[i]; ans = max(ans,l+r-1); } return n-ans; } int main() { while(scanf("%d",&n)!=EOF) { for(int i = 1;i <= n;++i) scanf("%d",&p[i]); printf("%d\n",solve()); } return 0; }
这是线性 dp 的另一种类型。给出 n 个整数,求一个连续的子串,使得这个子串的和在所有子串中最大。
原题:http://acm.nyist.net/JudgeOnline/problem.php?pid=44
思路:这道题的空间复杂度可以降到 O(1),不需要把所有的数存起来,只要一边输入一边判断就好。我们用 last 表示到前一位的最大子串和,初始化是 0, 那么以某个数结尾的最大子串和的最坏情况就是它本身,如果 last >0,到当前数的最大子串和肯定是当前数加上 last,然后动态更新 ans 就好。
代码:
/* * http://acm.nyist.net/JudgeOnline/problem.php?pid=44 */ #include <iostream> #include <cstdio> using namespace std; #define INF -99999999 #define max(a,b) (a)>(b)?(a):(b) int main() { int T,n,last,cur; scanf("%d",&T); while(T--) { int sum = INF; last = 0; scanf("%d",&n); for(int i = 0;i < n;++i) { scanf("%d",&cur); //这是关键一步,如果前面的子串和 >0,那么这一步的最大子串和为这一个数加上前一步的最大子串和 if(last > 0) cur += last; sum = max(sum,cur); last = cur; } printf("%d\n",sum); } return 0; }
原题:http://acm.nyist.net/JudgeOnline/problem.php?pid=252
题意:长度为 n 的所有 0 1 串中,不含 11 的有多少个。
思路:dp[i] 表示长度为 i 的 0 1串不含 11 的有多少个。初始条件是 dp[1] = 2 dp[3] = 3
算dp[i] 的时候,如果 i 位为 0 ,那么前 i-1 位任取,所以 dp[i] += dp[i-1]
如果 i 位为 1,那么 i-1 位只能为 0,前 i-2 位任取,所以 dp[i] += dp[i-2]
状态方程: dp[i] = dp[i-1] + dp[i-2]
代码:
/* * http://acm.nyist.net/JudgeOnline/problem.php?pid=252 dp[i]表示第i为不含11的01串个数 第i位为0,则前 i-1 位可任取 第i位为1,则 i-1 位必为0,前 i-2 位任取 所以 dp[i] = dp[i-1] + dp[i-2]; */ #include<iostream> #include<cmath> using namespace std; #define MAXN 42 int main() { int dp[MAXN],n; dp[2] = 3; dp[3] = 5; for(int i = 4;i <= 40;++i) dp[i] = dp[i-1] + dp[i-2]; cin>>n; while(n--) { int k; cin>>k; cout<<dp[k]<<endl; } return 0; }
题意:使用一种加密方法,把 A-Z 映射为 1 - 26,那么根据一个加密后的序列,可能有多种解密方法,比如给 124,你可以理解成 1、2、4,也可以看成 12、4,或者1 、24,所以要求求出共有多少中可能。
思路:用 dp[i] 表示到第 i 位有多少种解密方法,则可以看到 dp[i] 和 dp[i-1] dp[i-2] 的关系。如果第 i 个数不是 0 ,那么这个数可以作为单独一个字母进行解码,前面 i-1 位任取,所以 dp[i] += dp[i-1] 。如果第 i-1 位是 1 或者 i-1 位是2,i 位是小于 7 的数,那么它们可以翻译为一个字母解码,前 i-2 位任取,所以 dp[i] += dp[i-2]。
代码:
/* *http://soj.me/1001 */ #include <iostream> #include <string> #include <cstring> using namespace std; int main() { string str; int ans[10000]; int n; while(cin>>str&&str!="0") { memset(ans,0,sizeof(ans)); ans[0] = 1;ans[1] = 1; n = str.size(); for(int i=1;i<n;++i) { if(str[i]!=‘0‘) ans[i+1] += ans[i]; if(str[i-1]==‘1‘||(str[i-1]==‘2‘&&str[i]<‘7‘)) ans[i+1] += ans[i-1]; } cout<<ans[n]<<endl; } return 0; }
原文:http://blog.csdn.net/jcjc918/article/details/22575359