【No.15】蓝桥杯动态规划上|最少硬币问题|0/1背包问题|小明的背包1|空间优化滚动数组(C++)

本文介绍了动态规划在解决最少硬币问题中的应用,通过状态转移和递推关系展示了如何通过贪心策略优化,以及如何通过记忆化避免重复计算。同时提及了0/1背包问题作为动态规划的经典案例。

DP初步:状态转移与递推

最少硬币问题
  • 有多个不同面值的硬币(任意面值)
  • 数量不限
  • 输入金额S,输出最少硬币组合。
    回顾用贪心求解硬币问题
    硬币面值1、2、5。支付13元,要求硬币数量最少
    贪心:
    (1)5元硬币,2个
    (2)2元硬币,1个
    (3)1元硬币,1个
    硬币面值1、2、4、5、6.,支付9元。
    贪心:
    (1)6元硬币,1个
    (2)2元硬币,1个
    (3)1元硬币,1个
    错误!
    答案是:5元硬币+4元硬币=2个
硬币问题的正解是动态规划

type = [1,5,10,25,50] 5种面值
定义数组Min[],记录最少硬币数量:
对输入的某个金额i,Min[i]是最少的硬币数量
第一步,只考虑1元面值的硬币

		金额i: 0,1,2,3,4,5
硬币数量`Min[]`:0,1,2,3,4
  • i=1元时,等价于:i = i - 1 = 0 元需要的硬币数量,加上1个1元硬币
  • i=2元时,等价于:i = i - 1 = 1 元需要的硬币数量,加上1个1元硬币

  • 在1元硬币的计算结果基础上,再考虑加上5元硬币的情况,从i=5开始就行了
    i=5元时,等价于
  1. i = i - 5 = 0元需要的硬币数量,加上1个5元硬币,Min[5] = 1
  2. 原来的Min[5] = 5
    取1和2的最小值,所以Min[5] = 1
    i = 6元时,等价于
  3. i = i - 5 = 1元需要的硬币数量,加上1个5元硬币,Min[6] = 2
  4. 原来的Min[6] = 6
    取1和2的最小值,所以Min[6] = 2
    Min[6] = Min[5] + 1
    递推关系:
    Min[i] = min (Min[i], Min[i - 5] + 1)
    继续处理其它面值硬币
#include <iostream>
#include <vector>
#include <limits.h>
using namespace std;

void solve(int s)
{
	int cnt = 5;   //5种硬币
	vector<int> type = {1,5,10,25,50};  //5种面值
	vector<int> Min(s+1, INT_MAX);   //初始化为无穷大
	Min[0] = 0;
	for (int j = 0; j < cnt; j ++)  //5种硬币
	{
		Min[i] = min (Min[i], Min[i - type[j]] + 1);
	}
	cout << Min[s] << endl;
}

int main()
{
	int s;
	cin >> s;
	solve(s);
	return 0;
}
DP的两个特征
  1. 重叠子问题
    在递归算法中,尤其是在解决最优化问题时,经常会遇到这样的情况:在求解大问题的过程中,我们需要多次求解规模更小、结构相同的问题。这些小问题被称为子问题。如果这些子问题在大问题求解过程中被重复计算多次,这将导致算法效率低下,因为大量时间被重复的子问题求解所占据。动态规划通过存储子问题的解(通常在二维数组中,称为DP表),确保每个子问题只计算一次,从而避免了重复计算。当需要某个子问题的解时,直接从DP表中查找,如果该子问题尚未解决,则先解决它,然后存储其解。
  2. 最优子结构
    这是动态规划能够成功解决许多问题的另一个关键特性。最优子结构是指一个问题的最优解包含其子问题的最优解。换句话说,如果我们能找到所有子问题的最优解,那么我们可以通过这些子问题的最优解来构建原问题的最优解。动态规划利用这个性质,通过自底向上的方式(即先解决最基础的子问题,然后逐步解决更大规模的子问题)来构建问题的最优解。
