开始前先进行一个全民投票吧!
投票:遇到静态树链问题,你会选择(点击选项进行投票):
1、直接树链剖分!
2、其他在线算法?
3、考虑离线算法。
4、本蒟蒻不会……
简介
本文主要讨论静态的树链问题。树链问题,可以看作树上的区间问题,这个区间可以看作一个结点 \(u\) 到另一个结点 \(v\) 所经最短路径形成的序列。这种问题大多需要用树链剖分来解决,但树链剖分会在解决对应区间问题的数据结构上增加一个 \(\log\),这无疑会大大增加算法的常数压力,因此本文将在静态树链问题上讨论其他解法。
由于作者能力有限,因此仅提供了三道例题,而且知道的解法并不多,因此本文的代码量十分饱满,理论方面的讲解对新人比较不友好,也希望大家能够提出其他高效的思路和建议。
例题一:树链求和
解法一:树链剖分
思路非常简单,就是纯粹的树链剖分,由于没有修改操作,可以直接跑前缀和,然后以 \(O(1)\) 的复杂度完成每次子区间查询,单次查询的时间复杂度为 \(O(\log n)\)。
这种做法的时间复杂度主要看查询的数据结构,在遇到复杂的静态树链问题上并不吃香,很容易被卡常。
参考代码如下:
- #include<bits/stdc++.h>
- using namespace std;
- //#define int long long
- #define I_love_Foccarus return
- #define rep(i,a,b) for(int i=a;i<=b;i++)
- #define deap(i,a,b) for(int i=a;i>=b;i--)
- #define in(a) a=read()
- #define fi first
- #define se second
- const int N = 1e5 + 5;
- const int inf = INT_MAX;
- inline int read() { //浓缩后的快读
- int x=0,f=1,ch=getchar();
- while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
- while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
- I_love_Foccarus x*f;
- }
- struct Edge { //链式前向星存树
- int act , nex;
- } edge[N];
- int head[N] , eid;
- void eadd(int u , int v) {
- edge[++eid].act = v , edge[eid].nex = head[u] , head[u] = eid;
- }
- int c[N] , a[N];
- int sz[N] , w[N] , top[N] , dfn[N] , dfm[N] , dep[N] , fa[N], vis;
- void dfsw(int u,int father) { //求重链
- sz[u] = 1;
- dep[u] = dep[father] + 1;
- for(int i = head[u] ; i ; i = edge[i].nex) {
- int v = edge[i].act;
- dfsw(v , u);
- sz[u] += sz[v];
- if(sz[v] > sz[w[u]]) w[u] = v;
- }
- }
- void dfs(int u , int ftop) { //树链剖分
- dfn[u] = ++vis , dfm[vis] = u , top[u] = ftop;
- if(w[u]) dfs(w[u] , ftop);
- for(int i = head[u] ; i ; i = edge[i].nex) {
- int v = edge[i].act;
- if(v == w[u]) continue;
- dfs(v , v);
- }
- }
- int query(int x , int y) { //查询
- int ans = 0;
- while(top[x] != top[y]) {
- if(dep[top[x]] < dep[top[y]]) swap(x , y);
- ans += c[dfn[x]] - c[dfn[top[x]] - 1]; //统计子区间和
- x = fa[top[x]];
- }
- if(dfn[x] < dfn[y]) swap(x , y);
- ans += c[dfn[x]] - c[dfn[y] - 1];
- I_love_Foccarus ans;
- }
- signed main() {
- int n , root;
- in(n);
- rep(i , 1 , n)in(a[i]);
- rep(i , 1 , n) {
- in(fa[i]);
- if(fa[i] != i)eadd(fa[i] , i);
- else root = i;
- }
- dfsw(root , 0);
- dfs(root , 0);
- rep(i , 1 , n)c[i] = c[i - 1] + a[dfm[i]]; //前缀和初始化
- int q;
- in(q);
- while(q--){
- int u , v;
- in(u) , in(v);
- printf("%d\n",query(u , v));
- }
- I_love_Foccarus 0;
- }
复制代码 解法二:容斥原理(树上差分)
不难发现,查询的内容可减,因此可以先通过一轮 dfs 求出每个结点到根结点的路径形成的序列的序列和,用一个数组 \(f\) 储存下来。
然后举个例子,如图,查询 \(3,5\):
不难发现 \(f_{\operatorname{LCA}(u,v)}\) 被计算了两次,需要减去,然而 \(\operatorname{LCA}(u,v)\) 也在查询的树链内,因此还需将 \(\operatorname{LCA}(u,v)\) 的权值加上,从而得出结点 \(u\) 到结点 \(v\) 所经最短路径形成的序列的序列和为 \(f_u+f_v- 2f_{\operatorname{LCA}(u,v)}+a_{\operatorname{LCA}(u,v)}\),其中 \(a\) 表示点权。
而 \(a_{\operatorname{LCA}(u,v)}\) 又可以由 \(f_{\operatorname{LCA}(u,v)}-f_{fa_{\operatorname{LCA}(u,v)}}\) 得出,其中 \(fa\) 表示该结点的父亲结点。因此也可以写作 \(f_u+f_v- f_{\operatorname{LCA}(u,v)}-f_{fa_{\operatorname{LCA}(u,v)}}\),特殊的,如果 \(\operatorname{LCA}(u,v)\) 为根结点,那么查询的值就应为 \(f_u+f_v- f_{\operatorname{LCA}(u,v)}\),因为根结点没有父亲结点。
这种做法需要求 LCA,因此单次查询的时间复杂度同样为 \(O(\log n)\)。
虽然这种思路很简单,代码也很好写,但是处理的问题必须满足可减,这就是它的局限性。面对复杂的静态树链问题,这种做法往往不会被采用。
参考代码如下:
- #include<bits/stdc++.h>
- using namespace std;
- //#define int long long
- #define I_love_Foccarus return
- #define rep(i,a,b) for(int i=a;i<=b;i++)
- #define deap(i,a,b) for(int i=a;i>=b;i--)
- #define in(a) a=read()
- #define fi first
- #define se second
- const int N = 1e5 + 5;
- const int inf = INT_MAX;
- inline int read() { //浓缩后的快读
- int x=0,f=1,ch=getchar();
- while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
- while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
- I_love_Foccarus x*f;
- }
- struct Edge { //链式前向星存树
- int act , nex;
- } edge[N];
- int head[N] , eid;
- void eadd(int u , int v) {
- edge[++eid].act = v , edge[eid].nex = head[u] , head[u] = eid;
- }
- int n , q;
- int fa[N][21] , d[N] , f[N];
- void dfs(int u,int dep) { //倍增 LCA 初始化
- d[u] = dep;
- for(int i = head[u] ; i ; i = edge[i].nex){
- int v = edge[i].act;
- f[v] += f[u]; //记录该结点到根结点的权值和
- dfs(v , dep + 1);
- }
- }
- int LCA(int x , int y) { //倍增 LCA
- if(d[x] < d[y])swap(x , y);
- for(int i = 20 ; i >= 0 ; i--) {
- int v = fa[x][i];
- if(d[v] < d[y])continue;
- x = v;
- }
- if(x == y)return x;
- for(int i = 20 ; i >= 0 ; i--) {
- if(fa[x][i] == fa[y][i])continue;
- x = fa[x][i];
- y = fa[y][i];
- }
- I_love_Foccarus fa[x][0];
- }
- signed main() {
- int root;
- in(n);
- rep(i , 1 , n)in(f[i]);
- rep(i , 1 , n) {
- in(fa[i][0]);
- if(fa[i][0] != i)eadd(fa[i][0] , i);
- else root = i , fa[i][0] = 0; //让根结点指向权值为零的数
- }
- dfs(root , 1);
- for(int i = 1 ; i <= 20 ; i++) { //倍增 LCA 初始化
- for(int j = 1 ; j <= n ; j++) {
- int v = fa[j][i - 1];
- fa[j][i] = fa[v][i - 1];
- }
- }
- in(q);
- while(q--){
- int u , v;
- in(u) , in(v);
- int lca = LCA(u , v);
- printf("%d\n",f[u] + f[v] - f[lca] - f[fa[lca][0]]); //容斥原理求解
- }
- I_love_Foccarus 0;
- }
复制代码 解法四:树上倍增
就像求倍增 LCA,在记录成倍增长的父结点信息的同时合并权值,然后在倍增求 LCA 的过程中将权值合并,也就是把查询的要合并的两条链拆解成若干个二进制片段,然后合并。信息不可重复,需要去重,为了权值避免权值重复合并,合并记录的信息不能计入成倍跳到了的结点的权值,然后在查询时加上漏掉的 LCA 的点权。
时间复杂度为 \(O(q\log n)\)。
参考代码如下:
- #include<bits/stdc++.h>
- using namespace std;
- //#define int long long
- #define I_love_Foccarus return
- #define rep(i,a,b) for(int i=a;i<=b;i++)
- #define deap(i,a,b) for(int i=a;i>=b;i--)
- #define in(a) a=read()
- #define fi first
- #define se second
- const int N = 1e5 + 5 , maxn = 1e6 + 5;
- const int inf = INT_MAX;
- inline int read() { //浓缩后的快读
- int x=0,f=1,ch=getchar();
- while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
- while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
- I_love_Foccarus x*f;
- }
- struct Edge { //链式前向星存树
- int act , nex;
- } edge[N];
- int head[N] , eid;
- void eadd(int u , int v) {
- edge[++eid].act = v , edge[eid].nex = head[u] , head[u] = eid;
- }
- vector< pair< pair<int , int> , int > >solve[maxn]; //LCA 标记
- vector< pair<int , int> >Q[maxn]; //离线储存查询
- int n , q , fa[N] , vis[N] , d[N] , a[N] , ans[maxn];
- int find(int x) { //带权并查集合并
- if(fa[x] != fa[fa[x]]){ //如果父亲结点不是根结点
- int root = find(fa[x]);
- d[x] += d[fa[x]]; //将父亲结点权值合并到自身
- fa[x] = root; //路径压缩
- }
- I_love_Foccarus fa[x];
- }
- void Tarjan(int u) { //Tarjan 离线求树链和
- fa[u] = u , vis[u] = 1 , d[u] = a[u]; //结点初始化
- for(int i = head[u] ; i ; i = edge[i].nex) {
- int v = edge[i].act;
- Tarjan(v);
- fa[v] = u; //可以直接合并
- }
- for(auto v:Q[u]){ //求 LCA 和打标记
- if(!vis[v.fi])continue;
- solve[find(v.fi)].push_back(make_pair(make_pair(u , v.fi) , v.se));
- }
- for(auto v:solve[u]){ //处理每次询问
- find(v.fi.fi) , find(v.fi.se); //进行权值合并
- if(v.fi.fi == v.fi.se) ans[v.se] = a[u]; //分类讨论
- else if(v.fi.fi == u || v.fi.se == u ) ans[v.se] = d[v.fi.fi] + d[v.fi.se];
- else ans[v.se] = d[v.fi.fi] + d[v.fi.se] + a[u];
- }
- }
- signed main() {
- int root;
- in(n);
- rep(i , 1 , n)in(a[i]);
- rep(i , 1 , n) {
- int f;
- in(f);
- if(f != i)eadd(f , i);
- else root = i;
- }
- in(q);
- rep(i , 1 , q) { //离线记录每次询问
- int u , v;
- in(u) , in(v);
- Q[u].push_back(make_pair(v , i));
- Q[v].push_back(make_pair(u , i));
- }
- Tarjan(root); //Tarjan 离线求树链和
- rep(i , 1 , q) printf("%d\n",ans[i]);
- I_love_Foccarus 0;
- }
复制代码 解法二:猫树分治
树链剖分的时间主要浪费在线段树的查询上,可以在这方面进行优化。由于查询的是一个静态区间,因此考虑用猫树分治实现,至于如何合并,可参考解法三。
由于树链剖分需要查询 \(O(\log n)\) 个子区间,因此猫树需要处理 \(O(q\log n)\) 次查询,如果采用朴素的猫树,时间复杂度为 \(O(q\log^2 n)\),不能接受。猫树的一只 \(\log\) 主要消耗在确定 mid 上,可以在此方面进行优化。据说有一种二进制的方法可以直接实现,但是蒟蒻不会,因此用了 ST 表,维护区间内的一个数 \(x\),满足以 \(x\) 为 mid 的最大区间长度,可以证明,查询的区间一定处于该区间的内部。该算法常数稍大,但把优化打满后勉强能过。
总时间复杂度为 \(O(q\log n)\)。
参考代码如下:
- #include<bits/stdc++.h>
- using namespace std;
- //#define int long long
- #define I_love_Foccarus return
- #define rep(i,a,b) for(int i=a;i<=b;i++)
- #define deap(i,a,b) for(int i=a;i>=b;i--)
- #define in(a) a=read()
- #define fi first
- #define se second
- const int N = 1e5 + 5;
- const int inf = INT_MAX;
- inline int read() { //浓缩后的快读
- int x=0,f=1,ch=getchar();
- while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
- while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
- I_love_Foccarus x*f;
- }
- struct Edge { //链式前向星存树
- int act , nex;
- } edge[N];
- int head[N] , eid;
- void eadd(int u , int v) {
- edge[++eid].act = v , edge[eid].nex = head[u] , head[u] = eid;
- }
- int n , q;
- int fa[N][21] , f[N][21] , d[N] , vis[N] , a[N];
- void dfs(int u,int dep) { //树上倍增初始化
- d[u] = dep;
- for(int i = head[u] ; i ; i = edge[i].nex){
- int v = edge[i].act;
- dfs(v , dep + 1);
- }
- }
- int root;
- int solve(int x , int y) { //树上倍增
- int ans = 0;
- if(d[x] < d[y])swap(x , y);
- for(int i = 20 ; i >= 0 ; i--) {
- int v = fa[x][i];
- if(d[v] < d[y])continue;
- if(x != y)
- ans += f[x][i];
- x = v;
- }
- if(x == y) I_love_Foccarus ans + a[y];
- for(int i = 20 ; i >= 0 ; i--) {
- if(fa[x][i] == fa[y][i])continue;
- ans += f[x][i] + f[y][i];
- x = fa[x][i];
- y = fa[y][i];
- }
- I_love_Foccarus ans + f[x][0] + f[y][0] + a[fa[x][0]];
- }
- signed main() {
- in(n);
- rep(i , 1 , n)in(a[i]);
- rep(i , 1 , n) {
- in(fa[i][0]);
- f[i][0] = a[i];
- if(fa[i][0] != i)eadd(fa[i][0] , i);
- else root = i;
- }
- dfs(root , 1);
- for(int i = 1 ; i <= 20 ; i++) { //树上倍增初始化
- for(int j = 1 ; j <= n ; j++) {
- int v = fa[j][i - 1];
- if(fa[j][i - 1] == fa[v][i - 1])continue; //去重
- fa[j][i] = fa[v][i - 1];
- f[j][i] = f[j][i - 1] + f[v][i - 1]; //权值合并
- }
- }
- in(q);
- while(q--){
- int u , v;
- in(u) , in(v);
- printf("%d\n",solve(u , v));
- }
- I_love_Foccarus 0;
- }
复制代码 解法四:树上倍增
和例题一的思路一样,合并操作用加法重载后根本不用改,只需在返回值时分类讨论即可。时间复杂度一样。
参考代码如下:
[code]#includeusing namespace std;//#define int long long#define I_love_Foccarus return#define rep(i,a,b) for(int i=a;i=b;i--)#define in(a) a=read()#define fi first#define se secondconst int N = 1e5 + 5;const int inf = INT_MAX;inline int read() { //浓缩后的快读 int x=0,f=1,ch=getchar(); while(ch'9'){if (ch=='-') f=-1;ch=getchar();} while(ch>='0'&&cha = this->b = this->c = this->d = x; } node friend operator + (const node &u , const node &v) { //重载加法处理合并 node ans; ans.a = max(max(u.a , v.a) , v.c + u.d); ans.b = u.b + v.b; ans.c = max(u.c , u.b + v.c); ans.d = max(v.d , v.b + u.d); I_love_Foccarus ans; }}f[N][21]; struct Edge { //链式前向星存树 int act , nex;} edge[N];int head[N] , eid;inline void eadd(int u , int v) { edge[++eid].act = v , edge[eid].nex = head , head = eid;}int n , q;int fa[N][21] , d[N] , vis[N] , a[N];void dfs(int u,int dep) { //树上倍增初始化 d = dep; for(int i = head ; i ; i = edge.nex){ int v = edge.act; dfs(v , dep + 1); } } int root; inline int c(int x){ return x > 0 ? x : 0;}int solve(int x , int y) { //树上倍增 node ansx = node(-0x3f3f3f3f) , ansy = node(-0x3f3f3f3f); //分别处理两个结点到 LCA 的链的信息 ansx.b = ansy.b = 0; if(d[x] < d[y])swap(x , y); for(int i = 20 ; i >= 0 ; i--) { int v = fa[x]; if(d[v] < d[y])continue; if(x != y) ansx = ansx + f[x]; x = v; } if(x == y) I_love_Foccarus max(max(ansx.a , a[y]) , ansx.d + a[y]); for(int i = 20 ; i >= 0 ; i--) { if(fa[x] == fa[y])continue; ansx = ansx + f[x]; ansy = ansy + f[y]; x = fa[x]; y = fa[y]; } ansx = ansx + f[x][0]; ansy = ansy + f[y][0]; I_love_Foccarus max(max(ansx.a , ansy.a) , c(ansx.d) + c(ansy.d) + a[fa[x][0]]); }signed main() { in(n); rep(i , 1 , n)in(a); rep(i , 1 , n) { in(fa[0]); f[0] = node(a); if(fa[0] != i)eadd(fa[0] , i); else root = i; } dfs(root , 1); for(int i = 1 ; i > 1; if(q > 1 , tmp = tree[tree[idxu].cl].ans + tree[tree[idxv].cl].ans - tree[tree[lca].cl].ans - tree[tree[lca2].cl].ans; //容斥原理求解 if(q |