PAT A1068 Find More Coins

思路:本题为0-1背包的变形版,求0-1背包的最小字典序问题。

其中 dp[i][j] 代表从 1 ~ i 号硬币中选择,在最大支付额度为 j 的条件下能支付的最大金额。

由于本题中负重和价值为同一数据,由状态转移方程 dp[i][j] = max{dp[i-1][j], dp[i-1][j - coins[i]] + coins[i]} 知其最大金额不会超出最大额度。

所以判断是否有解的条件为 dp[n][m] == m,其中 n 为硬币总数,m 为目标支付额度。

最小字典序:将硬币从大到小排序,先考虑大硬币,遍历到小硬币时可以进行拆分,即 dp[i][j] <= dp[i-1][j-coins[i]] + coins[i] 时进行的拆分。

求出路径时,对于 i 号硬币,判断 dp[i][j] == dp[i-1][j-coins[i]] + coins[i] 成立即需要选择该硬币。

空间优化:将 dp 优化成一维数组,根据状态转移方程需要改变 v 的遍历方向,另需要设置 choose 和 flag 数组来判断,代码可见《算法笔记》。

// 二维数组版
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

const int MAXN = 10005, MAXM = 105;
int coins[MAXN], dp[MAXN][MAXM];
int n, m;
vector<int> ans;

bool cmp(int a, int b) {
    return a > b;
}

int main() {
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        cin >> coins[i];
    sort(coins + 1, coins + n + 1, cmp);
    // dp过程
    for(int i = 1; i <= n; i++) {
        for(int v = coins[i]; v <= m; v++) {
            dp[i][v] = max(dp[i-1][v], dp[i-1][v-coins[i]] + coins[i]);
        }
    }
    // 输出路径
    if(dp[n][m] != m) cout << "No Solution";
    else {
        int j = m;
        for(int i = n; i >= 1; i--) {
            if(j >= coins[i] && dp[i][j] == dp[i - 1][j - coins[i]] + coins[i]) {
                ans.push_back(coins[i]);
                j -= coins[i];
            }
        }
        for(int i = 0; i < ans.size(); i++) {
            cout << ans[i];
            if(i < ans.size() - 1) cout << " ";
        }
    }
    return 0;
}