基础算法[四]之图的那些事儿

Posted Huterox

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基础算法[四]之图的那些事儿相关的知识,希望对你有一定的参考价值。

文章目录

前言

okey,欢迎来到2023年,那么在这里预祝各位在新的一年里面能够达到自己的预期。那么本篇博文也是作为我最近复习算法带来的一篇博文,那么这篇博文主要是针对图的那些基本算法,当然还是偏向竞赛方面。 (当然有些C++ 和 Python相差不大的代码统一给出C++的代码,看得懂,就没有切换语言了)当然这篇博文稍微有点长,大概2W个字左右吧,以后“水文”就不写了,要写就写详细一点的,长一点的。没办法,人都这么长,这个博文也也得长一点。

那么本篇博文一共有9个算法,一个常用套路。当然对于比较抽象的点,还是会有题目进行举例的。

同时先声明一下,文中提到的n,m分别是指带图中节点的个数和边的个数。并且我们对于节点的编号都是1~n,这个统一一下。同时g是图的意思,因为graph嘛

图的表示

邻接矩阵

关于咱们这个图的话,我们有两种方案进行存储,一个就是非常传统的方案,就是这个使用邻接矩阵,这个的话非常简单,没什么好说的。然后存有相无相图,都是一样的。

邻接表

之后的是我们第二种方式进行存储,也就是使用到咱们的这个邻接表。当然使用这个邻接表的话也是有非常多的一种方案。
但是总体的意思呢就是这样的:

那么在这里的话,那么对于这个的话,我们也是有一个不错的,也在用的一个模板,只要维护三个数组就好了。当然你选择别的模板,如果是Python,Java,C++ 之类的,你直接用一个大数组,然后数组里面的每一个元素是这个表示边节点的一个数组也是可以的。按照图中的也可以,我们待会儿的模板是按照这个来的。

结构

okey,那么我们这边提供的模板,其实就是先前提到的用数组模拟链表,现在再用这个链表来串成一个图,仅此而已。那么好处的话,就是修改一些边方便,因为模拟的链表它具备,查询和修改的优势嘛。

int N = 100000;
int idx;
int e[N]; 
int w[N];
int ne[N]; 
int h[N] 初始化的值为-1

我们定义这几玩意来存储我们的一个图,那么接下来我来简单解释一下这几个数组的含义,这些数组数组的含义,以及这个idx的含义。

idx:表示的是我们对于这个图的节点进行一个编号,就是存储的时候,我们自己对这个存进去的节点进行一个简单的编号。
e[i]: 表示的是,我们自己定义的小标i,所对于图中那个节点的标号是啥。例如e[1]=5,表示我们这里面,定义的序号为1的玩意,对应的节点的编号为5.说白了,我们存进去的时候,是按照题目的输入存的顺序存储的,这个i就是idx。
ne[i]: 表示的是,当前我们定义的下标i,和他相连的下一个节点的下标(这个也是我们定义的)。假设,你想知道我们这边定义的下标为i的节点相连的那个节点在图中的序号,那么你就这样e[ne[i]]。
h[j]: 表示的是,在图中这个序号为j的节点,在我们这边定义的下标是多少,相当于存储头结点的数组。初始化的值为-1.
w[i]: 表示的就是我们这边编号为i的边的权重是多少,就是边权,然后的比较有意思的就是,假设i = h[j] ,i 是咱们图中j号节点在我们这边的下标,那么和他相连的第一个节点就是 k = e[ne[i]],然后这个w[i]的值,其实就是图中编号为j和k两个家伙之间的边的权重。原因的话看到下面咱们这个存储。

存储

明白了咱们上面的定义,那么咱们马上来看到如何去存储,加入这个边。
我们假设输入是这样的:

a b c
前面两个表示,图中的两个节点,c表示他们之间边权

这里使用的是头插法

void add(int a,int b,int c)

	e[idx] = b;
	w[idx] = c;
	ne[idx] = h[a];
	h[a] = idx++;

如果是无向图的话,那么在加入的时候就加入两次呗。

add(a,b,c);
add(b,a,c);

用这种结构的话,对于边的修改要快一点。当然一般可能也不会去动。

