一类树上问题的解决办法

目录

本文参考自 梁晏成《树上数据结构》 ,感谢他在雅礼集训的讲解。

转化成序列问题

dfs序

按照 \(dfs\) 的入栈顺序形成一个序列。

例如对于这棵树

img

它的 \(dfs\) 序就是 \(1~2~3~4~5~6~7~8\) 。(假设我遍历儿子是从左到右的)

树链剖分的运用

对于这个我们常常配合 树链剖分 来使用。ba

这样对于一个点,它的子树编号是连续的一段区间,便于做子树修改以及查询问题。

重链上所有节点的标号也是连续的一段区间。

所以我们可以解决大部分链或子树修改以及查询的问题,十分的优秀。

也就是常常把树上问题转化成序列问题的通用解法。

括号序列

\(dfs\) 时候,某个节点入栈时加入左括号,出栈时加入右括号。

也就是在 \(dfs\) 序旁边添加括号。

同样对于上面那颗树 。

为了方便观看,我们在其中添入一些数字。

它的括号序列就是 \((1(2)(3(4)(5(6(7))))(8))\)

求解树上距离问题

这个可以对于一些有关于树上距离的问题有用,比如 [BZOJ1095 [ZJOI2007] Hide 捉迷藏 (括号序列 + 线段树)](https://www.cnblogs.com/zjp-shadow/p/8672799.html)

也就是对于树上两点的距离,就是他们中间未匹配的括号数量。这个是很显然的,因为匹配的括号必定不存在于他们之间的路径上,其他的都存在于他们的路径上。

也就是说向上路径的括号是 \()\) 向下路径的括号就是 \((\)

树上莫队转化成普通莫队

\(L_x\)\(x\) 左括号所在的位置,\(R_x\)\(x\) 右括号所在的位置。

我们查询树上一条路径 \(x \sim y\) 满足 \(L_x \le L_y\) ,考虑:

  • 如果 \(x\)\(y\) 的祖先,那么 \(x\)\(y\) 的链与括号序列 \([L_x, L_y]\) 对应。
  • 如果 \(x\) 不是 \(y\) 的祖先,那么 \(x\)\(y\) 的链除 \(lca\) 部分与括号序列中区间 \([R_x, L_y]\) 对应。

第二点是因为 \(lca\) 的贡献会在其中被抵消掉,最后暴力算上就行了。

每次移动的时候就修改时候判断一个点被匹配了没,匹配减去,没匹配加上就行了。

SP10707 COT2 - Count on a tree II

题意

多次询问树上一条路径上不同颜色种数。

题解

我们利用括号序列,把树上的问题直接拍到序列上来做暴力莫队就行了,和之前莫队模板题一样的做法。

欧拉序列

\(dfs\) 时,某个节点入栈时加入队列,出栈时将父亲加入队列。

还是对于上面那颗树,

它的欧拉序列就是 \(1~2~1~3~4~3~5~6~7~6~5~3~1~8~1\)

这个有什么用呢qwq 常常用来做 \(lca\) 问题。

具体来说就是,对于欧拉序列每个点记住它的深度,然后对于任意两个点的 \(lca\) 就是他们两个点第一次出现时候的点对之间 深度最小 的那个点。

这就转化成了一个 \(RMQ\) 问题,用普通的 \(ST\) 表预处理就可以达到 \(O(n \log n)\) ,询问就是 \(O(1)\) 的。

如果考虑用约束 \(RMQ\) 来解决,就可以达到 \(O(n)\) 预处理,\(O(1)\) 询问的复杂度。

虽然看起来特别优秀,但是并不常用qwq

差分思想

  • 对于一对点 \(x, y\) ,假设它们 \(lca\)\(z\) ,那么这条 \(x\)\(y\) 的链可以用 \(x, y, z, fa[z]\) 的链表示。

    例如给一条 \(x \to y\) 的链加上一个数 \(v\) ,最后询问每个点的权值。

    我们可以把 \(x,y\) 处加上 \(v\)\(z, fa[z]\) 处减去 \(v\) 。最后对于整棵树遍历一遍,记下当前的权值就行了。

    注意要特判 \(lca = x ~ or ~ y\) 的情况。

  • 对于两条相同的边上的信息可以抵消(链上所有边异或的值),可以直接拆成 \(x, y\) 到根的路径表示。

单点、链、子树的转化

在某些情况下,我们需要修改和改变查询的对象来减小维护的难度。

下面我都把链看成从底向上的一条,其他链其实都可以拆分成两条这种链(一条 \(x \to lca\) 向上,另一条 \(lca \to x\) 向下),也可以类比接下来的方法进行讨论。

  • 单点修改链上查询 \(\Leftrightarrow\) 子树修改单点查询

    这个如何理解呢,例如对于这颗树。

    img

    我们考虑对于修改 \(x\) 的点权值,不难发现它影响的链就是类似 \(y,z \to anc[x]\)\(x\) 自己 以及 它的祖先)的点。

    然后就可以在 \(x\) 处给子树修改权值,每次查询一条链就是看它链底的权值和减去链顶的权值和。

    反过来也是差不多的思路。

  • 链上修改单点查询 \(\Leftrightarrow\) 单点修改子树查询

    \(y \to x\) 这条链上修改权值,查询一个点的权值。

    不难发现,这就等价于给 \(x, y\) 处打差分标记,然后每次查询一颗子树的信息。

    这样的话,对于一个点所包含的子树信息,就是整个所有之前链覆盖它的信息。

    这个常常可以用于最后询问很多个点,然后用线段树合并子树信息。

  • 链上修改子树查询 \(\Leftrightarrow\) 单点修改子树查询

    似乎是利用 \(dep\) 数组实现的,不太记得怎么搞了,以后做了题再来解释吧。

