本篇博客记录AcWing算法基础课中的一些有关图论的算法,从熟知的dfs和bfs算法,再到求解单源路径,还有多源路径,最小生成树,以及二分图。
文章目录
- [一. DFS](#一. DFS)
- [1. N 皇后问题](#1. N 皇后问题)
- 二,BFS
-
- [1. 走迷宫](#1. 走迷宫)
- [三. 树和图的遍历](#三. 树和图的遍历)
-
- [1. 树的重心](#1. 树的重心)
- [2. 图中点的层次](#2. 图中点的层次)
- [四. 拓扑排序](#四. 拓扑排序)
- [五. 最短路径](#五. 最短路径)
-
- [1. 朴素的Dijkstra算法](#1. 朴素的Dijkstra算法)
- [2. 堆优化的Dijkstra算法](#2. 堆优化的Dijkstra算法)
- [3. bellman-ford 算法](#3. bellman-ford 算法)
- [4. spfa算法](#4. spfa算法)
- [5. spfa判断负环](#5. spfa判断负环)
- [5. Floyd算法](#5. Floyd算法)
- [六. 最小生成树](#六. 最小生成树)
-
- 1.Prim算法
- [2. Kruskal算法](#2. Kruskal算法)
- [七. 二分图](#七. 二分图)
-
- [1. 二分图的判定(染色法)](#1. 二分图的判定(染色法))
- [2. 二分图的最大匹配 (匈牙利算法)](#2. 二分图的最大匹配 (匈牙利算法))
一. DFS
dfs就是深度优先搜索的意思,在之前接触最多就是对于树和图的遍历,与之相对应的还有广度优先搜索(BFS)。深度优先搜索的话就是一条路走到底,直到发现路行不通,不可以再继续往下走的话,然后再回去,看看下一条路是否可行直到走完。
1. N 皇后问题
这道题首先会给我一个 n 表示 n * n 的棋盘,然后对棋盘上放皇后,皇后放下的位置,
其对应的行,对应的列,以及主对角线,副对角线,都不能再放了。这是个非常经典dfs问题。
下图中是4 * 4 的一个矩阵,Q代表皇后,上面两张图是正确答案,而下面的则不正确。
- 首先我们可以发现就是说一行只能放一个皇后,这个是肯定的。
- 然后我们利用三个数组
col[N] -- 表示列, dg[N] 主对角线,udg[N] - - 副对角线
- 用这三个数组分别来看当前的列,主对角线,副对角线是否能放皇后。
- 如果x == n就说明找到结果了,打印出来就好了。
- 我们可以直接用 col[y] 来看当前这一列是否能放。
但是对角线该如何判断。
看这篇题解把,就对ok,传送门嗖~~~
c
#include <stdio.h>
#include <stdbool.h>
#define N 20
int n;
char board[N][N]; //棋盘
bool col[N], dg[N],udg[N]; //列,主对角线以及副对角线
void dfs(int x)
{
if(x == n)
{
for (int i = 0; i < n; i++)
printf("%s\n",board[i]);
printf("\n");
}
//
for (int y = 0; y < n; y++)
{
if(!col[y] && ! dg[x + y] && !udg[y - x + n])
{
col[y] = dg[x + y] = udg[y - x + n] = true;
board[x][y] = 'Q';
dfs(x + 1);
col[y] = dg[x + y] = udg[y - x + n] = false;
board[x][y] = '.';
}
}
}
int main()
{
scanf("%d",&n);
//init
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
board[i][j] = '.';
}
}
dfs(0); //按照行来
return 0;
}
二,BFS
与dfs齐名的bfs出现了,宽度优先搜索,常常用于求解最短路的问题,它不会像dfs那样子,一条路走到黑,它看的更广的感觉,同时需要运用到队列,而dfs则是需要用栈,我们之所以没有实现栈,是因为利用了递归,它每调用一次函数么其实就是一次调用栈的操作。
1. 走迷宫
如下图:从左上角出发移动到右下角的最短路径,最少移动多少次。
- 首先还是得创建出一个图来,同时还需要创建一个代表移动次数的数组
d[N][N]
,里面放着从(0,0)~ (x ,y)
两点之间的最少移动次数。 - 最开始将路径数组初始化为-1,表示没有路,将起点入队列,起点到起点的路径为0
- bfs的循环条件就是只要队列不为空,那么我就继续下去。
- 然后将队首的元素入队列,接下来将与其有关的顶点全部入队列,
- 入队列的条件是,自身不是墙
g[x][y] != 1
还有就是没有走过d[x][y] != -1
- 当队列为空,也就说明所有的点都遍历完了,能过去话就过去了,过不去就拉到。。。。。
c
#include <stdio.h>
#include <string.h>
#define N 110
int n,m;
int g[N][N],d[N][N]; //g是整个图,d存放的是从左上角到各个顶点间的最短距离。
int que[N * N][2]; //que[x][y] 队列。
int front, rear;
int bfs()
{
//路径长度为-1
memset(d,-1,sizeof(d));
//起点入队列
que[rear][0] = 0;
que[rear++][1] = 0;
d[0][0] = 0;
int coordX[4] = {-1,0,1,0},coordY[4] = {0,1,0,-1};
while(front != rear)
{
int x = que[front][0],y = que[front++][1];
for (int i = 0; i < 4; i++)
{
int dx = x + coordX[i], dy = y + coordY[i];
if(dx >= 0 && dx < n && dy >= 0 && dy < m && g[dx][dy] == 0 && d[dx][dy] == -1)
{
d[dx][dy] = d[x][y] + 1;
que[rear][0] = dx;
que[rear++][1] = dy;
}
}
}
return d[n - 1][m - 1];
}
int main()
{
scanf("%d%d",&n,&m);
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
scanf("%d",&g[i][j]);
}
}
printf("%d\n",bfs());
return 0;
}
三. 树和图的遍历
树的遍历无非就是前中后序这三种是dfs来实现,层序遍历则是bfs来遍历,图的话bfs和dfs也是都可以进行遍历的,如果对于这几种遍历不是很熟悉的话,我以前的文章有树和图的全部遍历方式,这里就不细说了。
树的遍历还有图的遍历.
下面则是分别对应的两道题目
1. 树的重心
这道题目首先要搞懂树的重心是什么,看下图,分别将节点1和 节点 4 删掉之后,
可以发显示其变成了图,然后我们返回每个图中节点数量的最大值,然后再去另一幅图的最大值,去比较,然后去它俩的最小值。最后返回那个最小的值就好了。
有点绕,题目链接
当然以下例子只展示了两种,真正算法中应该尝试全部删除,然后取值更新。
- 看完上述图中,其实其核心思想就是找出树中每个根节点,已经想对应的每棵子树的数量,然后拿数量对其进行比较。
- 要求树中每棵子树的节点数量的话是可以求出来的,但要如何比较?
- 我们不妨设置一些变量出来.
sum - 当前根节点所有子树的总和,size - 当前根节点中每颗子树的最大值,
s 每一棵子树的节点数量。
所以这个s 是非常重要的,size = Max(size,s) 即可求出当前根节点中最大子树大小
sum += s, 即可求出当前根节点中所有子树的数量。
- 当这个此节点走完之后,即可得到一个size,利用size 即可算出当前图中的 size
size = Max(size, n - 1 - sum)
- 最后于ans取最小值即可
ans = Min(ans,size)
代码实现如下:
c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>
#define N 100010
int n;
int h[N],data[N * 2],next[N * 2],idx;
bool visit[N];
int ans = N;
//添边
void Add(int x, int y)
{
// x 到 y
data[idx] = y, next[idx] = h[x], h[x] = idx++;
}
int dfs(int u)
{
visit[u] = true;
int sum = 0, size = 0;
for (int i = h[u]; i != -1; i = next[i])
{
int j = data[i];
if(!visit[j])
{
int s = dfs(j); //s 是每棵子树的大小
size = fmax(s,size); //size 是以 u 为根节点,最大的子树。
sum += s; //sum 是以 u 为根节点, 下面所有子树的节点数量。
}
}
size = fmax(size,n - sum - 1); // 所有节点个个数 减去u节点以下所有的节点 (包括 u 节点 所以还得减1),
ans = fmin(size,ans); //更新答案,
return sum + 1;
}
int main()
{
scanf("%d",&n);
memset(h,-1,sizeof(h));
for (int i = 0; i < n - 1; i++) //n个节点 n - 1 条无向边。
{
int a,b;
scanf("%d%d",&a,&b);
Add(a,b),Add(b,a);
}
dfs(1);
printf("%d\n",ans);
return 0;
}
2. 图中点的层次
这道题是通过bfs来求出图中从 1 ~ n的最短路径,要是到不了返回 -1,和之前的走迷宫是一样的,只是将迷宫中的控制上下左右移动的数组,变成了图中邻接表的形式。
c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define N 100010
int n,m;
int h[N],data[N],next[N],idx; //图
int path[N]; //路径长度
int que[N]; //队列
int front,rear;
//添加边
void Add(int x, int y)
{
data[idx] = y, next[idx] = h[x], h[x] = idx++;
}
int bfs()
{
memset(path,-1,sizeof(path));
//先将第一个入队列
que[rear++] = 1;
path[1] = 0;
while(front != rear)
{
int t = que[front++]; //出队列
for (int i = h[t]; i != -1; i = next[i])
{
int j = data[i];
if(path[j] == - 1)
{
path[j] = path[t] + 1;
que[rear++] = j;
}
}
}
return path[n];
}
int main()
{
scanf("%d%d",&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);
}
printf("%d\n",bfs());
return 0;
}
四. 拓扑排序
如果你也系统的学习过一遍数据结构,那么这个知识点其实也不用太详细说了,我之前的文章里也有,就不太细的说了,拓扑排序链接.
- 这里因为数组模拟的邻接表,所以要记录一个顶点的入度需要再开辟一个数组,记录其入度 in_d[N];
- 入度操作在刚开始构建图的时候就可以完成。
- 然后在bfs算法中,先将入度为0的顶点全部入队列。
- 然后遍历队列,对于队列中的顶点挨个出队列的同时,就相当于将此顶点从图中删除,然后将其对应边的入度也随之删除,如果其删除后入度为0的话,那么就将其也入队列。
- 当整个队列为空的时候,图中所有顶点也就遍历完了,如果队列中元素的个数正好等于 n 的话,就说明全部顶点都入队列,其顺序也正好是拓扑排序的顺序。
c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#define N 100010
int n,m;
//图
int h[N],data[N],next[N],idx;
int in_d[N]; //该点的入度是多少
int que[N]; //队列
void Add(int a, int b)
{
data[idx] = b, next[idx] = h[a], h[a] = idx++;
}
bool TopSort()
{
int front = 0, rear = 0;
int i;
//将所有入度为0的点入队列
for (i = 1; i <= n; i++)
if(in_d[i] == 0)
que[rear++] = i;
while(front != rear)
{
int t = que[front++];
for (i = h[t]; i != -1; i = next[i])
{
int j = data[i];
if(--in_d[j] == 0)
que[rear++] = j;
}
}
return rear == n;
}
int main()
{
int i;
scanf("%d%d",&n,&m);
memset(h,-1,sizeof(h));
for (i = 0; i < m; i++)
{
int a, b;
scanf("%d%d",&a,&b);
Add(a,b);
in_d[b]++;
}
if(TopSort())
{
for (i = 0; i < n; i++)
printf("%d ",que[i]);
printf("\n");
}
else
printf("-1\n");
return 0;
}
五. 最短路径
最短路呢,y总给我们总结了一个模板出来,总共有5种方式,分别对应的不同的题型,感觉非常清晰哈。
1. 朴素的Dijkstra算法
这段算法主要运用于稠密图,就是对于边的数量远远大于点的数量时候,我们就需要用到整个算法。 原题链接
比如下面这道题:
-
首先呢对于图的初始化则是用邻接矩阵的方式,因为在改题目中是一个稠密图,稀疏图的话用邻接表。
-
然后读入每条边,因为图中肯能存在重边或者是自环,所以我们需要再读入边的时候就将其最短的那条边读进来。
-
接下来就是Dijkstra算法的具体步骤了。
-
约定dist数组中存放的是每个节点的最短路径,最开始初始化成一个很大的值。
-
然后 源点到其自身的路径是0,
dist[1] = 0
. -
然后遍历所有的顶点进行一次遍历。
-
在每次遍历时候找出当前路径数组中最短的那个顶点 t,也就是当前会最先到的那个
-
对 t 经过t 可以到达的顶点更新其的最短路径。
dist[j] = min(dist[j], dist[t] + g[t][j])
-
最后将t 标记 为 true 即可。
c
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <math.h>
#define N 510
#define M 100010
int n,m;
int g[N][N],dist[N];
bool st[N];
int Dijkstra()
{
memset(dist,0x3f,sizeof(dist));
//先将源点到自身的距离为0
dist[1] = 0;
//遍历所有的顶点
for (int i = 1; i <= n; i++)
{
int t = -1;
//找出当前路径数组中最短的那个顶点 t,也就是当前会最先到的那个
for (int j = 1; j <= n; j++)
{
if(!st[j] && (t == -1 || dist[j] < dist[t]))
{
t = j;
}
}
//将 t 中所有能到顶点进行更新。
for (int j = 1; j <= n; j++)
{
dist[j] = fmin(dist[j],dist[t] + g[t][j]);
}
//t标记访问过。
st[t] = true;
}
if(dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
scanf("%d%d",&n,&m);
memset(g,0x3f,sizeof(g));
while(m--)
{
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
g[a][b] = fmin(g[a][b],w);
}
printf("%d\n",Dijkstra());
return 0;
}
2. 堆优化的Dijkstra算法
我们从上一个朴素的算法中,可以发现,每次进入循环都需要遍历一遍全部顶点,然后取路径最小的那个顶点出来,遍历的时间是O(n),这就很容易想到一点,我们可以用一个小根堆来处理,每次只需要O(1)的时间就可以取出当前所有路径中的最小值是多少。
下面是改动后的代码图片,只是说在维护最短路径的数组变成了用堆来维护。
- 在Dijkstra算法中,首先还是将源点到自身的距离设置为0,将源点入堆。
- 在每次循环开始的时候,出堆,然后将于堆顶元素有关的边全部扫描一遍,如果当前的经过当前顶点的距离 小于 了它原本的距离,那么就更新dist数组中的值。
下面是代码的逻辑,堆的话如果你选的是C语言,是需要自己手写一个。。。
很**。
c
//dijkstra 算法
int Dijkstra()
{
memset(dist,0x3f,sizeof(dist));
dist[1] = 0; //源节点到自身的距离是 0.
// 将源顶点入队列
HeapPush(0,1);
while(size != 0)
{
int dis = heap[1][0], ver = heap[1][1];
HeapPop();
if(st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; i != -1; i = next[i])
{
int j = data[i];
if(dist[j] > dist[ver] + wight[i])
{
dist[j] = dist[ver] + wight[i];
HeapPush(dist[j],j);
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
scanf("%d%d",&n,&m);
//创建邻接表
memset(h,-1,sizeof(h));
while(m--)
{
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
Add(a,b,w);
}
printf("%d\n",Dijkstra());
return 0;
}
整体代码:
c
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#define N 1000010
int n,m;
int heap[N][2]; //堆 //0号位置存距离,1号位置存顶点
int size; //size 是堆中的元素个数。
int h[N],data[N],wight[N],next[N],idx; //邻接表
int dist[N]; //路径数组。
bool st[N]; //标记是否访问。
//邻接表的插入
void Add(int a, int b, int w)
{
data[idx] = b, wight[idx] = w, next[idx] = h[a], h[a] = idx++;;
}
//heap fun--------
//交换两行
void Swap(int x, int y)
{
int i;
for (i = 0; i < 2; i++)
{
int tmp = heap[x][i];
heap[x][i] = heap[y][i];
heap[y][i] = tmp;
}
}
//向下调整
void Down(int u)
{
int min = u, lc = 2 * u, rc = 2 * u + 1;
if(lc <= size && heap[lc][0] < heap[min][0])
min = lc;
if(rc <= size && heap[rc][0] < heap[min][0])
min = rc;
if(min != u)
{
Swap(min,u);
Down(min);
}
}
//上
void Up(int u)
{
while(u / 2 != 0 && heap[u / 2][0] > heap[u][0])
{
Swap(u,u/2);
u /= 2;
}
}
//将距离于顶点入堆
void HeapPush(int dis, int v)
{
heap[++size][0] = dis,heap[size][1] = v;
Up(size);
}
//出堆
void HeapPop()
{
Swap(1,size--);
Down(1);
}
// heap fun 上---------
//dijkstra 算法
int Dijkstra()
{
memset(dist,0x3f,sizeof(dist));
dist[1] = 0; //源节点到自身的距离是 0.
// 将源顶点入队列
HeapPush(0,1);
while(size != 0)
{
int dis = heap[1][0], ver = heap[1][1];
HeapPop();
if(st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; i != -1; i = next[i])
{
int j = data[i];
if(dist[j] > dist[ver] + wight[i])
{
dist[j] = dist[ver] + wight[i];
HeapPush(dist[j],j);
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
scanf("%d%d",&n,&m);
//创建邻接表
memset(h,-1,sizeof(h));
while(m--)
{
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
Add(a,b,w);
}
printf("%d\n",Dijkstra());
return 0;
}
3. bellman-ford 算法
Bellman-Ford 算法是一种用于解决单源最短路径问题的算法。它可以处理负权边,但不能处理负权环 。该算法通过对边进行松弛操作来逐步更新从源点到各个节点的最短路径估计值,直到得到最终的最短路径。
- 其算法的核心就是对所有的边进行k此循环。
- 然后每次循环时候更新dist数组的大小,a -> b 的权值 为 w
dist[b] = min(dist[b], dist[a] + w)
双for循环即可搞定。
c
#include <iostream>
#include <string.h>
#include <algorithm>
#define N 510
#define M 10010
using namespace std;
int m,n,k;
int backup[N],dist[N];
struct Edge
{
int a,b,w;
}edges[M]; //边集数组
void bellman_ford()
{
memset(dist,0x3f,sizeof(dist));
dist[1] = 0;
for (int i = 0; i < k; i++)
{
//上一层的路径数组。
memcpy(backup,dist,sizeof(dist));
for (int j = 0; j < m; j++)
{
auto e = edges[j];
dist[e.b] = min(dist[e.b],backup[e.a] + e.w);
}
}
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for (int i = 0; i < m; i++)
{
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
edges[i].a = a, edges[i].b = b,edges[i].w = w;
}
bellman_ford();
if(dist[n] > 0x3f3f3f3f / 2)
printf("impossible\n");
else
printf("%d\n",dist[n]);
return 0;
}
4. spfa算法
spfa算法是对于bellman-ford算法的优化,相比较上一个算法只是会盲目的松弛所有的顶点,暴力枚举每一种可能,而spfa算法而是维护一个备选的节点的队列,并且仅有节点被松弛后才会将其放入队列中。
- spfa算法虽然是bellman-ford算法的优化版,但是其实现上和Dijkstra算法还是很像的,要注意st数组不再是像Dijkstra算法中一样表示已经构成最短路径,在此算法中的st数组表示该节点是否在队列中,也就意味著一个节点可以松弛很多次。
- 先将源点入队列,然后在队列的循环中,不断的取出队首元素。
- 然后遍历可以通过队首可以到到达的节点,是否能更新长度,即变短长度,如果可以的话就进行更新长的。
- 如果可更新的节点在队列中则不用入队列,否则的话就入队列。
c
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#define N 100010
int n, m;
int h[N],e[N],w[N],ne[N],idx;
int dist[N];
bool st[N];
void Add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void spfa()
{
int que[N];
int front = 0, rear = 0;
memset(dist,0x3f,sizeof(dist));
dist[1] = 0;
que[rear++] = 1;
st[1] = true;
while(front != rear)
{
int t = que[front++];
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])
{
que[rear++] = j;
st[j] = true;
}
}
}
}
}
int main()
{
scanf("%d%d",&n,&m);
memset(h,-1,sizeof(h));
while(m--)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
Add(a,b,c);
}
spfa();
if(dist[n] == 0x3f3f3f3f)
printf("impossible\n");
else
printf("%d\n",dist[n]);
return 0;
}
5. spfa判断负环
- 利用spfa算法判断负环需要额外提供一个cnt数组,表示当前顶点的已经构造了多少条边了。
- 如果边数大于等于了n总共的顶点数,就说明路径中至少有两个点是一样的,那么就肯定出现了负权回路。
- 在下面的题目中没有问你说是否从1顶点出发有没有负权回路,所以初始化时侯将顶点全部入队列,dist数组不要初始化成很大的值,每一个顶点都可以是起点,所以全部为0就好了。
c
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdbool>
using namespace std;
const int N = 2010, M = 10010,QSIZE = 1e7;
int n,m;
int h[N],e[M],w[M],ne[M],idx;
int dist[N],cnt[N],que[QSIZE];
bool st[N];
void Add(int a,int b,int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool spfa()
{
int front = 0, rear = 0;
//全部入队列
for (int i = 1; i <= n; i++)
{
que[rear++] = i;
st[i] = true;
}
while(front != rear)
{
int t = que[front++];
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];
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n) return true;
if(!st[j])
{
que[rear++] = j;
st[j] = true;
}
}
}
}
//
return false;
}
int main()
{
scanf("%d%d",&n,&m);
memset(h,-1,sizeof(h));
while(m--)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
Add(a,b,c);
}
if(spfa())
printf("Yes\n");
else
printf("No\n");
return 0;
}
5. Floyd算法
Floyd算法是这些算法中唯一一个可以求出所有点到任意点之间的最短路径。
其实实现算法也很简单,三层for循环就可以搞定.而我的理解是根据下面这个公式:
AB + BC = AC
在数学中学习向量中有过这个向量加减的方法
而引用到下面的图中, 让 k 充当上面公式中的B,。
即d[i][j] = min (d[i][j],d[i][k] + d[k][j]) 因为 ik + kj = ij。
代码如下:
c
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 210,INF = 0x3f3f3f3f;
int n,m,Q;
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]);
//AB + BC = AC
//ik + kj = ij
}
int main()
{
scanf("%d%d%d",&n,&m,&Q);
//init
memset(d, INF, sizeof d);
for (int i = 1; i <= n; i++)
d[i][i] = 0;
//输入
for (int i = 0; i < m; i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
d[a][b] = min(d[a][b],c);
}
Floyd();
while(Q--)
{
int i,j;
scanf("%d%d",&i,&j);
if(d[i][j] > INF / 2)
printf("impossible\n");
else
printf("%d\n",d[i][j]);
}
return 0;
}
六. 最小生成树
首先最小生成树的概念呢就是在一个无向图中,将其所有的边进行较好的选择,其最后会成为一颗权值最小生成树,比如下图中红色的边构成的图新的图就是最小生成树。
1.Prim算法
这个算法根前面讲的Dijkstra算法中求最短路径其实是很相似的,都是每次从当前的路径数组中选出那个最小的,不过在prim算法中,dist数组存放的边,而并非路径。
prim算法主要运用求解稠密图中的最小生成树。
- 既然是稠密图,那么就可以用邻接矩阵来存储,其还是无向图,在题目中还有重边的情况下,存储方式如下:
g[i][j] = g[j][i] = min(g[i][j], w)
- 在Prim算法内部先初始化dist数组,使其全部为无穷大。
- 对于Prim算法和Dijkstra算法一样,都是循环n次即可。
- 然后对于第一个顶点需要进行特判一下,啥也不用干,只需要将其所能到达的顶点,将其有关的边录入dist数组即可,然后将自己标记为访问过的顶点。
- 从第一个顶点以后顶点,需要选出当前dist数组中哪条边的权值最小,也就确定了可以去哪一顶点,然后将这条边加入res。
- 同时扫面这个顶点加入后,通过这个顶点的边是否比其余顶点边要小,更新即可。
- 但要主要除了第一个顶点外,如果其在此之后选出的边是无穷大,证明此图不是一个连通图,就说明没有最小生成树。
c
#include <iostream>
#include <cstring>
#include <algorithm>
#include <stdbool.h>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int n,m;
int g[N][N],dist[N];
bool st[N];
int Prim()
{
memset(dist,INF,sizeof(dist));
int res = 0;
//循环n次
for (int i = 0; i < n; i++)
{
//先找出最短的边 t
int t = -1;
for (int j = 1; j <= n; j++)
if(!st[j] && (t == -1 || dist[j] < dist[t]))
t = j;
//判断是否能加入已经加入的集合
if(i != 0 && dist[t] == INF)
return INF;
//第一个顶点除外。
if(i != 0)
res += dist[t];
//用新加入集合点去更新到达别的点的长度
for (int j = 1; j <= n; j++)
dist[j] = min(dist[j],g[t][j]);
st[t] = true;
}
return res;
}
int main()
{
scanf("%d%d",&n,&m);
memset(g,INF,sizeof(g));
while(m--)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
g[a][b] = g[b][a] = min(g[a][b],c);
}
int res = Prim();
if(res == INF)
printf("impossible\n");
else
printf("%d\n",res);
return 0;
}
2. Kruskal算法
这个算法多用于稀疏图中,他是利用排序+并查集来做的,比Prim算代码的流程更加简洁。
- 首先将其所有边的关系一样,像之前的bellman_ford算法中一样,将其全部存入边集数组中去。
- 然后对边集数组进行排序,按照权值进行排序。
- 遍历边集数组的时候,去判断a和b是否已经在一个集合内了。
- 如果不在的话,将当前的权值加入res,并且将他俩合并到一个集合内,接着记录边的个数。
- 到了最后,判断以下边的个数是否是 n - 1条, 如果是n - 1条那么就有最小生成树,否则就没有。
c
#include <iostream>
#include <algorithm>
#include <cstdbool>
using namespace std;
const int N = 100010, M = 200010, INF = 0x3f3f3f3f;
int n,m;
int p[N]; //并查集
struct Edge
{
int a,b,w;
bool operator< (const Edge &W)
{
return w < W.w;
}
}edges[M];
int Find(int x)
{
if(p[x] != x)
p[x] = Find(p[x]);
return p[x];
}
int Kruskal()
{
int res = 0, cnt = 0; // res 表示最小生成树, cnt 表示边。
sort(edges,edges + m);
for (int i = 0; i < m; i++)
{
auto e = edges[i];
int ap = Find(e.a),bp = Find(e.b); //分别表示两条边的父亲节点。
//如果不在一个集合内,就加入集合。
if(ap != bp)
{
res += e.w;
p[ap] = bp;
cnt++;
}
}
if(cnt != n - 1)
return INF;
else
return res;
}
int main()
{
scanf("%d%d",&n,&m);
for (int i = 0; i < m; i++)
{
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
edges[i] = {a,b,w};
}
for (int i = 1; i <= n; i++)
p[i] = i;
int res = Kruskal();
if(res == INF)
printf("impossible\n");
else
printf("%d\n",res);
return 0;
}
七. 二分图
1. 二分图的判定(染色法)
对于一个二分图来说可以通过两种颜色对图进行染色,简单来说就是一条边上对应的两个顶点,的颜色不能是一致的,比如下图:
因此呢我们只需要模拟以下图的染色,即可直到此图是否是二分图了。
- 那么我们对所有的顶点进行遍历,只要它没有被染过色,我们就对其进行dfs染色过程。
- 进入dfs函数,首先第一步就是对其进行染色,我们拿1当红色,2当成蓝色。
- 然后依次去遍历它的边,从而得到顶点,只要经过改变的顶点没有被染过色,那么我们就对其进行染色,递归进去。
- 否则的话我们就去判断当前边对应的顶点是否于自身的颜色一致,如果一致的话就说明出现问题了,直接返回false即可。
- 我只需要发现一组false 就可以说明此图不是二分图了。
c
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010, M = 200010;
int n,m;
int h[N],e[M],ne[M],idx;
int color[N]; //记录顶点是否染色
void Add(int a,int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool dfs(int u, int c)
{
//对其进行染色
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if(!color[j])
{
//如果没有染过色,直接然就好了。
if(!dfs(j,3 - c))
return false;
}
else if (color[j] == c) //染过色的话就去看是否和现在相同。
return false;
}
return true;
}
int main()
{
scanf("%d%d",&n,&m);
memset(h,-1,sizeof(h));
while(m--)
{
int a,b;
scanf("%d%d",&a,&b);
Add(a,b),Add(b,a);
}
bool flag = true;
for (int i = 1; i <= n; i++)
{
if(!color[i])
{
if(!dfs(i,1))
{
flag = false;
break;
}
}
}
if(flag)
printf("Yes\n");
else
printf("No\n");
return 0;
2. 二分图的最大匹配 (匈牙利算法)
既然可以判断一个图是否是二分图了,接下来看看二分图的应用,我们可以通过匈牙利算法 计算出中一个二分图中的最大匹配次数。
原题链接
- 在这个算法中,我们只需要保存左边的图就好了,然后右半边开一个match数组,来判断右边的顶点于左边的那一个成功匹配。
- 那么我们就可以遍历左边的全部顶点,使左边的每一个顶点去和右边的顶点去匹配,所以每次的ds数组都需要初始化成false,因为match数组里存放着已经匹配成功的点,所以不用但新算法过程中会出现重复匹配的问题。
- 接下来就对于每左边顶点去寻找右边的顶点,如果发现此顶点没有被访问过,那么我就去访问它,其次判断此顶点是否已经于前面顶点进行配对,如果没有那么皆大欢喜,直接匹配我自己就好了,
- 如果已经有了,那么就去看看谁跟你匹配成功了,你能不能换一个匹配,把这个让给我,哈哈确实就是这样子的。
if(match [j] == 0 || Find(match[j]))
- 如果实在没有办法,就只能return false了。
看起来很麻烦,但是实际在代码中却使很巧妙的用递归来实现了一个让步的过程,这也许就是编程的魅力把,代码就那么一段段。
c
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 100010;
int n1, n2, m;
int h[N], e[M], ne[M], idx; //存储左边的邻接表
int match[N]; //看右边的是否已经配对
bool st[N]; //在对于左边顶点去寻找右边时候,是否访问过。
void Add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool Find(int u)
{
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if(!st[j]) //在此次询问中是否配对过该节点。
{
st[j] = true;
if(match[j] == 0 || Find(match[j])) //如果该位置能配对,或者说是与其配对完成的顶点是否能再换一个.
{
match[j] = u;
return true;
}
}
}
return false;
}
int main()
{
scanf("%d%d%d",&n1,&n2,&m);
memset(h,-1,sizeof(h));
while(m--)
{
int a,b;
scanf("%d%d",&a,&b);
Add(a,b);
}
int res = 0;
for (int i = 1; i <= n1; i++)
{
memset(st, false , sizeof(st));
if(Find(i))
res++;
}
printf("%d\n",res);
return 0;
}