ARC096 F - Sweet Alchemy
問題ページ
F - Sweet Alchemy
解法
問題の整理
d[i] = c[i] - c[p[i]] とするとドーナツをつくれる個数の条件である c[p[i]] <= c[i] <= c[p[i]] + D を 0 <= d[i] <= D と言い換えることができる。ただし根に関してはdに制限はなく 0 <= d[0] となる。
頂点iを根とする部分木について全ての頂点について1つずつドーナツを作る操作を考える。この操作は頂点iを根とする部分木の頂点jについてm[j]の総和のコストがかかり、部分木の頂点数分のドーナツをつくることができる。また、この操作を行うとd[i]が1増える。したがってこの操作は各部分木についてd[i]の上限までしか行うことができない。
ここまでをまとめると、重さがX[i]=(頂点iを根とする部分木の頂点jについてm[j]の総和)で価値がY[i]=(部分木の頂点数分)、個数の上限がZ[i]=(根はINF、それ以外の頂点でD)となるアイテムがN個存在するナップザック問題に帰着できる。
ナップザック問題
まず貪欲に考える。そのアイテムのコスパをY[i]/X[i]で定義し降順にソートする。
p<qであるような2つのアイテムに着目する。もしアイテムqからY[p]個取り出すことができ、アイテムpにY[q]個加算することができれば、この操作は必ず得になる。価値はY[p]d[p] + Y[q]d[q] → Yp + Yq = Y[p]d[p] + Y[q]d[q]で変動なし。重さは X[p]d[p] + X[q]d[q] >= Xp + Xq = X[p]d[p] + X[q]d[q] + X[p]Y[q] - X[q]Y[p] となり小さくなっている。Y[i]<=50なのでpに50個以上まだ取れる余裕があり、qが50個以上なら必ずこの操作は行える。
各アイテムを50個と残りに2分割する。すると、50個の方から一部選び、残りの方はgreedyに選べばいいことがわかる。50個の方はDP、残りは貪欲で解ける。
DP
蟻本P302に載っている個数制限付きナップザックをします。価値の最大値は50^3、個数がNでO(N^4logN)になります。蟻本とは重さと価値が逆ですが本質は同じです。
貪欲
コスパがよいものから取れるだけ取っていく貪欲をします。DPの方でi個取ったとするとX-dp[i]グラムでつくれるだけつくります。
全体でO(N^4logN)で解けました。
ソースコード
#include <bits/stdc++.h> using namespace std; using ll = long long; #define int ll using VI = vector<int>; using VVI = vector<VI>; using PII = pair<int, int>; #define FOR(i, a, n) for (ll i = (ll)a; i < (ll)n; ++i) #define REP(i, n) FOR(i, 0, n) #define ALL(x) x.begin(), x.end() #define PB push_back const ll LLINF = (1LL<<60); const int INF = (1LL<<30); const int MOD = 1000000007; template <typename T> T &chmin(T &a, const T &b) { return a = min(a, b); } template <typename T> T &chmax(T &a, const T &b) { return a = max(a, b); } template <typename T> bool IN(T a, T b, T x) { return a<=x&&x<b; } template<typename T> T ceil(T a, T b) { return a/b + !!(a%b); } template<class S,class T> ostream &operator <<(ostream& out,const pair<S,T>& a){ out<<'('<<a.first<<','<<a.second<<')'; return out; } template<class T> ostream &operator <<(ostream& out,const vector<T>& a){ out<<'['; REP(i, a.size()) {out<<a[i];if(i!=a.size()-1)out<<',';} out<<']'; return out; } int dx[] = {0, 1, 0, -1}, dy[] = {1, 0, -1, 0}; VI g[55]; int m[55]; signed main(void) { cin.tie(0); ios::sync_with_stdio(false); int n, x, d; cin >> n >> x >> d; cin >> m[0]; FOR(i, 1, n) { int p; cin >> m[i] >> p; g[p-1].PB(i); } // 頂点iの部分木を取ったときのコストXと価値Yを求める VI X(n), Y(n); function<void(int)> dfs = [&](int v) { X[v] = m[v], Y[v] = 1; for(int i: g[v]) { dfs(i); X[v] += X[i]; Y[v] += Y[i]; } }; dfs(0); VI idx(n); REP(i, n) idx[i] = i; // Y[x]/X[x] >= Y[y]/X[y] ⇔ Y[x]*X[y] >= Y[y]*X[x] でソート sort(ALL(idx), [&](int x, int y){return Y[x]*X[y]>Y[y]*X[x];}); // dp[i] = (価値がiのときの最小の重さ) VI dp(50*50*50+5, INF); // i番目のアイテムについて重さと価値がそれぞれ(X[i],Y[i])(2X[i],Y[i])…(2^kX[i],2^kY[i])が一つずつ存在すると考える dp[0] = 0; REP(i, n) { int num = idx[i]?min(50LL,d):50; for(int k=1; num; k<<=1) { int mul = min(k, num); for(int j=50*50*50; j>=0; --j) { if(j+mul*Y[idx[i]] <= 50*50*50) { chmin(dp[j+mul*Y[idx[i]]], dp[j] + mul*X[idx[i]]); } } num -= mul; } } int ans = 0; REP(i, 50*50*50+1) { // i個作ったとして残りをgreedyに決める if(dp[i]>x) continue; int ret=i, w = x-dp[i]; REP(j, n) { // cnt個取る int cnt = idx[j]?max(0LL, d-50):INF; chmin(cnt, w/X[idx[j]]); ret += cnt*Y[idx[j]]; w -= cnt*X[idx[j]]; } chmax(ans, ret); } cout << ans << endl; return 0; }