基本思想:
把一个较复杂的问题按照阶段划分,分解为若干个较小的局部问题,然后按照局部问题的递推关系,依次作出一系列决策,直至整个问题达到总体最优的目标。
动态规划四部曲:
- 确定状态:
最后一步:最优策略中使用的最后一枚硬币值ak;
化成子问题:最少的硬币数量拼出更小的面值M-ak; - 转义方程:
f[X]=min{f[X-2]+1,f[X-5]+1,f[X-7]+1} - 初始条件和边界条件:
f[0]=0,如果不能拼出Y,f[Y]=正无穷 - 计算顺序:
f[0],f[1],f[2]……
例题1:
现在有3种硬币,分别面值2元、5元和7元,每种硬币都有足够多
买一本书需要27元
如何用最少的硬币组合正好付清,不需要对方找钱
#include<iostream>
#include<limits.h>
using namespace std;
/*
现在有3种硬币,分别面值2元、5元和7元,每种硬币都有足够多
买一本书需要27元
如何用最少的硬币组合正好付清,不需要对方找钱
coin[]代表硬币的种类[2,5,7]
M代表拼凑的值 27
*/
int min(int a,int b){
return a>b?b:a;
}
int main(){
int coin[]={2,5,7};
int M=27;
int* f;
f[0]=0;
for(int i=1;i<=M;i++){
f[i]=INT_MAX;//一开始就表示为无穷大,表示不能拼凑
for(int j=0;j<3;j++){
//这里右三种面值的硬币,需要循环一次
if(i-coin[j]>=0 && f[i-coin[j]]!=INT_MAX){
f[i]=min(f[i],f[i-coin[j]]+1);
}
}
}
if(f[M]==INT_MAX){
printf("===");
}
cout<<f[M]<<endl;
return 0;
}
例题2:
给定m行n列的网格,有一个机器人从左上角(0,0)出发,每一步可以向下或者是向右走一步。
问:有多少种不同的方式走到右下角。
解题四部曲:
一:确定状态
做后一步:
-
最后挪动的一步要么向右,要么向下
-
右下角的坐标设为(m-1,n-1)
-
那么机器人的前一步一定是在(m-2,n-1)或者是(m-1,n-2)
化成子问题:
如果有X种方式从左上角走到(m-2,n-1),有Y种方式从左上角走到(m-1,n-2),则机器人有X+Y种方法从左上角走到(m-1,n-1),
这样问题就可以转化为机器人有多少种方式从左上角走到(m-2,n-1)和(m-1,n-2)
状态:设f[i][j]为机器人有多少种方式从左上角走到(i,j)
二:确定转移方程
对于任意一个格子(i,j)
f[i][j] = f[i-1][j] + f[i][j-1]
三:确定开始和边界条件:
初始条件:f[0][0] = 1,只有一种方式到左上角
边界条件:i=0或j=0,则前一步只能有一个方向过来:
f[i][j] =1
四:计算顺序 -
f[0][0]=1
-
计算第0行:f[0][j]=1
-
计算第1行
-
……
-
答案是:f[m-1][n–1]
#include<iostream>
#include<stdlib.h>
using namespace std;
/*
给定m行n列的网格,有一个机器人从左上角(0,0)出发,每一步可以向下或者是向右走一步。
问:有多少种不同的方式走到右下角。
*/
int main(){
int m,n;
cout<<"请先输入行数和列数:";
cin>>m>>n;
int **f;
f=(int**)malloc(sizeof(int*)*m);
for(int i=0;i<m;i++)
f[i]=(int*)malloc(sizeof(int)*n);
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(i==0 || j==0){
f[i][j]=1;
}
else{
f[i][j]=f[i-1][j]+f[i][j-1];
}
}
}
//从左上角到表上的各个点的方式有多少种
for(int i=0;i<m;i++){
for(int j=0;j<n;j++)
cout<<f[i][j]<<" ";
cout<<endl;
}
cout<<"机器人从左上角走到右下角有"<<f[m-1][n-1]<<"条路\n";
}
例题3——存在型动态规划
有n块石头分别在x轴的0,1,2,3,……,n-1位置上
一只青蛙在石头0处,想跳到石头n-1处
如果青蛙在第i块石头上,它最多可以向右跳距离ai
问青蛙能否跳到石头n-1上
- 确定状态:
最后一步:能跳到n-1位置
则它最后从i起跳,i<n-1, n-1-i<=ai
子问题:能跳到i位置
状态 :设f[j]表示青蛙能不能跳到j - 确定转移方程:
f[j] = OR 0<=i<j (f[i] and i+a[i] >= j) - 确定开始和边界条件:
初始条件:f[0] = True - 计算顺序:
设f[j]表示青蛙能不能跳到j
f[j]=OR 0<=i<j (f[i] and i+a[i] >= j)
初始化f[0]=true
计算f[1],f[2],……,f[n-1]
答案是f[n-1]
#include<iostream>
#include<stdlib.h>
using namespace std;
/*
有n块石头分别在x轴的0,1,2,3,……,n-1位置上
一只青蛙在石头0处,想跳到石头n-1处
如果青蛙在第i块石头上,它最多可以向右跳距离ai
问青蛙能否跳到石头n-1上
*/
int main(){
int n;
cout<<"请输入n:";
cin>>n;
cout<<"请输入"<<n<<"个数字:";
int *a;
a=(int*)malloc(sizeof(int)*n);
for(int i=0;i<n;i++)
cin>>a[i];
bool *f;
f=(bool*)malloc(sizeof(bool)*n);
f[0]=true;
for(int j=1;j<n;j++){
f[j]=false;
for(int i=0;i<j;i++){
if(f[i]&&a[i]+i>=j){
f[j]=true;
break;
}
}
}
if(f[n-1]==true)
cout<<"可以到达";
else cout<<"不可以到达";
return 0;
}
例题4—最长公共子序列(LCS)与最长公共子串(DP)
公共子序列:在母串中都出现过并且出现的顺序和母串保持一致。
子串:在母串中连续出现的子序列
1.确定状态:
最后一步:判断A和B中的最后一个元素是否相同,
子问题:若A和B中的倒数第二个元素相同,则序列长度=倒数第二个元素前相同的元素个数+1
状态:c[i][j]表示A中第一个元素到第i个元素和B中第一个元素到第j个元素的最长公共子序列。
2. 确定转移方程:
3. 初始条件:
c[i][0]=0, c[0][j]=0
#include<iostream>
#include<stdlib.h>
using namespace std;
int main(){
char A[]="helloworld",B[]="loop";
int n,m;
int **c;
// cout<<"A、B串各需要输入多少个字符:";
//cin>>n>>m;
n=10;m=4;
if(n==0 || m==0){
cout<<"串里没有东西啊"<<endl;
return 0;
}
c=(int**)malloc(sizeof(int*)*n);
for(int i=0;i<=n;i++)
c[i]=(int*)malloc(sizeof(int)*m);
for(int i = 0 ; i <= n; i++)//初始状态
c[i][0] = 0;
for(int i = 0; i <= m; i++)
c[0][i] = 0;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(A[i-1]==B[j-1])
c[i][j]=c[i-1][j-1]+1;
else
c[i][j]=max(c[i-1][j],c[i][j-1]);
}
}
cout<<c[n][m];
return 0;
}
最长公共子串
转移方程:
#include<iostream>
#include<stdlib.h>
using namespace std;
int main(){
int m=7,n=6;
char A[]="abcbdab",B[]="bcdaba";
int **c;
c=(int**)malloc(sizeof(int*)*(m+1));
for(int i=0;i<m;i++)
c[i]=(int*)malloc(sizeof(int)*(n+1));
for(int i = 0 ; i <= m; i++)//初始状态
c[i][0] = 0;
for(int i = 0; i <= n; i++)
c[0][i] = 0;
int res=0;
// char *C;
//int k=0;
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(A[i-1]==B[j-1]){
c[i][j]=c[i-1][j-1]+1;
res=max(res,c[i][j]);
// C[k++]=A[i-1];
// cout<<A[i-1];
}
else{
c[i][j]=0;
}
}
}
cout<<res;
// for(int i=0;i<k;i++)
// cout<<C[i];
return 0;
}
背包问题:
假设现有容量10kg的背包,另外有3个物品,分别为a1,a2,a3。物品a1重量为3kg,价值为4;物品a2重量为4kg,价值为5;物品a3重量为5kg,价值为6。将哪些物品放入背包可使得背包中的总价值最大?
- 确定状态:
最后一步:最后一次装了w[i] kg
子问题:前面i-1次最多装多少价值的东西
状态 :
w[i] : 第i个物体的重量;
p[i] : 第i个物体的价值;
c[i][m] : 前i个物体放入容量为m的背包的最大价值;
c[i-1][m] : 前i-1个物体放入容量为m的背包的最大价值;
c[i-1][m-w[i]] : 前i-1个物体放入容量为m-w[i]的背包的最大价值; - 确定转移方程:
c[i][m]=max{c[i-1][m-w[i]]+p[i],c[i-1][m]} - 确定开始和边界条件:
初始条件:c[i][0]=c[0][j]=0 - 计算顺序:
设f[j]表示青蛙能不能跳到j
f[j]=OR 0<=i<j (f[i] and i+a[i] >= j)
初始化c[i][0]=c[0][j]=0
计算c[i][j]
#include<iostream>
using namespace std;
int main(){
int m=10;
int n=3;
int w[]={3,4,5};
int p[]={4,5,6};
//c[i][v]表示前i件物品恰放入一个重量为m的背包可以获得的最大价值
int c[n+1][m+1];
for(int i=0;i<n+1;i++)
c[i][0]=0;
for(int i=0;i<m+1;i++)
c[0][i]=0;
for(int i=1;i<n+1;i++){
for(int j=1;j<=m+1;j++){
//当物品为i件重量为j时,如果第i件的重量w[i-1]小于重量j时,c[i][j]为下列两种情况之一:
//(1)物品不放入背包中,所以c[i][j]为c[i-1][j]的值
//(2)物品i放入背包中,则背包剩余重量为j-w[i-1],所以c[i][j]为c[i-1][j]-w[i-1]]的值加上当前物品i的价值
if(w[i-1]<=j){
if(c[i-1][j]<(c[i-1][j-w[i-1]]+p[i-1]))
c[i][j] = c[i-1][j-w[i-1]]+p[i-1];
else
c[i][j] = c[i-1][j];
}else
c[i][j] = c[i-1][j];
}
}
int x[n];
for(int i=n;i>0;i--){
//如果c[i][m]大于c[i-1][m],说明c[i][m]这个最优值中包含了w[i-1](注意这里是i-1,因为c数组长度是n+1)
if(c[i][m]>c[i-1][m]){
x[i-1] = 1;
m-=w[i-1];
}
}
for(int j=0;j<n;j++){
cout<<x[j];
}
return 0;
}