YACS 2022年9月月赛 乙组 T4 购买商品 题解
$T2$ 太水了,$T3$ 我不会(现在知道也是状压了),讲一下 $T4$ 。
状压DP题解
首先我们需要记住一个结论就是:
这种需要你算排列组合的但是你打 $n!$ 暴力过不了的,数据范围又在 $20$ 以内的,多半是状压。
相似题目:$Luogu P1433$ 吃奶酪
先来解释一下状压 $DP$ 为什么能将 $n!$ 暴力转换成 $2^n DP$
如果是爆搜的话,我们需要考虑选择的顺序。
如果是状态压缩的话,你只需要告诉他哪些选,哪些不选。
他就会运用之前的状态组合出最佳解。
所以状压就是省去了排列组合。
我们设 $f[i][j]$ 为当前使用了 $i$ 张购物卡,每张购物卡用没用的状态是 $j$ 时,最多能支付到第几件商品。
为了有序枚举,我们需要把这些 $01$ 状态预处理一下,
按照这个状态用了多少张购物卡把他们存到一个 $vector$ 里,
$vector[1]$ 中存储的就是用了 $1$ 张购物卡的状态
$vector[2]$ 就是用 $2$ 张的,以此类推...
这样就能方便转移。
我们枚举用了多少张购物卡,然后遍历这些合法的状态,枚举最后一张用的哪张购物卡,就可以转移了。
答案也很好找,每次算的时候我们把这个状态没用的购物卡剩余的金额加起来,等于 $sum$。
如果当前这个状态能支付所有的商品,那么剩余的钱就是 $sum$ ,选一个最大的即可。
如果 $ans$ 没有被更新,那就是没有答案,输出" $Balance is insufficient$ "即可。
状态转移方程为
$f[i][vector[i][j]] = max(f[i][vector[i][j]],shop(v[z[m]],f[i - 1][vector[i][j] - (1 << z[m] - 1)] + 1))$;
( $v[i][j]$ 就是用i张购物卡的状态,$shop$ 就是计算当前支付完第 $i-1$ 件,现在你有一张价值为 $w[z[m]]$ 的卡,你能支付到第几件商品)
理解大意即可,时间复杂度为 $O(n\times k\times 2^k)。$
过了一小会儿我想到了二分优化(为什么总是二分?)
二分你可以支付到第几件商品。
加一个预处理前缀和,就能过了。
这是新的 $shop$ 函数:
long long shop(long long sur,long long num) { long long ans; long long l = num,r = n,mid; while(l <= r) { mid = l + r >> 1; if(p[mid] - p[num - 1] <= sur) { ans = mid; l = mid + 1; } else r = mid - 1; } return ans; }
最后终于卡过了!(理论上说,这题可以卡到 $17,825,792$,但是因为我技术高 ($ruo$) 超 ($bao$),给了点面子,就过了)
最终时间复杂度为 $O(k*logn*2^k)$
#include <vector> #include <iostream> using namespace std; long long n,k,ans = -2147283648; long long p[100010],w[17]; long long f[17][1 << 16]; vector<long long> v[17],z; long long fun(long long x) { long long cnt = 0; while(x) { x &= x - 1; cnt ++; } return cnt; } long long shop(long long sur,long long num) { long long ans; long long l = num,r = n,mid; while(l <= r) { mid = l + r >> 1; if(p[mid] - p[num - 1] <= sur) { ans = mid; l = mid + 1; } else r = mid - 1; } return ans; } signed main() { scanf("%d",&n); for(int i = 1;i <= n;i ++) { scanf("%d",&p[i]); p[i] += p[i - 1]; } scanf("%d",&k); for(int i = 1;i <= k;i ++) scanf("%d",&w[i]); for(int i = 1;i <= k;i ++) for(int j = 1;j < 1 << k;j ++) if(fun(j) == i) v[i].push_back(j); for(int i = 1;i <= k;i ++) { for(int j = 0;j < v[i].size();j ++) { z.clear(); long long sum = 0; for(int m = 1;m <= k;m ++) { if(long(v[i][j] & long(1 << m - 1)) == 0) { sum += w[m]; continue; } z.push_back(m); } for(int m = 0;m < z.size();m ++) { long long res = f[i - 1][v[i][j] - (1 << z[m] - 1)]; f[i][v[i][j]] = max(f[i][v[i][j]],shop(w[z[m]],res + 1)); } if(f[i][v[i][j]] == n) ans = max(ans,sum); } } if(ans >= 0) cout << ans << endl; else cout << "Balance is insufficient" << endl; return 0; }
如果你还想做这种状压,可以去尝试下 $Luogu P1433$ 吃奶酪