最小生成树相关性质
最小生成树的性质
总结几点性质:
性质1:
对于一张边权互不相同的无向图,其存在唯一的最小生成树。
证明:
这个可以反证法证明,若存在两棵不同的最小生成树称为 \(T\) 和 \(T'\),记一条边 \(e\in T ,e\not\in T'\) ,那么显然我们在 \(T\) 中删去这条边会得到两个连通分量,而对于 \(T'\) 则存在一条边 \(e'\) 连接两条连通分量,且 \(e'\not \in T\) ,如果我们在 \(T\) 中断开 \(e\) 并且连上 \(e'\) 可以得到一棵不同的生成树。那么由于 \(w(e)\not=w(e')\) ,这棵新树不可能为最小生成树。
Q.E.D.
性质2:
对于一个无向图,其每种最小生成树的每种边权的数量是一定的。
证明:
对于一棵最小生成树 \(T\) ,我们断掉一条边 \(e\) ,那么我们要使得总边权不变,要么通过加入一条等权的边,要么增加一些边,且权值和等于 \(w(e)\) ,但是这样出现的显然不是一棵生成树,所以只能加入边权相等的。
若删除两条边 \(e_1,e_2\) 且 \(w(e_1)<w(e_2)\),则一定加入两条权值和与删除的边边权和相等的边 \(e_1',e_2'\),且 \(w(e_1')<w(e_2')\),如果删除的两条边和加入的两条边权值 \(e(e_1)\) 与 \(w(e_1')\) 相等,\(w(e_2)\) 与 \(2(e_2')\) 相等,那么可行。否则,如果 \(w(e_1')<w(e_1)\) 且 \(w(e_2)<w(e_2')\) ,那么显然原树 \(T\) 不为最小生成树,矛盾。 如果 \(w(e_1')>w(e_1)\) 且 \(w(e_2)>w(e_2')\) ,\(T\) 也不为最小生成树,矛盾。
对于多条边的情况同理。所以可得,对于一个无向图,其每种最小生成树的每种边权的数量是一定的。
Q.E.D.
性质3:
对于一个无向图,其最小生成树上的简单路径都为最小瓶颈路。(但是最小瓶颈路不一定都在最小生成树上)
证明:
由性质2,可知一张的无向图每一种最小生成树,都是满足一个点到另一个点的最大边权是固定的且都为最小值。这是因为我们 Kruskal 使用尽可能小的边权去使得图连通。
应用
题目大意:求一种特殊的最小生成树。给定一个有 \(n\) 个节点和 \(m\) 条边的图,找出一个生成树满足从根节点 \(1\) 直接连向其余节点的边要恰好是 \(k\) 条,在此条件下生成树的权值和最小。
我们先用wqs二分求出满足条件的最小权生成树的最小权,然后我们发现可能这种情况下我们建出的树不一定满足与 \(1\) 连边的边正好为 \(k\) 。我们称与 \(1\) 相连的边为白边,其他为黑边。这种情况我们运用最小生成树的第二条性质,考虑用等权的满足条件的白边替换黑边。
先说做法,我们算出此时白边数量与目标的差值,然后对于每一串权值相等的边先考虑加入之前求出的最小生成树的白边,再尽可能的加新的白边,直到填满差值。之后再考虑加入黑边。这样做满足性质2,等价于将等权黑边替换为白边。但实际上我们建图符合Kruskal,建出来肯定为最小生成树。这么构造就可以过题了。
参考代码:
#include<bits/stdc++.h>
#define ll long long
#define db double
#define filein(a) freopen(#a".in","r",stdin)
#define fileot(a) freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
template<class T>
inline void read(T &s){
s=0;char ch=gc();bool f=0;
while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
if(ch=='.'){
db p=0.1;ch=gc();
while('0'<=ch&&ch<='9') {s=s+p*(ch^48);ch=gc();}
}
s=f?-s:s;
}
template<class T,class ...A>
inline void read(T &s,A &...a){
read(s);read(a...);
}
inline bool blank(char c){
return c==' ' or c=='\t' or c=='\n' or c=='\r' or c==EOF;
}
inline void gs(std::string &s){
s+='#';char c=gc();
while(blank(c) ) c=gc();
while(!blank(c) ){
s+=c;c=gc();
}
}
inline void gs(char *s){
char ch=gc();
while(blank(ch) ) {ch=gc();}
while(!blank(ch) ){
*s++=ch;ch=gc();
}
*s=0;
}
};
using IO::read;
using IO::gs;
const int N=5e3+3;
const int M=1e5+3;
int n,m,d;
struct Edge{
int u,v,w,c;
int id;bool use;
}e[M];
int fa[N];
int find(int x){
return x==fa[x]?x:fa[x]=find(fa[x]);
}
std::vector<int>s;
inline int check(int k,bool op){
static int tmpk;
tmpk=k;
std::sort(e+1,e+1+m,[](Edge x,Edge y){
int vx=x.w+(x.c?-tmpk:0),vy=y.w+(y.c?-tmpk:0);
if(vx==vy) return x.c<y.c;
return vx<vy;
});
for(int i=1;i<=n;++i){
fa[i]=i;
}
int cnt=0,tim=0;
for(int i=1;i<=m;++i){
int u=e[i].u,v=e[i].v;
if(find(u)!=find(v) ){
fa[find(v)]=find(u);
if(e[i].c) ++cnt;
if(op) s.push_back(e[i].id);
++tim;
}
if(tim==n-1) break;
}
return cnt;
}
int cl[N],cr[N],Ctot;
int delta;
inline void getans(int k){
static int tmpk;
tmpk=k;
std::sort(e+1,e+1+m,[](Edge x,Edge y){
int vx=x.w+(x.c?-tmpk:0),vy=y.w+(y.c?-tmpk:0);
if(vx==vy) return x.c<y.c;
return vx<vy;
});
for(int i=1;i<=n;++i){
fa[i]=i;
}
cl[1]=1;cr[1]=1;Ctot=1;
for(int i=1;i<=m;++i){
if(i!=1)
if(e[i-1].w+(e[i-1].c+(e[i-1].c?-k:0) )==e[i].w+(e[i].c?-k:0) ){
cr[Ctot]=i;
}else{
++Ctot;
cl[Ctot]=i;
cr[Ctot]=i;
}
int u=e[i].u,v=e[i].v;
if(find(u)!=find(v) ){
fa[find(v)]=find(u);
if(e[i].c) e[i].use=1;
}
}
for(int i=1;i<=n;++i){
fa[i]=i;
}
int tim=0;
for(int i=1;i<=Ctot;++i){
for(int l=cl[i];l<=cr[i];++l){
if(!e[l].use) continue;
int u=e[l].u,v=e[l].v;
fa[find(v)]=find(u);
++tim;
s.push_back(l);
}
if(delta!=0)
for(int l=cl[i];l<=cr[i];++l){
if(e[l].c and !e[l].use){
int u=e[l].u,v=e[l].v;
if(find(u)!=find(v) ){
fa[find(v)]=find(u);
--delta;
++tim;
s.push_back(l);
}
}
if(delta==0) break;
}
for(int l=cl[i];l<=cr[i];++l){
if(!e[l].c){
int u=e[l].u,v=e[l].v;
if(find(u)!=find(v) ){
fa[find(v)]=find(u);
++tim;
s.push_back(l);
}
}
}
if(tim==n-1) break;
}
}
int main(){
//filein(a);fileot(a);
read(n,m,d);
for(int i=1;i<=m;++i){
int u,v,w;
read(u,v,w);
e[i]={u,v,w,(u==1 or v==1),i};
}
int l=-1e5,r=1e5,res=-1e9;
while(l<=r){
int mid=(l+r)>>1;
if(check(mid,0)<=d){
l=mid+1;res=mid;
}else{
r=mid-1;
}
}
if(res==-1e9){
printf("-1\n");
}else{
delta=d-check(res,0);
if(delta){
getans(res);
printf("%d\n",s.size() );
for(auto it:s){
printf("%d ",e[it].id);
}
}else{
check(res,1);
printf("%d\n",s.size() );
for(auto it:s){
printf("%d ",it);
}
}
}
return 0;
}
Kruskal重构树
我们把原来的点看作点权为 \(0\) 的点。我们做Kruskal的时候,把连接的两个点合并成一个新点,点权为边权,并以新点为根。然后其左右儿子分别为被合并的两个点。
这个东西我们用并查集合并即可。
性质
不难发现,原图中两个点之间的所有简单路径上最大边权的最小值 = 最小生成树上两个点之间的简单路径上的最大值 = Kruskal 重构树上两点之间的 LCA 的权值。
因为节点的点权从根向下是单调不增的。
那么,如果我们要查找有多少点到 \(x\) 的简单路径上最大边权的最小值 \(\le k\) ,直接找到根到 \(x\) 的路径上,第一个(也叫深度最浅的)权值 \(\le k\) 的点,其子树中所以叶子都满足上面的性质。
这个可以用来求最小瓶颈路之类的东西(最小瓶颈路:路径上边权最大值最小的简单路径)。最大瓶颈路同理,Kruskal从大到小加边即可。