DP:完全背包问题


在这里插入图片描述

在这里插入图片描述
博客主页:lyyyyyrics

🚀完全背包问题

🛸1. 引言

动态规划(DP)是算法中的重要技术,背包问题则是其中的经典问题之一。本篇博客将介绍完全背包问题及其解决方案。

🛸2. 问题定义

🛰️完全背包问题

在完全背包问题中,每种物品有无限个可用,目标是在限定的背包容量内,选择物品使得总价值最大。

🛰️数学描述

给定n种物品,每种物品有重量weight[i]和价值value[i]。背包容量为C,求解在不超过容量C的情况下,可以获得的最大价值。

🛸3. 动态规划思想

🛰️DP思想概述

动态规划通过将问题分解为子问题,利用子问题的解来构建最终解。完全背包问题具有最优子结构性质和重复子问题结构,非常适合用动态规划求解。

🛸4. 状态转移方程

🛰️方程推导

完全背包问题的公式推导如下:

我们有一个背包,容量为 V \text{V} V,并有 n n n 种物品,每种物品的重量分别为 w 1 , w 2 , . . . , w n w_1, w_2, ..., w_n w1,w2,...,wn,价值分别为 v 1 , v 2 , . . . , v n v_1, v_2, ..., v_n v1,v2,...,vn

d p [ i ] [ j ] dp[i][j] dp[i][j] 表示在前 i i i 种物品中,背包容量为 j j j 时能够获得的最大价值。

对于第 i i i 种物品,我们有两种选择:放入背包或不放入背包。

如果我们选择放入背包,那么背包容量减少 w i w_i wi,价值增加 v i v_i vi,即有 d p [ i ] [ j ] = d p [ i ] [ j − w i ] + v i dp[i][j] = dp[i][j-w_i] + v_i dp[i][j]=dp[i][jwi]+vi

如果我们选择不放入背包,那么背包容量不变,仍然有 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i1][j]

综上所述,完全背包问题的状态转移方程为:

d p [ i ] [ j ] = max ⁡ ( d p [ i ] [ j − w i ] + v i , d p [ i − 1 ] [ j ] ) dp[i][j] = \max(dp[i][j-w_i] + v_i, dp[i-1][j]) dp[i][j]=max(dp[i][jwi]+vi,dp[i1][j])

其中, 0 ≤ i ≤ n 0 \leq i \leq n 0in 0 ≤ j ≤ V 0 \leq j \leq \text{V} 0jV

初始条件为 d p [ 0 ] [ j ] = 0 dp[0][j] = 0 dp[0][j]=0,表示没有任何物品可选时,背包的价值为 0; d p [ i ] [ 0 ] = 0 dp[i][0] = 0 dp[i][0]=0,表示背包容量为 0 时,无法放入任何物品,价值也为 0。

最终的答案为 d p [ n ] [ V ] dp[n][\text{V}] dp[n][V],表示在前 n n n 种物品中,背包容量为 V \text{V} V 时能够获得的最大价值。

🚀例题

🛸1.【模版】完全背包

题目

在这里插入图片描述

样例输出和输入:

在这里插入图片描述

算法原理:
第一个问题:求背包不装满时,背包能装的最大价值。
状态表示:dp[i][j]表示前i个物品中,能选出的不超过容量j的最大价值。
状态转移方程:
在这里插入图片描述

推导出:
d p [ i ] [ j ] = max ⁡ ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v [ i ] ] + w [ i ] , d p [ i − 1 ] [ j − 2 v [ i ] ] + 2 w [ i ] , d p [ i − 1 ] [ j − 3 v [ i ] ] + 3 w [ i ] , . . . . . . , + d p [ i − 1 ] [ j − k v [ i ] ] + k w [ i ] ) dp[i][j] = \max(dp[i-1][j], dp[i-1][j-v[i]]+w[i], dp[i-1][j-2v[i]]+2w[i], dp[i-1][j-3v[i]]+3w[i], ......, +dp[i-1][j-kv[i]]+kw[i]) dp[i][j]=max(dp[i1][j],dp[i1][jv[i]]+w[i],dp[i1][j2v[i]]+2w[i],dp[i1][j3v[i]]+3w[i],......,+dp[i1][jkv[i]]+kw[i])
可以得出:

