做题SDOI2017苹果树——dfs序的运用

Posted cly_none

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了做题SDOI2017苹果树——dfs序的运用相关的知识,希望对你有一定的参考价值。

原文链接 https://www.cnblogs.com/cly-none/p/9845046.html

题意:给出一棵\\(n\\)个结点的树,在第\\(i\\)个结点上有\\(a_i\\)个权值为\\(v_i\\)的物品。\\(1\\)号结点是根结点。你需要选出若干个物品(设选了\\(t\\)个),满足:

  • 如果选了结点\\(i\\)上的物品,那么\\(i\\)到根的链上每个结点都至少要选一个物品。
  • 设有选取物品的结点的最大深度为\\(h\\),那么\\(t \\leq h + k\\)\\(k\\)为一个给定的常数。

在此基础上,你需要最大化所选的物品的权值和。
\\(n \\leq 2 \\times 10^4, \\, k \\leq 5 \\times 10^5, \\, n \\times k \\leq 2.5 \\times 10^7\\)

显然,最终做法的复杂度应该是\\(O(nk)\\)的。

但这个问题比较复杂,直接想比较困难。因此,我们先考虑问题的简化版。

问题1

当第二个条件改为\\(t \\leq k\\)时,怎么做?

对于这种一个结点的决策影响其子树的问题,我们可以对dfs序倒过来dp。确切地说,考虑当前是\\(i\\),那么\\(i\\)的子树就是\\(dfn_i\\)之后的一段连续区间。那么,把dfs序倒过来后,结点\\(i\\)就有两种可能:

  • 选了\\(i\\)上的物品。就是一个多重背包,从\\(dp_{dfn_i + 1}\\)上更新过来。
  • 不选\\(i\\)上的物品。那\\(i\\)子树中的所有物品都不能选。从\\(dp_{dfn_i + sz_i}\\)上更新过来。

用单调队列优化多重背包后,就能做到\\(O(nk)\\)


然而,回过头来,我们依旧对\\(t \\leq h + k\\)感到棘手。尝试按常规方法dp对\\(k+h-t\\)记录答案,但没有用。这个限制其实就在于,选出一条一段是根结点的链,链上每个点都取一个不计入\\(t\\)的物品。我们设这条链除\\(1\\)外的端点为\\(x\\)。考虑\\(\\forall i, \\, a_i = 1\\)的部分分。那么,假如我们已经确定了\\(x\\),则剩下的答案就是删去\\(1\\)\\(x\\)的链,对剩下的森林做问题1的结果。

因此,我们可以考虑下面这个问题:

问题2

预处理:对于所有\\(x\\),删去\\(x\\)到根的路径后剩下的森林的问题1的答案。

博主认为,这个问题的解法相当有趣,也挺难想到的。

考虑剩下的森林的一半就是在dfs序上,从\\(dfn_i + 1\\)\\(n\\)的一段区间(包括了\\(i\\)的子树)。这个部分我们在dp时就已经把答案求出来了。然而,另一部分在dfs序上既不是一段后缀,也不是连续的区间。\\([1,dfn_i-1]\\)中还混入了\\(i\\)的所有祖先。

因此,我们把这棵树左右翻转,把剩下森林的两半交换位置。也就是,再生成一个dfs序,但每个结点反序访问它的孩子结点。这样,我们就把森林的另一部分也表示为了dfs序的一个后缀。值得注意的是,\\(i\\)的子树不能算两次,所以这个后缀应该是[dfn_i + sz_i,n]。

这样,我们做出两个dfs序,对每个做问题1的dp,就能解决此问题。


