[算法学习记录] 动态规划基础、01背包、完全背包、多重背包

何为动态规划

动态规划的核心思想是利用已知信息逐步推导未知信息,像斐波那契数列就是典型的根据已知推未知的问题。在动态规划的过程中,我们围绕着一个数组或表格进行操作,需要明确我们要维护的状态,状态的初始化,以及状态的转移,像斐波那契数列的状态就是数字,初始化就是第一、二位都为1,而状态的转移就是从第三位开始,每一位都等于前两位之和。

状态的定义

状态需要满足无后效性,即状态一旦确定,之后的操作都不会改变已有的状态,且之后的状态只能有先前的状态转移过来。

状态转移方程

状态转移方程就是状态转移的方式,像斐波那契数列,状态转移方程就是f[i] = f[i-1] + f[i-2].

解题的基本步骤

对于DP问题,我们一般按照确定状态定义确定状态转移方程确定初始状态确定答案状态这个步骤进行。

简单的DP问题——背包DP

背包问题是最简单的一类动态规划问题,根据每种物品的数量可以分为01背包(每种物品只有一个,每个物品只有选与不选两种状态)完全背包(每种物品的数量有无限个)多重背包(每种物品有有限个)

01背包

如何在有限的背包容量下选择有着一定体积的物体并求得最大总价值,这是01背包问题的一般形式,之所以叫它01背包,是因为每个物品只有选择与不选择两种状态,分别对应二进制的0和1。我们用一个例题来详细讲解:
例题 星码 采药
本题是一个典型的01背包问题,根据做题步骤,很容易就能发现状态就是一件物品的选择与否,状态转移方程就是当前总价值与选择后的总价值之间的最大值,而状态的初始化就是,背包总价值为0,总重量也为0,而答案所要的状态是在有限的背包容量下使背包总价值最大,参考代码如下:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e3+5;
int w[N],v[N],dp[N],t,m;
//w代表体积,v代表价值,dp为状态
void solve()
{
    for(int i = 1;i<=t;i++) dp[i] = 0;
    //状态的初始化,初始状态背包为空
	for(int i = 1;i<=m;i++)	cin >> w[i] >> v[i];
	
	for(int i = 1;i<=m;i++)
		for(int j = t;j>=w[i];j--)
			dp[j] = max(dp[j],dp[j-w[i]] + v[i]);
    //j代表当前背包的容量
	//状态的转移,我们这里采用了滚动数组(新状态会覆盖旧状态)进行优化,为了确保dp[j-w[i]]是上一个状态我们需要倒序操作,
    //如果正序操作,在计算dp[j]时dp[j-w[i]]就已经被新状态覆盖了假设j = 7,w[i] = 3,很容易就能发现,当j = 8时,它的上一个状态dp[4]已经被修改过
    //j>=w[i]是为了防止数组越界
	cout << dp[t] <<"\n";
    //最终状态即为背包容量为t时价值的最大值
}

int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	while (cin >> t >> m)
	{
		if(t==0 && m==0)break;
		solve();
	}
    //本题有多个样例,且在t,m均为0时终止运行
	return 0;
}

完全背包

与01背包不同,完全背包的每种物品都各有无限个,体现在题目中,我们在使用滚动数组解题时,将倒序操作改为正序操作,在01背包问题中,这样可能会导致重复计算,但在完全背包问题中,我们要的就是重复计算,所以只需要对01背包问题的代码稍加修改即可解决完全背包问题。
例题 星码 无穷背包

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5+5;
int m, n,w[505],v[505],dp[N];
//w是物品的体积,v是价值,dp是状态数组
void solve()
{
	cin >> m >> n;
	for(int i = 1;i<=n;i++)cin >> w[i] >> v[i];
	
	for(int i = 1;i<=n;i++)
		for(int j = v[i];j<=m;j++) dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
        //只有j(当前背包容量)大于v[i]时才能考虑装载新物品,所以j直接从v[i]开始
        //由于每种物品都有无数个,所一01背包问题中正序操作的重复问题就成了我们解题的捷径,为了打倒最终状态,我们可以一直向背包中装入某种物品
	cout << dp[m] <<"\n";
    //最终状态,当背包容量为m时的最大价值
}

int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _ = 1;
	while(_--)solve();
	return 0;
}

多重背包

多重背包的每种物品有有限个,但我们可以把每个物品都归为一“类”使多重背包问题转化为01背包问题。转化的方式有很多种,如果数据量较小,可以直接重复添加一类物品多次;如果数据量较大,则可考虑利用二进制的性质(每个十进制数都能由二进制表示)对数据进行压缩,以优化时间复杂度。
例题 星码 多重背包
本题的数据量较小,最多有1e4“类”物品,所以我们可以直接暴力存储这些“类”,在完成数据存储后,按照01背包的方法即可解答。


#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e4+10;
int m,n,x,y,z,w[N],v[N],t = 0;
ll dp[N];

