目录
单源最短路:从特定起点出发到某一点的最短距离
多源最短路:从一点出发到零一点的最短距离
1.图的解释与分类。
现实中,我们遇到的图形形色色,他们有色彩,有各种稀奇的形象,构成了多彩的世界。
然而在面临一些实际问题时,这些过于具体的色彩形象给我们的研究也带来了麻烦,16世纪左 右,数学家欧拉把每个事物看成点,把它们之间的关系简化成边,把这些有点和边组成的几何图 形称为图,包括树形图(树,图意)和点线图在在(图二),利用这些简化后的图解决了戈尼斯 堡七桥问题,引入了欧拉图,并开创了图论先河。
图一
图二
树其实就是一种特殊的图
Note:
不同数据结构中树的构建是不一样的,比如说算法竞赛常用的字典树,他也是一棵树,不过编号方式不同,在读者学习过字典树后,可以自行去体会。
树和图的区别如下:
树 | 图表 |
树是一种特殊的图,它永远不会有多个路径。 从A到B总是有一种方法。 | 图是一种具有多种方法来从任何点A到达任何其他点B的系统。 |
2.必须连接树。 | 2.可能未连接图形。 |
3.由于它已连接,所以我们可以从一个特定节点到达所有其他节点。 这种搜索称为遍历。 | 3.遍历始终不适用于图形。 因为图形可能未连接。 |
4.树不包含回路,电路。 | 4.图可能包含自循环,循环。 |
5.树中必须有一个根节点。 | 5.图中没有这种根节点 |
6.我们在树上遍历。 这意味着从某个角度出发,我们进入树的每个节点。 | 6.我们在图上进行搜索。 这意味着从任何节点开始尝试找到我们需要的特定节点。 |
7.前序,有序,后序是树中的某种遍历。 | 7. 广度优先搜索 , 深度优先搜索是图形中的某种搜索算法。 |
8.树是有向无环图。 | 8.图是循环的或非循环的。 |
9.树是一个分层的模型结构。 | 9.图是网络模型。 |
10.所有树都是图。 | 10.但是所有图形都不是树。 |
11.根据不同的属性,树可以分为二叉树,二叉搜索树,AVL树,堆。 | 11.我们区别于有向图和无向图之类的图。 |
12.如果树具有“ n”个顶点,则它必须仅具有精确的“ n-1”条边。 | 12.在图中,边的数量不取决于顶点的数量。 |
13.树木的主要用途是进行分类和遍历。 | 13.图形的主要用途是着色和作业计划。 |
14.与图形相比,复杂度更低。 | 14.由于循环,比树要复杂。 |
图的分类:
1. 树根据分叉数也就是每个节点的儿子数分类,有n个分叉就叫做n叉树
其中二叉树应用最为广泛(线段树~),此外还有26叉树(构建trie 树)用根节点,父节点,边权,子节点,来表述具有的特性。
2.图分为有向图和无向图,用(重边,自环,负环,边权,顶点,入度,出度)表述具有的特性。
具体参见离散数学图论相关内容。
2.树和图的建立。
如何构建一棵树:
以一棵二叉数为例
要构建一棵树,是比较简单的,我们需要定义每个节点的编号,假如我们把父节点设为1号节点,颜色相同的看作是同一层,并且每层从左到右编号,如图,那么1号节点的两个子节点分别是2号和3号,仔细观察,发现每个节点如果他的编号为n那么他的两个子节点分别是2*n ,和2*n+1
我们发现在这棵树中,我们所需要的关键信息就变成了结点的编号,因为知道了结点的编号,就知道了他的子节点和父节点,我们只需要保留这些关键信息就能还原出原来的树。
为了高效保存这些信息,我们可以用一维数组来存储所有结点的标号,每个节点储存其对应的值。
经典的线段树
代码至上:
#define MAX 2333
int tree[4*MAX];
void init(){
memset(tree,0,sizeof(tree));
}
//初始化
然后就是建树了,我们需要将数组变成一个树。代码如下:
void build(int node,int l,int r){
if(l == r){ // 到达叶子节点,赋值
cin >> tree[node];
return;
}
int mid = (l+r)/2;
build(node*2,l,mid); // 进入子树开始递归
build(node*2+1,mid+1,r);
tree[node] = tree[node*2] + tree[node*2 + 1]; // 回溯
//递归建树
}
如何构建有向图/无向图:
在算法竞赛中,有向图和无向图都是借助邻接表或邻接矩阵构建的。
邻接表相当于一个个凹槽,每个槽里储存某个点的临边,邻边存储在链表中,如图
邻接表:
const int N=2e5+10;
int e[N],ne[N],h[N],idx,w[N];
//h[i] 为头结点的链表储存点 i 所有临边。
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
// w[idx]记录的是这条边的权值也就是a->b的这条边
}//建边
//无向图的建边就是见两条边,一条a->b,另一条b->a
邻接矩阵:
const int N=1010;
//g[a][b]=c,表示a->b的一条边,权重是c
//假设有n条边
for(int i=1;i<=n;i++)
{
int a,b,c;
cin>>a>>b>>c;
g[a][b]=c;
//无向图g[a][b]=g[b][a]=c;
}
结构体:
const int N=1e5+10;
int n;
struct Edge{
int u,v,w;
}edges[N];
//和邻接矩阵类似,一条从u->v边权重为c
for(int i=1;i<=n;i++){
int u,v,w;
edges[i]={u,v,w};
}
// 注意这里只能建单向边!
具体建图的过程考虑重边还要注意更新权重。
在具体建图的过程中我们会根据点数和边数选择建图的方式,如果边数为e,点数为u,一般来说
e>u*u时,是稠密图,选择邻接矩阵,否则是稀疏图一般采用邻接表。
3.图的遍历与访问。
树与图的深度优先遍历
例题:树的重心
846. 树的重心
给定一颗树,树中包含 nn 个结点(编号 1∼n1∼n)和 n−1n−1 条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
输入格式
第一行包含整数 n,表示树的结点数。
接下来 n−1 行,每行包含两个整数 a 和 b,表示点 a 和点 b 之间存在一条边。
输出格式
输出一个整数 m,表示将重心删除后,剩余各个连通块中点数的最大值。
数据范围
1 ≤ n ≤ 1e5
输入样例
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6
输出样例:
4
在本题中,我们用st[N]这一bool数组来存储
题目描述
给定一颗树,树中包含n个结点(编号1~n)和n-1条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
输入格式
第一行包含整数n,表示树的结点数。
接下来n-1行,每行包含两个整数a和b,表示点a和点b之前存在一条边。
输出格式
输出一个整数m,表示重心的所有的子树中最大的子树的结点数目。
数据范围
1≤n≤105
样例
输入样例
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6
输出样例:
4
DFS(深度优先遍历)
主要思路:
DFS(深度优先遍历)
主要思路:
C++ 代码
#include <iostream>
#include <cstring>
using namespace std;
const int N=100010;
bool state[N];
//因为是双向边
int h[N],e[2*N],ne[2*N],idx,ans=N;
int n;
int add(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
//返回的是以u为根的子树中点的数量
int dfs(int u){
//标记u这个点被搜过
state[u]=true;
//size是表示将u点去除后,剩下的子树中数量的最大值;
//sum表示以u为根的子树的点的多少,初值为1,因为已经有了u这个点
int size=0,sum=1;
for(int i=h[u];i!=-1;i=ne[i]){
int j=e[i];
if(state[j]) continue;
//s是以j为根节点的子树中点的数量
int s=dfs(j);
//
size=max(size,s);
sum+=s;
}
//n-sum表示的是减掉u为根的子树,整个树剩下的点的数量
size=max(size,n-sum);
ans=min(size,ans);
return sum;
}
int main(){
cin>>n;
memset(h,-1,sizeof h);
int a,b;
for(int i=0;i<n;i++){
cin>>a>>b;
add(a,b);
add(b,a);
}
dfs(1);
cout<<ans;
return 0;
}
树和图的广度优先遍历:
例题:
图中点的层次:
给定一个 n 个点 m条边的有向图,图中可能存在重边和自环。
所有边的长度都是 1,点的编号为 1∼n。
请你求出 1 号点到 n 号点的最短距离,如果从 11 号点无法走到 nn 号点,输出 −1−1。
输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含两个整数 a 和 b,表示存在一条从 a 走到 b 的长度为 1 的边。(边权唯一)
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
数据范围
1≤n,m≤1e5
输入样例:
用 dist 数组保存1号节点到各个节点的距离,初始时,都是无穷大。
如果队列非空,就取出队头,找到队头节点能到的所有节点。如果队头节点能到走到的节点没有标记过,就将节点的dist值更新为队头的dist值+1,然后入队。
这个时候,dist数组中就存储了 1 号节点到各个节点的距离了。如果距离是无穷大,则不能到达,输出 -1,如果距离不是无穷大,则能到达,输出距离。
用 h 数组保存各个节点能到的第一个节点的编号。开始时,h[i] 全部为 -1。
用 e 数组保存节点编号,ne 数组保存 e 数组对应位置的下一个节点所在的索引。
给定一个 n个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。
请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 -1。
若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。
接下来 m行,每行包含两个整数 x 和 y,表示存在一条从点 x 到点 y 的有向边 (x,y)。
共一行,如果存在拓扑序列,则输出任意一个合法的拓扑序列即可。
输出样例:
1
判断1号节点能否走到n号节点,广度优先遍历即可。
思路:
用 dist 数组保存1号节点到各个节点的距离,初始时,都是无穷大。
用 st 数组标记各个节点有没有走到过。
从 1 号节点开始,广度优先遍历:
1 号节点入队列,dist[1] 的值更新为 0。
如果队列非空,就取出队头,找到队头节点能到的所有节点。如果队头节点能到走到的节点没有标记过,就将节点的dist值更新为队头的dist值+1,然后入队。
重复步骤 2 直到队列为空。
这个时候,dist数组中就存储了 1 号节点到各个节点的距离了。如果距离是无穷大,则不能到达,输出 -1,如果距离不是无穷大,则能到达,输出距离。
图的存储:邻接表
用 h 数组保存各个节点能到的第一个节点的编号。开始时,h[i] 全部为 -1。
用 e 数组保存节点编号,ne 数组保存 e 数组对应位置的下一个节点所在的索引。
用 idx 保存下一个 e 数组中,可以放入节点位置的索引
插入边使用的头插法,例如插入:a->b。首先把b节点存入e数组,e[idx] = b。然后 b 节点的后继是h[a],ne[idx] = h[a]。最后,a 的后继更新为 b 节点的编号,h[a] = idx,索引指向下一个可以存储节点的位置,idx ++ 。
#include <cstring>
#include <iostream>
using namespace std;
const int N=1e5+10;
int h[N], e[N], idx, ne[N];
int d[N]; //存储每个节点离起点的距离 d[1]=0
int n, m; //n个节点m条边
int q[N]; //存储层次遍历序列 0号节点是编号为1的节点
void add(int a, int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int bfs()
{
int hh=0,tt=0;
q[0]=1; //0号节点是编号为1的节点
memset(d,-1,sizeof d);
d[1]=0; //存储每个节点离起点的距离
//当我们的队列不为空时
while(hh<=tt)
{
//取出队列头部节点
int t=q[hh++];
//遍历t节点的每一个邻边
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
//如果j没有被扩展过
if(d[j]==-1)
{
d[j]=d[t]+1; //d[j]存储j节点离起点的距离,并标记为访问过
q[++tt] = j; //把j结点 压入队列
}
}
}
return d[n];
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof h);
for(int i=0;i<m;i++)
{
int a,b;
cin>>a>>b;
add(a,b);
}
cout<<bfs()<<endl;
}
有向图的拓扑排序:
848. 有向图的拓扑序列
给定一个 n个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。
请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 -1。
若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。
输入格式
第一行包含两个整数 n 和 m。
接下来 m行,每行包含两个整数 x 和 y,表示存在一条从点 x 到点 y 的有向边 (x,y)。
输出格式
共一行,如果存在拓扑序列,则输出任意一个合法的拓扑序列即可。
否则输出 −1。
数据范围
1≤n,m≤1e5
输入样例:
3 3
1 2
2 3
1 3
输出样例:
1 2 3
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=100010;
int h[N],e[N],ne[N],idx;
int n,m;
int q[N],d[N];//q表示队列,d表示点的入度
void add(int a,int b)
{
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
bool topsort()
{
int hh=0,tt=-1;
for(int i=1;i<=n;i++)
if(!d[i])
q[++tt]=i;//将入度为零的点入队
while(hh<=tt)
{
int t=q[hh++];
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
d[j]--;//删除点t指向点j的边
if(d[j]==0)//如果点j的入度为零了,就将点j入队
q[++tt]=j;
}
}
return tt==n-1;
//表示如果n个点都入队了话,那么该图为拓扑图,返回true,否则返回false
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof(h));//如果程序时间溢出,就是没有加上这一句
for(int i=0;i<m;i++)
{
int a,b;
scanf("%d%d",&a,&b);
add(a,b);//因为是a指向b,所以b点的入度要加1
d[b]++;
}
if(topsort())
{
for(int i=0;i<n;i++)
printf("%d ",q[i]);
//经上方循环可以发现队列中的点的次序就是拓扑序列
//注:拓扑序列的答案并不唯一,可以从解析中找到解释
puts("");
}
else
puts("-1");
return 0;
}
更新到2022年4月1日愚人节
最短路问题:
单源最短路:从特定起点出发到某一点的最短距离
BFS求最短路:
证明:归纳推理,优先选择代价最小的点进行拓展,把拓展后的点加入队尾,这样就保证了代价最小的点在队头仍然优先被拓展。
特别的如果说权重只有两种比如0或1这种,代价最小的点始终是权重为0的点,那么用一个双端队列,把权重为0的点拓展后的点加入队头,把权重为1的点拓展后的点加入队尾,也可以保证代价最小的点在对头位置,这种最短路求解是线性的。
Dijkstra 求最短路
首先,我们更容易理解暴力版的dijkstra 算法,初始化dist 数组,然后标记已经确定好最短路的点,拿这些点更新没有确定最短路的点,每次枚举确定好的和没有确定好的,时间复杂度为O(n*n),但这并不能达到我们的预期,这个算法的效率并不尽人意。
我们发现在枚举的过程中,有很多其实是多余的在枚举第i个确定好最短路的点后,可以使用堆在O(1)的时间内找到距离点i 的最短路,由于建堆的时间复杂度为O(mlogm)整体时间复杂度为O(mlogm)
暴力做法
集合S为已经确定最短路径的点集。
1. 初始化距离
一号结点的距离为零,其他结点的距离设为无穷大(看具体的题)。
2. 循环n次,每一次将集合S之外距离最短X的点加入到S中去(这里的距离最短指的是距离1号点最近。
点X的路径一定最短,基于贪心,严格证明待看)。然后用点X更新X邻接点的距离。
时间复杂度分析
寻找路径最短的点:O(n^2)
加入集合S:O(n)
更新距离:O(m)
所以总的时间复杂度为O(n^2)
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=510;
int g[N][N]; //为稠密阵所以用邻接矩阵存储
int dist[N]; //用于记录每一个点距离第一个点的距离
bool st[N]; //用于记录该点的最短距离是否已经确定
int n,m;
int Dijkstra()
{
memset(dist, 0x3f,sizeof dist); //初始化距离 0x3f代表无限大
dist[1]=0; //第一个点到自身的距离为0
for(int i=0;i<n;i++) //有n个点所以要进行n次 迭代
{
int t=-1; //t存储当前访问的点
for(int j=1;j<=n;j++) //这里的j代表的是从1号点开始
if(!st[j]&&(t==-1||dist[t]>dist[j]))
t=j;
st[t]=true;
for(int j=1;j<=n;j++) //依次更新每个点所到相邻的点路径值
dist[j]=min(dist[j],dist[t]+g[t][j]);
}
if(dist[n]==0x3f3f3f3f) return -1; //如果第n个点路径为无穷大即不存在最低路径
return dist[n];
}
int main()
{
cin>>n>>m;
memset(g,0x3f,sizeof g); //初始化图 因为是求最短路径
//所以每个点初始为无限大
while(m--)
{
int x,y,z;
cin>>x>>y>>z;
g[x][y]=min(g[x][y],z); //如果发生重边的情况则保留最短的一条边
}
cout<<Dijkstra()<<endl;
return 0;
}
堆优化版本的dijkstra
思路
堆优化版的dijkstra是对朴素版dijkstra进行了优化,在朴素版dijkstra中时间复杂度最高的寻找距离最短的点O(n^2)可以使用最小堆优化。
1. 一号点的距离初始化为零,其他点初始化成无穷大。
2. 将一号点放入堆中。
3. 不断循环,直到堆空。每一次循环中执行的操作为:
弹出堆顶(与朴素版diijkstra找到S外距离最短的点相同,并标记该点的最短路径已经确定)。
用该点更新临界点的距离,若更新成功就加入到堆中。
时间复杂度分析
寻找路径最短的点:O(n)
加入集合S:O(n)
更新距离:O(mlogn)
具体问题
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 100010; // 把N改为150010就能ac
// 稀疏图用邻接表来存
int h[N], e[N], ne[N], idx;
int w[N]; // 用来存权重
int dist[N];
bool st[N]; // 如果为true说明这个点的最短路径已经确定
int n, m;
void add(int x, int y, int c)
{
w[idx] = c; // 有重边也不要紧,假设1->2有权重为2和3的边,再遍历到点1的时候2号点的距离会更新两次放入堆中
e[idx] = y; // 这样堆中会有很多冗余的点,但是在弹出的时候还是会弹出最小值2+x(x为之前确定的最短路径),并
ne[idx] = h[x]; // 标记st为true,所以下一次弹出3+x会continue不会向下执行。
h[x] = idx++;
}
int dijkstra()
{
memset(dist, 0x3f, sizeof(dist));
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap; // 定义一个小根堆
// 这里heap中为什么要存pair呢,首先小根堆是根据距离来排的,所以有一个变量要是距离,其次在从堆中拿出来的时
// 候要知道知道这个点是哪个点,不然怎么更新邻接点呢?所以第二个变量要存点。
heap.push({ 0, 1 }); // 这个顺序不能倒,pair排序时是先根据first,再根据second,这里显然要根据距离排序
while(heap.size())
{
PII k = heap.top(); // 取不在集合S中距离最短的点
heap.pop();
int ver = k.second, distance = k.first;
if(st[ver]) continue;
st[ver] = true;
for(int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i]; // i只是个下标,e中在存的是i这个下标对应的点。
if(dist[j] > distance + w[i])
{
dist[j] = distance + w[i];
heap.push({ dist[j], j });
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
else return dist[n];
}
int main()
{
memset(h, -1, sizeof(h));
scanf("%d%d", &n, &m);
while (m--)
{
int x, y, c;
scanf("%d%d%d", &x, &y, &c);
add(x, y, c);
}
cout << dijkstra() << endl;
return 0;
}
spfa算法
spfa和dijkstra的区别:
st用来检验队列中是否有重复的点
spfa从队列中使用了当前的点,会把该点pop掉,状态数组st[i] = false(说明堆中不存在了) ,更新临边之后,把临边放入队列中, 并且设置状态数组为true,表示放入队列中 。如果当前的点距离变小,可能会再次进入队列,因此可以检验负环:
每次更新可以记录一次,如果记录的次数 > n,代表存在负环(环一定是负的,因为只有负环才会不断循环下去)。
st是一个集合,不是检验队列中的点。
dijkstra使用当前点更新临边之后,把该点加入到一个集合中,使用该点更新临边,并把临边节点和距离起点的距离置入堆中(不设置状态数组)。下一次从堆中取最小值,并把对应的节点放入集合中,继续更新临边节点,直到所有的点都存入集合中。因此dijkstra不判断负环。
从上述描述中能看出,dijkstra存放节点的堆,具有单调性,而spfa的队列不需要具有单调性。
#include<bits/stdc++.h>
using namespace std;
#define me(a,b) memset(a,b,sizeof a)
int n,m;
const int N=2e5+10;
int e[N],h[N],ne[N],idx,st[N],dist[N],w[N];
queue<int >q;
void add(int a,int b,int c){
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int spfa()
{
me(dist,0x3f);
dist[1]=0;
q.push(1);
st[1]=1;
while(q.size())
{
int t=q.front ();
q.pop();
st[t]=false;
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
if(dist[j]>dist[t]+w[i])
{
dist[j]=dist[t]+w[i];
if(!st[j])
{
q.push(j);
st[j]=true;
}
}
}
}
if(dist[n]>0x3f3f3f3f/2) return 0x3f3f3f3f;
return dist[n];
}
signed main(){
cin>>n>>m;
me(h,-1);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
int t=spfa ();
if(t==0x3f3f3f3f) cout<<"impossible"<<endl;
else cout<<t<<endl;
return 0;
}
多源最短路:
Floyd求最短路:
就是暴力出奇迹,有dp的思想在其中,不多废话,直接上代码
#include<bits/stdc++.h>
using namespace std;
const int N=220;
int n,m,k;
int d[N][N];
void floyd(){
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}
}
}
}
signed main(){
cin>>n>>m>>k;
memset(d,0x3f,sizeof d);
for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) if(i==j) d[i][j]=0;
while(m--){
int a,b,c;
cin>>a>>b>>c;
d[a][b]=min(d[a][b],c);
}
floyd();
while(k--){
int a,b;
cin>>a>>b;
if(d[a][b]>=0x3f3f3f3f/2) cout<<"impossible"<<endl;
else cout<<d[a][b]<<endl;
}
return 0;
}
最小生成树与kruskrzal算法
判断边是否应该加入到集合中
这是当前的全部集合,此时总集合边的数量为4
此时我们可以看到2-5这条边的值最小,因为2所在的集合与5所在的集合不同,所以可以连接,边数加1
这是又枚举到了6-8这一条边,此时总集合边的数量为5,因为6和8属于同一个集合,加入6-8这条边之后,集合中会构成环,所以将6-8这条边舍弃
思路比较清晰,每次选取边权最小的两条边,如果它们没有构成环,就把它们加入到我们的集合里,最后判断集合里的边数是否为n-1,如果是因为每次都是最小的边权,最后求和,权值之和也一定最小,输出这个即可,否则,就输出impossible
其中判断是否成环可以用一个并查集来维护,如果某个点已经在这个集合中,再次加入就会构成环,我们会避免这种情况发生。
#include<bits/stdc++.h>
using namespace std;
int n,m;
const int N=2e5+10;
struct Edge{
int a,b,w;
}edges[N];
bool cmp(Edge a,Edge b){
return a.w<b.w;
}
int p[N];
int find(int x)
{
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int a,b,w;
cin>>a>>b>>w;
edges[i]={a,b,w};
}
sort(edges+1,edges+m+1,cmp);
int res=0,cnt=0;
for(int i=1;i<=n;i++) p[i]=i;
for(int i=1;i<=m;i++)
{
int a=edges[i].a,b=edges[i].b,w=edges[i].w;
int pa=find(a),pb=find(b);
if(pa!=pb)
{
res+=w;
p[pa]=pb;
cnt++;
}
}
if(cnt<n-1) cout<<"impossible"<<endl;
else cout<<res<<endl;
return 0;
}
更新至2022年4月2日