BZOJ 3090: Coci2009 [podjela]

描述

n 个农民, 他们住在 n 个不同的村子里,这 n 个村子形成一棵树。每个农民初始时获得 X 的钱,每一次操作, 一个农民可以从它自己的钱中, 取出任意数量的钱, 交给某个相邻村子的农民。对于每个农民给定一个值 vi, 问最少需要多少次操作, 使得每个农民最终拿到的钱 >= 给定的值。

约定: n2000 , X104 ,一定有解。

Sol

(这道题不像它的通过人数显示出来的那么难。。。)

注意到一个重要的事实:每条树边连接的两个村民之间至多进行一次操作,证明可以用归(xian)纳(ran)法。

这样的话就可以考虑树上DP,原问题要最小化操作次数,可能会想当然地列一个 f[x][i] 表示 x 为根的子树,进行一系列操作之后根节点可以给出/必须接受 i 元的最小操作次数,这样当然也能正确转移,只不过时空复杂度均无法承受。

只需要从上文的陷阱中跳出来,我们发现操作次数是 O(n) 的,那就可以考虑设 f[x][i] 表示 x 为根的子树,操作 i 次之后,根节点最多可以给出多少钱(如果这个值为负,那就代表必须要接受多少钱)。

来看转移,这个DP的决策为每条边是否进行操作,对每个点 x 依次考虑它的每个孩子,对于一条父子边 (x,y),有:

  1. 这条边加入图中 f[x][i]+f[y][j]f[x][i+j+1]

    • 如果 f[y][j]0 ,那么可以不要这条边,让树从这里断开: f[x][i]f[x][i+j]
    • 而答案即为最小的 i 使得 f[root][i]0

      稍有常识的人就能看出,这是一个背包的模型,而且 x 为根的子树的背包大小恰为 size[x],根据树上背包按 size 合并的理论,不难得到复杂度 O(n2)

      Code

      (感觉没有Code根本没人看。。。)

      #include <bits/stdc++.h>
      
      #define rep(i, x, y) for (int i = (x), _ = (y); i <= _; ++i)
      #define down(i, x, y) for (int i = (x), _ = (y); i >= _; --i)
      #define x first
      #define y second
      #define mp make_pair
      #define pb push_back
      #define bin(x) (1<<(x))
      
      using namespace std;
      typedef long long LL;
      typedef pair<int, int> pii;
      
      template<typename T> inline void upmax(T &x, T y) { x < y ? x = y : 0; }
      template<typename T> inline void upmin(T &x, T y) { x > y ? x = y : 0; }
      
      template<typename T>
      inline void read(T &x) {
          char c;
          while ((c = getchar()) < '0' || c > '9');
          for (x = c - '0'; (c = getchar()) >= '0' && c <= '9'; x = x * 10 + c - '0');
      }
      
      const int inf = 0x3f3f3f3f;
      const int N = 2005;
      
      vector<int> adj[N];
      
      int n, X, val[N];
      int size[N], f[N][N];
      
      void treeDP(int x, int fa) {
          size[x] = 1;
          rep (i, 0, adj[x].size()-1) {
              if (adj[x][i] != fa) {
                  treeDP(adj[x][i], x);
                  size[x] += size[adj[x][i]];
              }
          }
          static int g[2][N];
          int t = 0, tot = 0;
          rep (i, 0, tot)  g[t][i] = -inf;
          g[t][0] = val[x];
          rep (i, 0, adj[x].size()-1) {
              int to = adj[x][i];
              if (to != fa) {
                  rep (i, 0, tot+size[to])  g[t ^ 1][i] = -inf;
                  rep (j, 0, size[to]-1) {
                      int v = f[to][j];
                      rep (i, 0, tot) {
                          upmax(g[t ^ 1][i + j + 1], g[t][i] + v);
                      }
                  }
                  int mi = 0;
                  while (mi < size[to] && f[to][mi] < 0) ++mi;
                  if (mi < size[to]) {
                      rep (i, 0, tot) upmax(g[t ^ 1][i + mi], g[t][i]);
                  }
                  tot += size[to];
                  t ^= 1;
              }
          }
          rep (i, 0, tot) f[x][i] = g[t][i];
      }
      
      int main() {
      #ifdef LX_JUDGE
          freopen("in.txt", "r", stdin);
      #endif
          read(n), read(X);
          int x, y;
          rep (i, 1, n) {
              read(val[i]);
              val[i] = X - val[i];
          }
          rep (i, 2, n) {
              read(x), read(y);
              adj[x].pb(y);
              adj[y].pb(x);
          }
          treeDP(1, 0);
          int cur = 0;
          while (f[1][cur] < 0) ++cur;
          cout << cur << endl;
          return 0;
      }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值