生成树的应用
最小生成树的理论基础
Kruskal 算法的正确性
数学归纳法。设 Kruskal 算法当前维护的边集为 \(E\),最小生成树的边集为 \(T_1,T_2,\cdots\),我们要证明任意时刻都有 \(\exists i,E\subseteq T_i\)。当 \(T=\varnothing\) 时显然成立。
设目前有 \(E\subsetneqq T_i\),我们要加入一条边 \(e\),\(E'=E\cup\{e\}\)。若 \(e\in T_i\),结论仍然成立。否则,\(e\) 将与 \(T_i\) 中的一些边形成环。那么,若这个环中有边权比 \(e\) 大的边,则可以把这条边换成 \(e\),与 \(T_i\) 是最小生成树矛盾。否则,若只有比 \(e\) 边权更小者,则它们已经全部在 \(E\) 中,算法不可能选取 \(e\) 作为即将要加入的边。因此,环中只有可能是有一些边已经在 \(E\) 中,且必定存在不在 \(E\) 中的边,这些边的边权必定等于 \(e\)。因此使用 \(e\) 替换其中之一,得到的仍是一颗最小生成树。故 \(\exists j,E'\subseteq T_j\)。
证毕。
Boruvka 算法
该算法维护一个最小生成树的边集。每一次执行算法都会统计每个连通块向外连出的边中最小的那一条,然后把这条边加入边集中,并合并其两端的连通块。
初始时,每个节点单独构成一个连通块。每次执行算法都要遍历每条边,并且连通块数量至少减半。因此该算法最多执行 \(\log|V|\) 次,时间复杂度为 \(\mathcal O(|E|\log|V|)\)。
MST 的性质
- 在一张图上,两点间的路径在最小生成树上时,其上面的最大边权最小。
- 一张图的不同最小生成树上,每一种不同的边权边的数目相同。
- 进行 Kruskal 算法时,在加入一种边权的边时,以不同顺序加边直至不能再加这种边,所得森林的连通性相同。
这里,两张图的连通性相同,是指对于两张图中任意一对编号相同的点,与其连通的点的集合相同。
证明如下(不是很严谨,感性理解就行):
- 假设存在一条不完全在最小生成树上的路径,其上面的最大边权更小。如果最大边权在最小生成树上,说明路径上的其他边可以替换这条边,与生成树权值和最小矛盾。否则,可以用其替换构成的环上的最大边权。所以假设不成立。
- 是第三条性质的推论。
- 如果这种边被全部加入,显然成立;否则,这种边一定在某些地方形成了环,所以连通性不变。
最小生成树的应用
P5994 [PA2014]Kuglarz
区间化单点是一个重要思想。询问区间 \([l,r]\) 球的奇偶性,就可以被认为是询问 \([1,l-1]\) 球的数量与 \([1,r]\) 球的数量差的奇偶性。
我们将所得的结果抽象出来,设 \(x_i\) 表示第 \(i\) 个位置是否有球,询问一次相当于得知 \(\operatorname{xor}_{i=l}^rx_i\)。设 \(s_i=\operatorname{xor}_{j=1}^ix_j\),那么询问一次相当于得知 \(s_{l-1}\operatorname{xor}s_r\) 的值。询问多次则相当于得到一个方程组。我们只有解出所有的 \(s\) 才能保证确定。
发现 \(l-1\) 可能越界,故认为 \(s_0=0\),并把它当做一个节点。这么做也能确保变量间的推导。
由于我们已经知道 \(s_0=0\),因此方程组只有 \(n\) 个未知量,所以需要 \(n\) 个方程且所有未知量和 \(s_0\) 都要在方程组中出现。可以看出是要确定有 \(n+1\) 个点的图的最小生成树,边权为询问一段区间的代价,边代表互相推导的关系。因为 \(s_0\) 的存在,我们得以推导整棵树所有节点的未知量。
代码如下:
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=2e3+5;
int n,m,fa[N];ll ans;
int get(int x){return x==fa[x]?x:fa[x]=get(fa[x]);}
inline void merge(int x,int y){fa[get(x)]=get(y);}
struct Edge{
int u,v;ll w;
friend bool operator<(const Edge&A,const Edge&B){
return A.w<B.w;
}
}e[N*N];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
fa[i]=i;
for(int j=i;j<=n;j++){
m++;scanf("%lld",&e[m].w);
e[m].u=i-1;e[m].v=j;
}
}
sort(e+1,e+m+1);
for(int i=1,cnt=0;i<=m;i++){
if(cnt==n)break;
if(get(e[i].u)==get(e[i].v))continue;
merge(e[i].u,e[i].v);ans+=e[i].w;
}
printf("%lld\n",ans);
return 0;
}
BZOJ4883 棋盘上的守卫
常见的网络流建模套路是,把一个 \(n\times m\) 的网格变成一张 \(n+m\) 个点的图,分别代表网格中的一行和一列。
据说可以费用流,然而我早就不记得网络流怎么写了。
一种方法是,在位置 \((i,j)\) 放一个守卫就相当于在代表行的第 \(i\) 个点和代表列的第 \(j\) 个点之间连一条边。并且,边是有向的,其方向代表这个守卫守的是行还是列。那么我们就要求每个点入度都是 \(1\)。所以这是一棵外向树森林。
确定边集后,方向可以随便定。所以我们只需要确定基环树森林就行了。因此,本题就是要求一张图的最小生成基环树森林。可以给每个点加一个标记,表示其所在连通块是否在基环树中,然后用 Kruskal 算法维护即可。
代码如下:
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
const int N=1e5+5;
int n,m,fa[N<<1];bool lop[N<<1];long long ans;
struct Edge{
int u,v,w;
friend bool operator<(const Edge&A,const Edge&B){
return A.w<B.w;
}
}e[N];
int get(int x){return x==fa[x]?x:fa[x]=get(fa[x]);}
inline void merge(int x,int y){
x=get(x);y=get(y);
fa[x]=y;lop[y]=lop[y]||lop[x];
}
int main(){
scanf("%d%d",&n,&m);
for(int i=0,w;i<n;i++)for(int j=1;j<=m;j++){
scanf("%d",&w);e[i*m+j]={i+1,j+n,w};
}
for(int i=1;i<=n+m;i++)fa[i]=i;
sort(e+1,e+n*m+1);
for(int i=1,cnt=0,u,v;i<=n*m;i++){
if(cnt==n+m)break;
u=get(e[i].u);v=get(e[i].v);
if(u==v){
if(!lop[u]){
lop[u]=1;cnt++;ans+=e[i].w;
}
continue;
}
if(lop[u]&&lop[v])continue;
merge(u,v);cnt++;ans+=e[i].w;
}
printf("%lld\n",ans);
return 0;
}
Kruskal 重构树
在 Kruskal 过程中,每次合并两个集合。将合并的过程以树的形式记录下来,这棵树就叫 Kruskal 重构树。其叶子节点与原图中的点一一对应,其他节点的点权表示这一次合并的边权。
根据 MST 的性质,两点之间的所有路径的最大边权的最小值等于 MST 上两点之间路径的最大值。容易发现它也等于 Kruskal 重构树上两点 LCA 的边权。
所以,从某一个点出发,只经过权值 \(\le x\) 的边能够到达的点一定在 Kruskal 重构树的某个子树内。
P4768 [NOI2018] 归程
水位线的限制容易通过 Kruskal 重构树解决。预处理出每个点到 \(1\) 号点的距离,直接在 Kruskal 重构树上查询即可。
#include<bits/stdc++.h>
using namespace std;
struct DSU{
vector<int>f;
DSU(int n){f.resize(n*2);iota(f.begin(),f.end(),0);}
int get(int x){return x==f[x]?x:f[x]=get(f[x]);}
void merge(int x,int y,int tot){f[get(x)]=f[get(y)]=tot;}
};
struct Graph{
int n;vector<vector<pair<int,int>>>e;
Graph(int _n){n=_n;e.resize(n+1);}
void addE(int u,int v,int w){e[u].emplace_back(v,w);e[v].emplace_back(u,w);}
void Dijkstra(vector<int>&d){
priority_queue<pair<int,int>>q;
vector<int>vis(n+1);
q.emplace(d[1]=0,1);while(q.size()){
int u=q.top().second;q.pop();
if(vis[u])continue;else vis[u]=1;
for(auto[v,w]:e[u])if(d[u]+w<d[v]){
d[v]=d[u]+w;q.emplace(-d[v],v);
}
}
}
};
struct Tree{
int n;vector<vector<int>>e;
vector<vector<int>>f;vector<int>d,h;
Tree(int _n){
n=_n;e.resize(n*2);f.resize(n*2);d.resize(n*2);
fill(d.begin(),d.end(),(int)2e9);h.resize(n*2);
for(auto&v:f)v.resize(20);
}
void merge(int u,int v,int x,int W){
e[x].push_back(u);e[x].push_back(v);
f[u][0]=f[v][0]=x;h[x]=W;
}
void dfs(int u){
for(int k=1;k<20;k++)f[u][k]=f[f[u][k-1]][k-1];
for(int v:e[u])dfs(v),d[u]=min(d[u],d[v]);
}
int query(int u,int p){
for(int k=19;k>=0;k--)if(h[f[u][k]]>p)u=f[u][k];
return d[u];
}
};
void ct(){
int n,m;cin>>n>>m;
vector<tuple<int,int,int>>E(m);
DSU D(n);Graph G(n);
for(int i=0;i<m;i++){
int u,v,l,a;cin>>u>>v>>l>>a;
G.addE(u,v,l);E[i]=make_tuple(a,u,v);
}
sort(E.begin(),E.end(),greater<>());
int tot=n;Tree T(n);
for(auto[w,u,v]:E){
if(D.get(u)==D.get(v))continue;
T.merge(D.get(u),D.get(v),++tot,w);
D.merge(u,v,tot);if(tot==n*2-1)break;
}
G.Dijkstra(T.d);T.dfs(tot);
int Q,K,S,ans=0;cin>>Q>>K>>S;
while(Q--){
int v0,p0;cin>>v0>>p0;
int v=(v0+K*ans-1)%n+1,p=(p0+K*ans)%(S+1);
ans=T.query(v,p);printf("%d\n",ans);
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);int T;
cin>>T;while(T--)ct();
return 0;
}