图论理论基础
!info\] 图论题目使用ACM模式练习 图论的题目在笔试面试中以ACM模式考察居多,且图的数据输入输出相对不好处理,需要掌握。
图的基本概念
图的种类
有向图 v.s 无向图 加权图
度
无向图中,一个点的度数等于连接它的边的数量 有向图中,出度是从节点出去的边数,入度是指向此节点的边数
连通性
在图中表示节点的连通情况
连通图
在无向图中,任意两个节点之间可到达
非连通图
在无向图中,存在两个节点不能到达
强连通图
在有向图中,任何两个节点之间可以相互到达
连通分量
在无向图中的极大连通子图,称为连通分量
强连通分量
在有向图中的极大强连通子图,成为该图的强连通分量
图的构造(用代码表示图)
朴素存储
将每条边所连接的两个点存储下来。图中有n条边,就申请n*2
的数组,当然也可以用map等可以展现这种关系的类。总之这种存储的特点是,当我们想知道点和点的连接情况,需要枚举整个存储空间
邻接矩阵
邻接矩阵 使用二维数组表示图结构。邻接矩阵从节点的角度来表示图,有多少节点就申请多大的二维数组 例如: grid[2][5] = 6
,表示 节点 2 连接 节点5 为有向图,节点2 指向 节点5,边的权值为6。
如果想表示无向图,即:grid[2][5] = 6
,grid[5][2] = 6
,表示节点2 与 节点5 相互连通,权值为6。
优点
- 表达方式简单,易于理解
- 检查任意两个顶点之间是否存在边的操作非常快
- 适合稠密图,在边数接近顶点数平方的图中,邻接矩阵是一种空间效率较高的表示方法
缺点
- 遇到稀疏图,会导致申请过大的二维数组造成空间浪费,且遍历所有边需要遍历整个
n*n
矩阵,造成时间浪费
邻接表
邻接表 使用数组➕链表的方式表示图。邻接表从边的角度表示图,有几条边则会申请对应大小的链表。 数组的大小是图中点的个数,数组中的元素是链表的头节点。 一条链表除了头节点以外的元素,都是头节点所指向的点
优点
- 对于稀疏图的存储,只需要存储边,空间利用率高
- 遍历节点连接情况相对容易
缺点
- 检查任意两个节点间是否存在边,效率相对低,需要O(V)时间,V表示某节点连接其他节点的数量
- 实现相对复杂,不易理解
图的遍历方式
基本分为两大类:
- 深度优先搜索(dfs)
- 广度优先搜索(bfs)
深搜理论基础
DFS v.s BFS
- DFS的搜索简要概括为朝一个方向一直走,直到没有路了会开始回溯
- BFS会在同一层把本节点所连接的所有节点依次遍历,再走到下一个节点重复这个过程
DFS 搜索过程
- 搜索方向,是认准一个方向搜,直到回到原来走过的或者到达终点,再换方向
- 换方向就是撤销之前的路径,改为此节点连接的下一个路径,也是代码的回溯
代码框架
DFS在搜索过程中涉及到回溯,那么用递归实现就很方便。 含有回溯的递归的一般代码模版如下:
c
void dfs(param) {
处理节点;
dfs(, 选择的节点); // 递归
回溯,撤销处理结果
}
DFS的代码模版如下:
c
void dfs(参数) {
if (终止条件) {
存放结果;
return ;
}
for (选择 : 本节点所连接的其他节点) {
处理节点;
dfs(图, 选择的节点);
回溯,撤销处理结果;
}
}
深搜三部曲
- 确认递归函数和参数
- 确认终止条件
- 处理目前搜索节点出发的路径
kama 98 所有可达路径
题目链接:kamacoder.com/problempage... 文档讲解:www.programmercarl.com/kamacoder/0...
题目
【题目描述】
给定一个有 n 个节点的有向无环图,节点编号从 1 到 n。请编写一个程序,找出并返回所有从节点 1 到节点 n 的路径。每条路径应以节点编号的列表形式表示。
【输入描述】
第一行包含两个整数 N,M,表示图中拥有 N 个节点,M 条边
后续 M 行,每行包含两个整数 s 和 t,表示图中的 s 节点与 t 节点中有一条路径
【输出描述】
输出所有的可达路径,路径中所有节点的后面跟一个空格,每条路径独占一行,存在多条路径,路径输出的顺序可任意。 如果不存在任何一条路径,则输出 -1。 注意输出的序列中,最后一个节点后面没有空格! 例如正确的答案是 1 3 5
,而不是 1 3 5
, 5后面没有空格!
【数据范围】
- 图中不存在自环
- 图中不存在平行边
- 1 <= N <= 100
- 1 <= M <= 500
思路
这道题就是深度优先搜索的模版题型,所以重点是如何处理图的输入和存储,下面对邻接表和邻接矩阵的Java实现详述。
邻接矩阵
邻接矩阵从点的角度存储图,本题有N个节点,所以我们申请N*N的二维数组。不过为了让从1开始的节点标号和数组索引对齐,我们申请(N+1)*(N+1)的二维数组。
java
int[][] graph = new int[N+1][N+1];
处理本题输入,数组存储1表示s到t存在边
Java
Scanner scanner = new Scanner(System.in);
for (int i = 0; i < M; i++) {
int s = scanner.nextInt();
int t = scanner.nextInt();
graph[s][t] = 1;
}
邻接表
邻接表从边的角度存储图,本题有N个节点,所以申请一个N+1大小的链表数组
java
List<LinkedList<Integer>> graph = new ArrayList<>(N+1);
for (int i = 0; i <= N; i++) {
graph.add(new LinkedList<>());
}
处理本题输入
Java
for (int i = 0; i < M; i++) {
int s = scanner.nextInt();
int t = scanner.nextInt();
graph.get(s).add(t);
}
DFS
处理完输入,我们用深搜三部曲来分析代码框架
- 确认递归函数,参数
- 全局变量paths用来存储所有可达路径
- 全局变量path用来存储当前路径
- 确认终止条件
- 当走到终点n时,找到了一条可达路径,把path加入paths(注意用深拷贝)
- 由于不存在自环,我们走不到path中的节点,不需要考虑这个终止
- 处理目前搜索节点出发的路径
- 走向下一个节点,把当前遍历到的节点加入路径
- 递归
- 撤销,把节点从路径中删除
解法
邻接矩阵解法
Java
import java.util.*;
public class Main {
static List<List<Integer>> paths = new ArrayList<>();
static List<Integer> path;
static int n;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
int m = scanner.nextInt();
int[][] graph = new int[n + 1][n + 1];
for (int i = 0; i < m; i++) {
int s = scanner.nextInt();
int t = scanner.nextInt();
graph[s][t] = 1;
}
path = new ArrayList<>();
path.add(1);
dfs(graph, 1);
for (int i = 0; i < paths.size(); i++) {
System.out.print("1");
for (int j = 1; j < paths.get(i).size(); j++) {
System.out.print(" " + paths.get(i).get(j));
}
System.out.println();
}
if (paths.size() == 0) {
System.out.println(-1);
}
}
private static void dfs(int[][] graph, int node) {
if (node == n) {
paths.add(new ArrayList<>(path));
return;
}
for (int i = 1; i <= n; i++) {
if (graph[node][i] == 1) {
path.add(i);
dfs(graph, i);
path.remove(path.size() - 1);
}
}
}
}
邻接表解法
Java
import java.util.*;
public class Main {
static List<List<Integer>> paths = new ArrayList<>();
static List<Integer> path;
static int n;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
int m = scanner.nextInt();
List<LinkedList<Integer>> graph = new ArrayList<>(n+1);
for (int i = 0; i <= n; i++) {
graph.add(new LinkedList<>());
}
for (int i = 0; i < m; i++) {
int s = scanner.nextInt();
int t = scanner.nextInt();
graph.get(s).add(t);
}
path = new ArrayList<>();
path.add(1);
dfs(graph, 1);
for (int i = 0; i < paths.size(); i++) {
System.out.print("1");
for (int j = 1; j < paths.get(i).size(); j++) {
System.out.print(" " + paths.get(i).get(j));
}
System.out.println();
}
if (paths.size() == 0) {
System.out.println(-1);
}
}
private static void dfs(List<LinkedList<Integer>> graph, int node) {
if (node == n) {
paths.add(new ArrayList<>(path));
return;
}
for (int i : graph.get(node)) {
path.add(i);
dfs(graph, i);
path.remove(path.size() - 1);
}
}
}
广搜理论基础
广搜的使用场景
广搜适用于解决两个点之间的最短路径问题。因为广搜是一圈一圈向外搜索,所走到的每个节点到起点的最短距离都是他们所在的圈层数。
也有一些问题,BFS和DFS都可以解决,比如岛屿问题。这类问题的特征就是不涉及具体的遍历方法,只要能吧相邻且相同属性的节点标记上就可以。
广搜的搜索过程

