kruskal 及其应用
kruskal
最小生成树
kruskal 是一种常见且好理解的最小生成树(MST)算法。
前置知识
生成树
在有 n 的顶点的无向图中,取其中 n-1 条边相连,所得到的树即为生成树。
最小生成树就是生成树边权和最小。
kruskal 求 MST
kruskal 基于贪心。
如果让你的选择之和最小,该怎么选?
每次选择的边权都是没选过的最小的,直到选了 n-1 条边。
但这样选有时会出问题。
如上图,选最小的边应该是:
但显然,这不是一个树。
所以在连边之前,还要判断一下两个点是否在同一个连通块内。
判联通用什么?
对,并查集!
那么整个 kruskal 的过程就是:
排序,判联通,加边。
Code(P3366)
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
int re()
{
int s=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
s=s*10+ch-48,ch=getchar();
return s*f;
}
void wr(int s)
{
if(s<0)putchar('-'),s=-s;
if(s>9)wr(s/10);
putchar(s%10+48);
}
const int inf=1e5+7;
int n,m,cnt,ans;
struct kruskal{
int from,to,val;
bool operator <(const kruskal &b)const
{
return val<b.val;
}
};
vector<kruskal>h;
int fa[inf];
int find(int x)
{
if(x==fa[x])return fa[x];
return fa[x]=find(fa[x]);
}
int main()
{
n=re();m=re();
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=m;i++)
{
int u=re(),v=re(),w=re();
h.push_back((kruskal){u,v,w});
}
sort(h.begin(),h.end());
int len=h.size();
for(int i=0;i<len;i++)
{
int r1=find(h[i].from),r2=find(h[i].to);
if(r1==r2)continue;
cnt++;ans+=h[i].val;
fa[r1]=r2;
if(cnt==n-1)break;
}
wr(ans);
return 0;
}
练习
次小生成树
前置知识:
为方便叙述,最小生成树中的 \(n-1\) 边叫做树边,剩余的 \(m-n+1\) 条边叫非树边。
非严格次小生成树
显然,对于已经生成的最小生成树来说,每一条非树边的加入,都会形成一个环。那么再将环上的树边中最大的边删除,就能得到次小生成树的一颗候选树。
令最小生成树大小为 \(minn\),新加入的非树边权值为 \(new\),环上的最大树边为 \(max\),那么候选树的大小就是 \(minn-max+new\),我们所求则是 \(min\{minn-max+new\}\)。
严格次小生成树
当 \(new=max\) 时,若按上述方法进行维护,得到的 \((minn-max+new)=minn\)。
此时,就应该选择环上树边的次大值 \(nexm\),\((minn-nexm+new)>minn\)。
解法
现在的问题就在于,如何快速求出两点间树边的最大值和次大值。
若直接用两个二维数组将两点间的最大值,次大值存下来是不现实的,\(O(n^2)\) 的空间复杂度不允许。
考虑倍增,储存下来每个节点到其 \(2^k\) 级祖先的最大值和次大值,然后在跳 lca 的过程中维护最大值和次大值。
比如,求从 u 到 v 的最大值,可以先找出 u 和 v 的 lca,然后分别求出 u 到 lca 和 v 到 lca 的最大值,两者取最大即可。
Code:
#include<cstdio>
#include<algorithm>
#include<vector>
#define int long long
using namespace std;
int re()
{
int s=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
s=s*10+ch-48,ch=getchar();
return s*f;
}
void wr(int s)
{
if(s<0)putchar('-'),s=-s;
if(s>9)wr(s/10);
putchar(s%10+48);
}
const int inf=3e5+7;
int n,m,minn,ans=1e18;
int fa[inf];
struct kruskal{
int from,to,val;
bool operator <(const kruskal &b)const
{
return val<b.val;
}
};
vector<kruskal>k;
struct edge{
int to,val;
edge(int to,int val):
to(to),val(val){}
};
vector<edge>e[inf];
bool vis[inf<<1];int cnt;
int find(int s)
{
if(s==fa[s])return s;
return fa[s]=find(fa[s]);
}
int dep[inf],fat[inf][20];
int maxn[inf][20],nexm[inf][20];
void dfs(int now,int from)
{
dep[now]=dep[from]+1;
fat[now][0]=from;
for(int i=0;i<e[now].size();i++)
{
int p=e[now][i].to;
if(p==from)continue;
maxn[p][0]=e[now][i].val;
dfs(p,now);
}
}
void swap(int &a,int &b){a^=b^=a^=b;}
int max(int a,int b){return a>b?a:b;}
int _lca(int x,int y)
{
if(dep[x]<dep[y])swap(x,y);
for(int i=19;i>=0;i--)
if(dep[fat[x][i]]>=dep[y])
x=fat[x][i];
if(x==y)return x;
for(int i=19;i>=0;i--)
if(fat[x][i]!=fat[y][i])
x=fat[x][i],y=fat[y][i];
return fat[x][0];
}
int ask(int x,int y,int z)
{
int maxi=0;
for(int i=19;i>=0;i--)
{
if(dep[fat[x][i]]>=dep[y])
{
if(maxn[x][i]==z)
maxi=max(maxi,nexm[x][i]);
else maxi=max(maxi,maxn[x][i]);
x=fat[x][i];
}
}
return maxi;
}
signed main()
{
n=re();m=re();ans+=7;
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=m;i++)
{
kruskal data;
data.from=re(),data.to=re(),data.val=re();
k.push_back(data);
}
sort(k.begin(),k.end());
for(int i=0;i<m;i++)
{
int k1=k[i].from,k2=k[i].to,v=k[i].val;
int r1=find(k1),r2=find(k2);
if(r1==r2)continue;
vis[i]=1;fa[r1]=r2;
cnt++;minn+=v;
e[k1].push_back(edge(k2,v));
e[k2].push_back(edge(k1,v));
if(cnt==n-1)break;
}
dfs(1,1);
for(int i=1;i<20;i++)
{
for(int j=1;j<=n;j++)
{
fat[j][i]=fat[fat[j][i-1]][i-1];
maxn[j][i]=max(maxn[j][i-1],maxn[fat[j][i-1]][i-1]);
if(maxn[j][i-1]==maxn[fat[j][i-1]][i-1])
nexm[j][i]=max(nexm[j][i-1],nexm[fat[j][i-1]][i-1]);
else if(maxn[j][i-1]<maxn[fat[j][i-1]][i-1])
nexm[j][i]=max(maxn[j][i-1],nexm[fat[j][i-1]][i-1]);
else if(maxn[j][i-1]>maxn[fat[j][i-1]][i-1])
nexm[j][i]=max(nexm[j][i-1],maxn[fat[j][i-1]][i-1]);
}
}
for(int i=0;i<m;i++)
{
if(vis[i])continue;
int k1=k[i].from,k2=k[i].to,val=k[i].val;
int lca=_lca(k1,k2);
int max1=ask(k1,lca,val),max2=ask(k2,lca,val);
ans=min(ans,minn+val-max(max1,max2));
}
wr(ans);putchar('\n');
return 0;
}
kruskal 重构树
用途
巧妙地求解询问连接两点的所有路径中最大边的最小值或者最小边的最大值问题。
思路
kruskal 求 MST 的时候是逐步加边,而 kruskal 重构树则是将要加的边转化成点,并连接原来的两个点,边权为点权。
就像这样:
实例(图片来自 OI-Wiki)
原无向图
重构树
性质
- kruskal 重构树是一棵二叉堆
- 最小生成树的重构树是大根堆,最大生成树的重构树是小根堆
- 图上两点最小路径的最大值或最大路径的最小值为重构树上两点的 lca 的点权。
- 重构树上共 \(n\times 2-1\) 的点,其中,n 个叶节点为原来图中的节点。
例题讲解
归程
这个题可以用可持久化并查集切掉。
原谅我不会
海拔比水位高的路车可以通过,剩下的路只能步行涉水。
那么就先预处理出整张图的 kruskal 重构树,然后找到树上的一个节点 x,满足 val[x]>p&&val[fa[x]]<=p
,这样,以 x 为根的子树中的叶节点就是能通过车连通的。
提前预处理出图上每个节点到 1 号节点的最短路,然后在重构树中维护每个节点为根时子树中距离 1 号节点的最小值。
至于怎么找到重构树上的节点 x,那就要用到倍增了。
坑点
- 最短路用 dijkstra,因为 SPFA 死了。
- 多测记得清空,尤其是
lastans
容易忘。
Code
#include<cstdio>
#include<cstring>
#include<queue>
#include<vector>
#include<algorithm>
using namespace std;
int re()
{
int s=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0')
{
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
s=s*10+ch-48,ch=getchar();
return s*f;
}
void wr(int s)
{
if(s<0)putchar('-'),s=-s;
if(s>9)wr(s/10);
putchar(s%10+48);
}
const int inf=4e5+7;
int t,n,m,q,k,s,last;
int fir[inf],nex[inf<<1],poi[inf<<1],val[inf<<1],cnt;
void ins(int x,int y,int z)
{
nex[++cnt]=fir[x];
poi[cnt]=y;
val[cnt]=z;
fir[x]=cnt;
}
int dis[inf];bool vis[inf];
struct dij{
int id,dis;
dij(int id,int dis):id(id),dis(dis){}
bool operator <(const dij &b)const
{
return dis>b.dis;
}
};
struct kruskal{
int from,to,val;
bool operator <(const kruskal &b)const
{
return val>b.val;
}
};
int fat[inf],cntm,cntn;
int find(int x)
{
if(x==fat[x])return fat[x];
return fat[x]=find(fat[x]);
}
struct SR_heap{
int lc,rc,val,minn;
#define lc(i) T[i].lc
#define rc(i) T[i].rc
};
SR_heap T[inf];
int fa[inf][20];
void dfs(int now,int from)
{
fa[now][0]=from;
if(lc(now)==0)
{
T[now].minn=dis[now];
return;
}
dfs(lc(now),now);
dfs(rc(now),now);
T[now].minn=min(T[lc(now)].minn,T[rc(now)].minn);
}
int tiao(int x,int k)
{
for(int i=19;i>=0;i--)
{
if(T[fa[x][i]].val<=k)continue;
x=fa[x][i];
}
return x;
}
int main()
{
t=re();
while(t--)
{
cntn=n=re(),m=re();
cnt=cntm=last=0;
memset(fir,0,sizeof(fir));
memset(nex,0,sizeof(nex));
memset(poi,0,sizeof(poi));
memset(val,0,sizeof(val));
memset(dis,127,sizeof(dis));
memset(vis,0,sizeof(vis));
memset(fa,0,sizeof(fa));
memset(T,0,sizeof(T));
priority_queue<dij>heap;
vector<kruskal>h;
for(int i=1;i<=m;i++)
{
int u=re(),v=re(),l=re(),a=re();
ins(u,v,l),ins(v,u,l);
h.push_back((kruskal){u,v,a});
}
heap.push(dij(1,0));dis[1]=0;
while(heap.size())
{
dij now=heap.top();heap.pop();
if(vis[now.id])continue;
vis[now.id]=1;
for(int i=fir[now.id];i;i=nex[i])
{
int p=poi[i];
if(dis[p]>dis[now.id]+val[i])
{
dis[p]=dis[now.id]+val[i];
heap.push(dij(p,dis[p]));
}
}
}
for(int i=1;i<=n;i++)fat[i]=i;
sort(h.begin(),h.end());
int len=h.size();
for(int i=0;i<len;i++)
{
int r1=find(h[i].from),r2=find(h[i].to);
if(r1==r2)continue;
cntm++;cntn++;
fat[r1]=fat[r2]=fat[cntn]=cntn;
T[cntn]=(SR_heap){r1,r2,h[i].val};
}
for(int i=1;i<=cntn;i++)
fat[i]=find(i);
dfs(fat[1],fat[1]);
for(int j=1;j<20;j++)
for(int i=1;i<=cntn;i++)
fa[i][j]=fa[fa[i][j-1]][j-1];
q=re(),k=re(),s=re();
for(int i=1;i<=q;i++)
{
int v=re(),p=re();
v=(v+k*last-1)%n+1;
p=(p+k*last)%(s+1);
wr(last=T[tiao(v,p)].minn);putchar('\n');
}
}
return 0;
}
练习
kruskal 相关题目
kruskal 及其应用的更多相关文章
- 图的生成树(森林)(克鲁斯卡尔Kruskal算法和普里姆Prim算法)、以及并查集的使用
图的连通性问题:无向图的连通分量和生成树,所有顶点均由边连接在一起,但不存在回路的图. 设图 G=(V, E) 是个连通图,当从图任一顶点出发遍历图G 时,将边集 E(G) 分成两个集合 T(G) 和 ...
- 最小生成树---Prim算法和Kruskal算法
Prim算法 1.概览 普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树.意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (gra ...
- 最小生成树(prim&kruskal)
最近都是图,为了防止几次记不住,先把自己理解的写下来,有问题继续改.先把算法过程记下来: prime算法: 原始的加权连通图——————D被选作起点,选与之相连的权值 ...
- Kruskal 最小生成树算法
对于一个给定的连通的无向图 G = (V, E),希望找到一个无回路的子集 T,T 是 E 的子集,它连接了所有的顶点,且其权值之和为最小. 因为 T 无回路且连接所有的顶点,所以它必然是一棵树,称为 ...
- 权重最小生成树的思想与Kruskal算法
晚上做携程的笔试题,附加题考到了权重最小生成树.OMG,就在开考之前,我还又看过一遍这内容,可因为时间太紧,也从来没有写过代码,就GG了.又吃了眼高手低的亏.这不,就好好总结一下,亡羊补牢. 权重最小 ...
- 最小生成树 kruskal算法 codevs 1638 修复公路
1638 修复公路 时间限制: 1 s 空间限制: 256000 KB 题目等级 : 钻石 Diamond 题解 题目描述 Description A地区在地震过后,连接所有村庄的公 ...
- 洛谷P1991无线通讯网[kruskal | 二分答案 并查集]
题目描述 国防部计划用无线网络连接若干个边防哨所.2 种不同的通讯技术用来搭建无线网络: 每个边防哨所都要配备无线电收发器:有一些哨所还可以增配卫星电话. 任意两个配备了一条卫星电话线路的哨所(两边都 ...
- NOIP2013货车运输[lca&&kruskal]
题目描述 A 国有 n 座城市,编号从 1 到 n,城市之间有 m 条双向道路.每一条道路对车辆都有重量限制,简称限重.现在有 q 辆货车在运输货物, 司机们想知道每辆车在不超过车辆限重的情况下,最多 ...
- 最小生成树的Kruskal算法实现
最近在复习数据结构,所以想起了之前做的一个最小生成树算法.用Kruskal算法实现的,结合堆排序可以复习回顾数据结构.现在写出来与大家分享. 最小生成树算法思想:书上说的是在一给定的无向图G = (V ...
- poj2485 kruskal与prim
Kruskal: #include<iostream> #include<cstdio> #include<algorithm> using namespace s ...
随机推荐
- 实现深拷贝还在用JSON.parse(JSON.stringify(obj))?带你用JS实现一个完整版深拷贝函数
使用JavaScript实现深拷贝 1.JSON序列化实现深拷贝 在JS中,想要对某一个对象(引用类型)进行一次简单的深拷贝,可以使用JSON提供给我们的两个方法. JSON.stringfy():可 ...
- Java学习day33
线程池: 背景:经常创建和销毁.使用量特别大的资源,比如并发情况下的线程,对性能影响很大 思路:提前创建好多个线程.实现重复利用. 好处:提高响应速度,减少了创建新线程的时间:降低资源消耗,重复利用线 ...
- 迷惑小错 之 :requests.exceptions.ProxyError
缘由 当打开代理或者抓包工具时 pycharm运行发包请求报错: requests.exceptions.ProxyError.关掉代理后又能正常的请求,这样对于我们日常操作很不方便吗.四处查找资料无 ...
- transform动画
1. html 结构 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&quo ...
- python基础练习题(题目 斐波那契数列II)
day16 --------------------------------------------------------------- 实例024:斐波那契数列II 题目 有一分数序列:2/1,3 ...
- Linux网络流量相关
一直以来对Linux网络这块都感觉比较乱 遇到一个UDP丢包的问题:在测试中,一台VM虚拟机,CPU利用率55%左右,内存利用率7%左右,网卡流量也远没到限制的时候出现了丢包情况 使用netstat ...
- Linux发行版--发行版之间的关系--哲学思想--目录的命名规则及用途
作业2 点此链接查看centos7安装 点此链接查看Ubuntu安装 点此链接查看作业3.5 点此链接查看作业7.8.9 作业1.4.6 Linux发行版--发行版之间的关系 1.Linux是什么 L ...
- Apache Kafka 集群部署指南
公众号关注 「开源Linux」 回复「学习」,有我为您特别筛选的学习资料~ Kafka基础 消息系统的作用 应该大部分小伙伴都清楚,用机油装箱举个例子. 所以消息系统就是如上图我们所说的仓库,能在中间 ...
- 生成器对象(自定义迭代器),自定义range方法,模块
自定义迭代器 一 .生成器与yield ''' 我们得到一个迭代器通常都是调用可迭代对象的__iter__方法 ,例如 list.iter() 得到一个迭代器, 但是当list很大时候,就违背了pyt ...
- 并发编程系列之Lock锁可重入性与公平性
一.相似之处:Lock锁 vs Synchronized 代码块 Lock锁是一种类似于synchronized 同步代码块的线程同步机制.从Java 5开始java.util.concurrent. ...