首页 > 其他分享 >P3703 树点涂色笔记

P3703 树点涂色笔记

时间:2024-01-25 14:57:47浏览次数:29  
标签:rt fa P3703 son 树点 int dfn findroot 涂色

又是一道妙题,加深了蒟蒻对 \(\text{LCT}\) 的理解。

题意

给定一棵 \(n\) 个节点的有根树,根节点为 \(1\)。最开始每个节点都有颜色,且颜色互不相同。

定义一条路径的权值为:改路径上点的不同颜色数。

现在一共会有 \(m\) 组询问,每组询问有三种:

  • 1 x 将 \(x\) 到根节点 \(1\) 上的所有点都染色为以前从未出现过的颜色。
  • 2 x y 询问 \(x \to y\) 路径的权值。
  • 3 x 询问 \(x\) 的子树内的结点中,到根节点的路径的最大权值

题解

思路

考虑如何维护 \(1\) 操作,容易想到:任一时刻,对于每种颜色,拥有该颜色的点在树上的联通块有且仅有一个,且一定是直上直下的链。

这个性质的存在,使得此题可以用 \(\text{LCT}\) 解决。一个经典的处理方法是:

  • 对于 \(\text{LCT}\) 上的一条边,若两端点的颜色相同,则该边为实边。
  • 若两端点颜色不同,该边为虚边。

容易证明这种赋实边、虚边的方法是符合 \(\text{LCT}\) 的性质的。

所以操作 \(1\) 就可以转换成 \(\text{LCT}\) 的 access 操作,即:将 \(x \to 1\) 路径上的结点实边断开,再将路径上的边改为实边。

对于操作 \(2\),记录一个 \(dis_x\) 表示 \(x \to 1\) 的路径上经历的虚边数量,即该路径的权值。操作 \(2\) 的答案就是 \(dis_{x} + dis_{y} - 2 \times dis_{LCA} + 1\)。

而操作 \(3\) 就是在查询 \(x\) 子树内的 \(dis\) 最大值,可以用线段树配合 DFS 序解决。

代码实现

这里要详细讲一下 \(\text{LCT}\) 里的 access

回想一下普通 \(\text{LCT}\) 里的 access

普通 LCT 里的 access
void access(int x) {
  for (int y = 0; x; y = x, x = fa[x])
    splay(x), son[x][1] = y, pushup(x);
}

每次 for 循环内的流程是:1. 将 \(x\) 旋转到当前 splay 的根。2. 将 \(x\) 原本实边断开。3. 将 \(x\) 现在的实边连到 \(y\)。4. \(x\) 的实儿子改变,故 pushup

那么对于这道题,2、3 两个步骤会对 \(dis\) 产生影响,具体就是(有点绕):

  • 原树中 \(x\) 的,子树中包含 \(son_{x, 1}\),的儿子子树 \(dis\) 全部加上 \(1\)。
  • 原树中 \(x\) 的,子树中包含 \(y\),的儿子子树 \(dis\) 全部减去 \(1\)。

所有为了找到那个儿子,又由于 splay 的性质:splay 中序遍历得到的序列,深度递增。所以只需找到 \(\bold{\text{LCT}}\) 里 \(son_{x, 1} / y\) 子树最左的儿子就行了。

findroot 代码
int findroot(int x) {
  while (son[x][0]) x = son[x][0];
  return x;
}
access 代码
void access(int x) {
  for (int y = 0; x; y = x, x = fa[x]) {
    splay(x); int rt;
    if (son[x][1]) {
      rt = findroot(son[x][1]);
      t.update(1, dfn[rt], dfn[rt] + sz[rt] - 1, 1);
    }
    if (y) {
      rt = findroot(y);
      t.update(1, dfn[rt], dfn[rt] + sz[rt] - 1, -1);
    }
    son[x][1] = y;
  }
}

很不幸,上面两个代码是错的,原因是:findroot 时未 splay,这导致复杂度变为 \(O(n^2)\)。
但是如果在 findrootsplay,会使 access 中的 \(x\) 无法成为当前 splay 的根节点,最终死循环。

解决办法也不难,只需将 findroot 中用到的所有 \(x\) 存放在一个 vector 中,access 完毕后将 vector 中的所有结点都 splay 一遍即可。

