[算法学习笔记] 树链剖分(重链剖分)
概述
树链剖分能将一棵树剖分成若干条链的形式。从而能在树上用一些线性数据结构如线段树,树状数组等维护。树链剖分分为重链剖分,长链剖分,LCT 剖分等。最常用的是重链剖分。本文将讲解重链剖分。
本文将持续更新。
我们接下来给出一些定义。
-
重儿子:指一个节点所有儿子中,子树最大的儿子。
-
轻儿子:指一个节点除了重儿子其他的儿子。
-
重边:连接一个节点和它重儿子的边。
-
轻边:连接一个节点和它轻儿子的边。
(图源 https://blog.csdn.net/qq_41418281/article/details/108220247 ,侵删。)
在上图中,红色的节点即为重儿子,红色的边为重链。黄色的点和黄色的边分别对应其轻儿子和轻链。
实现
实现重链剖分,我们需要两边 dfs。第一遍 dfs 预处理出重儿子,每个节点的父节点(便于后面操作),子树大小(重儿子),节点深度(便于后面操作)。
第二遍 dfs,我们找到每条重链的链头(top)
这里给出参考代码。
重链剖分
void dfs1(int p, int d)
{
int size = 1, ma = 0;
depth[p] = d;
for (auto q : Edge[p])
if (!depth[q])
{
dfs1(q, d + 1);
f[q] = p;
size += siz[q];
if (siz[q] > ma)
son[p] = q, ma = siz[q];
}
siz[p] = size;
}
void dfs2(int p)
{
// cout<<u<<"qwq"<<endl;;
for(auto q:Edge[p])
{
if(!top[q])
{
if(q == son[p])
top[q] = top[p];
else
top[q] = q;
dfs2(q);
}
}
}
性质
通过上图,不难发现,重链是一条以轻儿子为链头的一条链(根节点除外),而轻链是连接重链与重链之间的桥梁。每个重链的链头的父亲一定在另外一条重链上。因此,我们记录每个节点的父亲,每条链的链头,就能快速实现重链与重链之间的跳跃。
其次,每个节点最多只能位于一条重链上。且一棵节点数量为 \(n\) 的树,从树上任意一条节点走到根节点,所经过的轻边数量不超过 \(\log n\) 条。
剖分完成后,我们便能在树上实现快速跳跃,重链剖分求解 LCA 是一种典型应用。下文将讲解。
重链剖分求 LCA
下文假设要求 \(a,b\) 的 LCA。
首先,若 \(a,b\) 位于同一条链上,则 LCA 即为深度较小的那个点。
若不在同一条链上。则 LCA 要么在链头深度较小的那条链上,要么就是两个链头的 LCA。绝不可能在链头深度较大的链上。这很显然,一条链只有到了链头才有轻边与其他重链连接。因此,我们不断让链头深度较大的往上跳到上一条链,直到到达同一条链为止。
具体实现见代码。
重链剖分求 LCA(省去重链剖分部分)
int lca(int a,int b)
{
if(a > b) swap(a,b);
while(top[a] != top[b])
{
// cout<<a<<" "<<b<<endl;
if(depth[top[a]] > depth[top[b]]) a = f[top[a]];
else b = f[top[b]];
}
return (depth[a] > depth[b] ? b : a);
}
应用
树剖套线段树模板
树剖的精髓在于我们可以将一条链上信息用数据结构维护,上文提到从树上任意一点走到根节点,所经过的轻边数量不超过 \(\log n\) 条,很多情况下我们可以将一条重链当作整体,套用数据结构处理。例如可以用线段树,树状数组等维护链。
下面将讲解重链剖分套用线段树典型题。
Description
如题,已知一棵包含 \(N\) 个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:
-
1 x y z
,表示将树从 \(x\) 到 \(y\) 结点最短路径上所有节点的值都加上 \(z\)。 -
2 x y
,表示求树从 \(x\) 到 \(y\) 结点最短路径上所有节点的值之和。 -
3 x z
,表示将以 \(x\) 为根节点的子树内所有节点值都加上 \(z\)。 -
4 x
表示求以 \(x\) 为根节点的子树内所有节点值之和
对于 \(100\%\) 的数据: \(1\le N \leq {10}^5\),\(1\le M \leq {10}^5\),\(1\le R\le N\),\(1\le P \le 2^{30}\)。所有输入的数均在 int
范围内。
显然,对于树上 \(\forall\{x,y\}\),它们之间的最短路径一定是 \((x-lca(x,y)-y)\), 我们显然可以树上差分。但是后面不好操作。
下文“链” 若无特殊说明,都指重链。
前文提到,在重链剖分求 LCA 时,我们用到了类似于 “跳链” 的操作。具体地,对于点对 \((a,b)\),我们确信 \(LCA(a,b)\) 要么是 \(a,b\) 所处重链链头的 LCA,要么 LCA 位于链头深度较小的链上。绝不可能位于链头深度较大的链上。因此,对于链头深度较大的链,我们完全可以直接跳到上面的链上去。不要忘记跳到同一条链上后还要把两点之间的点扔到线段树里处理。
因此,对于操作 1 和操作 2,我们在跳链求 LCA 的同时,把跳过的链扔到线段树里处理一下就好了。因为对于同一条链,它们的 dfs 序是连续的。可以直接处理。
在处理之前,我们可以将所有点的点权映射到一个新数组中,便于处理。
对于操作 3, 4。令 \(dfn_i\) 表示 点 \(i\) 的 dfs 序,\(size_i\) 表示点 \(i\) 的子树大小。比较显然的是,以点 \(i\) 为根节点的子树所对应的 dfs 序一定是 \(dfn_i~dfn_i+size_i-1\)。显然是连续的,直接扔到线段树处理即可。
上文描述非常简单,实现需要注意一些细节。
模板题代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1000010;
vector <int> Edge[N];
int depth[N],son[N],dfn[N],siz[N],top[N],f[N];
int n,m,r,p;
int mark[N];// seg tree
int tree[N];
int val[N];
int cnt = 0;
int a[N]; // new array
int rk[N];
void dfs1(int u,int fat)
{
f[u] = fat;
depth[u] = depth[fat] + 1;
siz[u] = 1; // size
int maxn = 0,maxx = 0; // heavy_son
for(auto v:Edge[u])
{
if(v == fat) continue;
dfs1(v,u);
if(siz[v] > maxn)
{
maxn = siz[v];
maxx = v;
}
siz[u] += siz[v];
}
son[u] = maxx;
}
void dfs2(int u,int t)
{
// cout<<u<<endl;
top[u] = t;
dfn[u] = ++ cnt; // dfs 序
a[cnt] = val[u];
rk[cnt] = u;
if(!son[u]) return;
dfs2(son[u],t);
// cout<< son[u] <<"qwq"<<endl;
for(auto v:Edge[u])
{
if(v != son[u] && v != f[u])
{
dfs2(v,v);
}
}
}
//----------------------- seg tree --------------------------------
void pushdown(int p,int len)
{
tree[p*2] += (len-len/2)*mark[p];
tree[p*2+1] += (len/2)*mark[p];
mark[p*2] += mark[p];
mark[p*2+1] += mark[p];
mark[p] = 0;
//if(p*2+1 == 5) cout<<len<<"erqiwe"<<endl;
// cout<<p*2+1<<" "<<tree[p*2+1]<<endl;
}
void build(int l,int r,int p)
{
if(l == r)
{
tree[p] = a[l];
// // cout<<a[l]<<"aya"<<endl;
// cout<<"tree["<<p<<"]="<<tree[p]<<endl;
return;
}
int mid = (l+r) / 2;
build(l,mid,p*2);
build(mid+1,r,p*2+1);
tree[p] = tree[p*2] + tree[p*2+1];
}
void modify(int l,int r,int pl,int pr,int p,int d)
{
if(l > pr || r < pl) return;
else if(l >= pl && r <= pr)
{
tree[p] += (r-l+1)*d;
mark[p] += d;
return;
}
else
{
int mid = (l+r) / 2;
pushdown(p,r-l+1);
modify(l,mid,pl,pr,p*2,d);
modify(mid+1,r,pl,pr,p*2+1,d);
tree[p] = tree[p*2] + tree[p*2+1];
}
}
int query(int l,int r,int pl,int pr,int p)
{
if(l > pr || r < pl) return 0;
else if(l >= pl && r <= pr) return tree[p];
else
{
int mid = (l+r) / 2;
pushdown(p,r-l+1);
return query(l,mid,pl,pr,p*2) + query(mid+1,r,pl,pr,p*2+1);
}
}
//------------------------------------- seg tree -------------------------------------
void update_path(int x,int y,int z)
{
while(top[x] != top[y])
{
if(depth[top[x]] > depth[top[y]])
{
modify(1,n,dfn[top[x]],dfn[x],1,z);
x = f[top[x]];
}
else
{
modify(1,n,dfn[top[y]],dfn[y],1,z);
y = f[top[y]];
}
}
if(depth[x] > depth[y]) modify(1,n,dfn[y],dfn[x],1,z);
else modify(1,n,dfn[x],dfn[y],1,z);
}
int query_path(int x,int y)
{
int ans = 0;
while(top[x] != top[y])
{
if(depth[top[x]] > depth[top[y]])
{
ans += query(1,n,dfn[top[x]],dfn[x],1);
x = f[top[x]];
}
else
{
ans += query(1,n,dfn[top[y]],dfn[y],1);
y = f[top[y]];
}
}
if(depth[x] > depth[y]) ans += query(1,n,dfn[y],dfn[x],1);
else ans += query(1,n,dfn[x],dfn[y],1);
return ans;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>r>>p;
for(int i=1;i<=n;i++) cin>>val[i];
for(int i=1;i<n;i++) // build tree
{
int u,v;
cin>>u>>v;
Edge[u].push_back(v);
Edge[v].push_back(u);
}
dfs1(r,0);
top[r] = r;
dfs2(r,r);
// cout<<rk[5]<<endl;
build(1,n,1);
//cout<<tree[rk[5]]<<endl;
// cout<<dfn[2]<<endl;
// cout<<tree[5]<<endl;
while(m--)
{
// cout<<tree[5]<<"qwq"<<endl;
int op,x,y,z;
cin>>op>>x;
if(op == 1)
{
cin>>y>>z;
update_path(x,y,z);
}
else if(op == 2)
{
cin>>y;
cout<<query_path(x,y)%p<<endl;
}
else if(op == 3)
{
cin>>z;
modify(1,n,dfn[x],dfn[x] + siz[x]-1 ,1,z);
// cout<<tree[5]<<"!!!"<<endl;
}
else
{
cout<<query(1,n,dfn[x],dfn[x]+siz[x]-1,1)%p<<endl;
}
}
return 0;
}
习题
Description
有一棵有 \(n\) 个点的有根树,树的根编号为 \(1\). 一开始每个节点上面的值都为 \(0\)。
现在有 \(q\) 个询问,每个询问有两种类型:
1 v x k
你需要给 \(v\) 节点的值增加 \(x\),如果 \(v\) 的子孙 \(u\),与 \(v\) 的距离为 \(i\),那么需要给节点 \(u\) 上的值增加 \((x - i \times k)\) 。两个点间的距离定义为两点之间的最短路。
2 v
输出节点 \(v\) 上的值。
对于每个询问,你需要输出答案 $\mod 10^9+7 $后的值。
\(1\le n,q \ge 3\times 10^5\)
乍看问题比较棘手,对于修改操作,虽然是子树修改,但是子树内的修改内容不一致。
注意到只有单点询问,且对于子树内的修改都是有规律性的,和到子树根节点的距离有关。这使我们想到一个经典算法:差分。
具体地,令序列 \(b\) 为点权。差分数组 \(a_i=b_i-b_{i-1}\)。每次操作我们只需要令 \(b_v+x,b_v-k(v\in [dfn_u+1,dfn_u+siz_u-1])\) (数组名同上文模板题讲解)。
对于单点查询,只需要前缀和扫一遍即可。树剖完我们可以直接跳链查询。查询 \(\sum \limits_{i=1}^vb_i\) 即可。
其他就是树剖和线段树模板。注意线段树不要开小了。
Code
// LUOGU_RID: 163719782
#include <bits/stdc++.h>
#define int long long
#define lx (x<<1)
#define rx (x<<1|1)
using namespace std;
const int N = 3e5+5;
const int mod = 1000000007;
int n,q;
int fa[N];
int depth[N],siz[N],son[N],dfn[N],top[N];
int cnt = 0;
vector <int> Edge[N];
int tree[N<<2],mark[N<<2];
int upd(int x)
{
return (x%mod+mod)%mod;
}
void dfs1(int u,int f)
{
fa[u] = f;
depth[u] = depth[f] + 1;
siz[u] = 1;
int maxn = 0 ,maxx = 0;
for(auto v:Edge[u])
{
if(v == f) continue;
dfs1(v,u);
if(siz[v] > maxn)
{
maxn = siz[v];
maxx = v;
}
siz[u] += siz[v];
}
son[u] = maxx;
}
void dfs2(int u,int tp)
{
dfn[u] = ++ cnt;
top[u] = tp;
if(!son[u]) return;
dfs2(son[u],tp);
for(auto v:Edge[u])
{
if(v == fa[u] || top[v]) continue;
dfs2(v,v);
}
}
// ------------------ seg tree ------------------
void pushdown(int x, int l, int r) {
int mid((l + r) >> 1);
if (mark[x]) {
(mark[lx] += mark[x]) %= mod; (mark[rx] += mark[x]) %= mod;
(tree[lx] += mark[x] * (mid - l + 1)) %= mod; (tree[rx] += mark[x] * (r - mid)) %= mod;
mark[x] = 0;
}
}
void pushup(int p)
{
tree[p] = tree[p*2] + tree[p*2+1];
}
void modify(int l,int r,int pl,int pr,int p,int d)
{
if(pl > pr) swap(pl,pr);
if(l > pr || r < pl) return;
if(l >= pl && r <= pr)
{
(mark[p] += d) %= mod, (tree[p] += d * (r - l + 1)) %= mod;
return;
}
int mid = (l+r) >> 1;
pushdown(p,l,r);
if(pl <= mid) modify(l,mid,pl,pr,p*2,d);
if(mid < pr) modify(mid+1,r,pl,pr,p*2+1,d);
pushup(p);
}
int query(int l,int r,int pl,int pr,int p)
{
if(pl > pr) swap(pl,pr);
// if(l > pr || r < pl) return 0;
if(l >= pl && r <= pr) return tree[p];
int mid = (l+r) >> 1;
pushdown(p,l,r);
int ans = 0;
if(pl <= mid) ans += query(l,mid,pl,pr,p*2);
if(mid < pr) ans += query(mid+1,r,pl,pr,p*2+1);
pushup(p);
return ans;
}
// ------------------ seg tree ------------------
int query_path(int x, int y) {//求x--y链上的权值和
int res(0);
while (top[x] != top[y]) res += query(1,n,dfn[x], dfn[top[x]],1), x = fa[top[x]];
return upd(res + query(1,n,dfn[x], dfn[y],1));
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=2;i<=n;i++)
{
cin>>fa[i];
Edge[i].push_back(fa[i]);
Edge[fa[i]].push_back(i);
}
dfs1(1,-1);
dfs2(1,1);
cin>>q;
while(q--)
{
int op,u,x,k;
cin>>op>>u;
if(op == 1)
{
cin>>x>>k; modify(1,n,dfn[u],dfn[u],1,x);
if(siz[u] > 1)
modify(1,n,dfn[u]+1,dfn[u]+siz[u]-1,1,-k);
}
else
{
// int ans = query_path(u,1);
cout<< query_path(u,1)<<endl;
}
}
}
练习
- [COCI2011-2012#3] PLAĆE 树剖基本操作板子。
- [HAOI2015]树上操作 板子。
边权转点权
Description
共有 \(n\) 座城市,\(m\) 条道路。每条道路都有一个限重 \(w_i\) 表示通过该边的车辆重量不得超过 \(w_i\)。每辆车要从 \(u\) 开到 \(v\),求不超过限重的前提下每辆车最多运多重的货物。
Analysis
简化题意:求两点之间最大边权最小值。
建出 kruskal 最大重构树后求两点 LCA 即可。kruskal 重构树上两点 LCA 即为亮点最大边权最小值/最小边权最大值。且 经过不大于/不小于边权 \(w_i\) 的点能到达的点为 \(w_i\) 的儿子。
我们也可以求出最大生成树后求树上两点路径上边权最小值。反证:若一条边不在最大生成树上,我们走最大生成树上的边一定更优不劣。这显然。用树链剖分即可轻松解决。
此时问题在于 题目给定边权,而树链剖分是对点权进行操作。我们最好将边权转换为我们熟悉的点权。
注意到树上每个儿子只有一个父亲,因此我们可定义点权 \(a_ii\) 为 \(i\) 到 \(fa_i\) 的边权。(\(fa_i\) 为 \(i\) 的父亲。)
树剖完跳重链查询时,跳重链不受影响,当 \(x,y\) 在一条重链时,设 \(depth_x < depth_y\),我们要求 \(dfn_{x+1},dfn_y\) 的区间值。原因显然。
Code
Code
#include <bits/stdc++.h>
#define x first
#define y second
using namespace std;
constexpr int N = 1000010;
constexpr int INF = 0x3f3f3f3f;
typedef pair<int,int> PAIR;
struct Node
{
int u,v,w;
bool operator<(const Node &a)const{
return w > a.w;
}
};
int val[N],fa[N],a[N]; // dsu
int fat[N],depth[N],son[N],siz[N],tp[N],dfn[N];// poufen
int tree[N];
int cnt;
int n,m;
int vis[N];
vector <PAIR> Edge[N];
vector <Node> qwq;
void init(){for(int i=1;i<=n;i++) fa[i] = i;}
int find(int x)
{
if(x == fa[x]) return x;
fa[x] = find(fa[x]);
return fa[x];
}
void kruskal() // MST (MAX)
{
for(auto t:qwq)
{
int u = t.u,v = t.v,w = t.w;
int i = find(u),j = find(v);
if(i == j) continue;
fa[i] = fa[j];
Edge[u].push_back(PAIR(v,w));
Edge[v].push_back(PAIR(u,w));
}
}
void dfs1(int u,int f)
{
vis[u] = 1;
fat[u] = f;
depth[u] = depth[f] + 1;
siz[u] = 1;
int maxn = 0,maxx = 0;
for(auto t:Edge[u])
{
int v = t.x,w = t.y;
if(v == f) continue;
dfs1(v,u);
siz[u] += siz[v];
if(siz[v] > maxn)
{
maxn = siz[v];
maxx = v;
}
val[v] = w;
}
son[u] = maxx;
}
void dfs2(int u,int topp)
{
dfn[u] = ++cnt;
tp[u] = topp;
a[cnt] = val[u]; // hash
if(!son[u]) return;
dfs2(son[u],topp);
for(auto t:Edge[u])
{
int v = t.x,w = t.y;
if(v == fat[u] || v == son[u]) continue;
dfs2(v,v);
}
}
void build(int l,int r,int p)
{
if(l == r)
{
tree[p] = a[l];
return;
}
int mid = (l+r) /2;
build(l,mid,p*2);
build(mid+1,r,p*2+1);
tree[p] = min(tree[p*2],tree[p*2+1]);
}
int query(int l,int r,int pl,int pr,int p)
{
if(l > pr || r < pl) return INF;
if(l >= pl && r <= pr) return tree[p];
int mid = (l+r) /2;
return min(query(l,mid,pl,pr,p*2),query(mid+1,r,pl,pr,p*2+1));
}
int query_path(int u,int v)
{
int minn = INF;
while(tp[u] != tp[v])
{
if(depth[tp[u]] > depth[tp[v]])
{
minn = min(minn,query(1,n,dfn[tp[u]],dfn[u],1));
u = fat[tp[u]];
}
else
{
minn = min(minn,query(1,n,dfn[tp[v]],dfn[v],1));
v = fat[tp[v]];
}
}
if(u == v) return minn;
if(depth[u] > depth[v]) swap(u,v);
minn = min(minn,query(1,n,dfn[u]+1,dfn[v],1));
return minn;
}
void solve()
{
int q;
cin>>q;
while(q--)
{
int u,v;
cin>>u>>v;
if(find(u) != find(v)) cout<<"-1"<<endl;
else cout<<query_path(u,v)<<endl;
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>m;
memset(a,0x3f,sizeof(a));
init();
for(int i=1;i<=m;i++)
{
int u,v,w;
cin>>u>>v>>w;
qwq.push_back({u,v,w});
qwq.push_back({v,u,w});
}
sort(qwq.begin(),qwq.end());
kruskal();
for(int i=1;i<=n;i++)
{
if(!vis[i])
{
dfs1(i,0);
dfs2(i,0);
}
}
build(1,n,1);
solve();
return 0;
}
一些和树剖有关的题目
P4180 [BJWC2010] 严格次小生成树
Description
求一棵 严格次小生成树。
结论:次小生成树和最小生成树只有一条边不同。
考虑 kruskal 求最小生成树过程。将所有带选边从小到大排序,若该边的两点不连通则加边。次小生成树是在最小生成树基础上 “放弃了”一条更优的边,选择了一条更劣的边,从而导致次小。显然,放弃多条更优的边不如放弃一条更优。
因此,我们首先求出最小生成树,记录哪些边是最小生成树上的边,称为树边,反之为非树边。
枚举添加哪条非树边,添加一条非树边意味着构成环,成了一个基环树。这也很套路,设当前非树边为 \((u,v)\),我们只需删除找MST 上点 \(u,v\) 之间边权最大的边,再添加该非树边即可。删除 MST 上更小的边显然不优。
用树剖即可。当然倍增 LCA 复杂度也是对的。
但是这么做有问题,我们要求 严格次小生成树,若 MST 上点 \((u,v)\) 间的最大边权 \(w_1=w\),(\(w\) 为当前枚举到的非树边边权。)就不是严格次小了。
这也很简单,树剖的时候同时维护一下边权次大值就好了。咋做呢?线段树每个节点维护当前区间边权最大和次大值。合并的时候最大值非常容易,对于次大值,将左儿子,右儿子的最大值,次大值这四个数排序就求出来了。
实现的时候挺麻烦的。
实现
#include <bits/stdc++.h>
#define int long long
#define x first
#define y second
using namespace std;
constexpr int N = 10000010;
constexpr int INF = 1e15;
typedef pair<int,int> PAIR;
struct Node
{
int u,v,w,tag;
bool operator <(const Node &a)const{
return w < a.w;
}
};
int n,m;
vector <PAIR> Edge[N];
vector <Node> qwq;
vector <Node> EDGE;
int fa[N];
int MST = 0;
int dfn[N],siz[N],depth[N],son[N],tp[N],fat[N],val[N],a[N];
int cnt = 0;
PAIR tree[N];
int ans = INF;
void init(){for(int i=1;i<=n;i++) fa[i] = i;}
int find(int x)
{
if(x == fa[x]) return x;
fa[x] = find(fa[x]);
return fa[x];
}
void kruskal()
{
int count = 0;
for(auto &t:qwq)
{
int u = t.u,v = t.v,w = t.w;
int fu = find(u),fv = find(v);
if(fu == fv)
{
// cout<<"/qq"<<endl;
// t.tag = 1;
continue;
}
t.tag = 1;
// cout<<u<<" "<<v<<endl;
Edge[u].push_back(PAIR(v,w));
Edge[v].push_back(PAIR(u,w));
MST += w;
fa[fu] = fv;
if(++count == n-1) return;
}
}
bool CMP(int a,int b){return a > b;}
inline int getse(int q,int w,int e,int r)
{
int qaq[5] = {q,w,e,r};
sort(qaq,qaq+4,CMP);
for(int i = 1;i < 3;i++)
if(qaq[i] != qaq[0]) return qaq[i];
return 0;
}
void dfs1(int u,int f)
{
depth[u] = depth[f] + 1;
siz[u] = 1;
fat[u] = f;
int maxn = 0,maxx = 0;
// cout<<Edge[u].size()<<endl;
for(auto t:Edge[u])
{
int v = t.x,w = t.y;
// cout<<val[v]<<"qwq"<<endl;
if(v == f) continue;
val[v] = w;
// cout<<val[v]<<endl;
dfs1(v,u);
siz[u] += siz[v];
if(siz[v] > maxn)
{
maxn = siz[v];
maxx = v;
}
}
son[u] = maxx;
}
void dfs2(int u,int topp)
{
dfn[u] = ++cnt;
a[cnt] = val[u];
tp[u] = topp;
if(!son[u]) return;
dfs2(son[u],topp);
for(auto t:Edge[u])
{
int v = t.x,w = t.y;
if(v == fat[u] || v == son[u]) continue;
dfs2(v,v);
}
}
void build(int l,int r,int p)
{
if(l == r)
{
tree[p].x = a[l];
// cout<<tree[p]<<endl;
return;
}
int mid = (l+r) >> 1;
build(l,mid,p<<1);
build(mid+1,r,p<<1|1);
tree[p].x = max(tree[p<<1].x,tree[p<<1|1].x);
tree[p].y = getse(tree[p<<1].x,tree[p<<1|1].x,tree[p<<1].y,tree[p<<1|1].y);
}
PAIR query(int l,int r,int pl,int pr,int p)
{
if(l > pr || r < pl) return PAIR(-INF,-INF);
if(l >= pl && r <= pr) return PAIR(tree[p].x,tree[p].y);
int mid = (l+r) >> 1;
PAIR t1 = query(l,mid,pl,pr,p<<1),t2 = query(mid+1,r,pl,pr,p<<1|1);
return PAIR(max(t1.x,t2.x),getse(t1.x,t2.x,t1.y,t2.y));
}
int query_path(int a,int b,int d)
{
int maxn = -INF;
while(tp[a] != tp[b])
{
if(depth[tp[a]] > depth[tp[b]]) swap(a,b);
PAIR tmp = query(1,n,dfn[tp[b]],dfn[b],1);
b = fat[tp[b]];
maxn = max(maxn,(tmp.x==d)?tmp.y:tmp.x);
}
if(depth[a] > depth[b]) swap(a,b);
PAIR tmp = query(1,n,dfn[a]+1,dfn[b],1);
return max(maxn,(tmp.x==d)?tmp.y:tmp.x);
}
void solve()
{
// cout<<EDGE.size()<<endl;
for(auto t:EDGE)
{
int u = t.u,v = t.v,w = t.w;
int maxn = query_path(u,v,w);
if(maxn == w) continue;
// cout<<maxn<<endl;
int tmp = MST + w - maxn;
if(ans > tmp && tmp != MST + w && tmp > MST) ans = tmp;
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
// memset(a,INF,sizeof(a));
// memset(val,INF,sizeof(val));
cin>>n>>m;
init();
for(int i=1;i<=m;i++)
{
int u,v,w;
cin>>u>>v>>w;
qwq.push_back({u,v,w,0});
}
sort(qwq.begin(),qwq.end());
kruskal();
dfs1(1,0);
dfs2(1,0);
build(1,n,1);
for(auto t:qwq)
if(!t.tag) EDGE.push_back(t);
solve();
//cout<<MST<<endl;
cout<<ans<<endl;
return 0;
}
本文作者:SXqwq,转载请注明原文链接:https://www.cnblogs.com/SXqwq/p/18276175