点、边

一些与“链相交”的问题,我们可以在点上赋正权,边上赋负权的方式简化问题。

例题

题意

  • 插入一条链
  • 给定一条链,问有多少条链于这条链相交。

题解

我们只需要在插入的时候,给链上的点 \(+1\) ,链上的边 \(-1\) ,询问的时候就等价于一个链上求和。

这为什么是正确的呢?对于两条链,我们把负的边权和下面正的点权抵消掉,那么就只剩下了最上面共有的交点有多的 \(1\) 的贡献了。

提取关键点

我们可以在一棵树中取不超过 \(\sqrt n\) 个关键点,保证每个点到最近的祖先距离 \(\le \sqrt n\)

具体地,我们自底向上标记关键点。如果当前点子树内到它最远的点距离 \(\ge \sqrt n\) 就把当前点标记成关键点。

其实类似于序列上的分块处理。

HDU 6271 Master of Connected Component

题意

给定两颗 \(n\) 个节点的树,每个节点有一对数 \((x, y)\) ,表示图 \(G\) 中的一条边。

对于每一个 \(x\) ,求出两棵树 \(x\) 到根路径上的所有边在图 \(G\) 中构成的子图联通块个数。

多组数据,\(n \le 10000\)

题解

考虑对于第一颗树提取关键点,然后对于每个点的询问挂在它最近的关键点祖先处。

到每个关键点处理它所拥有的询问,到第二颗树上进行遍历。

对于一个关键点 \(p\) 将它到根节点路径上的节点全部放入并查集中,然后用支持撤回的并查集维护联通块个数。

具体来说,对于那个撤回并查集只需要按秩合并,也就是深度小的连到深度大的上,然后记一下上次操作的深度,以及连的边。

然后这个时间复杂度就是 \(O(n \sqrt n \log n)\) 的,其实跑的很快?

代码

强烈建议认真阅读代码,提高码力。

#include <bits/stdc++.h>

#define For(i, l, r) for(int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << x << endl

using namespace std;

inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}

inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
    for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
    return x * fh;
}

void File() {
#ifdef zjp_shadow
    freopen ("6271.in", "r", stdin);
    freopen ("6271.out", "w", stdout);
#endif
}

const int N = 2e4 + 50, M = N * 2, blksize = 350;

typedef pair<int, int> PII;
#define fir first
#define sec second
#define mp make_pair

struct Data {
    
    int x, y, type; 

    Data() {}

    Data(int a, int b, int c) : x(a), y(b), type(c) {}

} opt[N];

namespace Union_Set {

    int fa[N]; int Find(int x) { return x == fa[x] ? x : Find(fa[x]); }

    int height[N], tot = 0;
    inline Data Merge(int x, int y){
        int rtx = Find(x), rty = Find(y);
        if (rtx == rty) return Data(0, 0, 0);
        if (height[rtx] < height[rty]) swap(rtx, rty);
        fa[rty] = rtx; -- tot;
        if (height[rtx] == height[rty]) { ++ height[rtx]; return Data(rtx, rty, 2); }
        else return Data(rtx, rty, 1);
    }