然后就是处理\\(a_i \\neq 1\\)的情况。上面的算法会错误,就在于\\(x\\)到根的路径上的结点,可能选了多个物品。那么,我们就对每个结点\\(i\\)建一个辅助点\\(i\'\\),存放了\\(a_i - 1\\)个原来在\\(i\\)上的物品。这样,对于任何一个非辅助结点,它到根的路径上所有点都只有一个物品。

这样就能把最终问题转化为问题1,\\(O(nk)\\)地解决本题。

#include <bits/stdc++.h>
using namespace std;
const int N = 40010, K = 500010, SIZE = 51000010;
int n,k,val[N],num[N],dfn[N],sz[N],fa[N],cnt,dis[N],ans,rec[N],spadp[SIZE],spag[SIZE];
vector<int> ch[N];
int *dp[N],*g[N];
void dfs(int pos) {
  sz[pos] = 1;
  for (int i = 0 ; i < (int)ch[pos].size() ; ++ i) {
    dfs(ch[pos][i]);
    sz[pos] += sz[ch[pos][i]];
  }
  dfn[rec[pos] = ++cnt] = pos;
}
void fsd(int pos) {
  dis[pos] += val[pos];
  for (int i = (int)ch[pos].size() - 1 ; i >= 0 ; -- i) {
    dis[ch[pos][i]] = dis[pos];
    fsd(ch[pos][i]);
  }
  dfn[++cnt] = pos;
}
void update(int las,int cur) {
  static int q[K],l,r;
  l = 1, r = 0;
  q[++r] = 0;
  for (int i = 1 ; i <= k ; ++ i) {
    while (l <= r && i - q[l] > num[dfn[cur]])
      ++ l;
    if (l <= r)
      dp[cur][i] = dp[las][q[l]] + val[dfn[cur]] * (i - q[l]);
    else dp[cur][i] = 0;
    while (l <= r && dp[las][i] > dp[las][q[r]] + val[dfn[cur]] * (i - q[r]))
      -- r;
    q[++r] = i;
  }
}
void init() {
  ans = 0;
  for (int i = 0 ; i <= 2 * n ; ++ i) {
    ch[i].clear();
    dp[i] = spadp + i * (k + 1);
    g[i] = spag + i * (k + 1);
    memset(dp[i],0,sizeof(int) * (k + 1));
    memset(g[i],0,sizeof(int) * (k + 1));
  }
  dis[1] = 0;
}
int main() {
  int T;
  scanf("%d",&T);
  while (T --) {
    scanf("%d%d",&n,&k);
    init();
    for (int i = 1 ; i <= n ; ++ i)
      scanf("%d%d%d",&fa[i],&num[i],&val[i]);
    for (int i = 2 ; i <= n ; ++ i)
      ch[fa[i]].push_back(i);
    for (int i = 1 ; i <= n ; ++ i) {
      ch[i].push_back(i+n);
      val[i+n] = val[i];
      num[i+n] = num[i] - 1;
      num[i] = 1;
    }
    cnt = 0;
    dfs(1);
    for (int i = 1 ; i <= 2 * n ; ++ i) {
      update(i-1,i);
      for (int j = 1 ; j <= k ; ++ j)
        dp[i][j] = max(dp[i][j],dp[i - sz[dfn[i]]][j]), dp[i][j] = max(dp[i][j],dp[i][j-1]);
    }
    for (int i = 1 ; i <= 2 * n ; ++ i)
      for (int j = 1 ; j <= k ; ++ j)
	g[i][j] = dp[i][j];
    cnt = 0;
    fsd(1);
    for (int i = 1 ; i <= 2 * n ; ++ i) {
      update(i-1,i);
      for (int j = 1 ; j <= k ; ++ j)
	dp[i][j] = max(dp[i][j],dp[i - sz[dfn[i]]][j]), dp[i][j] = max(dp[i][j],dp[i][j-1]);
    }
    for (int i = 1 ; i <= 2 * n ; ++ i) {
      if (dfn[i] > n) continue;
      int p = rec[dfn[i]] - sz[dfn[i]];
      for (int j = 0 ; j <= k ; ++ j)
	ans = max(ans,dis[dfn[i]] + dp[i-1][j] + g[p][k-j]);
    }
    printf("%d\\n",ans);
  }
  return 0;
}

小结:一道对dfs序上dp进行拓展的好题。当一个问题分成了性质相同的两半,而前者容易解决,后者难以解决的问题时,寻找方式来交换这两部分的位置,最后合并。这个思路应该记住。

以上是关于做题SDOI2017苹果树——dfs序的运用的主要内容,如果未能解决你的问题,请参考以下文章

做题记录

[BZOJ4817][SDOI2017]树点涂色(LCT+DFS序线段树)

[SDOI2017]苹果树 题解

BZOJ.4817.[SDOI2017]树点涂色(LCT DFS序 线段树)

「Luogu P3302」[SDOI2013]森林

[SDOI2015]寻宝游戏