Kruskal 重构树学习笔记
Kruskal 重构树学习笔记
前置知识
最小生成树的 Kruskal 算法、最近公共祖先(LCA)。
重构树
在 Kruskal 算法执行过程中(不妨设求最小生成树),我们会按边权升序依次加入若干条边。
首先初始化并查集,共 \(n\) 个集合,第 \(i\) 个集合初始包含且仅包含第 \(i\) 个点,维护当前的连通块情况。每一次加边到最小生成树中时,我们都合并了目前的两个连通块,也就是两个集合。我们要建立重构树,在每次合并操作时新建一个点,点权为加入的这条边的边权,将合并的两个集合的根节点作为这个新建点的左右儿子,并将两个集合以及新建点合并,以新建点为集合的根(代表元)。
例如,对于下面这个无向图:
它的最小生成树的 Kruskal 重构树如下:(不知道画图的时候怎么了,虚拟节点的编号都大了 \(1\),但懒得重画了,将就看吧)
重构树性质
不妨设求最小生成树,Kruskal 重构树有如下性质:
- 重构树是一棵恰有 \(n\) 个叶子节点的完满二叉树,每个非叶子节点都恰有 \(2\) 个儿子,重构树的点数为 \(2n-1\)。
- 重构树的点权符合大根堆的性质。
- 原图中两点间所有简单路径的最大边权最小值,等于最小生成树上两点之间边权最大值,等于重构树上两点 LCA 的点权。
- 到点 \(u\) 的简单路径上最大边权最小值 \(\le k\) 的所有节点 \(v\) 均在重构树上的某棵子树内,且恰为该子树内的所有叶子节点。
例题
P1967 [NOIP2013 提高组] 货车运输
本题可以用最大生成树+倍增做,但是这里讲一下 Kruskal 重构树的做法。
求最大生成树的重构树,所求即为重构树上 \(x,y\) 两点的 LCA 的点权。
代码
//By: OIer rui_er
#include <bits/stdc++.h>
#define rep(x,y,z) for(int x=(y);x<=(z);x++)
#define per(x,y,z) for(int x=(y);x>=(z);x--)
#define debug(format...) fprintf(stderr, format)
#define fileIO(s) do{freopen(s".in","r",stdin);freopen(s".out","w",stdout);}while(false)
using namespace std;
typedef long long ll;
const int N = 1e5+5;
int n, m, q, now, dis[N], fa[N][20], val[N];
template<typename T> void chkmin(T& x, T y) {if(x > y) x = y;}
template<typename T> void chkmax(T& x, T y) {if(x < y) x = y;}
struct Edge {
int u, v, w;
Edge(int x=0, int y=0, int z=0) : u(x), v(y), w(z) {}
}G[N];
vector<int> e[N];
struct Dsu {
int fa[N];
void init(int x) {rep(i, 1, x) fa[i] = i;}
int find(int x) {return x == fa[x] ? x : fa[x] = find(fa[x]);}
bool merge(int x, int y) {
int u = find(x), v = find(y);
if(u == v) return 0;
fa[u] = v;
return 1;
}
}dsu;
void kruskal() {
dsu.init(2*n-1);
sort(G+1, G+1+m, [](const Edge& x, const Edge& y) {
return x.w > y.w;
});
now = n;
rep(i, 1, m) {
ll u = dsu.find(G[i].u), v = dsu.find(G[i].v), w = G[i].w;
if(u != v) {
val[++now] = w;
dsu.merge(u, now);
dsu.merge(v, now);
e[now].push_back(u);
e[now].push_back(v);
}
}
}
void dfs(int u, int f) {
dis[u] = dis[f] + 1;
fa[u][0] = f;
rep(i, 1, 19) fa[u][i] = fa[fa[u][i-1]][i-1];
for(int v : e[u]) dfs(v, u);
}
int LCA(int u, int v) {
if(dis[u] < dis[v]) swap(u, v);
per(i, 19, 0) if(dis[fa[u][i]] >= dis[v]) u = fa[u][i];
if(u == v) return u;
per(i, 19, 0) if(fa[u][i] != fa[v][i]) u = fa[u][i], v = fa[v][i];
return fa[u][0];
}
int main() {
scanf("%d%d", &n, &m);
rep(i, 1, m) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
G[i] = Edge(u, v, w);
}
kruskal();
per(i, now, 1) if(!dis[i]) dfs(i, 0);
for(scanf("%d", &q);q;q--) {
int s, t;
scanf("%d%d", &s, &t);
int lca = LCA(s, t);
if(!lca) puts("-1");
else printf("%d\n", val[lca]);
}
return 0;
}
CF1706E Qpwoeirut and Vertices
以边的读入顺序为边权,求最小生成树的重构树,所求即为重构树上 \(l\sim r\) 点的 LCA 点权。
有性质:区间 LCA 等于区间内 dfs 序最小点和最大点的 LCA。
使用 ST 表维护 dfs 序最小值和最大值然后求解即可。
需要特判 \(l=r\)。
代码
//By: OIer rui_er
#include <bits/stdc++.h>
#define rep(x,y,z) for(int x=(y);x<=(z);x++)
#define per(x,y,z) for(int x=(y);x>=(z);x--)
#define debug(format...) fprintf(stderr, format)
#define fileIO(s) do{freopen(s".in","r",stdin);freopen(s".out","w",stdout);}while(false)
using namespace std;
typedef long long ll;
const int N = 5e5+5;
int T, n, m, q, now, dfn[N], tms, dis[N], id[N], fa[N][20], val[N];
template<typename T> void chkmin(T& x, T y) {if(x > y) x = y;}
template<typename T> void chkmax(T& x, T y) {if(x < y) x = y;}
struct Edge {
int u, v, w;
Edge(int x=0, int y=0, int z=0) : u(x), v(y), w(z) {}
}G[N];
vector<int> e[N];
struct Dsu {
int fa[N];
void init(int x) {rep(i, 1, x) fa[i] = i;}
int find(int x) {return x == fa[x] ? x : fa[x] = find(fa[x]);}
bool merge(int x, int y) {
int u = find(x), v = find(y);
if(u == v) return 0;
fa[u] = v;
return 1;
}
}dsu;
void kruskal() {
dsu.init(2*n-1);
sort(G+1, G+1+m, [](const Edge& x, const Edge& y) {
return x.w < y.w;
});
now = n;
rep(i, 1, m) {
ll u = dsu.find(G[i].u), v = dsu.find(G[i].v), w = G[i].w;
if(u != v) {
val[++now] = w;
dsu.merge(u, now);
dsu.merge(v, now);
e[now].push_back(u);
e[now].push_back(v);
}
}
}
void dfs(int u, int f) {
dfn[u] = ++tms;
dis[u] = dis[f] + 1;
fa[u][0] = f;
rep(i, 1, 19) fa[u][i] = fa[fa[u][i-1]][i-1];
for(int v : e[u]) dfs(v, u);
}
int LCA(int u, int v) {
if(dis[u] < dis[v]) swap(u, v);
per(i, 19, 0) if(dis[fa[u][i]] >= dis[v]) u = fa[u][i];
if(u == v) return u;
per(i, 19, 0) if(fa[u][i] != fa[v][i]) u = fa[u][i], v = fa[v][i];
return fa[u][0];
}
struct SparseTable {
int lg[N], mx[N][20], mn[N][20];
void init(int* a, int n) {
lg[0] = -1;
rep(i, 1, n) lg[i] = lg[i>>1] + 1;
rep(i, 1, n) mx[i][0] = mn[i][0] = a[i];
rep(j, 1, 19) {
rep(i, 1, n-(1<<j)+1) {
mx[i][j] = max(mx[i][j-1], mx[i+(1<<(j-1))][j-1]);
mn[i][j] = min(mn[i][j-1], mn[i+(1<<(j-1))][j-1]);
}
}
}
int qmax(int l, int r) {
int k = lg[r-l+1];
return max(mx[l][k], mx[r-(1<<k)+1][k]);
}
int qmin(int l, int r) {
int k = lg[r-l+1];
return min(mn[l][k], mn[r-(1<<k)+1][k]);
}
}st;
int main() {
for(scanf("%d", &T);T;T--) {
scanf("%d%d%d", &n, &m, &q);
rep(i, 1, m) {
int u, v;
scanf("%d%d", &u, &v);
G[i] = Edge(u, v, i);
}
kruskal();
dfs(now, 0);
st.init(dfn, now);
rep(i, 1, now) id[dfn[i]] = i;
while(q--) {
int l, r;
scanf("%d%d", &l, &r);
int u = id[st.qmax(l, r)], v = id[st.qmin(l, r)];
if(u == v) printf("%d%c", 0, " \n"[!q]);
else printf("%d%c", val[LCA(u, v)], " \n"[!q]);
}
tms = 0;
rep(i, 1, now) e[i].clear();
}
return 0;
}
P4768 [NOI2018] 归程
转化题意,即从节点 \(v\) 出发,先在海拔大于 \(p\) 的边上乘车走一段,然后走最短路径回到节点 \(1\)。
首先通过 Dijkstra 算法预处理每个节点到节点 \(1\) 的最短路径,然后对海拔求最大生成树的重构树。
由上面提到的性质三和四,即求到节点 \(v\) 最小海拔最大值大于 \(p\) 的所有节点中,到节点 \(1\) 最近的那个点的最短路径长度。
在重构树上 DP 预处理每棵子树内的叶子节点中,到节点 \(1\) 最短路径长度的最小值。对于每次询问,先从 \(v\) 开始在重构树上倍增,找到性质四所说的那棵子树,然后子树的 DP 值即为答案。
关于 SPFA:它死了。
代码
//By: OIer rui_er
#include <bits/stdc++.h>
#define rep(x,y,z) for(ll x=(y);x<=(z);x++)
#define per(x,y,z) for(ll x=(y);x>=(z);x--)
#define debug(format...) fprintf(stderr, format)
#define fileIO(s) do{freopen(s".in","r",stdin);freopen(s".out","w",stdout);}while(false)
using namespace std;
typedef long long ll;
const ll N = 4e5+5;
ll T, n, m, q, k, s, dis[N], vis[N], val[N], fa[N][20], lst;
template<typename T> void chkmin(T& x, T y) {if(x > y) x = y;}
template<typename T> void chkmax(T& x, T y) {if(x < y) x = y;}
struct Edge1 {
ll u, v, a;
Edge1(ll x=0, ll y=0, ll z=0) : u(x), v(y), a(z) {}
}G[N];
struct Edge2 {
ll v, l;
Edge2(ll x=0, ll y=0) : v(x), l(y) {}
};
vector<Edge2> e[N];
vector<ll> et[N];
void dijkstra(ll s) {
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof(vis));
priority_queue<tuple<ll, ll> > q;
dis[s] = 0;
q.emplace(0, s);
while(!q.empty()) {
ll u = get<1>(q.top()); q.pop();
if(vis[u]) continue;
vis[u] = 1;
for(Edge2 i : e[u]) {
ll v = i.v, l = i.l;
if(dis[v] > dis[u] + l) {
dis[v] = dis[u] + l;
q.emplace(-dis[v], v);
}
}
}
}
struct Dsu {
ll fa[N];
void init(ll x) {rep(i, 1, x) fa[i] = i;}
ll find(ll x) {return x == fa[x] ? x : fa[x] = find(fa[x]);}
bool merge(ll x, ll y) {
ll u = find(x), v = find(y);
if(u == v) return 0;
fa[u] = v;
return 1;
}
}dsu;
void kruskal() {
dsu.init(2*n-1);
sort(G+1, G+1+m, [](const Edge1& x, const Edge1& y) {
return x.a > y.a;
});
ll now = n;
rep(i, 1, m) {
ll u = dsu.find(G[i].u), v = dsu.find(G[i].v), a = G[i].a;
if(u != v) {
val[++now] = a;
dsu.merge(u, now);
dsu.merge(v, now);
et[now].push_back(u);
et[now].push_back(v);
}
}
}
void dfs(ll u, ll f) {
fa[u][0] = f;
rep(i, 1, 19) fa[u][i] = fa[fa[u][i-1]][i-1];
for(ll v : et[u]) {
dfs(v, u);
chkmin(dis[u], dis[v]);
}
}
int main() {
for(scanf("%lld", &T);T;T--) {
scanf("%lld%lld", &n, &m);
rep(i, 1, m) {
ll u, v, l, a;
scanf("%lld%lld%lld%lld", &u, &v, &l, &a);
G[i] = Edge1(u, v, a);
e[u].emplace_back(v, l);
e[v].emplace_back(u, l);
}
dijkstra(1);
kruskal();
dfs(2*n-1, 0);
for(scanf("%lld%lld%lld", &q, &k, &s);q;q--) {
ll v, p;
scanf("%lld%lld", &v, &p);
v = (v + k * lst - 1) % n + 1;
p = (p + k * lst) % (s + 1);
per(i, 19, 0) if(val[fa[v][i]] > p) v = fa[v][i];
printf("%lld\n", lst=dis[v]);
}
lst = 0;
rep(i, 1, m) G[i] = Edge1();
memset(val, 0, sizeof(val));
memset(fa, 0, sizeof(fa));
rep(i, 1, n) e[i].clear();
rep(i, 1, 2*n-1) et[i].clear();
}
return 0;
}
P6765 [APIO2020] 交换城市
我还没有做。