    inline void Retract(Data now) {
        int x = now.x, y = now.y, type = now.type;
        if (!type) return ; height[x] -= (type - 1); fa[y] = y; ++ tot;
    }

}

PII Info[N];
inline Data Insert(int pos) {
    int x = Info[pos].fir, y = Info[pos].sec;
    return Union_Set :: Merge(x, y);
}

inline void Delete(int pos) {
    Union_Set :: Retract(opt[pos]); 
}

int from[N], nowrt;
inline int Get_Ans(int u) {
    static int stk[N], top; top = 0;
    while (u ^ nowrt) {
        opt[u] = Insert(u), stk[++ top] = u, u = from[u];
    }
    int res = Union_Set :: tot;
    while (top) Delete(stk[top --]);
    return res;
}

int Head[N], Next[M], to[M], e;
void add_edge(int u, int v) { to[++ e] = v; Next[e] = Head[u]; Head[u] = e; }

int maxd[N], vis[N];

#define Travel(i, u, v) for(int i = Head[u], v = to[i]; i; i = Next[i], v = to[i])
void Dfs_Init(int u, int fa = 0) {
    from[u] = fa; maxd[u] = 1;
    Travel(i, u, v) if (v != fa) {
        Dfs_Init(v, u);
        chkmax(maxd[u], maxd[v] + 1);
    }
    if (maxd[u] == blksize || u == 1) maxd[u] = 0, vis[u] = true;
}

int n, m;

vector<int> child[N];
inline bool App(int u) {
    vector<int> :: const_iterator it = lower_bound(child[nowrt].begin(), child[nowrt].end(), u);
    if (it == child[nowrt].end()) return false; return (*it == u);
}

int ans[N];
void Dfs2(int u, int fa = 0) {
    opt[u] = Insert(u);
    if (App(u - n)) ans[u - n] = Get_Ans(u - n);
    Travel(i, u, v) if (v != fa) Dfs2(v, u);
    Delete(u);
}

void Dfs1(int u, int fa = 0) {
    opt[u] = Insert(u);
    if (vis[u]) nowrt = u, Dfs2(n + 1, 0);
    Travel(i, u, v) if (v != fa) Dfs1(v, u);
    Delete(u);
}

inline void Init() {
    e = 0; 
    For (i, 1, n * 2) 
        from[i] = 0, Head[i] = 0, child[i].clear(), vis[i] = false;
    For (i, 1, m)
        Union_Set :: fa[i] = i, Union_Set :: height[i] = 1;
    Union_Set :: tot = m;
}

int main () {
    File();

    for (int cases = read(); cases; -- cases) {
        n = read(); m = read(); Init();

        For (id, 0, 1) {
            For (i, 1, n)
                Info[i + id * n] = mp(read(), read());
            For (i, 1, n - 1) {
                int u = read() + id * n, v = read() + id * n;
                add_edge(u, v); add_edge(v, u);
            }
            Dfs_Init(1 + id * n);
        }

        For (i, 1, n) {
            int u = i;
            for (; !vis[u]; u = from[u]) ;
            child[u].push_back(i);
        }

        Dfs1(1); For (i, 1, n) printf ("%d\n", ans[i]); Init();
    }

    return 0;
}

启发式合并

启发式合并即合并两个集合时按照一定顺序(通常是将较小的集合的元素一个个插入较大的集合)合并的一种合并方式,常见的数据结构有并查集、平衡树、堆、字典树等。

具体地,如果单次合并的复杂度为 \(O(B)\) ,总共有 \(M\) 个信息,那么总复杂度为 \(O(B M \log M)\)

树的特殊结构,决定了常常可以使用启发式合并优化信息合并的速度。

LOJ #2107. 「JLOI2015」城池攻占

此处例题有很多,就放一个还行的题目上来。

题意

请点下上面的链接,太长了不想写了。

题解

不难发现两个骑士经过同一个节点的时候,攻击力的相对大小是不会改变的;

然后我们每次找当前攻击力最小的骑士出来,判断是否会死亡。

这个可以用一个可并小根堆实现(也可以用 splay 或者 treap 各类平衡树实现)。

我们可以用 lazy 标记来支持加法和乘法操作就行了。

用斜堆实现似乎常数比左偏树小?还少了一行qwq

并且斜堆中每个元素的下标就是对应着骑士的编号,很好写!

复杂度是 \(O(m \log m)\)lych 说是 \(O(m \log ^ 2 m)\) ? 我也不知道是不是qwq

代码

