ferinの競プロ帳

競プロについてのメモ

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;
}