动态规划
故事引入
“曾经跨越诸多世界的我,受困于此”,你是一个从世界之外漂流而来的旅行者,你漫无目的地在这片大陆上探索着,直到你看到了这座石碑:
动态规划简介
古语有云:“尺有所短,寸有所长”,任何算法和思想都有它的局限性。动态规划(Dynamic Programming,简称DP)是一种用来解决一类具有重叠子问题(指的是在求解问题的过程中,会反复求解同一个子问题,导致算法效率低下)和最优子结构性质(指的是问题的最优解可以通过子问题的最优解来构造,也就是说每一个状态都可以从前一个状态转移过来)的问题的算法思想。通常采用自底向上的方式,通过将原问题划分为若干个子问题(分治算法),并先求解子问题的最优解,然后逐步将子问题的解组合起来,直到求解原问题的最优解。
动态规划问题的解决步骤如下所示:
1.定义状态:定义子问题的解以及问题的解。通常采用状态表示问题的某些属性或特征。
2.确定状态转移方程:确定从一个状态转移到另一个状态的具体方式。
3.初始状态(初始化):定义最小子问题的解或问题的初始解。
4.计算顺序:根据状态转移方程自底向上计算问题的解。
5.解释解:根据计算得到的解释解决方案。
动态规划之初探
递推
你开始理解石碑上的文字,并好像发现了什么。
走着走着,你登上了几阶楼梯,忽然,你突发奇想:我可以一步跨一级台阶,也可以两级台阶,那么我有多少种上楼梯的方法呢?
你接受了这个挑战:
题目描述
一段楼梯有n级台阶。你每次可以走一级或者走两级。请问走完共有几种方案?
输入格式
输入n(n≤40)。
输出格式
输出方案数量。
样例
Input 1
5
Output 1
8
你开始在有限的时间里思考,怎么解决这个问题呢?你开始在楼梯上徘徊,试图走出不同的可能性。但是你很快发现这种方法实在是太慢了。忽然,你发现了一件事:跨上第1级台阶的方案数始终只有一种,第2级有2种,一种是直接跨大步,一种是两个小步,但是第3级台阶就等于它们2个的方案数之和!你开始验证你的猜想是否正确,第4级=第2级+第3级,第5级=第3级+第4级······那么设台阶数为n,那么当n>2的时候总的方法数就等于n-1的方法数加上n-2的方法数!用一个数组来表示就是f[n]=f[n-1]+f[n-2]。“破绽,稍纵即逝”,你使用出了这一招:
- #include<bits/stdc++.h>
- using namespace std;
- int n,a[50];
- int main(){
- a[1]=1,a[2]=2;
- cin>>n;
- for(int i=3;i<=n;i++){
- a[i]=a[i-1]+a[i-2];
- }
- cout<<a[n];
- }
你成功的完成了这一个挑战并获取了100帕特森(pts,这个世界中的通行货币),你的脑海里回荡着这么一个声音:递推——你开始与石碑上的字联想:它们是一类东西吧!
动态规划之线性动态规划
你走在路上,回想着石碑上的字,由于你想的太入神,你走入了一个潮湿而又黑暗的洞穴,直到你发现光线的的变化时,门已经被关上了,只有解出谜题才能顺利逃脱,不然,你会饿死在这。
没办法,你只能继续往里走,你看到了一则紫色的铭文:
最长上升子序列(LIS)
时间:0.2s 空间:32M
题目描述:
对于一个数的序列bi,当b1<b2<...<bS的时候,我们称这个序列是上升的。
对于给定的一个序列(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).
你的任务,就是对于给定的序列,求出最长上升子序列的长度。
输入格式:
第一行输入一个整数n
第二行输入n个整数
输出格式:
输出一个整数,表示最长递增子序列的长度
样例输入:
7
1 7 3 5 9 4 8
样例输出:
4
约定:
1<=n<=1000,1<=序列元素<=10000
时间紧,任务重,你必须想到更快的算法才能解决如下的问题,否则就无法深入。你联想起石碑上的招式,你试着攻击它:
- #include<bits/stdc++.h>
- using namespace std;
- const int N=1145;
- int n,a[N],f[N],ans;
- int main()
- {
- scanf("%d", &n);
- for(int i=1;i<=n;i++)scanf("%d", &a[i]);
- for(int i=1;i<=n;i++){
- f[i]=1;
- for (int j=1;j<i;j++){
- if(a[i]>a[j])f[i]=max(f[i],f[j]+1);
- }
- }
- for(int i=1;i<=n;i++)ans=max(ans,f[i]);
- printf("%d",ans);
- return 0;
- }
你获得了100帕特森!没想到这么简单就击破了它!
你继续深入,你忽然就听到了机械的律动声,原来是遗迹守卫!它们向你发射导弹,你准备防御:
输入格式:
第一行有若干个以空格间隔的整数,依序表示每个导弹的高度
输出格式:
第一行一个整数,表示一套招式最多可以拦截的导弹数量
第二行一个整数,表示最少需要多少套招式才能拦截所有导弹
样例输入:
389 207 155 300 299 170 158 65
样例输出:
6
2
你忽然悟了:这不就是铭文上的LIS吗?
于是,你用铭文干掉了这些遗迹守卫:
- #include <bits/stdc++.h>
- using namespace std;
- int n,x,ans,a[114514],f[114514];
- int main(){
- while(cin>>x)a[++n]=x;
- memset(f,0x3f3f3f3f,sizeof(f));
- reverse(a+1,a+n+1);
- for(int i=1;i<=n;i++)
- {
- int l=1,r=i;
- while(l<r){
- int mid=(l+r)/2;
- if(f[mid]>a[i])r=mid;
- else l=mid+1;
- }
- f[l]=min(f[l],a[i]);
- ans=max(ans,l);
- }
- cout<<ans<<endl;
- memset(f,0x3f3f3f3f,sizeof(f));
- reverse(a+1,a+n+1);
- ans=0;
- for(int i=1;i<=n;i++){
- int l=1,r=i;
- while(l<r)
- {
- int mid=(l+r)/2;
- if(f[mid]>=a[i])r=mid;
- else l=mid+1;
- }
- f[l]=min(f[l],a[i]);
- ans=max(ans,l);
- }
- cout<<ans<<endl;
- return 0;
- }
动态规划之01背包
你使用遗迹守卫上的钥匙走出了门,但是你好像被划伤了,就在这个时候你遇到了一个名叫辰辰的药师:
时间限制:1s 空间限制:256M
题目描述:
松下问童子,言师采药去,云深不知处,只在此山中
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。附近这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。
输入格式:
第一行有两个整数T(1 ≤ T ≤ 1000)和M(1 ≤ M ≤ 100),用一个空格隔开,T代表总共能够用来采药的时间,M代表山洞里的草药的数目。接下来的M行每行包括两个在1到100之间(包括1和100)的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出格式:
包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。
样例输入:
70 3
71 100
69 1
1 2
样例输出:
3
数据范围:
对于30%的数据,M ≤ 10;
对于全部的数据,M ≤ 100。
你不认识药材,你的伤势还在恶化,你开始动脑:01背包的状态转移方程为f[i][j] = max(f[i - 1][j], f[i - 1][j - w[i]] + v[j])i代表对i件物体做决策,有两种方式—放入背包和不放入背包。j表示当前背包剩余的容量。
有了思路你就干:
- #include<bits/stdc++.h>
- using namespace std;
- struct node{
- int t,p;
- }a[114];
- int l,m,dp[1145][1145];
- int main(){
- scanf("%d%d",&l,&m);
- for(int i=1;i<=m;i++){
- scanf("%d %d",&a[i].t,&a[i].p);
- }
- for(int i=1;i<=m;i++){
- for(int j=0;j<=l;j++){
- if(j>=a[i].t){
- dp[i][j]=max(dp[i-1][j],dp[i-1][j-a[i].t]+a[i].p);
- }
- else{
- dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
- }
- }
- }
- printf("%d",dp[m][l]);
- return 0;
- }
动态规划之完全背包
你的伤势还是不见好转,突然,受到神秘力量影响,这个山洞里草药不受控制地长,拔了一株又来一株。
你也很快想出了解决方案:
0-1背包状态转移方程:
f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]
完全背包状态转移方程:
f[i][j]=max(f[i-1][j],f[i][j-w[i]]+v[i])
当不取的时候都是一样的最大价值等于f[i-1][j],等于i-1件物品在容量等于j的时候的最大值
当要取的时候这里有不同的地方
0-1背包是每一件物品只能选取一次 ,在解决0-1背包问题的时候我们需要找到f[i-1][j-w[i]最大价值。因为我们当前物品重量等于w[i],背包总容量等于j,我们就需要找到j-w[i]容量时第i-1件物品的时候最大价值然后加上我们这件物品的价值。
完全背包并不是找到上一件物品背包容量等于j-w[i]的时候,而是找到当前物品情况下j-w[i]的最大值,因为我们的物品可以无限制的使用,f[i][j-w[i]]+v[i]就是在求最大价值,是在放当前物品的时候容量等于j-w[i]的时候最大价值加上现在物品的价值,0-1背包是找到上一件物品背包容量等于j-w[i]的时候最大价值加上现在物品的价值,这就是0-1背包和完全背包不同的地方。
- #include<bits/stdc++.h>
- using namespace std;
- struct node{
- int t,p;
- }a[114];
- int l,m,dp[1145][1145];
- int main(){
- scanf("%d%d",&l,&m);
- for(int i=1;i<=m;i++){
- scanf("%d %d",&a[i].t,&a[i].p);
- }
- for(int i=1;i<=m;i++){
- for(int j=0;j<=l;j++){
- if(j>=a[i].t){
- dp[i][j]=max(dp[i-1][j],dp[i][j-a[i].t]+a[i].p);
- }
- else{
- dp[i][j]=dp[i-1][j];
- }
- }
- }
- printf("%d",dp[m][l]);
- return 0;
- }
动态规划之多重背包
为自己的绝地逢生,你决定犒劳一下自己:
输入格式:
第一行二个数n(n<=500),m(m<=6000),其中n代表希望购买的奖品的种数,m表示拨款金额。
接下来n行,每行3个数,v、w、s,分别表示第I种奖品的价格、价值(价格与价值是不同的概念)和购买的数量(买0件到s件均可),其中v<=100,w<=1000,s<=1000。
输出格式:
第一行:一个数,表示此次购买能获得的最大的价值。
样例输入1:
5 1000
80 20 4
40 50 9
30 50 7
40 30 6
20 20 1
样例输出1:
1040
这个题目和0-1背包问题很类似,相比较0-1背包问题多重背包对于每一件物品增加了数量上的限制,我们可以在0-1背包问题的基础上改一下代码。这个也使用了二进制优化的思想,对于任何一个大于0的正整数n,它都可以表示成几个2的某些次方相乘,可以大大的优化时间
- #include<bits/stdc++.h>
- using namespace std;
- int n,m,a,b,num,cnt,w[5114],v[5114],s[5114],dp[6514];
- int main() {
- cin>>n>>m;
- for(int i=1; i<=n; i++) {
- cin>>a>>b>>num;
- for(int j=1; j<=num; j*=2) {
- num-=j;
- w[++cnt]=a*j;
- v[cnt]=b*j;
- }
- if(num) {
- w[++cnt]=a*num;
- v[cnt]=b*num;
- }
- }
- for(int i=1; i<=cnt; i++) {
- for(int j=m; j>=w[i]; j--) {
- if(j>=w[i])
- dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
- }
- }
- cout<<dp[m];
- return 0;
- }
动态规划之混合背包
吃完庆功宴,你又踏上了旅行。
时间:1s 空间:128M
题目描述:
你有一个最多能用V公斤的背包,现在有n件物品,它们的重量分别是W1,W2,...,Wn,它们的价值分别为C1,C2,...,Cn。有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
输入格式:
第一行:二个整数,V(背包容量,V<=200),N(物品数量,N<=30);
第2..N+1行:每行三个整数Wi,Ci,Pi,前两个整数分别表示每个物品的重量,价值,第三个整数若为0,则说明此物品可以购买无数件,若为其他数字,则为此物品可购买的最多件数 (0≤Pi≤20) 。
输出格式:
仅一行,一个数,表示最大总价值。
样例输入:
10 3
2 1 0
3 3 1
4 5 4
样例输出:
11
提示:
选第一件物品1件和第三件物品2件。
其实可以把前面的几个结合一下就能过了。你想着。
- #include<bits/stdc++.h>
- using namespace std;
- int m,n,w[51], v[51],s[51], f[514];
- int main() {
- cin>>m>>n;
- for(int i=1; i<=n; i++) {
- cin>>w[i]>>v[i]>>s[i];
- }
- for (int i = 1; i <= n; i++) {
- if (s[i]==1) {
- for (int j =m; j >= w[i]; j--) {
- f[j] = max(f[j], f[j - w[i]] + v[i]);
- }
- } else if (s[i]==0) {
- for (int j = w[i]; j <=m; j++) {
- f[j] = max(f[j], f[j - w[i]] + v[i]);
- }
- } else {
- for (int j =m; j >= w[i]; j--) {
- for(int k=1;k<=s[i];k++){
- if(j<k*w[i]) break;
- f[j]=max(f[j],f[j-k*w[i]]+k*v[i]);
- }
- }
- }
- }
- cout <<f[m];
- return 0;
- }
动态规划之分组背包
突然你发现,你所选的东西互相冲突,所以你要想办法解决:
题目描述:
一个旅行者有一个最多能用V公斤的背包,现在有n件物品,它们的重量分别是W1,W2,...,Wn,它们的价值分别为C1,C2,...,Cn。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
输入格式:
第1行:三个整数,V(背包容量,V<=200),N(物品数量,N<=30)和T(最大组号,T<=10);
第2..N+1行:每行三个整数Wi,Ci,P,表示每个物品的重量,价值,所属组号。
输出格式:
仅一行,一个数,表示最大总价值。
样例输入:
10 6 3
2 1 1
3 3 1
4 8 2
6 9 2
2 8 3
3 9 3
样例输出:
20
因为多了相互冲突的缘故,所以你可以在01背包的基础上应用01背包的思想解决这道题:我们设f[i][j]为当前考虑到了第i组物品,剩余容里为j的背包能装物品的最大价值,那么很容易想到我们需要去枚举第i组物品,考虑选哪一个物品时最优的(或者不选
- #include<bits/stdc++.h>
- using namespace std;
- int n,m,t,w[514],g[514][514],kinds[514],v[514],s[514],dp[5114];
- int main(){
- cin>>m>>n>>t;
- for(int i=1;i<=n;i++){
- cin>>w[i]>>v[i]>>s[i];
- kinds[s[i]]++;
- g[s[i]][kinds[s[i]]]=i;
- }
- for(int i=1;i<=t;i++){
- for(int j=m;j>=0;j--){
- for(int k=1;k<=kinds[i];k++){
- if(j>=w[g[i][k]]){
- dp[j]=max(dp[j],(dp[j-w[g[i][k]]]+v[g[i][k]]));
- }
- }
- }
- }
- cout<<dp[m]<<endl;
- return 0;
- }
动态规划之区间动态规划
区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来由很大的关系。令状态 f ( i , j ) f(i,j)f(i,j) 表示将下标位置 到 的所有元素合并能获得的价值的最大值,那么 f ( i , j ) = m a x ( f ( i , k ) + f ( k + 1 , j ) ) + c o s t ) f(i,j)=max {( f(i,k)+f(k+1,j) )}+cost )f(i,j)=max(f(i,k)+f(k+1,j))+cost), c o s t costcost 为将这两组元素合并起来的代价。
区间 DP 的特点:
合并:即将两个或多个部分进行整合,当然也可以反过来;
特征:能将问题分解为能两两合并的形式;
求解:对整个问题设最优值,枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值。
突然,依托石子挡住了你的去路,你需要把他们聚成一堆才能安全通过
题目描述:
有N堆石子排成一排,其中第i堆的石子的重量为Ai,现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆合并成新的一堆,形成的新石子堆的重量以及消耗的体力是两堆石子的重量之和。
求把全部N堆石子合并成一堆最少需要消耗多少体力。
输入格式:
第一行一个正整数N(N<=300),表示石子的堆数N。
第二行N个正整数,表示每堆石子的质量(<=1000)。
输出格式:
一个正整数,表示最少需要消耗多少体力。
样例输入:
4
1 3 5 2
样例输出:
22
提示:
合并1、2堆,再合并3,4堆
- #include<iostream>
- #include<cstdio>
- using namespace std;
- int cnt[210],s[210][210],dp[210][210],n,temp,te,dp2[210][210],maxn,minn;
- int main()
- {
- scanf("%d",&n);
- for(int i=1;i<=n;i++)scanf("%d",&cnt[i]),cnt[i]+=cnt[i-1],s[i][i]=i,s[i+n][i+n]=i+n;
- for(int i=1;i<=n;i++)cnt[i+n]=cnt[i]+cnt[n];
- for(int i=n*2;i>=1;i--)
- for(int j=i+1;j<=n*2;j++)//参考第1点
- {
- temp=0x7fffffff;
- dp2[i][j]=max(dp2[i+1][j],dp2[i][j-1])+cnt[j]-cnt[i-1]; //参考第2点
- for(int k=s[i][j-1];k<=s[i+1][j];k++)
- {
- if(temp>dp[i][k]+dp[k+1][j]+cnt[j]-cnt[i-1])
- {
- temp=dp[i][k]+dp[k+1][j]+cnt[j]-cnt[i-1];
- te=k;
- }
- }
- dp[i][j]=temp;
- s[i][j]=te;
- }
- minn=0x7fffffff;
- for(int i=1;i<=n;i++)
- {
- minn=min(minn,dp[i][i+n-1]);
- maxn=max(maxn,dp2[i][i+n-1]);
- }
- printf("%d\n%d",minn,maxn);
- return 0;
- }
记忆化搜索
一转眼就到了冬天,你准备去滑雪
题目描述
滑雪是一项非常刺激的运动,为了获得速度,滑雪的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你。给出一个由二维数组表示的滑雪区域,数组的数字代表各点的高度。请你找出这个区域中最长的滑坡。
下面是一个例子:
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)。
输出格式
包括一行,只包含一个整数,表示滑雪区域中最长滑坡的长度。
样例
Input 1
5 5
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
Output 1
25
记忆化搜索是一种典型的空间换时间的思想=搜索的框架+动态规划的思想。
记忆化搜索的典型应用场景是可能经过不同路径转移到相同状态的dfs问题。
更明确地说,当我们需要在有层次结构的图(不是树,即当前层的不同节点可能转移到下一层的相同节点)中自上而下地进行dfs搜索时,大概率我们都可以通过记忆化搜索的技巧降低时间复杂度。
- #include<bits/stdc++.h>
- using namespace std;
- int r,c,maxn,nx,ny,f[114][114],a[114][114],dx[]={0,1,0,-1},dy[]={1,0,-1,0};
- int dfs(int x,int y){
- if(f[x][y])return f[x][y];
- f[x][y]=1;
- for(int i=0;i<4;i++){
- nx=x+dx[i],ny=y+dy[i];
- if(nx>=1&&nx<=r&&ny>=1&&ny<=c){
- if(a[nx][ny]<a[x][y]){
- f[x][y]=max(f[x][y],dfs(nx,ny)+1);
- }
- }
- }
- return f[x][y];
- }
- int main(){
- scanf("%d%d",&r,&c);
- for(int i=1;i<=r;i++){
- for(int j=1;j<=c;j++){
- scanf("%d",&a[i][j]);
- }
- }
- for(int i=1;i<=r;i++){
- for(int j=1;j<=c;j++){
- maxn=max(maxn,dfs(i,j));
- }
- }
- printf("%d",maxn);
- return 0;
- }