#include <bits/stdc++.h>

#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << x << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)

using namespace std;

inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}

typedef long long ll;
inline ll read() {
    ll x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
    for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
    return x * fh;
}

void File() {
#ifdef zjp_shadow
    freopen ("2107.in", "r", stdin);
    freopen ("2107.out", "w", stdout);
#endif
}

const int N = 3e5 + 1e3;
const ll inf = 1e18;

int n, m; ll Def[N];
int opt[N]; ll val[N];

namespace Lifist_Tree {

    ll val[N], TagMult[N], TagAdd[N];

    int ls[N], rs[N];

    inline void Mult(int pos, ll uv) { if (pos) val[pos] *= uv, TagAdd[pos] *= uv, TagMult[pos] *= uv; }

    inline void Add(int pos, ll uv) { if (pos) val[pos] += uv, TagAdd[pos] += uv; }

    inline void Push_Down(int x) {
        if (TagMult[x] != 1)
            Mult(ls[x], TagMult[x]), Mult(rs[x], TagMult[x]), TagMult[x] = 1;

        if (TagAdd[x] != 0)
            Add(ls[x], TagAdd[x]), Add(rs[x], TagAdd[x]), TagAdd[x] = 0;
    }

    int Merge(int x, int y) {
        if (!x || !y) return x | y;
        if (val[x] > val[y]) swap(x, y);
        Push_Down(x); 
        rs[x] = Merge(rs[x], y);
        swap(ls[x], rs[x]);
        return x;
    }

    inline int Pop(int x) {
        Push_Down(x);
        int tmp = Merge(ls[x], rs[x]);
        ls[x] = rs[x] = 0;
        return tmp;
    }

}

vector<int> G[N];
int dep[N], die[N], ans[N], rt[N];
void Dfs(int u) {
    int cur = rt[u];
    for (int v : G[u])
        dep[v] = dep[u] + 1, Dfs(v), cur = Lifist_Tree :: Merge(cur, rt[v]);

    while (cur && Lifist_Tree :: val[cur] < Def[u])
        die[cur] = u, cur = Lifist_Tree :: Pop(cur), ++ ans[u];
    if (opt[u])
        Lifist_Tree :: Mult(cur, val[u]);
    else
        Lifist_Tree :: Add(cur, val[u]);

    rt[u] = cur;
}

int pos[N];
int main () {

    File();

    n = read(); m = read();

    Def[0] = inf; For (i, 1, n) Def[i] = read();

    For (i, 2, n) {
        int from = read();
        G[from].push_back(i);
        opt[i] = read(); val[i] = read();
    }
    G[0].push_back(1);

    For (i, 1, m) {
        Lifist_Tree :: val[i] = read(); Lifist_Tree :: TagMult[i] = 1; pos[i] = read();
        rt[pos[i]] = Lifist_Tree :: Merge(rt[pos[i]], i);
    }

    Dfs(0);
    For (i, 1, n) 
        printf ("%d\n", ans[i]);
    For (i, 1, m)
        printf ("%d\n", dep[pos[i]] - dep[die[i]]);

    return 0;
}

直径的性质

\(F(S)\) 表示集合 \(S\) 中最远的两个点构成的集合,那么对同一棵树中的集合 \(S, T\)\(F(S \cup T) \subseteq F(S) \cup F(T)\)

这个证明。。。我不会qwq fakesky 说可以反证法来证明?

51nod 1766 树上最远点对

题意

给定一棵树,多次询问 \(a, b, c, d\) ,求 \(\displaystyle \max_{a \le i \le b, c \le j \le d} dist(i, j)\)

题解

用线段树维护区间最远点对,然后利用上面的性质。

每次合并的时候枚举 \(\displaystyle\binom 4 2 = 6\) 种情况,取最远的一对作为答案就行了。

用前面讲的欧拉序列和 \(ST\) 表求 \(lca\) ,复杂度可以优化成 \(O((n + q) \log n)\)

代码

#include <bits/stdc++.h>

#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << x << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)

using namespace std;

inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}

inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1; for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48); return x * fh; }

void File() {
    freopen ("1766.in", "r", stdin);
    freopen ("1766.out", "w", stdout);
}

const int N = 110000;

typedef pair<int, int> PII;
#define fir first
#define sec second

vector<PII> G[N];
int dep[N], dis[N], minpos[N * 2][21], tot = 0, Log2[N * 2], app[N];

inline bool cmp(int x, int y) { return dep[x] < dep[y]; }

