树形DP I



前言

本次内容比较水,算是接触树形DP吧,谈不上入门

  1. 树形DP
    https://blog.csdn.net/txl199106/article/details/45373507
    https://www.cnblogs.com/mhpp/p/6628548.html

Anniversary party - HDU - 1520


题意
给定一棵树,子节点和父亲节点不能同时选,问最大价值
思路
定义DP状态f[u][0]为不选u节点的最大价值,f[u][1]为选了u节点的最大价值,则对于u的孩子节点v
f[u][0] = sum(max(f[v][0], f[v][1])),不选了u,v也可以选也可以不选
f[u][1] = sum(f[v][0])选了u,v只能不选
DFS从上往下扫一遍,在回溯时DP即可

  #include <cstdio>
  #include <cstring>
  #include <cmath>
  #include <ctime>
  #include <algorithm>
  #include <functional>
  using namespace std;
  typedef long long ll;
  const int N = 6000 + 15;
  const int inf = 0x3f3f3f3f;

  struct edge{
      int v, nxt;
  };
  edge e[N];
  int  head[N], tot;
  int  used[N];
  int  f[N][2];

  inline void init(){
      memset(f, 0, sizeof(f));
      memset(head, -1, sizeof(head));
      tot = 0;
  }

  inline void addEdge(int u, int v){
      e[tot] = edge{v, head[u]};
      head[u] = tot++;
      used[v] = true;
  }

  void dfs(int u){
      used[u] = true;
      for(int i = head[u]; ~i; i = e[i].nxt){
          int v = e[i].v;
          if(!used[v])    dfs(v);
          f[u][0] += max(f[v][0], f[v][1]);
          f[u][1] += f[v][0];
      }
  }

  int main(){
      int n;
      while(~scanf("%d", &n)){
          init();
          for(int i = 1; i <= n; i++)     scanf("%d", &f[i][1]);

          int u,v;
          while(scanf("%d%d", &v, &u) && u + v){
              addEdge(u, v);
          }

          memset(used, false, sizeof(used));
          int ans = -inf;
          for(int u = 1; u <= n; u++){
              if(!used[u])    dfs(u);
              ans = max(ans, max(f[u][0], f[u][1]));
          }
          printf("%d\n", ans);
      }
  }


Computer HDU - 2196


题意
给定一棵树,求树上每个点能到达的最远距离
思路
看了题解,跪了 orz
首先定义DP状态dp[u][0]为从u点往下走的最大距离,dp[u][1]为从u点往下走的次大距离,dp[u][2]为从u点往上走的最大距离,那么显然最终的答案就是max(dp[u][0], dp[u][2]),同时引入标记tag[u] = v,表示v出现在u往下走的最大距离中,那么
dp[u][0] = max(dp[v][0] + w),往下走的最大距离是从孩子节点中选dp[v][0]加上边权最大的
dp[u][1]随dp[u][0]是否更新而更新(这部分具体看代码)
dp[v][2] = max(dp[u][2], tag[u] ? dp[u][1] : dp[u][0]) + w,往上走的最大距离要么从父节点u往上走加边权得到(往上走),要么从父节点往下走的最大距离或次大距离得到(先往上再往下),如果tag[u]为1说明u出现在dp[u][0]中,会导致往会走,所以只能取dp[u][1]
前两条DP方程和最后一条获取的方向不同,故需要两遍DFS

  #include <cstdio>
  #include <cstring>
  #include <algorithm>
  #include <iostream>
  using namespace std;
  const int N = 10000 + 15;
  const int inf = 0x3f3f3f3f;
  const double eps = 1e-6;
  typedef long long ll;

  struct edge{
      int v, nxt, w;
  };

  edge e[N << 1];
  int  head[N];
  int  tot;
  int  dp[N][3], tag[N];

  inline void init(){
      memset(head, -1, sizeof(head));
      memset(dp, 0, sizeof(dp));
      memset(tag, 0, sizeof(tag));
      tot = 0;
  }

  inline void addEdge(int u, int v, int w){
      e[tot] = edge{v, head[u], w};
      head[u] = tot++;
  }

  int dfs1(int u, int pre){
      dp[u][0] = dp[u][1] = dp[u][2] = tag[u] = 0;
      for(int i = head[u]; ~i; i = e[i].nxt){
          int v = e[i].v;
          if(v == pre)    continue;
          int res = dfs1(v, u) + e[i].w;
          if(dp[u][0] < res){
              dp[u][1] = dp[u][0];
              dp[u][0] = res;
              tag[u] = v;
          }else if(dp[u][1] < res){
              dp[u][1] = res;
          }
      }
      return dp[u][0];
  }

  void dfs2(int u, int pre){
      for(int i = head[u]; ~i; i = e[i].nxt){
          int v = e[i].v;
          if(v == pre)    continue;
          if(v == tag[u])     dp[v][2] = max(dp[u][2], dp[u][1]) + e[i].w;
          else                dp[v][2] = max(dp[u][2], dp[u][0]) + e[i].w;
          dfs2(v, u);
      }
  }

  int main(){
      int n;
      while(~scanf("%d", &n)){
          init();
          for(int i = 2; i <= n; i++){
              int v, w;
              scanf("%d%d", &v, &w);
              addEdge(i, v, w);
              addEdge(v, i, w);
          }
          dfs1(1, -1);
          dfs2(1, -1);
          for(int i = 1; i <= n; i++){
              printf("%d\n", max(dp[i][0], dp[i][2]));
          }
      }
  }


