【学习笔记】图论总体学习笔记
记录图论的学习。
省流:被吊打了
更改日志
2024/01/08:开坑,先不写。
2024/01/09:更新非严格次短路、判负环、差分约束系统、最短路计数、传递闭包。
学习背景
图论早就学了,只是把知识记一遍而已,有些太简单的就不记了。
最短路技巧
最短路大家应该都会,我就来写几个技巧。
1.非严格次短路
非严格次短路就是如果有多条最短路,次短路就是最短路。
先看例题吧,边讲例题边讲做法。
例题
P2865 [USACO06NOV] Roadblocks G
又是一道蓝题!
贝茜把家搬到了一个小农场,但她常常回到 FJ 的农场去拜访她的朋友。贝茜很喜欢路边的风景,不想那么快地结束她的旅途,于是她每次回农场,都会选择第二短的路径,而不象我们所习惯的那样,选择最短路。
贝茜所在的乡村有 \(R(1\le R \le 10^5)\) 条双向道路,每条路都联结了所有的 \(N(1\le N\le 5000)\) 个农场中的某两个。贝茜居住在农场 \(1\),她的朋友们居住在农场 \(N\)(即贝茜每次旅行的目的地)。
贝茜选择的第二短的路径中,可以包含任何一条在最短路中出现的道路,并且,一条路可以重复走多次。当然咯,第二短路的长度必须严格大于最短路(可能有多条)的长度,但它的长度必须不大于所有除最短路外的路径的长度。
解法
这里可以看出不就是个非严格次短路,就是如果有多条最短路,次短路就是最短路。
那么我们就可以想,能不能再求最短路时求出次短路呢?答案是肯定的。
我用的是 dijkstra
堆优化( dijkstra
好闪,拜谢 dijkstra
!)。
先设 \(u\) 为当前的点,\(v\) 为与 \(u\) 有边连接的点, \(w\) 代表从点 \(u\) 到点 \(v\) 的边的权值, \(dis\) 数组代表最短路,\(ret\) 数组代表次短路。
更新时有以下三种情况:
-
如果 \(dis_u + w < dis_v\),说明从按 \(u\) 的最短路再走到点 \(v\) 比原先的最短路更优,则更新最短路,就是 \(dis_v = dis_u + w\)。
-
如果 \(dis_u + w \ge dis_v\) 且 \(dis_u + w < ret_v\),说明从按 \(u\) 的最短路再走到点 \(v\) 比原先的次短路更优,则更新次短路,就是 \(ret_v = dis_u + w\)。
-
如果 \(ret_u + w < ret_v\),说明从按 \(u\) 的次短路再走到点 \(v\) 还比原先的次短路更优,则再更新次短路,就是 \(ret_v = ret_u + w\)。
若上述条件有一个成立,则将点 \(v\) 加入堆中,还要将点 \(v\) 对应的最短路 \(dis_v\) 加入堆中,那怎么知道是否有成立呢?可以用一个标记变量 \(flag\),每次更新就也把它的值给更新了,最后判断就行了。
设最后一个农场为点 \(N\) ,则最终答案为 \(ret_N\)。
CODE(就是非严格次短路模板啦):
#include<bits/stdc++.h>
#define fi first
#define se second
using namespace std;
int r,n,u,v,w;
vector<pair<int,int> >g[100010];//还要存上权值
int dis[10010],ret[10010];
struct node{
int fi,se;//节点和最短路
bool operator<(const node &b) const{//重载运算符,大根堆->小根堆
return se>b.se;
}
};
priority_queue<node>q;//堆
pair<int,int> m_p(int x,int y){
return make_pair(x,y);
}void dij(){
memset(dis,127,sizeof dis);//初始化
memset(ret,127,sizeof ret);
dis[1]=0;
q.push((node){1,0});//先入堆
while(!q.empty()){
int u=q.top().fi,dic=q.top().se
q.pop();
for(int i=0;i<g[u].size();i++){
int v=g[u][i].fi,w=g[u][i].se;
int f=0;
if(dic+w<dis[v]){//转移
dis[v]=dic+w,f=1;
}if(dic+w>dis[v]&&dic+w<ret[v]){
ret[v]=dic+w,f=1;
}if(ret[u]+w<ret[v]){
ret[v]=ret[u]+w,f=1;
}if(f==1){//有成立的条件,入队
q.push((node){v,dis[v]});
}
}
}
}int main(){
scanf("%d%d",&r,&n);
for(int i=1;i<=n;i++){
scanf("%d%d%d",&u,&v,&w);
g[u].push_back(m_p(v,w));
g[v].push_back(m_p(u,w));
}dij();
printf("%d",ret[r]);
return 0;
}
2.图的判负环
巨大的知识点。
这里我们知道,dijkstra
就是无法解决边权为负数的问题,但是其他的三个 Floyd
、Bellman-Ford
和 SPFA
(死了!)可以,还可以判负环,接下来我将用一道例题来讲一下这三种算法的判负环。
首先,先看一下如何判负环。
我们可以发现,若 Floyd
中 \(dis_{i,i}\) 小于 \(0\) 的情况存在,则一定会有实体负环的存在。
再来看看 Bellman-Ford
,如果它没有负环的话,进行了 \(n-1\) 次松弛操作后,最短路长度不会再改变了,若第 \(n\) 次松弛操作仍能执行成功,则一定存在负环。
最后再是 SPFA
,这里如果某顶点的入队次数超过 \(n-1\) 次(\(n\) 为图中顶点数),则存在负环。但是这样判断可能遇到特殊的卡 图,使得入队次数接近 \(n^{2}\) (此时的时间复杂度为 \(\mathcal{O}(n^{2})\) ),由此造成超时。但是我们可以换一种思考方式,若最短路径的边数超过 \(n\) (普通的最多只有 \(n-1\) 条),则一定存在负环,这里直接开个 \(cnt\) 数组统计就好了。SPFA
的
例题
P3385 【模板】负环
题目描述
给定一个 \(n\) 个点的有向图,请求出图中是否存在从顶点 \(1\) 出发能到达的负环。
负环的定义是:一条边权之和为负数的回路。
输入格式
本题单测试点有多组测试数据。
输入的第一行是一个整数 \(T\),表示测试数据的组数。对于每组数据的格式如下:
第一行有两个整数,分别表示图的点数 \(n\) 和接下来给出边信息的条数 \(m\)。
接下来 \(m\) 行,每行三个整数 \(u, v, w\)。
- 若 \(w \geq 0\),则表示存在一条从 \(u\) 至 \(v\) 边权为 \(w\) 的边,还存在一条从 \(v\) 至 \(u\) 边权为 \(w\) 的边。
- 若 \(w < 0\),则只表示存在一条从 \(u\) 至 \(v\) 边权为 \(w\) 的边。
输出格式
对于每组数据,输出一行一个字符串,若所求负环存在,则输出 YES
,否则输出 NO
。
提示
数据规模与约定
对于全部的测试点,保证:
- \(1 \leq n \leq 2 \times 10^3\),\(1 \leq m \leq 3 \times 10^3\)。
- \(1 \leq u, v \leq n\),\(-10^4 \leq w \leq 10^4\)。
- \(1 \leq T \leq 10\)。
提示
请注意,\(m\) 不是图的边数。
解法(代码部分,前面已经讲过方法)
Floyd
做法(不优)
这里的 \(n \leq 2 \times 10^3\),根据 Floyd
\(\mathcal{O}(n^{3})\) 的时间复杂度,会超时,仅可拿到部分分。
这样可以拿到 \(60\) 分
CODE:
#include<bits/stdc++.h>
using namespace std;
int t,n,m,u,v,w,dis[2010][2010];
void floyd(){
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(dis[i][k]==0x3f3f3f3f||dis[k][j]==0x3f3f3f3f){
continue;
}dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);转移
if(dis[1][i]!=0x3f3f3f3f&&dis[i][i]<0){//判断负环
printf("YES\n");
return;
}
}
}
}printf("NO\n");
}int main(){
scanf("%d",&t);
while(t--){
memset(dis,0x3f3f3f3f,sizeof dis);
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%d",&u,&v,&w);
if(w>=0){
dis[u][v]=min(w,dis[u][v]);//建图
dis[v][u]=min(w,dis[v][u]);
}else{
dis[u][v]=min(w,dis[u][v]);
}
}floyd();
}return 0;
}
Bellman-Ford
做法
注意初始化,前面已经讲过如何判负环,满分。
CODE:
#include<bits/stdc++.h>
using namespace std;
int t,n,m,u,v,w,dis[2010],cnt[2010],vis[2010];
struct node{
int v,w;
};
vector<node>g[2010];
void Bellman_Ford(int a){
memset(dis,0x3f,sizeof dis);
dis[a]=0;
for(int i=1;i<n;i++){
for(int j=1;j<=n;j++){
if(dis[j]!=0x3f3f3f3f){
for(int k=0;k<g[j].size();k++){
int v=g[j][k].v,w=g[j][k].w;
if(dis[j]+w<dis[v]){//尝试松弛
dis[v]=dis[j]+w;
}
}
}
}
}for(int j=1;j<=n;j++){
for(int k=0;k<g[j].size();k++){
int v=g[j][k].v,w=g[j][k].w;
if(dis[j]==0x3f3f3f3f||dis[v]==0x3f3f3f3f){//判断
continue;
}if(dis[j]+w<dis[v]){
printf("YES\n");
return;
}
}
}printf("NO\n");
}int main(){
scanf("%d",&t);
while(t--){
scanf("%d%d",&n,&m);
for(int i=1;i<=2000;i++){
g[i].clear();
}for(int i=1;i<=m;i++){
scanf("%d%d%d",&u,&v,&w);
if(w>=0){
g[u].push_back((node){v,w});//建图
g[v].push_back((node){u,w});
}else{
g[u].push_back((node){v,w});
}
}Bellman_Ford(1);
}return 0;
}
SPFA
做法
前面同样已经讲过如何判负环,满分。
CODE:
#include<bits/stdc++.h>
using namespace std;
int t,n,m,u,v,w,dis[2010],cnt[2010],vis[2010];
struct node{
int v,w;
};
vector<node>g[2010];
void spfa(int a){
memset(dis,0x3f,sizeof dis);//初始化
memset(cnt,0,sizeof cnt);
memset(vis,0,sizeof vis);
queue<int>q;
dis[a]=0,vis[a]=1,q.push(a);
while(!q.empty()){
int u=q.front();
vis[u]=0;
q.pop();
for(int i=0;i<g[u].size();i++){
int v=g[u][i].v,w=g[u][i].w;
if(dis[u]+w<dis[v]){//操作
dis[v]=dis[u]+w;
cnt[v]=cnt[u]+1;//统计边的条数
if(cnt[v]>=n){//如果边的数量超过了n
printf("YES\n");
return;
}if(vis[v]==0){//入队
vis[v]=1;
q.push(v);
}
}
}
}printf("NO\n");
}int main(){
scanf("%d",&t);
while(t--){
scanf("%d%d",&n,&m);
for(int i=1;i<=2000;i++){
g[i].clear();//初始化
}for(int i=1;i<=m;i++){
scanf("%d%d%d",&u,&v,&w);
if(w>=0){//建图
g[u].push_back((node){v,w});
g[v].push_back((node){u,w});
}else{
g[u].push_back((node){v,w});
}
}spfa(1);
}return 0;
}
3.差分约束系统
差分约束系统是一种特殊的 \(n\) 元一次不等式组(图论强大),这里包含了 \(n\) 个变量 \(x_1,x_2,......,x_n\) 和 \(m\) 个约束条件,每个约束条件形如 \(x_i-x_j \le C_k \left(1 \le i,j,k \le n\right)\),其中 \(C_k\) 被定义为一个常数,求满足约束的解。
这个 \(n\) 元一次不等式组,只有两种情况:
-
无解。
-
可以找到其中一组解 \(\left\{x_1,x_2,......,x_n\right\}\),再将其进行常数变换,进而得到无数组解(
怎么开始讲数学了)。
总所周知,我们的最短路若不存在负环,则 \(dis\) 数组满足 \(dis_i \le dis_j+w\left(i,j\right)\),这与我们的约束条件变换后的 \(x_i \le x_j+C_k\) 类似。
于是,我们就可以将约束条件中的变量 \(x_i,x_j\) 抽象为图的顶点,将 \(C_k\) 抽象为点 \(x_i\) 到点 \(x_j\) 边的权值,于是就可以将差分约束系统转换为一个图模型,求单源最短路。
抽象了图模型后,我们可以建立一个顶点 \(x_0\) ,将这个顶点与所有点顶点建边,权值为 \(0\),即 \(x_i-x_0=0\left(1 \le i \le n \right)\) 。
这样就可以从 \(x_0\) 开始求单源最短路,即为源。
从 \(x_0\) 开始到其他顶点的最短路 \(dis_i\) 就是解,若不存在最短路则无解。
例题
P1993 小 K 的农场
题目描述
小 K 在 MC 里面建立很多很多的农场,总共 \(n\) 个,以至于他自己都忘记了每个农场中种植作物的具体数量了,他只记得一些含糊的信息(共 \(m\) 个),以下列三种形式描述:
- 农场 \(a\) 比农场 \(b\) 至少多种植了 \(c\) 个单位的作物;
- 农场 \(a\) 比农场 \(b\) 至多多种植了 \(c\) 个单位的作物;
- 农场 \(a\) 与农场 \(b\) 种植的作物数一样多。
但是,由于小 K 的记忆有些偏差,所以他想要知道存不存在一种情况,使得农场的种植作物数量与他记忆中的所有信息吻合。
输入格式
第一行包括两个整数 \(n\) 和 \(m\),分别表示农场数目和小 K 记忆中的信息数目n
接下来 \(m\) 行:
- 如果每行的第一个数是 \(1\),接下来有三个整数 \(a,b,c\),表示农场 \(a\) 比农场 \(b\) 至少多种植了 \(c\) 个单位的作物;
- 如果每行的第一个数是 \(2\),接下来有三个整数 \(a,b,c\),表示农场 \(a\) 比农场 \(b\) 至多多种植了 \(c\) 个单位的作物;
- 如果每行的第一个数是 \(3\),接下来有两个整数 \(a,b\),表示农场 \(a\) 种植的的数量和 \(b\) 一样多。
输出格式
如果存在某种情况与小 K 的记忆吻合,输出 Yes
,否则输出 No
。
提示
对于 \(100\%\) 的数据,保证 \(1 \le n,m,a,b,c \le 5 \times 10^3\)。
这里 \(n\) 个农场可以抽象为 \(n\) 个变量,\(m\) 条信息可以抽象为 \(m\) 个约束条件,那么题目就是问这个不等式是否存在解。
这里的不等式分为 \(3\) 种情况:
-
\(v_i-v_j\ge c_1\),可以转化为 \(v_j-v_i\le -c_1\)。
-
\(v_i-v_j\le c_2\)。
-
\(v_i=v_j\)。
根据不同情况建边,后面还要多增加一个节点(前面讲了,不再赘述)。
CODE(我怎么越来越喜欢用SPFA了,不是死了吗):
#include<bits/stdc++.h>
using namespace std;
struct node{
int v,w;
};
int n,m,u,v,w,op,dis[20010],cnt[20010],vis[20010];
vector<node>g[20010];
queue<int>q;
void spfa(int a){
memset(dis,127,sizeof dis);
queue<int>q;
dis[a]=0,vis[a]=1,q.push(a);
while(!q.empty()){
int u=q.front();
vis[u]=0;
q.pop();
for(int i=0;i<g[u].size();i++){
int v=g[u][i].v,w=g[u][i].w;
if(dis[u]+w<dis[v]){//更新
dis[v]=dis[u]+w;
cnt[v]++;
if(cnt[v]>=n+1){//判环,若有环则不存在
printf("No");
return;
}if(vis[v]==0){//入队
vis[v]=1;
q.push(v);
}
}
}
}printf("Yes");
}int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%d",&op,&u,&v);
if(op==1){//根据情况建边
scanf("%d",&w);
g[u].push_back((node){v,-w});
}else if(op==2){
scanf("%d",&w);
g[v].push_back((node){u,w});
}else{
g[u].push_back((node){v,0});
g[v].push_back((node){u,0});
}
}for(int i=1;i<=n;i++){//增加节点,讲解如上
g[0].push_back((node){i,0});
}spfa(0);//以增加的节点为源点跑最短路(
return 0;
}
4.最短路计数
最短路计数也是图论中很常见的,实际运用到题目中总会有一些不同。
我们设 \(cnt_u\) 表示从源点到点 \(u\) 的最短路径条数,它的更新有两种情况:
-
\(dis_v>dis_u+w\left(u,v\right)\),说明从源点 \(1\) 到点 \(v\) 的最短路径就是从源点 \(0\) 到点 \(u\) 的最短路径加上边 \(\left<u,v\right>\),所以 \(cnt_v\) 等于 \(cnt_u\)。
-
\(dis_v=dis_u+w\left(u,v\right)\),说明原来从从源点 \(1\) 到点 \(v\) 的最短路径有 \(cnt_v\) 条,现在又可以加上经过点 \(u\) 的这些路径,所以 \(cnt_v=cnt_v+cnt_u\)。
源点 \(1\) 的最短路径是 \(cnt_s=1\),若有重边和自环,需要分开考虑。
例题
P1144 最短路计数
又是一道绿题
题目描述
给出一个 \(N\) 个顶点 \(M\) 条边的无向无权图,顶点编号为 \(1\sim N\)。问从顶点 \(1\) 开始,到其他每个点的最短路有几条。
输入格式
第一行包含 \(2\) 个正整数 \(N,M\),为图的顶点数与边数。
接下来 \(M\) 行,每行 \(2\) 个正整数 \(x,y\),表示有一条连接顶点 \(x\) 和顶点 \(y\) 的边,请注意可能有自环与重边。
输出格式
共 \(N\) 行,每行一个非负整数,第 \(i\) 行输出从顶点 \(1\) 到顶点 \(i\) 有多少条不同的最短路,由于答案有可能会很大,你只需要输出 $ ans \bmod 100003$ 后的结果即可。如果无法到达顶点 \(i\) 则输出 \(0\)。
提示
\(1\) 到 \(5\) 的最短路有 \(4\) 条,分别为 \(2\) 条 \(1\to 2\to 4\to 5\) 和 \(2\) 条 \(1\to 3\to 4\to 5\)(由于 \(4\to 5\) 的边有 \(2\) 条)。
对于 \(20\%\) 的数据,\(1\le N \le 100\);
对于 \(60\%\) 的数据,\(1\le N \le 10^3\);
对于 \(100\%\) 的数据,\(1\le N\le10^6\),\(1\le M\le 2\times 10^6\)。
模板,我使用的是 SPFA
,在松弛时更新 \(cnt\) 就好了。
CODE:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10,mod=1e5+3;
vector<int>g[N];
queue<int>q;
int cnt[N],dis[N],vis[N];
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
g[x].push_back(y),g[y].push_back(x);
}int s=1;
memset(dis,0x3f,sizeof dis);//初始化
dis[s]=0,vis[s]=1,cnt[s]=1;
q.push(s);
while(!q.empty()){
int u=q.front();
q.pop();
vis[u]=0;
for(int i=0;i<g[u].size();i++){
int v=g[u][i];
if(dis[u]+1<dis[v]){//情况1
dis[v]=dis[u]+1,cnt[v]=cnt[u];//松弛时更新
if(vis[v]==0){
q.push(v);
vis[v]=1;
}
}else if(dis[u]+1==dis[v]){//情况2
cnt[v]=(cnt[v]+cnt[u])%mod;//注意取模!
}
}
}for(int i=1;i<=n;i++){
printf("%d\n",cnt[i]);
}return 0;
}
5.传递凸闭包
传递闭包是给出若干的元素以及二元关系,要求这些二元关系存在传递性,通过传递性和二元关系来推出尽量多的元素之间的关系的问题。
注意,这里用到的关系必须是二元关系,若为多元关系,必须要拆解为二元关系。
可以将元素抽象为图的顶点(又tm是抽象),将二元关系抽象为边,这样就可以用 Floyd
求解获得任意两点是否连通的问题(就是问两个元素有没有关系)。
可以将 Floyd
中的转移方程改为 \(dis_{i,j}=dis_{i,j}|\left(dis_{i,k} \& dis_{k,j}\right)\),表示点 \(i\) 和点 \(j\) 之间有联通关系:
-
要么是直接点 \(i\) 和点 \(j\) 之间有联通关系。
-
要么是点 \(i\) 和点 \(k\) 之间有联通关系,且点 \(k\) 和点 \(j\) 之间有联通关系,可以根据传递性得到点 \(i\) 和点 \(j\) 之间有联通关系。
例题
P2419 [USACO08JAN] Cow Contest S
题目描述
FJ的 \(N\)(\(1 \leq N \leq 100\))头奶牛们最近参加了场程序设计竞赛。在赛场上,奶牛们按 \(1, 2, \cdots, N\) 依次编号。每头奶牛的编程能力不尽相同,并且没有哪两头奶牛的水平不相上下,也就是说,奶牛们的编程能力有明确的排名。 整个比赛被分成了若干轮,每一轮是两头指定编号的奶牛的对决。如果编号为 \(A\) 的奶牛的编程能力强于编号为 \(B\) 的奶牛 (\(1 \leq A, B \leq N\),\(A \neq B\)),那么她们的对决中,编号为 \(A\) 的奶牛总是能胜出。 FJ 想知道奶牛们编程能力的具体排名,于是他找来了奶牛们所有 \(M\)(\(1 \leq M \leq 4,500\))轮比赛的结果,希望你能根据这些信息,推断出尽可能多的奶牛的编程能力排名。比赛结果保证不会自相矛盾。
输入格式
第一行两个用空格隔开的整数 \(N, M\)。
第 \(2\sim M + 1\) 行,每行为两个用空格隔开的整数 \(A, B\) ,描述了参加某一轮比赛的奶牛的编号,以及结果(每行的第一个数的奶牛为胜者)。
输出格式
输出一行一个整数,表示排名可以确定的奶牛的数目。
提示
样例解释:
编号为 \(2\) 的奶牛输给了编号为 \(1, 3, 4\) 的奶牛,也就是说她的水平比这 \(3\) 头奶牛都差。而编号为 \(5\) 的奶牛又输在了她的手下,也就是说,她的水平比编号为 \(5\) 的奶牛强一些。于是,编号为 \(2\) 的奶牛的排名必然为第 \(4\),编号为 \(5\) 的奶牛的水平必然最差。其他 \(3\) 头奶牛的排名仍无法确定。
这里如果 \(A\) 强于 \(B\),\(B\) 强于 \(C\),则肯定 \(A\) 强于 \(C\),这里就有了传递性。
其实就是如果第 \(i\) 只奶牛与其他 \(n-1\) 只奶牛有关系,那么v排名就确定了。
那么我们就使 \(dis_{u,v}=1\) 代表奶牛 \(u\) 强于奶牛 \(v\)。
那么我们就三重跑一遍前面的状态转移方程 \(dis_{i,j}=dis_{i,j}|\left(dis_{i,k} \& dis_{k,j}\right)\),然后统计即可,但是统计是要避开自身的比较。
\(dis\) 的初始值根据实际情况分析。
CODE:
#include<bits/stdc++.h>
using namespace std;
int n,m,ret,u,v,cnt[110];
int dis[110][110];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d",&u,&v);
dis[u][v]=1;
}for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dis[i][j]=dis[i][j]|(dis[i][k]&dis[k][j]);//状态转移方程
}
}
}for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(dis[i][j]==1){
cnt[i]++,cnt[j]++;//可以确定这俩货的关系
}
}
}for(int i=1;i<=n;i++){
if(cnt[i]==n-1){//能确定排名
ret++;
}
}printf("%d",ret);
return 0;
}
由此,最短路的技巧讲完辣!
最小生成树
最小生成树就是从原图中删去任意条边,使得它的边权最小。
kruskal 算法
kruskal 算法是对边操作的。
kruskal 算法要先对边的权值排序,然后枚举所有的边,利用并查集判断会不会形成环,如果不会形成环,将这条边的两端合并一下,然后将记录这条边加入集合,如果统计了 \(n-1\) 条边,因为已经排过序,因此就是答案,就输出答案并结束程序。
例题
P3366 【模板】最小生成树
题目描述
如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出 orz
。
输入格式
第一行包含两个整数 \(N,M\),表示该图共有 \(N\) 个结点和 \(M\) 条无向边。
接下来 \(M\) 行每行包含三个整数 \(X_i,Y_i,Z_i\),表示有一条长度为 \(Z_i\) 的无向边连接结点 \(X_i,Y_i\)。
输出格式
如果该图连通,则输出一个整数表示最小生成树的各边的长度之和。如果该图不连通则输出 orz
。
提示
数据规模:
对于 \(20\%\) 的数据,\(N\le 5\),\(M\le 20\)。
对于 \(40\%\) 的数据,\(N\le 50\),\(M\le 2500\)。
对于 \(70\%\) 的数据,\(N\le 500\),\(M\le 10^4\)。
对于 \(100\%\) 的数据:\(1\le N\le 5000\),\(1\le M\le 2\times 10^5\),\(1\le Z_i \le 10^4\)。
这道题就是最小生成树模板,直接给出代码。
CODE:
#include<bits/stdc++.h>
using namespace std;
int n,m,x,cnt,ret,tmp,f[1001000];
struct node{
int u,v,w;
friend bool operator < (node a,node b){
return a.w<b.w;
}
}a[1001000];
int getfa(int x){
if(x==f[x]){
return x;
}return f[x]=getfa(f[x]);
}void merge(int x,int y){
f[getfa(x)]=getfa(y);
}int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
f[i]=i;
}for(int i=1;i<=m;i++){
scanf("%d%d%d",&a[i].u,&a[i].v,&a[i].w);
}sort(a+1,a+m+1);//排序
for(int i=1;i<=m;i++){
if(getfa(a[i].u)!=getfa(a[i].v)){//符合条件
merge(a[i].u,a[i].v);//合并
tmp++;
ret+=a[i].w;
if(tmp==n-1){//得到答案
printf("%d",ret);
return 0;
}
}
}printf("orz");
return 0;
}
咕咕咕