d p [ i ] [ j − v [ i ] ] = max ⁡ ( d p [ i − 1 ] [ j − v [ i ] ] , d p [ i − 1 ] [ j − 2 v [ i ] ] + w [ i ] , d p [ i − 1 ] [ j − 3 v [ i ] ] + 2 w [ i ] , . . . . . . . + d p [ i − 1 ] [ j − x v [ i ] ] + ( x − 1 ) w [ i ] ) dp[i][j-v[i]]=\max(dp[i-1][j-v[i]],dp[i-1][j-2v[i]]+w[i],dp[i-1][j-3v[i]]+2w[i],.......+dp[i-1][j-xv[i]]+(x-1)w[i]) dp[i][jv[i]]=max(dp[i1][jv[i]],dp[i1][j2v[i]]+w[i],dp[i1][j3v[i]]+2w[i],.......+dp[i1][jxv[i]]+(x1)w[i])
讨论下面式子中的最后一个x是否和上面的k相同,首先我们要知道,给定一个背包的容量,选择一个位置的值的极限是相同的,我们不能选择一个位置的值选择无穷多个,所以这里 j − k v [ i ] j-kv[i] jkv[i]的极限和 j − x v [ i ] j-xv[i] jxv[i]的极限是相同的,所以这里x和k应该相同,由于这两个相同,所以我们可以将第二个式子左右两边同时加上一个 w [ i ] w[i] w[i]可以得出:
d p [ i ] [ j − v [ i ] ] + w [ i ] = max ⁡ ( d p [ i − 1 ] [ j − v [ i ] ] + w [ i ] , d p [ i − 1 ] [ j − 2 v [ i ] ] + 2 w [ i ] , d p [ i − 1 ] [ j − 3 v [ i ] ] + 3 w [ i ] , . . . . . . . + d p [ i − 1 ] [ j − x v [ i ] ] + x w [ i ] ) dp[i][j-v[i]]+w[i]=\max(dp[i-1][j-v[i]]+w[i],dp[i-1][j-2v[i]]+2w[i],dp[i-1][j-3v[i]]+3w[i],.......+dp[i-1][j-xv[i]]+xw[i]) dp[i][jv[i]]+w[i]=max(dp[i1][jv[i]]+w[i],dp[i1][j2v[i]]+2w[i],dp[i1][j3v[i]]+3w[i],.......+dp[i1][jxv[i]]+xw[i])
上面这个式子和第一个式子只差了一个 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j]所以我们等效替换:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − v [ i ] ] + w [ i ] ) dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i]) dp[i][j]=max(dp[i1][j],dp[i][jv[i]]+w[i])
所以最后:
状态转移方程就是:dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i]),完全背包问题统一都是这个状态转移方程,基本上。
代码:
为优化:

#include <cstring>
#include<iostream>
#include<vector>
const int N = 1001;
int n, V;
int dp[N][N];
int v[N], w[N];
int main()
{
	cin >> n >> V;
	for (int i = 1;i <= n;i++)
	{
		cin >> v[i] >> w[i];
	}
	for (int i = 1;i <= n;i++)
	{
		for (int j = 1;j <= V;j++)
		{
			dp[i][j] = dp[i - 1][j];
			if (j >= v[i])dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]] + w[i]);
		}
	}
	cout << dp[n][V] << endl;
	memset(dp, 0, sizeof dp);
	for (int j = 1;j <= V;j++)
	{
		dp[0][j] = -1;
	}
	for (int i = 1;i <= n;i++)
	{
		for (int j = 1;j <= V;j++)
		{
			dp[i][j] = dp[i - 1][j];
			if (j >= v[i] && dp[i][j - v[i]] != -1)dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]] + w[i]);
		}
	}
	cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
	return 0;
}

滚动数组优化后的代码:

#include <cstring>
#include<iostream>
#include<vector>
using namespace std;

const int N = 1001;
int n, V;
int dp[N];
int v[N], w[N];
int main()
{
	cin >> n >> V;
	for (int i = 1;i <= n;i++)cin >> v[i] >> w[i];
	for (int i = 1;i <= n;i++)
		for (int j = v[i];j <= V;j++)
			dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
	cout << dp[V] << endl;
	memset(dp, 0, sizeof dp);
	for (int j = 1;j <= V;j++)dp[j] = -1;
	for (int i = 1;i <= n;i++)
		for (int j = v[i];j <= V;j++)
			if (dp[j - v[i]] != -1)dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
	cout << (dp[V] == -1 ? 0 : dp[V]) << endl;
	return 0;
}

运行结果:
在这里插入图片描述

🛸2.零钱兑换

题目

在这里插入图片描述

样例输出和输入:

在这里插入图片描述

这道题首先需要注意到无限,无限这个词就表示这道题很可能是背包问题中的完全背包问题,这道题很显然也是。
算法原理:
状态表示: d p [ i ] [ j ] dp[i][j] dp[i][j]表示 i i i位置之前的所有数中的组合能凑出来是 a m o u n t amount amount的最小的组合的硬币个数。
状态转移方程:
很显然这道题的状态转移方程是:

d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − c o i n s [ i ] ] + 1 ) dp[i][j]=max(dp[i-1][j],dp[i][j-coins[i]]+1) dp[i][j]=max(dp[i1][j],dp[i][jcoins[i]]+1)

代码:
未优化的代码:

class Solution {
public:
	int coinChange(vector<int>& coins, int amount) {
		int n = coins.size();
		vector<vector<int>> dp(n + 1, vector<int>(amount + 1));
		for (int j = 1; j <= amount; j++)
			dp[0][j] = 0x3f3f3f3f;
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= amount; j++) {
				dp[i][j] = dp[i - 1][j];
				if (j >= coins[i - 1])
					dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i - 1]] + 1);
			}
		}
		return dp[n][amount] == 0x3f3f3f3f ? -1 : dp[n][amount];
	}
};

