[洛谷P3092]【[USACO13NOV]沒有找零No Change】

狀壓\(DP\) + 二分

考慮構成\(k<=16\)因此根據\(k\)構造狀壓\(dp\),將全部硬幣的使用狀況進行狀態壓縮git

考慮狀態:數組\(dp[i]\)表示用\(i\)狀態下的硬幣能夠購買到第幾個商品 ,\(f[i]\)表示狀態\(i\)下的花費數組

考慮轉移:使用當前硬幣的狀態必定由使用上一個硬幣的狀態轉移而來優化

舉個例子:以前狀態\(x\)\(dp[x] = y\)\(i = 2 = (010)_2\) ,當前枚舉到的狀態\(i = 3 = (011)_2\) , \(dp[i] = (dp[x] + 1\)開始能買到哪裏\((<=n))\), 至關於狀態\(x\)能購買到\(y\)號物品,\(i\)要從\(y+1\)號開始購買 ,\(i\)狀態比\(x\)狀態在二進制的第三位多了1,說明比i狀態多用了一個編號爲1的硬幣,\(f[i] = f[x]\) + 硬幣\(1\)的價值
狀態轉移完成spa

考慮具體作法:外層循環枚舉全部狀態,內層循環枚舉每一位,若當前狀態\(i\)的第\(j\)位爲\(1\),則能夠進行轉移code

而後能夠進行枚舉\(n\)件物品,考慮每一件是否能夠購買,一直到不能購買爲止get

由於一種狀態能夠被更新屢次,因此要取\(max\),保證\(dp\)數組存的是能買到的最大編號,而後更新\(dp\)數組和\(f\)數組it

若是到第\(n\)件均可以買,則能夠購買所有物品,\(ans\)記錄當前的最小花費,最後用全部硬幣的總面值減去最小花費即爲答案io

若是\(ans\)沒有被更新過,說明不能購買,輸出\(-1\)class

時間複雜度\(O(2^kkn)\),超時循環

考慮優化:發現每次枚舉物品統計價值來檢查是否可以購買是冗餘操做,能夠用前綴和預處理一下,而後每次檢查的時候進行一次二分就能夠了

時間複雜度\(O(2^kklogn)\),可經過本題

考慮正確性:由於外層循環枚舉狀態是從小到大枚舉,因此保證當前狀態某一位少\(1\)(即當前使用硬幣數減一)的狀態已經被轉移過了

注意事項
1.不要錯誤理解題意,注意每次支付只能支付一枚硬幣 ,不能算把硬幣湊出來的總錢數而後判斷能購買多少,這種錯誤作法能拿到\(93\)分的好成績(大霧)是由於數據太水

2.二分的時候注意初始的左端點,由於從使用當前硬幣的狀態轉移過來,因此要從使用當前硬幣前狀態所能購買到的物品\(+1\)做爲左端點進行二分,右端點不會變化一直是\(n\)

3.由於二分時要檢查的值要與前綴和數組進行比較,因此比較時前綴和數組應該減去左端點以前的前綴

4.注意二分的邊界問題以及最後的返回值

代碼:

#include <cstdio>
#include <cctype>
#define min(a, b) a < b ? a : b
#define MAXN 100001
#define N 17

int n, m, tot_money, ans = 2147483647;
int dp[1 << N], f[1 << N], sum[MAXN], pay[MAXN], coin[MAXN];

inline int read() {
    int s = 1, w = 0; char ch = getchar();
    for(; ! isdigit(ch); ch = getchar()) if(ch == '-') s = -1;
    for(; isdigit(ch); ch = getchar()) w = w * 10 + ch - '0';
    return s * w;
}
inline int check(int x, int cha) {
    int l = cha, r = n, mid;
    while(l <= r) {
      mid = (l + r) >> 1;
      if(sum[mid] - sum[cha - 1] == x) return mid;
      if(sum[mid] - sum[cha - 1] < x) l = mid + 1;
      else r = mid - 1;
    }
    return r;
}

int main() {
    m = read(), n = read();
    for(int i = 1; i <= m; i ++) coin[i] = read(), tot_money += coin[i];
    for(int i = 1; i <= n; i ++) pay[i] = read(), sum[i] = sum[i - 1] + pay[i];
    for(int i = 1; i < (1 << m); i ++) {
      for(int j = 0; j < m; j ++) if(i & (1 << j)) {
        int x = (i ^ (1 << j)), sum;
      if((sum = check(coin[j + 1], dp[x] + 1)) > dp[i]) 
        dp[i] = sum, f[i] = f[x] + coin[j + 1];
          if(dp[i] == n) ans = min(f[i], ans);
      } 
    }
    printf("%d", (tot_money - ans) < 0 ? -1 : tot_money - ans);
    return 0;
}
相關文章
相關標籤/搜索