inline int Get_Lca(int x, int y) {
    int len = Log2[y - x + 1], 
        p1 = minpos[x][len], 
        p2 = minpos[y - (1 << len) + 1][len];
    return cmp(p1, p2) ? p1 : p2;
}

inline int Get_Dis(int x, int y) {
    int tmpx = app[x], tmpy = app[y];
    if (tmpx > tmpy) swap(tmpx, tmpy);
    int Lca = Get_Lca(tmpx, tmpy);
    return dis[x] + dis[y] - dis[Lca] * 2;
}

void Dfs_Init(int u, int fa = 0) {
    minpos[app[u] = ++ tot][0] = u;
    dep[u] = dep[fa] + 1;
    For (i, 0, G[u].size() - 1) {
        PII cur = G[u][i];
        int v = cur.fir;
        if (v != fa) dis[v] = dis[u] + cur.sec, Dfs_Init(v, u);
    }
    if (fa) minpos[++ tot][0] = fa;
}

typedef pair<int, int> PII;
#define fir first
#define sec second
#define mp make_pair

inline void Update(PII &cur, PII a, PII b, bool flag) {
    int lx = a.fir, ly = a.sec, rx = b.fir, ry = b.sec, res = 0;

    if (flag && chkmax(res, Get_Dis(lx, ly))) cur = mp(lx, ly);
    if (chkmax(res, Get_Dis(lx, rx))) cur = mp(lx, rx);
    if (chkmax(res, Get_Dis(lx, ry))) cur = mp(lx, ry);

    if (chkmax(res, Get_Dis(ly, rx))) cur = mp(ly, rx);
    if (chkmax(res, Get_Dis(ly, ry))) cur = mp(ly, ry);
    if (flag && chkmax(res, Get_Dis(rx, ry))) cur = mp(rx, ry);
}

namespace Segment_Tree {

#define lson o << 1, l, mid
#define rson o << 1 | 1, mid + 1, r

    PII Adv[N << 2];

    void Build(int o, int l, int r) {
        if (l == r) { Adv[o] = mp(l, r); return ; }
        int mid = (l + r) >> 1;
        Build(lson); Build(rson);
        Update(Adv[o], Adv[o << 1], Adv[o << 1 | 1], true);
    }

    PII Query(int o, int l, int r, int ql, int qr) {
        if (ql <= l && r <= qr) return Adv[o];
        PII tmp; int mid = (l + r) >> 1;
        if (qr <= mid) tmp = Query(lson, ql, qr);
        else if (ql > mid) tmp = Query(rson, ql, qr);
        else Update(tmp, Query(lson, ql, qr), Query(rson, ql, qr), true);
        return tmp;
    }

#undef lson
#undef rson

}

int n, m;

int main () {

    n = read();
    For (i, 1, n - 1) {
        int u = read(), v = read(), w = read();
        G[u].push_back(mp(v, w));
        G[v].push_back(mp(u, w));
    }
    Dfs_Init(1);

    For (i, 2, tot) Log2[i] = Log2[i >> 1] + 1;

    For (j, 1, Log2[tot]) For (i, 1, tot - (1 << j) + 1) {
        register int p1 = minpos[i][j - 1], p2 = minpos[i + (1 << (j - 1))][j - 1];
        minpos[i][j] = cmp(p1, p2) ? p1 : p2;
    }


    Segment_Tree :: Build(1, 1, n);

    m = read();
    For (i, 1, m) {
        int a = read(), b = read(), c = read(), d = read();

        PII ans;

        Update(ans, 
                Segment_Tree :: Query(1, 1, n, a, b), 
                Segment_Tree :: Query(1, 1, n, c, d), false);

        printf ("%d\n", Get_Dis(ans.fir, ans.sec));
    }

    return 0;
}

雅礼NOIp 7-22 Practice

题意

给你一棵以 \(1\) 为根的树,一开始所有点全为黑色。

需要支持两个操作:

  • \(C ~ p\) ,将 \(p\) 节点反色
  • \(G ~ p\) ,求 \(p\) 子树中最远的两个黑色节点的距离。

题解

[ZJOI2007] 捉迷藏 进行了加强,支持查询子树。

和上面那题是一样的,因为每棵树的子树的 \(dfs\) 序是连续的。

我们考虑用线段树维护一段连续 \(dfs\) 序的点的最远点对就行了。

猜你喜欢

转载自www.cnblogs.com/zjp-shadow/p/9355538.html