算法学习:动态规划

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<以上是关于算法学习:动态规划的主要内容,如果未能解决你的问题,请参考以下文章

代码随想录动态规划算法PDF

java-动态规划算法学习笔记

《数据结构与算法之美》27——初识动态规划

算法学习——动态规划1

动态规划算法学习总结

干货:图解算法——动态规划系列