代码框架
首先,我们需要一个容器去保存每一层中我们所遍历过的元素(队列/栈/数组) 如果用队列,那么每一层遍历的顺序是一样的。统一逆时针/顺时针 如果用栈,相邻层的遍历是相反的。如果第一层顺时针,第二层就是逆时针 而普通的BFS只要遍历到每个节点就行,用什么方向都可以 不过大家一般习惯是用队列。
下面是BFS代码模版,以上面的四方格地图为例:
Java
int dir[][] = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}}; // 表示四个方向
// gird是地图,visited标记访问过的点,x, y是起点坐标
void bfs(int[][] grid, boolean[][] visited, int x, int y) {
Queue<int[]> queue = new LinkedList<>();
queue.add(new int[]{x,y});
visited[x][y] = true;
while (!queue.isEmpty()) {
int[] cur = queue.poll();
for (int i = 0; i < 4; i++) {
int[] next = new int[]{cur[0]+dir[i][0], cur[1]+dir[i][1]};
if (next[0] < 0 || next[0] >= grid.length || next[1] < 0 || next[1] >= grid[0].length) {
continue;
}
if (!visited[next[0]][next[1]]) {
visited[next[0]][next[1]] = true;
queue.add(next);
}
}
}
}
今日收获总结
做完毕设终于可以捡起算法。正好是图论的开篇,图论也会是算法考察中很重要的一部份。 今天的重点是DFS和BFS,可以用二叉树的递归遍历和层序遍历来类比理解