Appleman and Tree - CodeForces - 461B


题意
给定一棵树,其中的点有黑的也有白的,问去掉边后使得每个联通块只有一个黑点的方案数
思路
继续不会做,翻了一下题解 qwq
定义DP状态dp[u][0]代表以u的根的联通块中没有黑点的方案数,dp[u][1]代表以u为根的连通块中有一个黑点的方案数,则
dp[u][1] = ((dp[u][1] * dp[v][0]) + (dp[u][0] * dp[v][1]) + (dp[u][1] * dp[v][1])),则若u有黑点v没有则接上,若u没黑点v有那也接上,都有则割开
dp[u][0] = ((dp[u][0] * dp[v][0]) + (dp[u][0] * dp[v][1])),则若u无黑点v无黑点则接上,u无黑点v有则割开

  #include <cstdio>
  #include <cstring>
  #include <algorithm>
  #include <iostream>
  using namespace std;
  const int N = 100000 + 15;
  const int inf = 0x3f3f3f3f;
  typedef long long ll;
  const ll MOD = 1000000007;

  struct edge{
      int v, nxt;
  };

  edge e[N << 1];
  int  head[N];
  int  tot;
  ll   dp[N][2];
  int  tag[N];

  inline void init(){
      memset(head, -1, sizeof(head));
      tot = 0;
  }

  inline void addEdge(int u, int v){
      e[tot] = edge{v, head[u]};
      head[u] = tot++;
  }

  void dfs(int u, int pre){
      dp[u][0] = (tag[u] == 0);
      dp[u][1] = (tag[u] == 1);
      for(int i = head[u]; ~i; i = e[i].nxt){
          int v = e[i].v;
          if(v == pre)    continue;
          dfs(v, u);

          dp[u][1] = ((dp[u][1] * dp[v][0])%MOD + (dp[u][0] * dp[v][1])%MOD + (dp[u][1] * dp[v][1])%MOD)%MOD;
          dp[u][0] = ((dp[u][0] * dp[v][0])%MOD + (dp[u][0] * dp[v][1])%MOD)%MOD;
      }
  }

  int main(){
      int n;
      while(~scanf("%d", &n)){
          init();
          for(int i = 1; i < n; i++){
              int v;
              scanf("%d", &v);
              addEdge(i, v);
              addEdge(v, i);
          }
          for(int i = 0; i < n; i++)     scanf("%d", &tag[i]);
          dfs(0, -1);
          printf("%lld\n", dp[0][1]);
      }
  }


Matrix - HDU - 4313


