算法篇之动态规划
--细说动态规划(细说不是胡说)
目录
前言
最近因为复试的原因,对几个算法进行了一定程度的了解,这一篇将详细讲述一下动态规划,让大家浅显易懂的明白动态规划。
一、什么是动态规划
官方解释:动态规划的英文名Dynamic Programming,是一种分阶段求解决决策问题的数学思想。
通俗来讲:这里借助一张从别的大佬csdn博客上看到的图片来展开,动态规划其实就是利用记住已经解决过的子问题的解,闲话不多说,让我们直接看例子吧。
二、动态规划的求解
下面将以一个问题的具体求解,来让大家具体的掌握动态规划的求解。
1.台阶问题
有一座高度时10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。要求用程序来求出一共有多少种走法。
思路讲解:
(1)递归求解(自顶向下)
假设这时候我们只差最后步就走到第10级台阶,那么此时我们有两种情况,一种是从第8级走一步上2级到第10级,另外一种是从第9级走一步上1级到底10级,即我们要想走到第10级,最后一步必然是从8级或者9级开始的。
那么假设我们已经知道0到9级台阶的走法有X种,0到8级台阶的走法有Y种,那么0到10级台阶的走法应该是X+Y。即可以表示为 F(10)=F(9)+F(8)
#include<iostream>
using namespace std;
int stairs(int n)
{
if(n==1)
return 1;
else if(n==2)
return 2;
else
return stairs(n-1)+stairs(n-2);
}
int main()
{
int n;
cin>>n;
cout<<stairs(n)<<endl;
return 0;
}
然而,这个时间复杂度有点高,时间复杂度如下图一棵二叉树,可以近似看复杂度为O(2^N)
(2)备忘录算法
回顾上面的地归途,我们可以看出有些相同的参数被重复计算量,越往下,重复的越多,这时候我们可以暂存一下结果。
#include<iostream>
#include<map>
using namespace std;
int stairs(int n)
{
map<int,int> mp;
if(n<1)
return 0;
else if(n==1)
return 1;
else if(n==2)
return 2;
if(mp[n]!=0)
return mp[n];
else
{
int temp=stairs(n-1)+stairs(n-2);
mp[n]=temp;
return temp;
}
}
int main()
{
int n;
cin>>n;
cout<<stairs(n)<<endl;
return 0;
}
(3)动态规划(自底向上)
接下来就来到了重头戏了,既然我们时间复杂度已经优化了,那么空间复杂度呢?我们从头开始迭代,在每一次迭代过程中,只要保留之前的两个状态,就可以推导出新的状态,而不需要像上述备忘录算法那样保留全部的子状态。
#include<iostream>
using namespace std;
int stairs(int n)
{
if(n<1)
return 0;
if(n==1)
return 1;
if(n==2)
return 2;
int stairs1=1;
int stairs2=2;
int temp=0;
for(int i=3;i<=n;i++)
{
temp=stairs1+stairs2;
stairs1=stairs2;
stairs2=temp;
}
return temp;
}
int main()
{
int n;
cin>>n;
cout<<stairs(n)<<endl;
return 0;
}
三、更多例子
通过例子大家更好的来掌握动态规划,我尽量将解法差不多的题目放在了一起。
1.数字三角形
题目描述 Description
如图所示的数字三角形,从顶部出发,在每一结点可以选择向左走或得向右走,一直走到底层,要求找出一条路径,使路径上的值最大。
输入描述 Input Description
第一行是数塔层数N(1<=N<=100)。
第二行起,按数塔图形,有一个或多个的整数,表示该层节点的值,共有N行。
输出描述 Output Description
输出最大值。
样例输入 Sample Input
5
13
11 8
12 7 26
6 14 15 8
12 7 13 24 11
样例输出 Sample Output
86
#include<iostream>
#include<algorithm>
#define maxsize 101
using namespace std;
int n;
int d[maxsize][maxsize];
int s[maxsize][maxsize];
int maxsum(int i,int j)
{
if(s[i][j]!=-1)
return s[i][j];
if(i==n)
s[i][j]=d[i][j];
else
{
int x=maxsum(i+1,j);
int y=maxsum(i+1,j+1);
s[i][j]=max(x,y)+d[i][j];
}
return s[i][j];
}
int main()
{
int i,j;
cin>>n;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
{
cin>>d[i][j];
s[i][j]=-1;
}
cout<<maxsum(1,1)<<endl;
return 0;
}
2.背包问题
(1)苹果
题目描述:
ctest有n个苹果,要将它放入容量为v的背包。给出第i个苹果的大小和价钱,求出能放入背包的苹果的总价钱最大值。
输入描述:
有多组测试数据,每组测试数据第一行为2个正整数,分别代表苹果的个数n和背包的容量v,n、v同时为0时结束测试,此时不输出。接下来的n行,每行2个正整数,用空格隔开,分别代表苹果的大小c和价钱w。所有输入数字的范围大于等于0,小于等于1000。
输出描述:
对每组测试数据输出一个整数,代表能放入背包的苹果的总价值。
样例输入:
3 3
1 1
2 1
3 1
0 0
样例输出:
2
第一种解法:
#include<iostream>
#include<algorithm>
#define maxsize 1001
using namespace std;
struct node{
int c;
int w;
};
node apple[maxsize];
int dp[maxsize][maxsize];
int main()
{
int n,v;
while(cin>>n>>v)
{
if(n==0&&v==0)
break;
for(int i=1;i<=n;i++)
cin>>apple[i].c>>apple[i].w;
for(int i=1;i<=n;i++)
for(int j=1;j<=v;j++)
{
if(j<apple[i].c)
dp[i][j]=dp[i-1][j];
else
dp[i][j]=max(dp[i-1][j],dp[i-1][j-apple[i].c]+apple[i].w);
}
cout<<dp[n][v]<<endl;
}
return 0;
}
第二种解法(第二种我在南阳oj上跑的话时间超时了)
#include<iostream>
#include<algorithm>
#define maxsize 1001
int dfs[maxsize];
using namespace std;
int main()
{
int n,v;
while(cin>>n>>v)
{
if(n==0&&v==0)
break;
while(n--)
{
int c,w;
cin>>c>>w;
for(int i=1;i<=n;i++)
{
for(int j=v;j>=c;j--)//从大到小来,物品不可重复
dfs[j]=max(dfs[j],dfs[j-c]+w);
}
}
cout<<dfs[v];
}
}
(2)完全背包问题
(这两个问题有点类似,对照着进行学习,一个物品可重复,一个物品不可重复)
题目描述:
直接说题意,完全背包定义有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的体积是c,价值是w。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。本题要求是背包恰好装满背包时,求出最大价值总和是多少。如果不能恰好装满背包,输出NO
输入描述:
第一行: N 表示有多少组测试数据(N<7)。
接下来每组测试数据的第一行有两个整数M,V。 M表示物品种类的数目,V表示背包的总容量。(0<M<=2000,0<V<=50000)
接下来的M行每行有两个整数c,w分别表示每种物品的重量和价值(0<c<100000,0<w<100000)
输出描述:
对应每组测试数据输出结果(如果能恰好装满背包,输出装满背包时背包内物品的最大价值总和。 如果不能恰好装满背包,输出NO)
样例输入:
2
1 5
2 2
2 5
2 2
5 1
样例输出:
NO
1
#include<iostream>
#include<algorithm>
#include<string.h>
#define maxsize 50050
using namespace std;
int dp[maxsize];
struct node{
int c;
int w;
};
node bag[maxsize];
int main()
{
int T;
cin>>T;
while(T--)
{
int m,v;
cin>>m>>v;
for(int i=1;i<=m;i++)
cin>>bag[i].c>>bag[i].w;
for(int i=0;i<=v;i++)
dp[i]=-0x3f3f3f3f;
dp[0]=0;
for(int i=1;i<=m;i++)
for(int j=bag[i].c;j<=v;j++)//从小到大来的,背包可重复
dp[j]=max(dp[j],dp[j-bag[i].c]+bag[i].w);
if(dp[v]>0)
cout<<dp[v]<<endl;
else
cout<<"NO"<<endl;
}
return 0;
}
(3)开心的小明
(这道题与前两道题就类似了,大家学以致用的做一下)
题目描述:
小明今天很开心,家里购置的新房就要领钥匙了,新房里有一间他自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过N 元钱就行”。今天一早小明就开始做预算,但是他想买的东西太多了,肯定会超过妈妈限定的N 元。于是,他把每件物品规定了一个重要度,分为5 等:用整数1~5 表示,第5 等最重要。他还从因特网上查到了每件物品的价格(都是整数元)。他希望在不超过N 元(可以等于N 元)的前提下,使每件物品的价格与重要度的乘积的总和最大。设第j 件物品的价格为v[j],重要度为w[j],共选中了k 件物品,编号依次为j1...jk,则所求的总和为:v[j1]*w[j1]+..+v[jk]*w[jk]请你帮助金明设计一个满足要求的购物单.
输入描述:
第一行输入一个整数N(0<N<=101)表示测试数据组数,每组测试数据输入的第1 行,为两个正整数,用一个空格隔开:N m(其中N(<30000)表示总钱数,m(<25)为希望购买物品的个数。)从第2 行到第m+1 行,第j 行给出了编号为j-1的物品的基本数据,每行有2 个非负整数v p(其中v 表示该物品的价格(v≤10000),p 表示该物品的重要度(1~5))
输出描述:
每组测试数据输出只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的 最大值(<100000000)
样例输入:
1
1000 5
800 2
400 5
300 5
400 3
200 2
样例输出:
3900
#include<iostream>
#include<algorithm>
#include<string.h>
#define maxsize 30001
using namespace std;
int dp[maxsize];
struct node{
int v;
int p;
};
int main()
{
int T;
node bags[maxsize];
cin>>T;
while(T--)
{
memset(dp,0,sizeof(dp));
int n,m;
int i,j;
cin>>n>>m;
for(i=1;i<=m;i++)
{
cin>>bags[i].v>>bags[i].p;
bags[i].p*=bags[i].v;
}
for(i=1;i<=m;i++)
for(j=n;j>=bags[i].v;j--)
{
dp[j]=max(dp[j],dp[j-bags[i].v]+bags[i].p);
}
cout<<dp[n]<<endl;
}
return 0;
}
3.回文字符串与最长公共子序列
(1)最长公共子序列内存限制:64MB 时间限
咱们就不拐弯抹角了,如题,需要你做的就是写一个程序,得出最长公共子序列。
tip:最长公共子序列也称作最长公共子串(不要求连续),英文缩写为LCS(Longest Common Subsequence)。其定义是,一个序列 S ,如果分别是两个或多个已知序列的子序列,且是所有符合此条件序列中最长的,则 S 称为已知序列的最长公共子序列。
输入描述:
第一行给出一个整数N(0<N<100)表示待测数据组数
接下来每组数据两行,分别为待测的两组字符串。每个字符串长度不大于1000.
输出描述:
每组测试数据输出一个整数,表示最长公共子序列长度。每组结果占一行。
样例输入:
2
asdf
adfsd
123abc
abc123abc
样例输出:
3
6
#include<iostream>
#include<algorithm>
#define maxsize 10001
using namespace std;
int a[maxsize];
int dp[maxsize];
int main()
{
int i,j;
int T;
int max=0;
cin>>T;
while(T--)
{
int n;
cin>>n;
for(i=1;i<=n;i++)
cin>>a[i];
for(i=1;i<=n;i++)
{
dp[i]=1;
for(j=1;j<i;j++)
if(a[i]<a[j]&&dp[i]<dp[j]+1)
dp[i]=dp[j]+1;
}
max=0;
for(i=1;i<=n;i++)
if(max<dp[i])
max=dp[i];
cout<<max<<endl;
}
return 0;
}
(2)单调递增最长子序列
题目描述:
求一个字符串的最长递增子序列的长度
如:dabdbf最长递增子序列就是abdf,长度为4
输入描述:
第一行一个整数0<n<20,表示有n个字符串要处理
随后的n行,每行有一个字符串,该字符串的长度不会超过10000
输出描述:
输出字符串的最长递增子序列的长度
样例输入:
3
aaa
ababc
abklmncdefg
样例输出:
1
3
7
#include<iostream>
#include<string.h>
#include<algorithm>
#define maxsize 10001
using namespace std;
char s[maxsize];
int dp[maxsize];
int main()
{
int T;
cin>>T;
int max=0;
while(T--)
{
cin>>s;
memset(dp,1,sizeof(dp));
for(int i=0;i<strlen(s);i++)
{
dp[i]=1;
for(int j=0;j<i;j++)
if(s[i]>s[j]&&dp[i]<dp[j]+1)
dp[i]=dp[j]+1;
}
max=0;
for(int i=0;i<strlen(s);i++)
if(max<dp[i])
max=dp[i];
cout<<max<<endl;
}
return 0;
}