findroot 和 access 的正确代码
vector <int> tosplay;
int findroot(int x) {
  while (son[x][0]) x = son[x][0];
  tosplay.emplace_back(x);
  return x;
}
void access(int x) {
  tosplay.clear();
  for (int y = 0; x; y = x, x = fa[x]) {
    splay(x); int rt;
    if (son[x][1]) {
      rt = findroot(son[x][1]);
      t.update(1, dfn[rt], dfn[rt] + sz[rt] - 1, 1);
    }
    if (y) {
      rt = findroot(y);
      t.update(1, dfn[rt], dfn[rt] + sz[rt] - 1, -1);
    }
    son[x][1] = y;
  }
  for (auto p : tosplay) splay(p);
}
本题的所有代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int N = 1E5 + 5, M = 20;
int n, m, dep[N], wt[N], dfn[N], tot, sz[N]; 
vector <int> G[N];
struct segt {
  struct node {
    int l, r;
    int tag, mx;
  } t[N << 2];
  int lson(int x) {return x << 1;}
  int rson(int x) {return x << 1 | 1;}
  void pushup(int x) {t[x].mx = max(t[lson(x)].mx, t[rson(x)].mx);}
  void build(int x, int l, int r) {
    t[x].l = l; t[x].r = r; t[x].tag = 0;
    if (l == r) {
      t[x].mx = wt[l];
      return ;
    } int mid = (l + r) >> 1;
    build(lson(x), l, mid); build(rson(x), mid + 1, r);
    pushup(x);
  }
  void upd(int x, int val) {t[x].mx += val; t[x].tag += val;}
  void pushdown(int x) {
    upd(lson(x), t[x].tag); upd(rson(x), t[x].tag);
    t[x].tag = 0;
  }
  void update(int x, int L, int R, int val) {
    if (t[x].l >= L && t[x].r <= R) return upd(x, val);
    int mid = (t[x].l + t[x].r) >> 1; pushdown(x);
    if (L <= mid) update(lson(x), L, R, val);
    if (R > mid) update(rson(x), L, R, val);
    pushup(x);
  }
  int query(int x, int L, int R) {
    if (t[x].l >= L && t[x].r <= R) return t[x].mx;
    int mid = (t[x].l + t[x].r) >> 1, res = 0; pushdown(x);
    if (L <= mid) res = max(res, query(lson(x), L, R));
    if (R > mid) res = max(res, query(rson(x), L, R));
    return res;
  }
  int query(int x) {return query(1, x, x);}
} t;
struct lct {
  int son[N][2], fa[N];
  bool checkroot(int x) {return son[fa[x]][0] != x && son[fa[x]][1] != x;}
  bool checkson(int x) {return son[fa[x]][1] == x;}
  void rotate(int x) {
    int y = fa[x], z = fa[y], chx = checkson(x), chy = checkson(y);
    if (!checkroot(y)) son[z][chy] = x; fa[x] = z;
    son[y][chx] = son[x][!chx]; fa[son[x][!chx]] = y;
    son[x][!chx] = y; fa[y] = x;
  }
  void splay(int x) {
    while (!checkroot(x)) {
      if (!checkroot(fa[x])) rotate(checkson(x) != checkson(fa[x]) ? x : fa[x]);
      rotate(x);
    }
  }
  vector <int> tosplay;
  int findroot(int x) {
    while (son[x][0]) x = son[x][0];
    tosplay.emplace_back(x);
    return x;
  }
  void access(int x) {
    tosplay.clear();
    for (int y = 0; x; y = x, x = fa[x]) {
      splay(x); int rt;
      if (son[x][1]) {
        rt = findroot(son[x][1]);
        t.update(1, dfn[rt], dfn[rt] + sz[rt] - 1, 1);
      }
      if (y) {
        rt = findroot(y);
        t.update(1, dfn[rt], dfn[rt] + sz[rt] - 1, -1);
      }
      son[x][1] = y;
    }
    for (auto p : tosplay) splay(p);
  }
} f;
struct bz {
  int yf[N][M + 1];
  void dfs(int x, int fa) {
    wt[dfn[x] = ++tot] = dep[x]; sz[x] = 1;
    for (auto v : G[x]) {
      if (v == fa) continue;
      dep[v] = dep[x] + 1; yf[v][0] = x; f.fa[v] = x;
      for (int i = 1; i <= M; ++i)
        yf[v][i] = yf[yf[v][i - 1]][i - 1];
      dfs(v, x);
      sz[x] += sz[v];
    }
  }
  int getlca(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);
    for (int i = M; ~i; --i)
      if (dep[u] - (1 << i) >= dep[v]) u = yf[u][i];
    if (u == v) return u;
    for (int i = M; ~i; --i)
      if (yf[u][i] != yf[v][i])
        u = yf[u][i], v = yf[v][i];
    return yf[u][0];
  }
  void solve() {
    for (int i = 0; i <= M; ++i) yf[1][i] = 1;
    dep[1] = 1; dfs(1, 0); t.build(1, 1, n);
  }
} b;
signed main(void) {
  ios :: sync_with_stdio(false);
  cin.tie(0); cout.tie(0);
  cin >> n >> m;
  for (int i = 1; i < n; ++i) {
    int u, v; cin >> u >> v;
    G[u].emplace_back(v);
    G[v].emplace_back(u);
  } b.solve();
  for (int i = 1; i <= m; ++i) {
    int opt, x; cin >> opt >> x;
    if (opt == 1) f.access(x);
    else if (opt == 2) {
      int y; cin >> y;
      int lca = b.getlca(x, y);
      cout << t.query(dfn[x]) + t.query(dfn[y]) - 2 * t.query(dfn[lca]) + 1 << '\n';
    } else cout << t.query(1, dfn[x], dfn[x] + sz[x] - 1) << '\n';
    for (int i = 1; i <= n; ++i) cout << t.query(dfn[i]) << ' ';
    cout << '\n';
  }
  return 0;
}

