前置知识:kruskal 求最小生成树,倍增。
1. 算法简介
以下所有讨论基于 最小生成树。
在 Kruskal 求最小生成树的过程:将所有边按照边权排序,若当前边 \((u,v)\) 两端不连通,则往生成树边集 \(E\) 中加入 \((u,v)\) 并连接 \(u,v\)。使用并查集维护连通性。
如果能用某种结构描述每条边被连接的先后顺序,因为越往后加入的边权越大,就可以快速刻画边权有限制时整张图的连通情况。
Kruskal 重构树诞生了。它在 kruskal 基础上进行了一些改进:连接 \(u,v\) 时,找到 \(u,v\) 的代表元 \(U,V\) (相当于 并查集中的 \(fa_u, fa_v\))。新建节点 \(c\),将并查集中 \(U,V\) 的父亲设为 \(c\),并在 \(T\) 上连边 \(c→U\) 和 \(c→V\)。注意 \(U,V\) 可能不是原树节点。
通常设 \(c\) 的权值 \(w_c\) 为 \(w_{u,v}\) 。为虚点设置权值方便解题,巧妙设置点权对解题有极大帮助。
若原图 \(G\) 连通,则得到一棵大小为 \(2n−1\) 且以 \(2n−1\) 为根的 有根树 \(T\)。它就是 Kruskal 重构树,其性质在下一节中介绍。
void merge(int u, int v, int w) {
if((u = find(u)) == (v = find(v))) return;
node++, fa[u] = fa[v] = fa[node] = node, val[node] = w;
add(node, u), add(node, v);
}
这是原图 \(G\)
这是他的 Kruskal 重构树
2.性质与应用
下文称 原节点 为原图节点,共 \(n\) 个,表示原图某个点;新节点 为新建节点,共有 \(n−1\)个,表示原图某条边。
Kruskal 重构树 \(T\) 有很多优秀性质:
- \(T\) 是一棵二叉树,由构建方法可知。对于部分题目,特殊重构树建法可有效减小常数,详见 Part 1.3 点权多叉重构树。
- \(G\) 的所有节点是 \(T\) 的 叶子。因此原节点和重构树叶子节点本质相同。
- 对于任意新节点 \(u\) 及其祖先 \(v,w_u≤w_v\) 。
上述性质均可以由上图直观得知。
性质 3 非常重要,它是 Kruskal 重构树的核心:原节点 \(x\) 在原图上经过权值 \(≤d\) 的边可达的所有点就是它在 \(T\) 上最浅的权值 \(≤d\) 的祖先 \(a\) 的子树内所有叶子节点。一般倍增求解 \(a\) 。
相当于从原节点 \(x\) 倍增找到权值 \(≤d\) 的最浅祖先 \(a\),那么 \(a\) 子树内所有叶子就是原图仅保留边权 \(≤d\) 的边时 \(x\) 所在连通块的所有点。
综上,我们可以总结出一个常用套路:当题目限制形如 “只经过权值不大于某个值的点或边” 时,从 Kruskal 重构树角度入手。部分题目也可以使用可持久化并查集,因为它同样能够刻画存在边权限制时图的连通情况:实现 Kruskal 最小生成树的过程中将并查集可持久化。相较于 Kruskal 重构树,可持久化并查集在时空复杂度上更劣,不建议使用。
3.点权多叉重构树
Kruskal 重构树不仅适用于限制边权的题目,也可以处理限制点权的情况。
方法一:
为原图每条边巧妙赋值,将点权转化为边权。若限制经过的点权最大值,因为走一条边 (u,v)
需满足 \(w_u,w_v\) 都不超过限制,所以 \(w_{u,v}=max(w_v,w_v)\)。类似地,若限制最小值则 \(w_{u,v}=min(w_u,w_v)\)。
方法二:
实际上我们几乎用不到 \(T\) 是二叉树这一性质,因此存在更高妙的做法。
不妨设题目限制点权最大值。将节点按权值从小到大排序,按序遍历每个点 \(i\)
及其所有出边 \((i,u)\)。若 \(u\) 已经遍历过,则 \(w_i≥w_u\),\(max(w_i,w_u)\) 取到 \(w_i\),此时若 \(i,u\) 不连通则从 \(i 向 u\) 的代表元连边。
上述做法与一般 Kruskal 重构树几乎等价:普通重构树中点权相同的虚点 仅有深度最小的有用;按权值从小到大枚举节点相当于对所有边排序,因为边权即 \(max(w_i,w_u)\)。这样做不用新建虚点,有效减小了常数。
推荐遇到点权重构树时尽量使用方法二,建出多叉重构树,其写法见例题 I。但读者应时刻记住 可以保证重构树是二叉树。
例题
Ⅰ. P4899 [IOI2018] werewolf 狼人
是否存在一条 \((s_i, t_i)\) 的路径,满足先只走编号超过 $ L_i$ 的点,再走编号不超过 \(R_i\) 的点。
由于给定了路径上点权上下界,很容易想 Kruskal 重构树。
建立两棵重构树。
\(L\) : 按照点权从小到大的重构树。
\(R\) : 按照点权从大到小的重构树。
在 \(R\) 上从 \(S\) 倍增到使 \(w_a\) 最小且 \(w_a \ge L\) 的祖先 \(a\), 在 \(L\) 上从 \(T\) 倍增到使 \(w_b\) 最大且 \(w_b \le R\) 的祖先 \(b\)。判断 \(a\) 的子树与 \(b\) 的子树有没有交集即可。
可以使用主席树,我这里用的是 BIT。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 67;
int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, m, q, lg;
struct linklist{
int tot, Head[N], Next[N << 2], to[N << 2];
linklist() {tot = 0, memset(Head, 0, sizeof(Head));}
void add(int u, int v){
to[++tot] = v, Next[tot] = Head[u], Head[u] = tot;
}
}E, ins, del;
struct Tree{
int fa[N];
int find(int x){return fa[x] == x ? x : fa[x] = find(fa[x]);}
void init(){for(int i = 1; i <= n; ++i) fa[i] = i;}
linklist G;
int f[N][20], sz[N], dfn[N], cnt;
void merge(int u, int v){
if((v = find(v)) == u) return ;
fa[v] = f[v][0] = u, G.add(u, v);
}
void dfs(int x){
dfn[x] = ++cnt, sz[x] = 1;
for(int i = G.Head[x]; i; i = G.Next[i]) dfs(G.to[i]), sz[x] += sz[G.to[i]];
}
void Build(int x){
dfs(x);
for(int i = 1; i <= lg; ++i)
for(int j = 1; j <= n; ++j)
f[j][i] = f[f[j][i - 1]][i - 1];
}
int anc(int u, int lim, bool t){ //寻找满足限制的最浅的祖先
for(int i = lg; ~i; --i)
if(f[u][i] && (t ? f[u][i] >= lim : f[u][i] <= lim))
u = f[u][i];
return u;
}
}L, R;
struct BIT{
int c[N];
void add(int x){for(; x <= n; x += x & -x) ++c[x];}
int ask(int x){
int res = 0;
for(; x; x -= x & -x) res += c[x];
return res;
}
int query(int l, int r){return ask(r) - ask(l - 1);}
}tr;
int l[N], r[N], ans[N], id[N];
int main(){
n = read(), m = read(), q = read(), lg = log2(n);
for(int i = 1; i <= m; ++i){
int u = read() + 1, v = read() + 1;
E.add(u, v), E.add(v, u);
}
L.init(), R.init();
for(int i = 1; i <= n; ++i)
for(int j = E.Head[i]; j; j = E.Next[j])
if(i > E.to[j])
L.merge(i, E.to[j]);
for(int i = n; i; --i)
for(int j = E.Head[i]; j; j = E.Next[j])
if(i < E.to[j])
R.merge(i, E.to[j]);
L.Build(n), R.Build(1);
for(int i = 1; i <= n; ++i) id[L.dfn[i]] = R.dfn[i];
for(int i = 1; i <= q; ++i){
int s = read() + 1, e = read() + 1, ql = read() + 1, qr = read() + 1;
s = R.anc(s, ql, 1);
e = L.anc(e, qr, 0);
del.add(L.dfn[e] - 1, i); // 不在范围内的点为 [1, L.dfn[e] - 1]
ins.add(L.dfn[e] + L.sz[e] - 1, i);
l[i] = R.dfn[s], r[i] = R.dfn[s] + R.sz[s] - 1;
}
for(int i = 1; i <= n; ++i){ //判断有没有交集
tr.add(id[i]);
for(int j = ins.Head[i]; j; j = ins.Next[j]){
int y = ins.to[j];
ans[y] += tr.query(l[y], r[y]);
}
for(int j = del.Head[i]; j; j = del.Next[j]){
int y = del.to[j];
ans[y] -= tr.query(l[y], r[y]); //减去不在范围内的点
}
}
for(int i = 1; i <= q; ++i) printf("%d\n", (ans[i] > 0));
return 0;
}
Ⅱ. P7834 [ONTAK2010] Peaks 加强版
看到只经过权值 \(\le x\) 的边可以想到建立 Kruskal 重构树。
静态区间第 \(k\) 大可以想到建立主席树。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 67;
int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, m, q, lg, ans;
int tot, Head[N], Next[N << 1], to[N << 1];
void add(int u, int v){
to[++tot] = v, Next[tot] = Head[u], Head[u] = tot;
}
struct Edge{
int u, v, w;
bool operator < (const Edge &I) const{return w < I.w;}
}A[N];
struct Tree{
int fa[N], val[N], id[N], f[N][20], sz[N], dfn[N], st[N], ed[N], cnt, node;
int find(int x){return fa[x] == x ? x : fa[x] = find(fa[x]);}
void init(){for(int i = 1; i <= n; ++i) fa[i] = i; node = n;}
void merge(int u, int v, int w){
if((v = find(v)) == (u = find(u))) return ;
++node, fa[u] = fa[v] = fa[node] = node, val[node] = w;
add(node, u), add(node, v);
}
void dfs(int x){
dfn[x] = ++cnt, id[cnt] = x, st[x] = cnt;
for(int i = Head[x]; i; i = Next[i]) dfs(to[i]), sz[x] += sz[to[i]], f[to[i]][0] = x;
if(!sz[x]) sz[x] = 1; ed[x] = cnt;
}
void Build(int x){
dfs(x);
for(int i = 1; i <= lg; ++i)
for(int j = 1; j <= node; ++j)
f[j][i] = f[f[j][i - 1]][i - 1];
}
int anc(int u, int lim){
for(int i = lg; ~i; --i)
if(f[u][i] && val[f[u][i]] <= lim)
u = f[u][i];
return u;
}
}T;
struct SegmentTree{
int lc[N << 5], rc[N << 5], val[N << 5], cnt;
int modify(int v, int l, int r, int w){
int u = ++cnt;
lc[u] = lc[v], rc[u] = rc[v], val[u] = val[v] + 1;
if(l == r) return u;
int mid = (l + r) >> 1;
if(w <= mid) lc[u] = modify(lc[v], l, mid, w);
else rc[u] = modify(rc[v], mid + 1, r, w);
return u;
}
int query(int u, int v, int l, int r, int k){
if(l == r) return l;
int x = val[rc[u]] - val[rc[v]];
int mid = (l + r) >> 1;
if(k > x) return query(lc[u], lc[v], l, mid, k - x);
else return query(rc[u], rc[v], mid + 1, r, k);
}
}tr;
int b[N], a[N], rt[N];
int main(){
n = read(), m = read(), q = read(), lg = log2(n) + 1;
for(int i = 1; i <= n; ++i) b[i] = a[i] = read();
sort(b + 1, b + 1 + n);
int nn = unique(b + 1, b + 1 + n) - b - 1;
for(int i = 1; i <= n; ++i) a[i] = lower_bound(b + 1, b + 1 + nn, a[i]) - b;
for(int i = 1; i <= m; ++i){
int u = read(), v = read(), w = read();
A[i] = (Edge){u, v, w};
}
sort(A + 1, A + 1 + m);
T.init();
for(int i = 1; i <= m; ++i) T.merge(A[i].u, A[i].v, A[i].w);
T.Build(T.node);
for(int i = 1; i <= T.node; ++i){
rt[i] = rt[i - 1];
if(T.id[i] <= n) rt[i] = tr.modify(rt[i - 1], 1, nn, a[T.id[i]]);
}
while(q--){
int u = read(), x = read(), k = read();
int v = T.anc(u, x);
if(T.sz[v] < k){
puts("-1");continue;
}
printf("%d\n", b[tr.query(rt[T.ed[v]], rt[T.st[v] - 1], 1, nn, k)]);
}
return 0;
}
Ⅲ. P4768 [NOI2018] 归程
我们需要现预处理出每个点到 1 号点的距离。
建立边权从大到小的 Kruskal 重构树。
找到车子能开过到的点, 显然都在可以转成一段连续的区间,用线段树来区间查询即可。
由于多组数据,一定要清空数组,不然直接 100 -> 5。(样例里居然没有多组数据)。
点击查看代码
#include<bits/stdc++.h>
#define ls u << 1
#define rs u << 1 | 1
using namespace std;
const int N = 5e5 + 67;
int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, m, q, lg, ans, T, k, s;
int dis[N], vis[N];
int tot, Head[N], Next[N << 1], to[N << 1], edge[N << 1];
void add(int u, int v, int w){
to[++tot] = v, Next[tot] = Head[u], Head[u] = tot, edge[tot] = w;
}
struct Edge{
int u, v, w;
bool operator < (const Edge &I) const{return w > I.w;}
}A[N];
struct Tree{
int tot, Head[N], Next[N << 1], to[N << 1];
void add(int u, int v){
to[++tot] = v, Next[tot] = Head[u], Head[u] = tot;
}
int fa[N], val[N], id[N], f[N][30], sz[N], dfn[N], st[N], ed[N], cnt, node;
int find(int x){return fa[x] == x ? x : fa[x] = find(fa[x]);}
void init(){
for(int i = 1; i <= n; ++i) fa[i] = i;
node = n, cnt = 0, tot = 0;
memset(Head, 0, sizeof(Head));
}
void merge(int u, int v, int w){
if((v = find(v)) == (u = find(u))) return ;
++node, fa[u] = fa[v] = fa[node] = node, val[node] = w;
add(node, u), add(node, v);
}
void dfs(int x){
dfn[x] = ++cnt, id[cnt] = x, st[x] = cnt, sz[x] = 0;
for(int i = Head[x]; i; i = Next[i]) dfs(to[i]), sz[x] += sz[to[i]], f[to[i]][0] = x;
if(!sz[x]) sz[x] = 1; ed[x] = cnt;
}
void Build(int x){
dfs(x);
for(int i = 1; i <= lg; ++i)
for(int j = 1; j <= node; ++j)
f[j][i] = f[f[j][i - 1]][i - 1];
}
int anc(int u, int lim){
for(int i = lg; ~i; --i)
if(f[u][i] && val[f[u][i]] >= lim)
u = f[u][i];
return u;
}
}Kr;
struct SegmentTree{
int minn[N << 2], cnt;
void PushUp(int u){
minn[u] = min(minn[ls], minn[rs]);
}
void Build(int u, int l, int r){
if(l >= r) return minn[u] = dis[Kr.id[l]], void();
int mid = (l + r) >> 1;
Build(ls, l, mid), Build(rs, mid + 1, r);
PushUp(u);
}
int Query(int u, int l, int r, int L, int R){
if(L <= l && r <= R) return minn[u];
int mid = (l + r) >> 1, ans = 0x3f3f3f3f;
if(L <= mid) ans = min(ans, Query(ls, l, mid, L, R));
if(R > mid) ans = min(ans, Query(rs, mid + 1, r, L, R));
return ans;
}
}tr;
struct DIJ{
int dis, s;
bool operator < (const DIJ &A) const{return A.dis < dis;}
};
void dijkstra(int s){
priority_queue<DIJ> q;
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof(vis));
dis[s] = 0, q.push((DIJ){0, s});
while(!q.empty()){
int x = q.top().s; q.pop();
if(vis[x]) continue;
vis[x] = 1;
for(int i = Head[x]; i; i = Next[i]){
int y = to[i];
if(dis[y] > dis[x] + edge[i]){
dis[y] = dis[x] + edge[i];
q.push((DIJ){dis[y], y});
}
}
}
}
int b[N], a[N];
int main(){
// freopen("1.in", "r", stdin);
// freopen("return.out", "w", stdout);
T = read();
while(T--){
n = read(), m = read(), lg = log2(n) + 1, ans = 0;
tot = 0; memset(Head, 0, sizeof(Head));
for(int i = 1; i <= m; ++i){
int u = read(), v = read(), l = read(), w = read();
A[i] = (Edge){u, v, w}, add(u, v, l), add(v, u, l);
}
dijkstra(1);
sort(A + 1, A + 1 + m); Kr.init();
for(int i = 1; i <= m; ++i) Kr.merge(A[i].u, A[i].v, A[i].w);
Kr.Build(Kr.node), tr.Build(1, 1, Kr.node);
q = read(), k = read(), s = read();
while(q--){
int u = (read() + k * ans - 1) % n + 1, p = (read() + k * ans) % (s + 1);
int v = Kr.anc(u, p + 1);
printf("%d\n", ans = tr.Query(1, 1, Kr.node, Kr.st[v], Kr.ed[v]));
}
}
return 0;
}