题意
给定一棵树,其中的点有黑的也有白的,问去掉边后使得黑点不能相通的最小代价是多少
思路1 —— 树形DP
和上题很像,结果四条方程写错一条WA了一发,inf写小了WA了好几发 qwqqq
继续不会做,翻了一下题解 qwq
定义DP状态dp[u][0]代表以u的根的联通块且其中无黑点,此时的最小价值,dp[u][1]代表以u为根的连通块且其中有一个黑点,此时的最小价值,另外引入tag[u]代表u该点是否有黑点,则
当tag[u]为0时,
dp[u][1] = min(dp[u][0] - min(dp[v][0], dp[v][1] + e[i].w) + dp[v][1]),即把其中一个v的连通块换成感染的
dp[u][0] = sum(min(dp[v][0], dp[v][1] + e[i].w)),要么不割,要么割开
当tag[u]为1时,
dp[u][1] = sum(min(dp[v][0], dp[v][1] + e[i].w)),要么不割,要么隔开
dp[u][0] = inf

  #include <cstdio>
  #include <cstring>
  #include <algorithm>
  #include <iostream>
  using namespace std;
  const int N = 100000 + 15;
  typedef long long ll;
  const ll MOD = 1000000007;
  const ll inf = 1LL << 45;

  struct edge{
      int v, nxt, w;
  };

  edge e[N << 1];
  int  head[N];
  int  tot;
  ll   dp[N][2];
  int  tag[N];

  inline void init(){
      memset(head, -1, sizeof(head));
      memset(tag, 0, sizeof(tag));
      tot = 0;
  }

  inline void addEdge(int u, int v, int w){
      e[tot] = edge{v, head[u], w};
      head[u] = tot++;
  }

  void dfs(int u, int pre){
      if(tag[u])  dp[u][0] = inf, dp[u][1] = 0;
      else        dp[u][1] = inf, dp[u][0] = 0;
      for(int i = head[u]; ~i; i = e[i].nxt){
          int v = e[i].v;
          if(v == pre)    continue;
          dfs(v, u);

          if(tag[u]){
              dp[u][1] += min(dp[v][0], dp[v][1] + e[i].w);
          }else{
              dp[u][0] += min(dp[v][0], dp[v][1] + e[i].w);
          }
      }
      for(int i = head[u]; ~i; i = e[i].nxt){
          int v = e[i].v;
          if(v == pre)    continue;

          if(!tag[u]){
              dp[u][1] = min(dp[u][1], dp[u][0] - min(dp[v][0], dp[v][1] + e[i].w) + dp[v][1]);
          }
      }
  }

  int main(){
      int t;
      scanf("%d", &t);
      while(t--){
          init();
          int n, k;
          scanf("%d%d", &n, &k);
          for(int i = 1; i <= n - 1; i++){
              int u, v, w;
              scanf("%d%d%d", &u, &v, &w);
              addEdge(u, v, w);
              addEdge(v, u, w);
          }
          while(k--){
              int u;
              scanf("%d", &u);
              tag[u] = 1;
          }
          dfs(0, -1);
          printf("%lld\n", min(dp[0][1], dp[0][0]));
      }
  }

思路2 —— 贪心,Kruskal MST思想
按边权从大到小排序,然后如果两个连通块都是黑点则加上价值,否则相连
个人认为这样做的正确性在于,先把大的边权丢出去,如果一开始就遇到两个都是黑点,那么无论如何都得割开,否则能够先把大的边权拿去扩大连通块,就剩下小的边权的边不得不割开了

  #include <cstdio>
  #include <cstring>
  #include <algorithm>
  #include <iostream>
  using namespace std;
  const int N = 100000 + 15;
  typedef long long ll;
  const ll MOD = 1000000007;
  const ll inf = 1LL << 45;

  struct edge{
      int u, v, w;
      bool operator < (const edge& b) const{ return w > b.w; }
  };

  edge e[N];
  int  ft[N];
  int  tag[N];

  inline void init(int n){
      for(int i = 0; i < n; i++)  ft[i] = i;
      memset(tag, 0, sizeof(tag));
  }
  int find(int x){ return ft[x] == x ? x : ft[x] = find(ft[x]); }
  void merge(int x, int y){
      int p = find(x), q = find(y);
      if(p != q)  ft[q] = p;
  }

  int main(){
      int t;
      scanf("%d", &t);
      while(t--){
          int n, k;
          scanf("%d%d", &n, &k);
          int m = n - 1;
          init(n);
          for(int i = 0; i < m; i++){
              scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
          }
          while(k--){
              int u;
              scanf("%d", &u);
              tag[u] = 1;
          }

          sort(e, e + m);
          ll ans = 0;
          for(int i = 0; i < m; i++){
              int u = e[i].u, v = e[i].v;
              if(tag[find(u)] && tag[find(v)]){
                  ans += e[i].w;
              }else if(tag[find(u)]){
                  merge(u, v);
              }else{
                  merge(v, u);
              }
          }
          printf("%lld\n", ans);
      }
  }