动态规划:完全背包下的组合数与排列数问题

首先,将一切模板忘记,将滚动数组忘记。把目光集中于 【动态规划】本身。什么是动态规划的特性?【最优子结构】、【重叠子问题】、【状态转移方程】!

如何判断这个问题和最优子结构是否有关?和重叠子问题是否有关?画出搜索树!准确的来说,【用回溯法手动画出搜索树】!

这是方法论根本!!依据搜索树,不仅可以查看是否存在【重叠子问题】,也可以发掘【状态转移方程】,所谓动态规划/记忆化搜索,只不过是**【优化穷举法的“奇技淫巧”】**

通过题目来入手吧!
 

完全背包下的组合数:518零钱兑换II

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

假设每一种面额的硬币有无限个。

题目数据保证结果符合 32 位带符号整数。

先说结论!实际上,这题画出搜索树作用不是很大

如图所示,图中的【重复节点】很少,由于组合数的性质,为了去重(例如:{2,1,1} 和 {1,2,1}是等价的),那么导致每个节点的状态之一 【选择集合】有所不同,因此我们只能通过dp数组的定义来推导它们之间的关系,也就是【状态转移方程】

并且需要强调的是!dp数组一定要定义成二维的,因为我们的状态之一【选择集合】在搜索中不断变化(在不考虑 【空间压缩】这一优化手段时 )。

  • dp数组的定义:dp[i][j],使用coins中的 i 个硬币,可以组成金额 j 的方法(组合数)是多少?
  • 状态转移方程如何确定?其实这一步就是利用了【”子集“节点】(这是我自己定义的节点,意思是其中一个节点的所有状态(选择集合,目标值)是另一个节点所有状态的子集)那么如何推导呢?从dp数组的定义入手!首先分情况考虑:
if(j >= coins[i-1]) {
		dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]];
}
else {
		dp[i][j] = dp[i-1][j];
}

if … 成立时,说明我们可以从【选择集合】中,挑选当前 硬币 ,来凑目标值。由于我们考虑的问题是 【凑成目标值的组合数】,那么我们当然可以不挑选 当前硬币,也可以挑选当前硬币。前者对应的就是 dp[i-1][j],后者对应的就是dp[i][j-coins[i-1]

为什么在第二个维度一定是 j-coins[i-1] 呢?为了保证真的做了挑选!构成 dp[i][j-coins[i-1] 时,由于第一维度是 i 而不是 i-1,因此在凑成 目标值 = j-coins[i-1] 时可以挑选 coins[i-1] ,也可以不挑选 coins[i-1] ( j-coins[i-1] >= j 的情况下)(本质和我们现在计算dp[i][j]的逻辑是一致的),为了避免重复计算 不挑选coins[i-1] 的情况,所以 **第二维度 一定是 j-coins[i-1] 保证我们真的挑选了 【coins[i-1]】

至此可以发现,画出回溯法搜索树,确实帮助不大!但是在排列数是大大不同

考虑 压缩空间 的完全背包组合数问题

压缩空间这回事,只和 【base case】 和 【状态转移方程】挂钩,在二维dp数组下,【base case】 也就是:dp[i][0] = 1 而【状态转移方程】 是 dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]]; 或 dp[i][j] = dp[i-1][j];

考虑压缩第一个维度 i ,如图所示。

 

我们考虑一下:

base case怎么处理?

  • 由于dp[i][0] = 1,去除维度i后,直接将dp[0]初始化为1即可

去除维度i后,在更新dp[j]之前:

  • dp[j]对应什么?——dp[i-1][j]
  • dp[j-coins[i-1]]对应什么?——dp[i][j-coins[i-1]]

那么很容易写出:

vector<int> dp(amount+1, 0);
dp[0] = 1;
for(int i = 1; i <= n; ++i) {
    for(int j = 1; j <= amount; ++j) {
        if(j >= coins[i-1]) {
            dp[j] = dp[j] + dp[j-coins[i-1]];
        }
        else {
            dp[j] = dp[j];
        }
    }
}
return dp[amount];

完全背包下的排列数:377. 组合总和 Ⅳ

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

此题画出搜索树大有可为!与组合数不同,排列数讲究顺序,因此在【选择集合】做出选择后,【选择集合】这一状态不变!

如图所示:

 

画出搜索树后可以发现,其实所谓的【状态】,只有一个!那就是【目标值】,至于【选择集合】,它在每个节点中都一样,是不变化的,所以它不是状态!

它仅仅是选择!是【导致状态变化的行为】

由图中 的【 {1,2,4} | 2 】可以看出,计算它的值,也就是计算它做每个选择时对应的值的和,也就是说它存在重叠子问题!

我们很自然可以写出dp数组的定义,dp[j] 意味着任意挑选nums的目标值为 j 的排列数有几种?

很自然写出:

vector<int> dp(target+1, 0);
// base case
dp[0] = 1;
for(int j = 1; j <= target; ++j) {
		// long int 是为了防止溢出
		// j_sum 记录目标值为j时,通过各个选择得到的排列数
    long int j_sum = 0;
    for(int i = 0; i < n; ++i) {
        if(j >= nums[i]) {
            j_sum += dp[j-nums[i]];
        }
				// 完整展示逻辑
				else {
						j_sum += 0;
				}
    }
    dp[j] = j_sum;
}
return dp[target];

至此,我们通过搜索树可以发现,完全背包下的排列数,其实只有一种状态;而完全背包下的组合数,是有两个状态的!它们是【选择集合】与【目标值】,在做不同选择时(选择nums[i-1]与否)可能会导致【选择集合的变化】,也会导致【目标值】的变化

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值