遍历

之后的话就是遍历,我们去通过我们刚刚的那个结构,去遍历出当前图中的每一个节点。如果要遍历的话我们就这样,当然这里有两个方法,一个是DFS,还有一个是BFS。

首先是DFS:
思路是一路走到底:
那么在这里还需要一个额外的数组这个是bool类型的

bool visited[N]

void dfs(int u)

	visited[u]=true;
	printf("%d",u);
	for(int i=h[u];i!=-1;i=ne[i])
	
		int j = e[i];
		if(!visited[j])
		
			//遍历当前和这个节点相连的节点相连的节点
			dfs(j);
		
	


当然咱们还有这个BFS,那么对于这个BFS的话我们还需要一个队列,当然思路其实也是一样的。

bool visited[N]
queue<int> q;
void bfs(int u)

	q.push(u);
	visited[u]=true;
	while(q.size())
	
		int t = q.front();
		printf("%d",t);
		for(int i=h[t];i!=-1;i=ne[i])
		//遍历和当前这个节点直接相连接的点
		
			int j = e[i];
			if(!visited[j])
			
				visited[j] = true;
				q.push(j);
			
		
	


之后就是调用,因为我们这边编号是从1开始的,因此的话,调用就是:

bfs(1);
//dfs(1);

同样的如果你是想要确定图中a,b序号的点是不是联通的,那么对应dfs的话这样:

bool find_dfs(int a, int b)

	visited[a]=true;
	for(int i=h[a];i!=-1;i=ne[i])
	
		int j = e[i]
		if(j==b) return true
		if(!visited[j]) return find(j,b)
	
	return false;

那么Python的话由于不太支持这种写法,那么就这样写:

def find_dfs(a,b):
    visited[a]=True
    i = h[a]
    while(i!=-1):
        j = e[i]
        if (j == b):
            return True
        if(not visited[j]):
            return find_dfs(j,b)
        i = ne[i]
    return False

这样的话就可以了,同样的,我们的这个用BFS的话,也简单

bool visited[N]
queue<int> q;
void find_bfs(int a,int b)

	q.push(a);
	visited[a]=true;
	while(q.size())
	
		int t = q.front();
		if(t==b) return true;
		for(int i=h[t];i!=-1;i=ne[i])
		//遍历和当前这个节点直接相连接的点
		
			int j = e[i];
			if(!visited[j])
			
				visited[j] = true;
				q.push(j);
			
		
	
	return false;

python的话也就是那个循环再改一下就好了。然后的话,这里我们python如果使用队列的话请使用这个:

import collections
q = collections.deque()

如果使用list的话,那么时间复杂度是O(n)。当然使用list然后搞个双指针去模拟队列的话那个也是O(1)。用法的话和list其实差不多,list主要是pop和insert这个操作的时间复杂度太高了。不想用这个的话,建议模拟队列。

路径搜索

okey,这里我们先进入第二个模块,就是咱们关于图的路径搜索。在这里我们将实现基本的路径搜索算法,以及对这些算法的一些优化,在什么时候使用这些算法都进行说明。同时对这些代码的模板进行一个说明,当然这些模板是需要记忆背诵的~这不仅仅针对算法竞赛,针对面试也是有帮助的。

