代码随想录算法训练营Day55|图论part01

图论理论基础

!info\] 图论题目使用ACM模式练习 图论的题目在笔试面试中以ACM模式考察居多,且图的数据输入输出相对不好处理,需要掌握。

图的基本概念

图的种类

有向图 v.s 无向图 加权图

无向图中,一个点的度数等于连接它的边的数量 有向图中,出度是从节点出去的边数,入度是指向此节点的边数

连通性

在图中表示节点的连通情况

连通图

在无向图中,任意两个节点之间可到达

非连通图

在无向图中,存在两个节点不能到达

强连通图

在有向图中,任何两个节点之间可以相互到达

连通分量

在无向图中的极大连通子图,称为连通分量

强连通分量

在有向图中的极大强连通子图,成为该图的强连通分量

图的构造(用代码表示图)

朴素存储

将每条边所连接的两个点存储下来。图中有n条边,就申请n*2的数组,当然也可以用map等可以展现这种关系的类。总之这种存储的特点是,当我们想知道点和点的连接情况,需要枚举整个存储空间

邻接矩阵

邻接矩阵 使用二维数组表示图结构。邻接矩阵从节点的角度来表示图,有多少节点就申请多大的二维数组 例如: grid[2][5] = 6,表示 节点 2 连接 节点5 为有向图,节点2 指向 节点5,边的权值为6。

如果想表示无向图,即:grid[2][5] = 6grid[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(图, 选择的节点);
		回溯,撤销处理结果;
	}
}

深搜三部曲

  1. 确认递归函数和参数
  2. 确认终止条件
  3. 处理目前搜索节点出发的路径

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

处理完输入,我们用深搜三部曲来分析代码框架

  1. 确认递归函数,参数
    1. 全局变量paths用来存储所有可达路径
    2. 全局变量path用来存储当前路径
  2. 确认终止条件
    1. 当走到终点n时,找到了一条可达路径,把path加入paths(注意用深拷贝)
    2. 由于不存在自环,我们走不到path中的节点,不需要考虑这个终止
  3. 处理目前搜索节点出发的路径
    1. 走向下一个节点,把当前遍历到的节点加入路径
    2. 递归
    3. 撤销,把节点从路径中删除

解法

邻接矩阵解法

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,可以用二叉树的递归遍历和层序遍历来类比理解

相关推荐
猎猎长风3 分钟前
【数据结构和算法】5. 堆栈和队列
数据结构·算法
小五Z4 分钟前
Redis--主从复制
数据库·redis·分布式·后端·缓存
雷渊10 分钟前
项目中不用redis分布式锁,怎么防止用户重复提交?
后端
mzlogin42 分钟前
Java|小数据量场景的模糊搜索体验优化
java·后端
海码0071 小时前
【Hot100】 73. 矩阵置零
c++·线性代数·算法·矩阵·hot100
lozhyf1 小时前
基于springboot的商城
java·spring boot·后端
小爷毛毛_卓寿杰1 小时前
【Dify(v1.2) 核心源码深入解析】App 模块:Entities、Features 和 Task Pipeline
人工智能·后端·python
java奋斗者1 小时前
健身房管理系统(springboot+ssm+vue+mysql)含运行文档
spring boot·后端·mysql
hy____1231 小时前
类与对象(上)
开发语言·c++·算法
奋斗者1号2 小时前
逻辑回归:损失和正则化技术的深入研究
算法·机器学习·逻辑回归