void solve()
{
	for(int i = 0;i<=m;i++)dp[i] = 0;
	//初始化状态,什么都不选时价值为0
	cin >> m >> n;
	
	while(n--)
	{
		cin >> x >> y >> z;
        //x,y,z分别是数量、价值、体积
		while(x--)
		{
			w[++t] = y;
			v[t] = z;
		}
        //把每件物品都看做一“类”
	}
	
	for(int i = 1;i<=t;i++)
		for(int j = m;j>=v[i];j--)
			dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
	//01背包的标准做法,不过要注意,此时物品的种类数从n(此时n为0)变成了t		
	cout << dp[m] <<"\n";
    //输出背包容量为m时,背包所能容纳的最大价值
}

int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _ = 1;
	while(_--)solve();
	return 0;
}

例题 星码 多重背包二周目

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 5e6+9;
int n,m,s,w,v,a[N],b[N],t = 0;
ll dp[N];
//数据量较大,给状态开long long防止溢出
void solve()
{
   cin >> m >> n;
   //m,n分别是背包容量,物品种类
   while(n--)
   {
   	cin >> s >> w >> v;
    //s,w,v分别是数量、价值、体积
   	int k = 1;
   	while(k<=s)
   	{
   		b[++t] = w*k;
   		a[t] = v*k;
   		s-=k;
        k*=2;
   	}
    //任意十进制数都能由二进制数表示
       if(s>0)
       {
           b[++t] = w*s;
           a[t] = v*s;
       }
    //若干次分割后,可能会有余量,通常较小,可以直接添加到数组中
   }
   
   for(int i = 1;i<=t;i++)
   	for(int j = m;j>=a[i];j--)
   		dp[j] = max(dp[j],dp[j-a[i]]+b[i]);
   	//01背包的标准做法
   cout << dp[m] <<"\n"; 
}

int main()
{
   ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
   int _ = 1;
   while(_--)solve();
   return 0;
}

有些问题可能还会将无穷背包与多重背包相结合,对于这种问题,我们只需要将数量无限的那一种的数量设置的足够大(模拟无限次选择),就能将问题转化为一个多重背包问题,再利用二进制优化,将题目转化为一个01背包问题即可解答。

例题 洛谷 P1833 樱花

本题是一个混合背包问题,它融合了多种背包问题,不过,只要我们对其进行一定的操作,就能将混合背包问题转化为01背包问题。
在本题中,赏花次数,也就是某个种类物品的数量是无限的,由于在实际问题中不可能实现无限次选择,所以我们只要把它的数据设置的足够大(根据题中所给数据,我们按极端情况计算,由1e4种花,每种最多看100遍,所以极端情况下总共看花1e6遍,我们的这个数值只要大于1e6即可),就能模拟无限次选择。

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e6+10;
ll n,h1,m1,h2,m2,p[N],c[N],t[N],cnt = 0;
ll dp[N],w[100000],v[100000];

void init()
{
	for(int i = 1;i<=n;i++)
	{
		int k = 1;
		while(p[i]>0)
		{
			w[++cnt] = k*t[i];
			v[cnt] = k*c[i];
			p[i]-=k;
			k*=2;
		    //扩大2倍,k的取值为1,2,4,8,也就是2的n次幂,也能用作2进制的位权
			if(p[i]<k)
			{
				w[++cnt] = p[i]*t[i];
				v[cnt] = p[i]*c[i];
				break;
			}
            //p[i]<k时,p[i]已充分分解,所以不必继续分解,用break跳出循环,防止陷入死循环
		}
        //只要p[i]>0(没分解完)就一直进行分解
	}
    //对每种物品都进行分解	
}
//二进制优化函数
void solve()
{
	scanf("%lld:%lld", &h1, &m1);
	scanf("%lld:%lld", &h2, &m2);
	scanf("%lld", &n);
	ll time1 = h1*60+m1;
	ll time2 = h2*60+m2;
	ll m = time2-time1;
	//计算总时间(背包容量)
	for(int i = 1;i<=n;i++)
	{
		scanf("%lld %lld %lld", &t[i], &c[i], &p[i]);
        //读取时间、价值、每种物品的数量
		if(p[i] == 0)p[i] = 1e6;
        //对于无限的物品,将它们的数量设置为总物品的最大值以模拟无限次选择
	}
	
	init(); 
    //二进制优化函数
	for(int i = 1;i<=cnt;i++)
		for(int j = m;j>=w[i];j--)
			dp[j] = max(dp[j],dp[j-w[i]]+v[i]);
	//01背包标准解法	
	cout << dp[m] <<"\n";
    //输出最终状态	
}

int main()
{
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	int _ = 1;
	while(_--)solve();
	return 0;
}

本人初学算法,能力有限,如有纰漏,敬请指正。

posted @ 2025-03-25 21:19  林克还是克林  阅读(166)  评论(0)    收藏  举报