标签:rt,fa,P3703,son,树点,int,dfn,findroot,涂色
From: https://www.cnblogs.com/CTHOOH/p/17986845

相关文章

  • [春季测试 2023] 涂色游戏
    题目描述有一天,小D在刷朋友圈时看到了一段游戏视频。这个游戏的名字叫涂色游戏,视频中的游戏界面是一个\(n\)行\(m\)列的网格,初始时每一个格子都是白色(用数字\(0\)表示)。其中每一行的左侧、每一列的上方都有一把带颜色的刷子。玩家点击某个刷子后,这个刷子会将其右侧(或下......
  • [SDOI2017] 树点涂色
    [SDOI2017]树点涂色题目描述Bob有一棵\(n\)个点的有根树,其中\(1\)号点是根节点。Bob在每个点上涂了颜色,并且每个点上的颜色不同。定义一条路径的权值是:这条路径上的点(包括起点和终点)共有多少种不同的颜色。Bob可能会进行这几种操作:1x表示把点\(x\)到根节点的路......
  • 涂色
    首先是一种对于这个问题的新的建树方法然后注意lazy标记最开始要初始化为0,不能为1或2,为0的时候表示自己当前的操作已经全部传递给子节点了注意lazy表示的是自己已经完成修改,但是子节点还没有修改,无论是这道题目还是普通的区间加减,我们在递归到一个节点的时候,他的所有祖先结点......
  • P4170 [CQOI2007] 涂色(天赋哥不要点进来)
    前言翻遍洛谷题解,看到大家都在套模板,却很少有人讲出为什么,使我十分崇拜天赋哥。原题链接关于这题的一些事实性证据事实1.来自事实2.来自事实3.来自事实4.来自整理上述事实1.每一次”最短“最优涂色,要么在其他颜色的基础上涂,这称之为融入一个整体;要么另辟蹊径单独......
  • Q7.4.1.2. 奇怪的方格涂色 题解
    原题链接首先想到暴力网络流:考虑最小割,\(S\)表示染黑色,\(T\)表示染白色。每个格子\(i\),连\((S,i,b_i)\),\((i,T,w_i)\)。怎么处理“奇怪的方格”?连\((i,i^\prime,p_i)\)和\((i^\prime,j,+\infty)\)。表示如果\(i\)归属于\(S\)(染黑色),那么就只能忍受奇怪所带来的\(p_i\)......
  • [刷题笔记] CF1132F Clear the String & [CQOI2007] 涂色
    Problem1Problem2双倍经验qwqDescription初始时数组为空,每次可以选择一个区间\(l-r\)将其赋为同一个值,赋的值可以覆盖,给定数组的目标形式,求至少经过多少次操作使得空数组变成目标形式。Solution我们发现每次选择一个区间,大区间包含小区间,小区间可以推到大区间。因此考虑区间......
  • el-tree树点击全选按钮,全部展开并且全选
    先看图:代码如下://全部选中qxClick(){this.isQx=!this.isQx;//判断按钮的状态this.expandAll();if(this.isQx){console.log(this.isQx,"-------------------------------",this.datas);//设置this.$r......
  • 「解题报告」P3703 [SDOI2017]树点涂色
    有趣题,代码超好写,而且思路超有趣!!!首先发现操作1的操作都是某个点到根,不难发现这样每种颜色一定对应着树上的一条链。那么操作2可以直接树上查分求答案,这样我们只需要考虑维护每个点到根的链的数量了。怎么维护链的数量?发现这个操作1长得和LCT的Access操作一模一样啊,所......
  • 得到 K 个黑块的最少涂色次数
    给你一个长度为n 下标从0 开始的字符串 blocks ,blocks[i] 要么是 'W' 要么是 'B' ,表示第 i 块的颜色。字符 'W'和 'B' 分别表示白色和黑色。给你一个整数 k ,表示想要 连续 黑色块的数目。每一次操作中,你可以选择一个白色块将它涂成 黑色块。请你返回至......
  • 区间涂色问题
    一眼区间dp设dp[i][j]为涂完i到j所需的最小次数当a[i]==a[j]时,dp[i][j]=min(dp[i+1][j-1]+1,min(dp[i+1][j],dp[i][j-1]));为什么是dp[i+1][j-1]+1,此时会产生一个异想天开的想法,就是取遍历一遍i+1到j-1这一段字符串,看是否有a[i]字符的出现,如果出现的话dp[i][j]=dp[i+1][j......