那么我们先由简到难吧,我们先来两个比较简单的算法,也就是直接使用到咱们的领接矩阵去做的一个算法。也就是咱们的Floyd算法,还有朴素版本的DijKstra算法,分别针对多源最短路径和,单源最短路径。只得一提的是,咱们关于路径搜索的算法有5个算法,其中4个是单源最短路径算法,只有一个是多源的。所以可以发现在使用我们先介绍的Floyd算法和朴素版本的DijKstra的情况是比较“无助”的情况,也就是比较极端的情况。必须解决多源最短路径的时候只能上Floyd,当题目为稠密图,并且节点个数n的平方,和边的个数m相似的时候,最好的办法就只有朴素DijKstra算法了,其他的算法SPFA,Bellman-Ford,以及优化后的Dijkstra,算法的时间复杂度分别是O(m)(最坏O(m*n),O(m*n),O(mlogn)。但是同时他们最简单,那么我们开始吧。

多源最短路问题

OK,我们来到第一个版块,就是多源最短路的问题。

问题描述

那么首先的话我们需要知道什么是多源的最短路径,这个说白了就是给定一个图,然后这个图上有很多的节点,我们需要知道图上面任意两个点之间的一个最短距离。那么这个就是多源的一个最短路径问题。

Floyd实现模板

okey,我们在这里再看一下如何实现这个东西,原理的话简单描述一下其实就是这样的(这个部分的原理其实类似的)
假设一个图,有A,B,C,D四个节点,并且节点之间是连同的。这个时候想要知道A->B 的距离,那么我们会看一下如果中间经过C点的距离会不短一点,也就是说求得 distance[A][B] 和 distance[A][C]+distance[C][B] 之间有没有更短,之后假设有更短,更新一下,这个时候distance[A][B] 表示的就是经过了中转站C的时候A到B的距离,之后,我还不满足,于是我再查看一下在刚刚的基础上,我再经过中转D会不会更短一点,如果有在更新,如果更新了,那么distance[A][B] 表示的就是从A到B经过C,D中转之后的距离,同时也是最短的。

原理的话其实就是这样,非常的朴实无华的一个思想,因此实现也是非常的朴实无华,时间复杂度是N三次幂。

使用邻接矩阵的写法:
当然不能有负回路,可以有负的权边哈。

for(int k=1;k<=n;k++)

	for(int i=1;i<=n;i++)
	
		for(int j=1;j<=n;j++)
		
			g[i][j] = min(g[i][k],g[k][j]);	
		
	

那么在这里比较难受就是,在存储距离的时候的话,我们还是要使用到这个二维表,原来存的是边,而且存的是相互相邻的边,所以没办法存多源的路径的时候还是要用到二维表,所以这个算法的话,没必要那啥。

单源最短路径问题

okey,我们接下来还有在这方面的三个算法,一个算法的优化,大概算是4个算法吧。那么这些算法解决的都是咱们的单源最短路的问题,也就是说以某一个节点出发,这个节点A到其他节点的最短路径的距离。虽然这边有大概4个算法,但是实际上需要掌握常用的算法其实就三个,两个很无奈的情况下的算法和一个比较通吃的算法。

Dijkstra算法

okey,我们先来简单朴素一点的算法。就是这个算法,算法原理其实是和咱们提到的多源最短路径的原理是类似的,只是说现在更新的只有一个点A。那么同样的我们假设我们使用的是邻接矩阵进行存储的一个东西,此时我们定义这几个东西:

int N = 10000;
bool st[N];
int dist[N];
int g[N][N];

dist 这个玩存储的意思是,1号点到其他点的最短距离。如果你要算的是2号点,那么你就初始化的时候先初始化2号点。
st[i] 表示1号点到i号点的最短距离已经确定了

朴素版本

那么这个的话,其实就是这个算法的核心,那么接下来就是编写代码了。

// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra(int u)

    memset(dist, 0x3f, sizeof dist);
    //如果求2,那就dist[2]=0
    dist[1]=0;
    for(int i=0;i<n-1;i++)
    
		int t = -1;
		//这一步是在dist数组当中找到没有确定最小距离的点当中,距离最小的节点。
		for(int j=1;j<=n;j++)
		
			if(!st[j]&&(t==-1||dist[t]>dist[j])
			
				t = j;
			
		
		//用t来对距离进行更新
		for(int j=1;j<=n;j++)
		
			dist[j] = min(dist[j], dist[t] + g[t][j]);
		
		st[t] = true;
	
	if(dist[u] == 0x3f3f3f3f) return -1;
	return dist[u];
    

python的写法的话和这个大差不差,难受的就是python不支持这种循环,需要使用到while循环进行代替,但是看模板的话建议还是这个模板好看。

堆优化

之后的话,我们来到一个优化版本。这个优化的话其实非常简单,就是把那个在dist当中找没有被用过的最小的那个节点的时候进行一个优化,就是用小顶堆,把这个给优化了一下。我们直接看到代码。

typedef pair<int, int> PII;
int dist[N];     
bool st[N];     
int g[N][N];
// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()

    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    //0表示距离,1表示当前的节点,如果要2号节点那么就0,2
    heap.push(0, 1);  

    while (heap.size())
    
        auto t = heap.top();
        heap.pop();

        int ver = t.second, distance = t.first;

        if (st[ver]) continue;
        st[ver] = true;

        for (int j=1;j<=n;j++)
        
            if (dist[j] > distance + g[ver][j])
            
                dist[j] = distance + g[ver][j];
                heap.push(dist[j], j);
            
        
    

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];


邻接表

那么之后的话,咱们肯定还是要使用到咱们的邻接表去做一些存储,那么代码的话,其实改动不大,就是遍历的结构换一下。这个是我们堆优化版本的,那么朴素版本的修改也是一样的,只需要吧遍历节点的代码改成使用咱们给的这个数据结构的边即可。

typedef pair<int, int> PII;
int n;    
int h[N], w[N], e[N], ne[N], idx;      
int dist[N];     
bool st[N];     
// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()

    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    //0表示距离,1表示当前的节点,如果要2号节点那么就0,2
    heap.push(0, 1);  

    while (heap.size())
    
        auto t = heap.top();
        heap.pop();

        int ver = t.second, distance = t.first;

        if (st[ver]) continue;
        st[ver] = true;

        for (int i=h[ver];i!=-1;i++)
        
        	int j = e[i];
            if (dist[j] > distance + w[i])
            
                dist[j] = distance + w[i];
                heap.push(dist[j], j);
            
        
    

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];

