零钱兑换:完全背包的变形

Give me your money!!1

「我的做题历程」:

step1:观察题面。

  「编写一个函数来计算可以凑成总金额」,可以得出这是一道背包 DP
  「每种硬币的数量是无限的」,进一步得出这是道完全背包。(题型:完全背包)
  「最少的硬币个数」,证明这要在背包的前提下,求出最小组成数量。
  「多组测试数据」,谨记多组输入 (论 Wrong Answer 与没有多组输入)。(注意:多组输入)


step2:思考解法。

  第一步,思考 dp 状态:

\(dp_{i,j}\):前 \(i\) 种硬币凑出面值 \(j\)

\(coins_{i}\) 而言,只有取或不取两种状态。
  若,取后的币数为前 \(i - 1\) 种硬币凑出面值 \(j-w_{i}\times k\) 的总币数加上当前种类所需币数 \(k\)。
  若不取,则说明前 \(i - 1\) 种硬币已经能够凑出面值 \(j\),不需要再取。

  第二步,思考状态转移方程:
  原本完全背包的状态转移方程是:

\[dp_{i, j} = \max\{dp_{i - 1, j}, dp_{i - 1, j - a_{i}}+a_{i}\}\ (a_{i}\le j\le amount) \]

  但这里我们并不是求总金额以内最大能凑出的面值,而是求凑成总金额的最少币数,于是就有:

\[dp_{i,j}=\min\{dp_{i - 1, j},\,dp_{i - 1, j - a_{i}} + 1\}\ (a_{i}\le j\le amount) \]

  通过观察发现,上述方程可以降维。由于对 \(dp_{i}\) 有影响的只有 \(i - 1\),故可以把前一维抹掉,但需要保证 \(dp_{i,j}\) 可以被 \(dp_{i, j - a_{i}}\)(即 \(dp_{i,j}\) 被计算时 \(dp_{i, j - a_{i}}\) 已经被算出),这才相当于物品 \(i\) 多次被放入背包,所以枚举当前面值 \(j\) 时要正序。

  第三步,打出完全背包的代码,把状态转移方程换一下,于是本题的算法部分就完成啦:

for (int i = 1; i <= n; i++) {
	for (int j = a[i]; j <= amount; j++) {
		dp[j] = min(dp[j], dp[j - a[i]] + 1);
	}
}


step3:完成代码:

  通过数据范围可以发现,一种硬币的面额是可以比总金额大的,因此可以预处理浅浅优化一下(虽然没什么大的效果)。
  因为找的是最小币数,所以 dp 数组要初始化成极大值,而前 \(0\) 种硬币凑成 面值 \(0\) 只需要 \(0\) 种硬币,由此可得 \(dp_{0} = 0\)。
  输出时值得注意的是,「如果没有任何一种硬币组合能组成总金额,输出 \(-1\)」;在代码中,这意味着「如果 \(dp_{amount}\) 没有被更新,则输出 \(-1\)」,所以只需要输出时特判一下 \(dp_{amount}\) 若仍是初始值就输出 \(-1\)。
\

代码(抵制学术不端行为,拒绝 Ctrl + C):

#include <bits/stdc++.h>
using namespace std;
const int N = 1e2 + 5, A = 1e4 + 5, INF = 0x3f3f3f3f;
int n, amount, a[N], dp[A];
/* 
dp(i, j): 前 i 个硬币凑出 j 的最少硬币个数
dp(i, j) = min(dp(i - 1, j - a[i]), dp(i - 1, j));
                       取这个硬币 or 不取这个硬币
*/
int main() {
    freopen("exchange.in", "r", stdin);
    freopen("exchange.out", "w", stdout);
    while (~scanf("%d %d", &n, &amount)) {
        memset(dp, 0x3f, sizeof dp);
        for (int i = 1; i <= n; i++) {
            scanf("%d", a + i);
        }
        dp[0] = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = a[i]; j <= amount; j++) {
                dp[j] = min(dp[j - a[i]] + 1, dp[j]);
            }
        }
        printf("%d\n", dp[amount] == INF ? -1 : dp[amount]); // 可以使用三目运算符来特判
    }
    return 0;
}