DP:记忆化

如果各个子问题不是独立的,如果能够保存已经解决的子问题的答案,在需要的时候再找出已求得的答案,可以避免大量的重复计算。
基本思路:用一个表记录所有已解决的子问题的答案,不管该问题以后是否被用到,只要它被计算过,就将其结果填入表中。
记忆化
解题步骤

  • 拆分问题
  • 定义状态(并找出初状态)
  • 状态转移方程
    一般的模型方法
  • 递归搜索法
  • 记忆化搜索(记忆化暴力)
  • 递推式法
最经典的DP问题:0/1背包

给定n种物品和一个背包,物品i的重量是wiw_{i}wi其价值为viv_{i}vi,背包的容量为C.
背包问题:选择装入背包的物品,使得装入背包中物品的总价值最大
如果在选择装入背包的物品时,对每种物品i只有两种选择:装入背包或不装入背包,称为0/1耆包问题,
与装载问题不同的是,0/1背包不能只装一部分,要么选,要么不选。

xix_{i}xi表示物品i装入背包的情况
xix_{i}xi=0,表示物品i没有被装入背包
xix_{i}xi=1,表示物品i被装入背包
约束条件:

### 滚动数组01背包问题中的实现与优化 #### 一、滚动数组的基本原理 在传统的动态规划解决01背包问题时,通常会使用二维数组`dp[i][j]`表示前`i`个物品放入容量为`j`的背包所能获得的最大价值。然而,这种方法的空间复杂度较高,为`O(nW)`,其中`n`是物品数量,`W`是背包容量。通过观察状态转移方程`dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])`[^3],可以发现当前状态`dp[i][j]`仅依赖于上一行的状态`dp[i-1][...]`,因此可以通过滚动数组将二维数组优化为一维数组。 #### 二、滚动数组优化过程 1. **空间优化** 使用一维数组`dp[j]`代替二维数组`dp[i][j]`,其中`dp[j]`表示容量为`j`的背包所能容纳的最大价值。通过逆序遍历背包容量`j`(从`W`到`w[i]`),可以确保每个物品只被考虑一次,避免重复计算。 这种逆序遍历的方式保证了在更新`dp[j]`时,使用的仍然是未更新的`dp[j-w[i]]`值,从而正确模拟了二维数组的状态转移过程[^4]。 2. **代码实现** 下面是一个基于滚动数组优化01背包问题的Java实现: ```java public class Knapsack { public static int knapsack(int[] weights, int[] values, int capacity) { int n = weights.length; int[] dp = new int[capacity + 1]; for (int i = 0; i < n; i++) { for (int j = capacity; j >= weights[i]; j--) { dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]); } } return dp[capacity]; } public static void main(String[] args) { int[] weights = {2, 3, 4}; int[] values = {3, 4, 5}; int capacity = 5; System.out.println("最大价值: " + knapsack(weights, values, capacity)); } } ``` 3. **时间复杂度与空间复杂度** - 时间复杂度:仍为`O(nW)`,因为需要对每个物品和每个背包容量进行遍历。 - 空间复杂度:由`O(nW)`降低至`O(W)`,显著减少了内存消耗[^2]。 #### 三、滚动数组的关键点 - **逆序遍历**:为了防止在更新`dp[j]`时覆盖掉尚未使用的`dp[j-w[i]]`值,必须从大到小遍历背包容量。如果正序遍历,则会导致同一物品被多次放入背包中,违背01背包问题的规则[^4]。 - **边界条件**:当背包容量`j`小于当前物品重量`w[i]`时,无法放入该物品,因此无需更新`dp[j]`[^3]。 #### 四、滚动数组的应用场景 滚动数组不仅适用于01背包问题,还可以推广到其他动态规划问题中,只要满足当前状态仅依赖于少数几个之前状态的情况。例如,在某些矩阵路径问题或字符串匹配问题中,也可以通过类似的方法减少空间开销[^5]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值