那么关于这个时间复杂度的话,如果选择使用邻接表来的话,那么复杂度大概是: O(mlogn)。但是还是那个矩阵的话,那么那就是还是提升不大。所以如果要用堆优化的话,那么建议使用邻接表法,但是和接下来要提到算法来说,这个就low了,所以这个的话,记住朴素版本的Dijikstra算法就好了,这个你看着办,为什么的话,我们介绍完这个路径搜索的部分后,有个小结,我们到时候来简单分析一下。

python实现

那么之后的话是关于python的一个实现,首先朴素的实现我就不给了,因为这个python改一下循环就好了。那么就是优化之后的版本的话使用到了这个堆,那么这里的话我们也是使用实现好的,python标准库实现好了的headp来实现即可。

import heapq
def Djikstra_q(n:int):

    dist[1] = 0
    #定义一个用来存储的list
    heap = []
    #默认就是小顶堆
    heapq.heappush(heap,(0,1))
    while(len(heap)):
        t = heapq.heappop(heap)
        distance,ver = t[0],t[1]
        if(st[ver]):
            continue
        st[ver] = True
        i = h[ver]
        while(i!=-1):
            j = e[i]
            if(dist[j]>dist[ver]+w[i]):
                dist[j] = dist[ver]+w[i]
                if(not st[j]):
                      heapq.heappush(heap,(dist[j],j))
            i = ne[i]

    if (dist[n] == float("inf")):
        return -1
    return dist[n]

Bellman-Ford 算法

接下来就是这个,刚刚我们提到的Dijkstra算法呢,针对的是正权边,但是在针对负的权边的时候,就用不了了,那么这个是时候的话,这个算法就出来了,而且这个算法的实现其实非常简单,同时它的时间复杂度在O(mn),并且它是有特殊含义的,在针对带有约束问题的时候,这个算法可能是唯一的解。

它的原理话非常直接,就是直接遍历全部的边,然后找到最短的边就好了。

实现

int n, m;     
int dist[N];    

struct Edge    

    int a, b, w;
edges[M];

// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()

    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;


    for (int i = 0; i < n; i ++ )
    
        for (int j = 0; j < m; j ++以上是关于基础算法[四]之图的那些事儿的主要内容,如果未能解决你的问题,请参考以下文章

算法动态规划之图的最短路径(C++源码)

数据结构与算法基础之图的应用-最短路径

有容云:容器网络那些事儿

数据结构算法之图的存储与遍历(Java)

JavaScript--数据结构与算法之图

机器学习|数学基础Mathematics for Machine Learning系列之图论:图的矩阵表示