树--图论

目录

1.图的解释与分类。

2.树和图的建立。

如何构建有向图/无向图:

3.图的遍历与访问。

树与图的深度优先遍历

例题:树的重心

DFS(深度优先遍历)

树和图的广度优先遍历:

例题:图中点的层次:

判断1号节点能否走到n号节点,广度优先遍历即可。

图的存储:邻接表

有向图的拓扑排序:

848. 有向图的拓扑序列

最短路问题:

源最短路:从特定起点出发到某一点的最短距离

spfa算法

多源最短路:从一点出发到零一点的最短距离

Floyd求最短路:

最小生成树与kruskrzal算法


1.图的解释与分类。

   现实中,我们遇到的图形形色色,他们有色彩,有各种稀奇的形象,构成了多彩的世界。

   然而在面临一些实际问题时,这些过于具体的色彩形象给我们的研究也带来了麻烦,16世纪左      右,数学家欧拉把每个事物看成点,把它们之间的关系简化成边,把这些有点和边组成的几何图     形称为图,包括树形图(树,图意)和点线图在在(图二),利用这些简化后的图解决了戈尼斯     堡七桥问题,引入了欧拉图,并开创了图论先河。

                                  图一

Graph Data Structure

                                     图二

树其实就是一种特殊的图

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

输入样例:

判断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 ++ 。

有向图的拓扑排序:

848. 有向图的拓扑序列

给定一个 n个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。

请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 -1。

若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。

输入格式

第一行包含两个整数 n 和 m。

接下来 m行,每行包含两个整数 x 和 y,表示存在一条从点 x 到点 y 的有向边 (x,y)。

输出格式

共一行,如果存在拓扑序列,则输出任意一个合法的拓扑序列即可。

否则输出 −1。


输出样例:

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日


  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

litian355

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值