动态规划习题
Q1:最长上升子序列(百练2757)
问题描述 一个数的序列ai,当a1 < a2 < ... < aS的时候,我们称这个 序列是上升的。对于给定的一个序列(a1, a2, ..., aN),我们可 以得到一些上升的子序列(ai1, ai2, ..., aiK),这里1 <= i1 < i2 < ... < iK <= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8), 有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序 列中最长的长度是4,比如子序列(1, 3, 5, 8). 你的任务,就是对于给定的序列,求出最长上升子序列的长度。
38
输入数据 输入的第一行是序列的长度N (1 <= N <= 1000)。第二行给出 序列中的N个整数,这些整数的取值范围都在0到10000。
输出要求 最长上升子序列的长度。
输入样例 7 1 7 3 5 9 4 8
输出样例 4
分析:最基本的LIS,需要处理一些细节,比如说一开始的dp[i]都要打成1.
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn = 1005; int main() { int num[maxn]; int dp[maxn]; int n; while(scanf("%d",&n)!=EOF) { memset(dp , 0 , sizeof(dp)); for(int i = 1;i<= n;i++) { scanf("%d",&num[i]); dp[i] = 1; } for(int i = 2;i <= n;i++) { for(int j = i - 1;j >= 1;j--) { if(num[i]>num[j]) dp[i] = max(dp[i],dp[j] + 1) ; } } int Max = 0; for(int i = 1;i <= n;i++) Max = max(Max,dp[i]); printf("%d\n",Max); } }
Q2:最长公共子序列(poj1458)
给出两个字符串,求出这样的一个最长的公共子序列的长度:子序列中的每个字符都能在两个原串中找到,而且每个字符的先后顺序和原串中的先后顺序一致。
Sample Input
abcfbc abfcab programming contest abcd mnp
Sample Output
4 2 0
分析:典型的LCS,这里子问题化和LIS是有一些区别的。
#include<cstdio> #include<iostream> #include<cstring> #include<algorithm> const int maxn = 1005; using namespace std; int dp[maxn][maxn]; char s1[maxn] , s2[maxn]; int main() { while(cin>>s1>>s2) { memset(dp , 0 , sizeof(dp)); int len1 = strlen(s1); int len2 = strlen(s2); for(int i = 1;i <= len1;i++) { for(int j = 1;j <= len2;j++) { if(s1[i-1] == s2[j-1]) { dp[i][j] = dp[i-1][j-1] + 1; } else dp[i][j] = max(dp[i-1][j] , dp[i][j-1]); } } printf("%d\n",dp[len1][len2]); } }
Q3:神奇的口袋(百练2755)
有一个神奇的口袋,总的容积是40,用这个口袋可以变出一 些物品,这些物品的总体积必须是40。 John现在有n(1≤n ≤ 20)个想要得到的物品,每个物品 的体积分别是a1,a2……an。John可以从这些物品中选择一 些,如果选出的物体的总体积是40,那么利用这个神奇的口 袋,John就可以得到这些物品。现在的问题是,John有多少 种不同的选择物品的方式。
分析:这其实是基于01背包的计数问题,笔者在《动态规划——背包问题》中对利用动态规划的计数问题进行过总结。
#include<cstdio> #include<cstring> using namespace std; int main() { int n; int a[25]; int dp[45]; while(scanf("%d",&n)!=EOF) { for(int i = 1;i <= n;i++) scanf("%d",&a[i]); memset(dp , 0 , sizeof(dp)); for(int i = 1;i <= n;i++) { for(int j = 40;j >= 1;j--) { if(j + a[i] <= 40 && dp[j] > 0) dp[j+a[i]] += dp[j]; } dp[a[i]]++; } printf("%d\n",dp[40]); } }
Q4:0-1背包问题(POJ3624)
有N件物品和一个容积为M的背包。第i件物品的体积w[i] ,价值是d[i]。求解将哪些物品装入背包可使价值总和 最大。每种物品只有一件,可以选择放或者不放 (N<=3500,M <= 13000)。
分析:01背包是可以降维的,利用一个一维的滚动数组即可实现。
#include<stdio.h> #include<string.h> #include<algorithm> using namespace std; const int maxn = 3405; int dp[12885]; int n, m; int w[maxn],d[maxn]; void ZeroOnePack(int i) { for(int j = m;j >= w[i];j--) dp[j] = max(dp[j],dp[j-w[i]]+d[i]); } int main() { while( scanf("%d%d",&n,&m) != EOF) { memset(dp,0,sizeof(dp)); memset(w,0,sizeof(w)); memset(d,0,sizeof(d)); for(int i = 1;i <= n;i++) scanf("%d%d",&w[i],&d[i]); for(int i = 1;i <= n;i++) { ZeroOnePack(i); } printf("%d\n",dp[m]); } return 0; }
Q5:
描述Michael喜欢滑雪百这并不奇怪, 因为滑雪的确很刺激。可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你。Michael想知道载一个区域中最长的滑坡。区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
一个人可以从某个点滑向上下左右相邻四个点之一,当且仅当高度减小。在上面的例子中,一条可滑行的滑坡为24-17-16-1。当然25-24-23-...-3-2-1更长。事实上,这是最长的一条。输入输入的第一行表示区域的行数R和列数C(1 <= R,C <= 100)。下面是R行,每行有C个整数,代表高度h,0<=h<=10000。输出输出最长区域的长度。
分析:这道题目其实糅合了贪心的思维,对高度进行预处理排序(由小到大),然后用dp实现每个状态和它上下左右4个分状态之间的转移,当然用dfs也是可做的。
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn = 105; int Map[maxn][maxn]; int dp[maxn][maxn]; struct node { int r , c; int value; }; bool cmp(node a , node b) { return a.value < b.value; } int main() { int R , C; struct node n[10005]; while(scanf("%d%d",&R,&C) != EOF) { int index = 0; for(int i = 0;i < R;i++) { for(int j = 0;j < C;j++) { scanf("%d",&Map[i][j]); n[index].r = i; n[index].c = j; n[index].value = Map[i][j]; index++; } } sort(n , n + index,cmp); for(int i = 0;i < R;i++) for(int j = 0;j < C;j++) dp[i][j] = 1; for(int i = 1;i < index;i++) { // printf("%d ",n[i].value); if(n[i].r-1 >= 0 && n[i].value > Map[n[i].r-1][n[i].c]) dp[n[i].r][n[i].c] = max(dp[n[i].r-1][n[i].c]+1,dp[n[i].r][n[i].c]); if(n[i].r+1 < R && n[i].value > Map[n[i].r+1][n[i].c]) dp[n[i].r][n[i].c] = max(dp[n[i].r+1][n[i].c]+1,dp[n[i].r][n[i].c]); if(n[i].c-1 >= 0 && n[i].value > Map[n[i].r][n[i].c-1]) dp[n[i].r][n[i].c] = max(dp[n[i].r][n[i].c-1]+1,dp[n[i].r][n[i].c]); if(n[i].c+1 < C && n[i].value > Map[n[i].r][n[i].c+1]) dp[n[i].r][n[i].c]= max(dp[n[i].r][n[i].c+1]+1,dp[n[i].r][n[i].c]); } int Max = 0; for(int i = 0;i < R;i++) for(int j = 0;j < C;j++) Max = max(Max , dp[i][j]); printf("%d\n",Max); } }
Q6:
- 描述:将正整数n 表示成一系列正整数之和,n=n1+n2+…+nk, 其中n1>=n2>=…>=nk>=1 ,k>=1 。 正整数n 的这种表示称为正整数n 的划分。正整数n 的不同的划分个数称为正整数n 的划分数。
- 输入:标准的输入包含若干组测试数据。每组测试数据是一个整数N(0 < N <= 50)。
- 输出:对于每组测试数据,输出N的划分数。
- 分析:很典型的无穷背包求组合情况数目,笔者在《动态规划——背包问题》中总结了该类问题。
#include <cstdio> #include <cstring> using namespace std; int const MAX = 8000; int dp[MAX]; int main() { int n; while(scanf("%d",&n) != EOF) { memset(dp , 0 , sizeof(dp)); dp[0] = 1; for(int i = 1; i <= n; i++) for(int j = 0; j <= n; j++) dp[j+i] += dp[j]; printf("%d\n",dp[n]); } }
- Q7(Problem source : 百练ACM暑期课练习题03):
- 描述
-
将正整数n 表示成一系列正整数之和,n=n1+n2+…+nk, 其中n1>=n2>=…>=nk>=1 ,k>=1 。 正整数n 的这种表示称为正整数n 的划分。
- 输入
- 标准的输入包含若干组测试数据。每组测试数据是一行输入数据,包括两个整数N 和 K。 (0 < N <= 50, 0 < K <= N)
- 输出
- 对于每组测试数据,输出以下三行数据:
- 第一行: N划分成K个正整数之和的划分数目
- 第二行: N划分成若干个不同正整数之和的划分数目
- 第三行: N划分成若干个奇正整数之和的划分数目
分析:第一小问容易诱导人往组合数学上想,但是去重非常麻烦,这里有很巧妙的减小规模的动规思路。二三小问和之前的题目很类似的,但是下面的代码显示超时。暂且贴在这里,优化问题后续会给出。
Ps:这道问题在nyoj571这道题目有多的设问,实践表明该题的超时是笔者忽视了数据的阀值导致的。最大值Maxn取55就可以轻松A过了。
参考代码如下,被注释掉的是nyoj571的(1)(3)问。
#include <cstdio> #include <cstring> using namespace std; int const MAX = 55; int dp[MAX]; int dp2[MAX][MAX]; int main() { int n , k; while(scanf("%d%d",&n,&k) != EOF) { /* memset(dp , 0 , sizeof(dp)); dp[0] = 1; for(int i = 1; i <= n; i++) for(int j = 0; j <= n; j++) dp[j+i] += dp[j]; printf("%d\n",dp[n]);*/ memset(dp2 , 0 , sizeof(dp2)); dp2[0][0] = 1; for(int i = 1;i <= n;i++) for(int j = 1;j <= i;j++) dp2[i][j] = dp2[i-j][j] + dp2[i-1][j-1]; int m1 = dp2[n][k]; printf("%d\n",m1); /*memset(dp , 0 , sizeof(dp)); dp[0] = 1; for(int i = 1;i <= k;i++) for(int j = 0;j <= n;j++) dp[j+i] += dp[j]; printf("%d\n",dp[n]);*/ memset(dp , 0 , sizeof(dp)); dp[0] = 1; for(int i =1;i <= n;i = i + 2) for(int j = 0;j <= n;j++) dp[j+i] += dp[j]; printf("%d\n",dp[n]); memset(dp , 0 , sizeof(dp)); dp[0] = 1; for(int i = 1;i <= n;i++) for(int j = n;j >= 1;j--) if(j - i >= 0) dp[j] += dp[j-i]; printf("%d\n",dp[n]); } }
Q8:(Problem source :百练ACM暑期课练习题05)
- 描述:阿福最近对回文串产生了非常浓厚的兴趣。
-
如果一个字符串从左往右看和从右往左看完全相同的话,那么就认为这个串是一个回文串。例如,“abcaacba”是一个回文串,“abcaaba”则不是一个回文串。
阿福现在强迫症发作,看到什么字符串都想要把它变成回文的。阿福可以通过切割字符串,使得切割完之后得到的子串都是回文的。
现在阿福想知道他最少切割多少次就可以达到目的。例如,对于字符串“abaacca”,最少切割一次,就可以得到“aba”和“acca”这两个回文子串。
- 输入:输入的第一行是一个整数 T (T <= 20) ,表示一共有 T 组数据。 接下来的 T 行,每一行都包含了一个长度不超过的 1000 的字符串,且字符串只包含了小写字母。
- 输出:对于每组数据,输出一行。该行包含一个整数,表示阿福最少切割的次数,使得切割完得到的子串都是回文的。
- 样例输入
-
3 abaacca abcd abcba
- 样例输出
-
1 3 0
- 提示:对于第一组样例,阿福最少切割 1 次,将原串切割为“aba”和“acca”两个回文子串。 对于第二组样例,阿福最少切割 3 次,将原串切割为“a”、“b”、“c”、“d”这四个回文子串。 对于第三组样例,阿福不需要切割,原串本身就是一个回文串。
- 分析:这个问题的子问题化其实很类似与LIS,我们设置dp[i]表示字符串s的子串s[0]~s[i-1]切割成几段回文的最小切割数,则最终的答案是dp[len-1],len是字符串数组s的长度。
- 下面我们关注的是如何建立递推关系,对于s[0]~s[i],可能出现如下的两种情况:
- 情况1:它是回文的,则dp[i] = 0.
- 情况2:它不是回文的,我们需要进一步的建立其与子状态的关系,既然它不是回文,那么至少切一刀,我们从最靠右端的这一刀入手,因为它是最靠右的,所以它右侧的区间[j+1,i]必然是回文的,可以通过这一点来反过来用于判断枚举某个位置是否能够作为最右端的切割点。
- 我们用j来记录枚举的最右端切割点,会出现如下的两种情况:
- i)如果这个点的右侧区间[j+1,i]不是回文的,那么该点不是最右端切割点,继续枚举。
- ii)如果这个点的右侧区间[j+1],i是回文的,那么该点可作为最右端切割点,我们得到了子问题表征的方案:dp[j] + 1.想到这就很简单了,这样继续枚举下去,维护dp[i]的最小值即可。
- 简单的伪码呈现如下:
- for i 0 to len
- if(s[0~i]回文) dp[i] = 0
- else
- for j i - 1 to 0
- if(dp[i] = 0 ) dp[i] = dp[j] + 1
- if(s[j+1 ~ i]回文) dp[i] = min(dp[i],dp[j] + 1)
#include<cstdio> #include<cstring> #include<algorithm> #include<iostream> using namespace std; const int maxn = 1005; char s[maxn]; int dp[maxn]; bool ok(int st , int en) { bool flag = true; for(int i = st , j = en;j > i;i++,j--) { if(s[i] != s[j]) {flag = false;break;} } return flag; } int main() { int t; while(scanf("%d",&t) != EOF) { while(t--) { cin>>s; int len = strlen(s); //if(ok(0,len-1)) printf("ok"); memset(dp , 0 , sizeof(dp)); for(int i = 0;i < len;i++) { if(ok(0,i)) dp[i] = 0; else { for(int j = i - 1;j >= 0;j--) if(ok(j+1,i)) { if(dp[i] == 0) dp[i] = dp[j] + 1; else dp[i] = min(dp[i],dp[j]+1); } } } printf("%d\n",dp[len-1]); } } }
Q9:(Problem source :百练ACM暑期课练习题06)
描述:最近越来越多的人都投身股市,阿福也有点心动了。谨记着“股市有风险,入市需谨慎”,阿福决定先来研究一下简化版的股票买卖问题。
假设阿福已经准确预测出了某只股票在未来 N 天的价格,他希望买卖两次,使得获得的利润最高。为了计算简单起见,利润的计算方式为卖出的价格减去买入的价格。
同一天可以进行多次买卖。但是在第一次买入之后,必须要先卖出,然后才可以第二次买入。
现在,阿福想知道他最多可以获得多少利润。
输入:输入的第一行是一个整数 T (T <= 50) ,表示一共有 T 组数据。 接下来的每组数据,第一行是一个整数 N (1 <= N <= 100, 000) ,表示一共有 N 天。第二行是 N 个被空格分开的整数,表示每天该股票的价格。该股票每天的价格的绝对值均不会超过 1,000,000 。
输出:对于每组数据,输出一行。该行包含一个整数,表示阿福能够获得的最大的利润。
样例输入
3 7 5 14 -2 4 9 3 17 6 6 8 7 4 1 -2 4 18 9 5 2
样例输出
28 2 0
提示对于第一组样例,阿福可以第 1 次在第 1 天买入(价格为 5 ),然后在第 2 天卖出(价格为 14 )。第 2 次在第 3 天买入(价格为 -2 ),然后在第 7 天卖出(价格为 17 )。一共获得的利润是 (14 - 5) + (17 - (-2)) = 28 对于第二组样例,阿福可以第 1 次在第 1 天买入(价格为 6 ),然后在第 2 天卖出(价格为 8 )。第 2 次仍然在第 2 天买入,然后在第 2 天卖出。一共获得的利润是 8 - 6 = 2 对于第三组样例,由于价格一直在下跌,阿福可以随便选择一天买入之后迅速卖出。获得的最大利润为0.
分析:这道题目算是比较隐含的动态规划的题目了。
子问题化:与很多动态规划问题“自相似”的子问题化(即子问题化过程直接关联了递推方程),这个问题似乎不能这么处理,因为买股票的次数是有限制的。因此在这里,只需要对全局问题进行一次子问题化然后形成多种状态,然后维护最大值即可,而不是像很多问题中将全局问题子问题化,然后再将子问题子问题化。在这里,我们用pre[i]记录前i天(包括第i天)交易1次收益最大值,用pro[i]i天后(包括第i天)的收益最大值,这样全局问题即变成了max{pre[i] + pro[i] | i∈[1,n]}.
递推方程:下面我们需要求解的是数组pre[]、pro[],这里将又会用到动态规划的思路。一般化的思路是O(n^2)的暴力算法,考虑数据阀值,会超时。我们需要采取优化策略。很基本的一个优化策略是所谓的“部分和”(《入门经典》、《算法问题实战策略》中谈及过),在输入过程或者输入之后,我们设置数组proMax[i],preMin[i]分别表示第i位之后最大的整数以及第i位之前最小的整数,这里的时间复杂度是O(n)。
对于子状态pre[i],我们容易看到它呈现出如下的两种情况:
i)它在第i天卖出得到了利益最大,即pre[i] = preMin[i] - a[i];
ii)它没有再第i天卖出,也就是说它前i-1天就完成了利益最大的一次买卖,即pre[i] = pre[i-1];同理,pro[i]是类似的道理。
求解pre[i]:遍历1~n,这里有递推方程——pre[i] = max(pre[i-1],a[i] - preMin[i]).
求解pro[i]:遍历n~1,这里有递推方程——pro[i] = max(pro[i+1],proMax[i]-a[i]).
O(n)的时间复杂度。
#include<cstdio> #include<algorithm> using namespace std; const int maxn = 100000 + 5; int pre[maxn]; int pro[maxn]; int a[maxn]; int preMin[maxn]; int proMax[maxn]; int main() { int t; while(scanf("%d",&t) != EOF) { while(t--) { int n; scanf("%d",&n); for(int i = 1;i <= n;i++) { scanf("%d",&a[i]); if(i == 1) preMin[i] = a[i]; else preMin[i] = min(preMin[i-1],a[i]); } for(int i = n;i >= 1;i--) { if(i == n) proMax[i] = a[i]; else proMax[i] = max(a[i],proMax[i+1]); } for(int i = 1;i <= n;i++) { if(i == 1) pre[i] = 0; else pre[i] = max(pre[i-1],a[i] - preMin[i-1]); } for(int i = n;i>= 1;i--) { if(i == n) pro[i] = 0; else pro[i] = max(pro[i+1],proMax[i] - a[i]); } //for(int i = 1;i <= n;i++) // printf("%d %d\n",pre[i],pro[i]); int Max = 0; for(int i = 1;i <= n;i++) { Max = max(Max , pre[i] + pro[i]); } if(Max < 0) Max = 0; printf("%d\n",Max); } } }
Q10(Problem source : poj 2192):
Description
For example, consider forming "tcraete" from "cat" and "tree":
String A: cat String B: tree String C: tcraete
As you can see, we can form the third string by alternating characters from the two strings. As a second example, consider forming "catrtee" from "cat" and "tree":
String A: cat String B: tree String C: catrtee
Finally, notice that it is impossible to form "cttaree" from "cat" and "tree".
分析:
子问题化:类似字符串的问题,在进行子问题化处理的时候很类似区间dp,对于一个区间我们用一个记录位置的参量用来表示状态,而这里既然关乎两个字符串,那么显然我们要设置两个维度的参量来表示每一个状态。我们设置dp[i][j]来表示s1前i个字符和s2前j个字符能否合成出s3前i+j个字符。
状态转移:基于子问题化,我们容易看到有如下的状态转移方程。
dp[i][j] = (dp[i-1][j] & s1[i] == s3[i+j]) || (dp[i][j-1] & s2[j] = s3[i+j]).
参考代码如下。
#include<cstdio> #include<cstring> using namespace std; const int maxn = 205; char s1[maxn] , s2[maxn]; char s3[2*maxn]; int dp[maxn][maxn]; int main() { int t; scanf("%d",&t); getchar(); for(int kase = 1;kase <= t;kase++) { memset(dp , 0 , sizeof(dp)); scanf("%s %s %s",s1+1,s2+1,s3+1); int len1 = strlen(s1 + 1); int len2 = strlen(s2 + 1); dp[0][0] = 1; for(int i = 0;i <= len1;i++) for(int j = 0;j <= len2;j++) { if (j > 0 && (dp[i][j-1] & s3[i+j] == s2[j])) dp[i][j] = 1; else if(i > 0 && (dp[i-1][j] & s3[i+j] == s1[i])) dp[i][j] = 1; else if(i > 0 || j > 0) dp[i][j] = 0; } printf("Data set %d: ",kase); if(dp[len1][len2] == 1) printf("yes\n"); else printf("no\n"); } }