算法学习:动态规划
Posted 布布要成为最强的人
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法学习:动态规划相关的知识,希望对你有一定的参考价值。
基础DP(状态转移和递推)
1.硬币问题
有多个不同面值的硬币,输入最少硬币组合。
这里要是用贪心,所得组合和实际可能不符合。
利用dp法,代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MONEY=1001; //定义最大金额
const int VALUE=5; //5种硬币
int type[VALUE]=1,5,10,25,50; //5种面值
int Min[MONEY]; //每个金额对应最少的硬币数量
void solve()
for(int k=0;k<MONEY;k++) //初始值为无穷大
Min[k]=INT_MAX;
Min[0]=0;
for(int j=0;j<VALUE;j++)
for(int i=type[j];i<MONEY;i++)
Min[i]=min(Min[i],Min[i-type[j]]+1); //递推式
int main()
int s;
solve(); //提前计算出所有金额对应的最少硬币数量。打表
while(cin>>s)
cout<<Min[s]<<endl;
return 0;
拓展问题:要求打印最少硬币的组合。
解决方法:增加一个记录表Min_path[i],记录金额i需要的最后一个硬币。利用Min_path[]逐步倒推,就能得到所有的硬币。
如:
Min_path[6]=5:最后一个硬币是5元的;
Min_path[6-5]=1:1元硬币;
Min_path[1-1]=0:结束;
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MONEY=1001; //定义最大金额
const int VALUE=5; //5种硬币
int type[VALUE]=1,5,10,25,50; //5种面值
int Min[MONEY]; //每个金额对应最少的硬币数量
int Min_path[MONEY]=0; //记录最小硬币的路径
void solve()
for(int k=0;k<MONEY;k++)
Min[k]=INT_MAX;
Min[0]=0;
for(int j=0;j<VALUE;j++)
for(int i=type[j];i<MONEY;i++)
if(Min[i]>Min[i-type[j]]+1)
Min_path[i]=type[j]; //在每个金额上记录路径,即某个硬币的面值
Min[i]=Min[i-type[j]]+1; //递推式
void print_ans(int *Min_path,int s) //打印硬币组合
while(s)
cout<<Min_path[s]<<" ";
s=s-Min_path[s];
int main()
int s;
solve();
while(cin>>s)
cout<<Min[s]<<endl; //输出最少硬币个数
print_ans(Min_path,s); //打印硬币组合
return 0;
拓展问题:硬币组合方案有几种
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int COIN=101; //题目要求不超过100个硬币
const int MONEY=251; //题目给定的钱数不超过250
int dp[MONEY][COIN]=0; // DP转移矩阵
int type[5]=1,5,10,25,50; //5种面值
void solve() // DP
dp[0][0]=1;
for(int i=0;i<5;i++)
for(int j=1;j<COIN;j++)
for(int k=type[i];k<MONEY;k++)
dp[k][j]+=dp[k-type[i]][j-1];
int main()
int s;
int ans[MONEY]=0;
solve(); //用DP计算完整的转移矩阵
for(int i=0;i<MONEY;i++) //对每个金额,计算有多少种组合方案。打表
for(int j=0;j<COIN;j++) //从0开始,注意 dp[0][0]=1
ans[i]+=dp[i][j];
while(cin>>s)
cout<<ans[s]<<endl;
return 0;
2.0/1背包问题
用dp[i][j]数组,表示将前i个物品放入容量为j的背包中产生的价值。
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
代码如下:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
typedef struct
int v;
int w;
Node;
int dp[1005][1005];
int main()
int N,V;
scanf("%d%d",&N,&V);
Node* node=new Node[N+1];
for(int i=1;i<=N;i++)
scanf("%d%d",&node[i].v,&node[i].w);
for(int i=1;i<=N;i++)
for(int j=1;j<=V;j++)
if(node[i].v>j)
dp[i][j]=dp[i-1][j];
else
dp[i][j]=max(dp[i-1][j-node[i].v]+node[i].w,dp[i-1][j]);
printf("%d\\n",dp[N][V]);
动态规划法的设计思想:
如果各个子问题的不是独立的,如果能够保存已经解决的子问题的答案,而在需要的时候再找出已求得的答案,这样就可以避免大量的重复计算。‘
基本思路是,用一个表记录所有已解决的子问题的答案,不管该问题以后是否被用到,只要它被计算过,就将其结果填入表中。
3.完全背包问题
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10
直接上一段代码:
#include<bits/stdc++.h>
using namespace std;
typedef struct
int v,w;
Node;
int dp[1005][1005];
int main()
int N,V;
scanf("%d%d",&N,&V);
Node* node=new Node[N+1];
for(int i=1;i<=N;i++)scanf("%d%d",&node[i].v,&node[i].w);
for(int i=1;i<=N;i++)
for(int j=0;j<=V;j++)
for(int k=0;k*node[i].v<=j;k++)
dp[i][j]=max(dp[i][j],dp[i-1][j-k*node[i].v]+k*node[i].w);
printf("%d\\n",dp[N][V]);
这个代码和0/1背包有些相似,这里求解dp数组时,用3个for循环,遍历前i个物品在背包容量为j的背包中产生的最大价值,方法为:
取定第i个物品,直接循环确定从0最大的k个i物品能产生的最大价值。可以理解为:装入k个i物品,相当于将前i-1个物品选定最大价值装入容量为j-knode[i].v的包中,那么这样做的价值为dp[i-1][j-knode[i].v]+k*node[i].w,和原始的dp[i][j]不断比较,逐渐选择出最大价值的dp[i][j]
理解完全背包问题时,可以和0/1背包做比较,加深理解。
但是这题如果是这样做的话,显得太过暴力,事实上,这样提交上去是会给超时的!
那么接下来,我们需要对这个问题做个优化:
首先了解dp更新时的内部关系:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w,dp[i-1][j-2v]+2w+dp[i-1][j-3v]+3w+…)
递推下去
dp[i][j - v] = max ( ,dp[i-1][j-v],dp[i-1][j-2v]+w+dp[i-1][j-3v]+2*w);
递推得出:dp[i][j]=max(dp[i-1][j],dp[i][j-v]+w);
有了上面的递推关系式,可以将代码优化为:
for(int i=1;i<=N;i++)
for(int j=0;j<=V;j++)
if(j-node[i].v>=0)
dp[i][j]=max(dp[i][j],dp[i][j-node[i].v]+ndoe[i].w);
else dp[i][j]=dp[i-1][j];
至此,完全背包优化代码如下:
#include<bits/stdc++.h>
using namespace std;
typedef struct
int v,w;
Node;
int dp[1005][1005];
int main()
int N,V;
scanf("%d%d",&N,&V);
Node* node=new Node[N+1];
for(int i=1;i<=N;i++)scanf("%d%d",&node[i].v,&node[i].w);
for(int i=1;i<=N;i++)
for(int j=0;j<=V;j++)
if(j-node[i].v>=0)
dp[i][j]=max(dp[i-1][j],dp[i][j-node[i].v]+node[i].w);
else dp[i][j]=dp[i-1][j];
printf("%d\\n",dp[N][V]);
4.最长公共子序列
给定两个长度分别为 N 和 M 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。
输入格式
第一行包含两个整数 N 和 M。
第二行包含一个长度为 N 的字符串,表示字符串 A。
第三行包含一个长度为 M 的字符串,表示字符串 B。
字符串均由小写字母构成。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N,M≤1000
输入样例:
4 5
acbd
abedc
分析:
分别遍历两个序列。
若Ai=Bj,求最长公共子序列的长度,则只需要求A的前i-1个和B的前j-1个公共子序列的长度,然后再加1即可。即:
dp[i][j]=dp[i-1][j-1]+1;
若Ai!=Bj,求最长公共子序列的长度,只有两种可能,一种是A的前i个和B的前j-1的最长公共子序列长度,另一种是A的前i-1个和B的前j个的最长公共子序列长度。即:
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
知晓了动态方程,代码就能很容易写出:
#include<bits/stdc++.h>
using namespace std;
int num[1005][1005];
int main()
int n,m;
string a,b;
scanf("%d%d",&n,&m);
cin>>a;
cin>>b;
memset(num,0,sizeof(num));
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(a[i-1]==b[j-1])num[i][j]=num[i-1][j-1]+1;
else
num[i][j]=max(num[i-1][j],num[i][j-1]);
printf("%d\\n",num[n][m]);
5.最长递增子序列(LIS问题)
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N。
第二行包含 N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤1000,
−109≤数列中的数≤109
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4
分析:
用双重循环,第一层循环遍历这个数列,第二个循环借助在第一层循环遍历到的数列下标i,再次遍历数组从0到i-1,如果遍历的数列num[i]>num[j],那么最长递增长度要么是以遍历到j的最长长度+1,要么就是原先计算出来的最长长度(一开始每个第一层循环开始时元素的最长递增长度都设置为1),接着再用一个元素记录最长的长度即可。
这样做的复杂度为O(n^2)。
代码如下:
#include<bits/stdc++.h>
using namespace std;
int main()
int N;
scanf("%d",&N);
int* num=new int[N];
int* dp=new int[N];
int len=1;
for(int i=0;i<N;i++)scanf("%d",&num[i]);
for(int i=0;i<N;i++)
dp[i]=1;
for(int j=0;j<i;j++)
if(num[i]>num[j])
dp[i]=max(dp[i],dp[j]+1);
len=max(len,dp[i]);
printf("%d\\n",len);
该题可以用更高效的算法,利用dp以及二分法。dp表示规则为:
dp[i]表示长度为i的递增子序列中最后一个元素最小的值。
状态计算:对所有num[i],如果大于dp[cnt-1],(下标从0开始,cnt长度的最长上升子序列,末尾最小的数字),那就cnt++,使得最长上升序列长度+1,当前末尾最小元素为num[i];若num[i]小于等于dp[cnt-1],说明不会更新当前的长度,但之前末尾的最小元素要发生变化,找到第一个 大于或等于 (这里不能是大于) num[i],更新以那时候末尾的最小元素。
dp[i]一定是一个单调递增的数组,所以可以用二分法查找,复杂度为O(nlogn)
代码如下:
#include<bits/stdc++.h>
using namespace std;
int main()
int N,cnt=0;
scanf("%d",&N);
int* num=new int[N];
int* dp=new int[N];
for(int i=0;i<以上是关于算法学习:动态规划的主要内容,如果未能解决你的问题,请参考以下文章