优化过后的代码:

class Solution {
public:
	int coinChange(vector<int>& coins, int amount)
	{
		int n = coins.size();
		int INF = 0x3f3f3f3f;
		vector <int> dp(amount + 1, INF);
		dp[0] = 0;
		for (int i = 1;i <= n;i++)
			for (int j = coins[i - 1];j <= amount;j++)
				dp[j] = min(dp[j], dp[j - coins[i - 1]] + 1);
		return dp[amount] == INF ? -1 : dp[amount];
	}
};

运行结果:

在这里插入图片描述

🛸3.零钱兑换Ⅱ

题目

在这里插入图片描述

样例输出和输入:

在这里插入图片描述

算法原理:
这道题和上一道题基本上是一样的:
状态表示: d p [ i ] [ j ] dp[i][j] dp[i][j]表示前 i i i个位置的硬币中能凑成 a m o u n t amount amount的方法总数。
状态转移方程:
第一种状态:不选择 i i i位置-> d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j]
第二种状态:选择 i i i位置-> d p [ i ] [ j − c o i n s [ i ] ] dp[i][j-coins[i]] dp[i][jcoins[i]]
总的方法数就是这两个之和:

d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i ] [ j − c o i n s [ i ] ] dp[i][j]=dp[i-1][j]+dp[i][j-coins[i]] dp[i][j]=dp[i1][j]+dp[i][jcoins[i]]

代码:
未优化的代码:

class Solution {
public:
    int change(int amount, vector<int>& coins)
    {
        int n = coins.size();
        vector<vector<int>> dp(n + 1, vector<int>(amount + 1));
        for (int i = 0;i <= n;i++)
        {
            dp[i][0] = 1;
        }
        for (int i = 1;i <= n;i++)
        {
            for (int j = 1;j <= amount;j++)
            {
                dp[i][j] = dp[i - 1][j];
                if (j >= coins[i - 1])dp[i][j] += dp[i][j - coins[i - 1]];
            }
        }
        return dp[n][amount];
    }
};

优化过后的代码:

class Solution {
public:
    int change(int amount, vector<int>& coins) 
    {
        int n = coins.size();
        vector<int> dp(amount + 1);
        dp[0] = 1;
        for (int i = 1;i <= n;i++)
            for (int j = coins[i - 1];j <= amount;j++)
                dp[j] += dp[j - coins[i - 1]];
        return dp[amount];
    }
};

运行结果:
在这里插入图片描述

🛸4.完全平方数

题目

在这里插入图片描述

样例输出和输入:

在这里插入图片描述

算法原理:
状态表示: d p [ i ] [ j ] dp[i][j] dp[i][j]表示前i个数中的完全平方和数之和能等于n的最少的那个组合的个数。
状态转移方程:
第一种状态:不选择 i i i位置 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j]
第二种状态:选择 i i i位置 d p [ i ] [ j − i ∗ i ] + 1 dp[i][j-i*i]+1 dp[i][jii]+1
最后两个状态中的最少的那个:

d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − i ∗ i ] ) dp[i][j]=min(dp[i-1][j],dp[i][j-i*i]) dp[i][j]=min(dp[i1][j],dp[i][jii])

代码:

class Solution {
public:
    int numSquares(int n) 
    {
        int m = sqrt(n);
        vector<vector<int>> dp(m + 1, vector<int>(n + 1));
        int INF = 0x3f3f3f3f;
        for (int j = 1;j <= n;j++)
        {
            dp[0][j] = INF;
        }
        for (int i = 1;i <= m;i++)
        {
            for (int j = 1;j <= n;j++)
            {
                dp[i][j] = dp[i - 1][j];
                if (j >= i * i)dp[i][j] = min(dp[i - 1][j], dp[i][j - i * i] + 1);
            }
        }
        return dp[m][n];
    }
};

优化:

class Solution {
public:
    int numSquares(int n)
    {
        int m = sqrt(n);
        int INF = 0x3f3f3f3f;
        vector<int> dp(n + 1,INF);
        dp[0] = 0;
        for (int i = 1;i <= m;i++)
            for (int j = i * i;j <= n;j++)
                dp[j] = min(dp[j], dp[j - i * i] + 1);
        return dp[n];
    }
};

运行结果:
在这里插入图片描述

🚀总结

通过对完全背包问题的深入探讨,我们了解了动态规划在解决这类问题中的重要性。完全背包问题在实际应用中非常广泛,例如货币兑换、资源分配和路径规划等。在解决过程中,我们学会了如何定义状态、确定状态转移方程,并通过优化空间复杂度提升算法效率。

关键在于,理解并掌握动态规划的核心思想,能够帮助我们从容应对各种复杂的优化问题。希望通过本文的介绍,大家对完全背包问题有了更清晰的理解,并能将其应用到实际问题中去。

未来的学习中,我们可以尝试更多变体问题的解决方法,不断拓展自己的算法知识。祝大家在学习动态规划的道路上取得更大进步!

  • 70
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 29
    评论
评论 29
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值