快轉到主要內容

CSES-1745 Money Sums

目錄

題目連結:https://cses.fi/problemset/task/1745

題意
#

你手上有 \( n \) 枚硬幣,每枚硬幣都有各自的面額。
你的任務是找出:利用這幾枚硬幣,你總共可以湊出幾種不同的金額?並把所有能湊出來的金額由小到大列印出來。

思路
#

這題的核心概念是 0/1 背包問題的變形,但在這我們只需要關心「這個金額能不能湊出來」,也就是一個布林值(True 或是 False)。

既然只需要儲存 0 狀態 跟 1 狀態,我們最直覺的作法是開一個布林的 dp 陣列,然後對於每一枚新的硬幣(面額 x),去檢查之前所有能湊出來的金額 v,把它變成能湊出 v + x 的狀態。

但既然狀態只有 0 跟 1,C++ 裡面有一個為這量身打造的超強武器:bitset
我們可以把整個可達成的金額狀態看成一長串的 bit(如果第 i 個 bit 是 1,代表金額 i 湊得出來)。
當我們拿到一枚面額為 x 的硬幣時,其實就是把原本所有能湊出來的金額統統加上 x。在位元運算裡面,這就等於是把整個 bitset 往左平移 x 格(bs << x)。
然後我們再把平移後的新狀態,跟原本舊狀態做個位元聯集(|),意思是「維持原本湊得出來的金額」加上「新湊出來的這些金額」。

這個 bs |= bs << x 的寫法,不僅程式碼極度精簡,底層位元運算更能一次處理多個狀態,比起用迴圈跑 DP,效能快上非常多。

程式碼
#

#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define fi first
#define se second
#define INF LONG_LONG_MAX/1000
#define WA() cin.tie(0)->sync_with_stdio(0)
#define all(x) (x).begin(), (x).end()
#define int long long
#define PII pair<int, int>

signed main() { WA();
    // 總面額上限是 100 * 1000 = 100000,所以開到 100001
    bitset<100001> bs; 
    // bs.set(0, 1) 代表湊出 0 元的方法永遠可行
    bs.set(0, 1);
    
    int n; 
    for (cin >> n; n--;) {
        int x; cin >> x;
        // 把舊的狀態左移 x 位,相當於所有能湊出的金額加上 x
        // 然後和舊狀態做 OR 運算,合併成新的可用狀態
        bs |= bs << x;
    }
    
    // count() 會計算裡面有幾個 1
    // 因為 0 元不算在題目要的答案裡,所以要扣掉 1
    cout << bs.count() - 1 << '\n';
    
    // 依序印出所有可以湊出來的金額
    for (int i = 1; i <= 100000; i++) {
        if (bs[i]) cout << i << ' ';
    }
}
Piau 的筆記本
作者
Piau 的筆記本
希望我寫下來的東西能夠長久的記在我的腦中