手把手刷图算法
一、图论基础及遍历算法
基于labualdong的算法网站,图论基础及遍历算法
1、图的逻辑结构
- 图是由点和边组成的,基本的数据结构很像多叉树,所以图可以用DFS和BSF;
- 一般由邻接表和邻接矩阵表示:
- 邻接表是将每个节点的直接邻居放入到一个列表中;
- 邻接矩阵是一个二维数组,如果节点x和节点y是相连的,就把matrix[x][y]设为true,表示相连;
下述是基本的表示节点:
// 邻接表
// graph[x] 存储 x 的所有邻居节点
List<Integer>[] graph;
// 邻接矩阵
// matrix[x][y] 记录 x 是否有一条指向 y 的边
boolean[][] matrix;
有向图的边有方向,所以有向图中每个节点的度被细分为入度(indegree)和出度(outdegree);
关于有向图的加权如何实现,如下:
// 邻接表
// graph[x] 存储 x 的所有邻居节点以及对应的权重
List<int[]>[] graph;
// 邻接矩阵
// matrix[x][y] 记录 x 指向 y 的边的权重,0 表示不相邻
int[][] matrix;
无向图可以看成是双向图
2、图的遍历
图的遍历可参考多叉树:
/* 多叉树遍历框架 */
void traverse(TreeNode root) {
if (root == null) return;
// 前序位置
for (TreeNode child : root.children) {
traverse(child);
}
// 后序位置
}
但是图可能成环,如果图包含环,就需要一个数组进行辅助:
// 记录被遍历过的节点
boolean[] visited;
// 记录从起点到当前节点的路径
boolean[] onPath;
/* 图遍历框架 */
void traverse(Graph graph, int s) {
if (visited[s]) return;
// 经过节点 s,标记为已遍历
visited[s] = true;
// 做选择:标记节点 s 在路径上
onPath[s] = true;
for (int neighbor : graph.neighbors(s)) {
traverse(graph, neighbor);
}
// 撤销选择:节点 s 离开路径
onPath[s] = false;
}
这个是利用了回溯,遍历好此节点后,需要在路径上删除该节点撤销已经做过的选择。
3、题目实践
力扣第797题,所有可能的路径;
[797]所有可能的路径
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public List<List<Integer>> allPathsSourceTarget(int[][] graph) {
LinkedList<Integer> path = new LinkedList<>();
traverse(graph, 0, path);
return res;
}
// 定义结果
List<List<Integer>> res = new LinkedList<>();
/**
* @param graph:图
* @param i:来到图中的位置
* @param path:已经走过的路径
*/
void traverse(int[][] graph, int i, LinkedList<Integer> path) {
// 将此时的节点加入到路径中
path.add(i);
// 判断是否到达最后一个节点
if (i == graph.length - 1) {
res.add(new LinkedList<>(path));
// 撤销路径选择
path.removeLast();
return;
}
// 遍历此时节点的所有邻居
for (int j : graph[i]) {
traverse(graph, j, path);
}
// 撤销节点选择
path.removeLast();
}
}
//leetcode submit region end(Prohibit modification and deletion)
二、环检测及拓扑排序算法
基于labuladong的算法网站,环检测及拓扑排序算法
1、环检测算法(DFS 版本)
力扣第207题,课程表;
思路:
- 首先将数字看成节点,节点编号为【0,numCourses-1】,将课程依赖关系看成是节点间的有向边;
- 判断每个节点之间是否成环,如果成环就代表有依赖关系;
- 成环的话就代表不能完成所有课程的学习;
[207]课程表
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<Integer>[] graph = buildGraph(numCourses, prerequisites);
visited = new boolean[numCourses];
onPath = new boolean[numCourses];
// 对每个节点都需要判断环
for (int i = 0; i < numCourses; i++) {
traverse(graph, i);
}
return !hasCircle;
}
boolean[] visited;// 是否已经访问过该节点
boolean[] onPath;// 路径
boolean hasCircle = false;// 判断是否成环
/**
* 遍历,判断图是否成环
*
* @param graph:图
* @param index:此时来到的节点的位置
*/
void traverse(List<Integer>[] graph, int index) {
// 来到此时的节点先判断该节点是否在路径上
if (onPath[index]) {
hasCircle = true;
}
// 如果已经访问过该节点或者是成环,就直接返回,不用再遍历了
if (visited[index] || hasCircle) {
return;
}
// 设置该节点的属性
visited[index] = true;
onPath[index] = true;
// 去节点的下个位置
for (int next : graph[index]) {
traverse(graph, next);
}
// 将节点从路径中撤销选择
onPath[index] = false;
}
/**
* 将课程看成节点,先修课程为节点之间的有向边,生成图
*
* @param numCourses:多少门课程
* @param preRequisites:课程之间的依赖关系
* @return
*/
List<Integer>[] buildGraph(int numCourses, int[][] preRequisites) {
List<Integer>[] graph = new LinkedList[numCourses];
// graph[i] 代表要想学第i门课程需要先学的课程
for (int i = 0; i < numCourses; i++) {
graph[i] = new LinkedList<>();
}
// 遍历 preRequisites 数组,添加有向边
for (int[] matrix : preRequisites) {
int from = matrix[0];
int to = matrix[1];
// 添加有向边
graph[from].add(to);
}
return graph;
}
}
//leetcode submit region end(Prohibit modification and deletion)
2、拓扑排序算法(DFS 版本)
力扣第210题,课程表 II
思路:
- 先将课程当成节点,学习课程的依赖关系当成节点的有向边;
- 判断这个图是否成环;
- 将遍历的顺序加入到链表中,采用后序遍历;
[210]课程表 II
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public int[] findOrder(int numCourses, int[][] prerequisites) {
List<Integer>[] graph = buildGraph(numCourses, prerequisites);
// 判断是否成环
visited = new boolean[numCourses];
onPath = new boolean[numCourses];
// 需要对每个节点进行判断是否成环
for (int i = 0; i < numCourses; i++) {
traverse(graph, i);
}
// 如果成环就不可能学完所有课程
if (hasCircle) {
return new int[]{};
}
int[] res = new int[numCourses];
for (int i = 0; i < numCourses; i++) {
res[i] = list.get(i);
}
return res;
}
boolean[] visited;// 已经访问过的节点
boolean[] onPath;// 路径
boolean hasCircle = false;// 是否成环
List<Integer> list = new LinkedList();// 后序遍历链表
/**
* 遍历图
*
* @param graph:图
* @param index:此时到达图中的节点的位置
*/
void traverse(List<Integer>[] graph, int index) {
// 判断此时的节点是否在路径中
if (onPath[index]) {
hasCircle = true;
}
// 判断是否成环
if (hasCircle || visited[index]) {
return;
}
// 给节点更新数据
visited[index] = true;
onPath[index] = true;
// 递归遍历
for (int next : graph[index]) {
traverse(graph, next);
}
// 更新路径数据(后序遍历,即将离开节点时候)
onPath[index] = false;
list.add(index);// 后序遍历时候放入结果
}
/**
* 根据课程和课程依赖关系,构件图
*
* @param numCourses:课程数
* @param prerequisites:课程依赖关系
* @return
*/
List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
List<Integer>[] graph = new LinkedList[numCourses];
// 生成节点
for (int i = 0; i < numCourses; i++) {
graph[i] = new LinkedList<>();
}
// 生成依赖关系
for (int[] matrix : prerequisites) {
int from = matrix[0];
int to = matrix[1];
graph[from].add(to);
}
return graph;
}
}
//leetcode submit region end(Prohibit modification and deletion)
3、环检测算法(BFS 版本)
力扣第207题,课程表;
思路:
- 构建图,使用邻接表;
- 根据课程依赖关系,构建入度数组;
- 构建队列,将入度为0的节点,加入队列中;
- 依次弹出队列中的节点,根据邻接表,修改入度数组,重新将入度为0的节点加入到队列中,记录队列中被弹出节点的个数;
- 根据被弹出结点的个数,判断是否有环;
[207]课程表
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 1.构建图
List<Integer>[] graph = buildGraph(numCourses, prerequisites);
// 2.构建入度数组,根据课程学习的依赖关系,将每个课程节点的初始入度整合为一个数组
int[] inDegree = new int[numCourses];
for (int[] matrix : prerequisites) {
int from = matrix[0];
int to = matrix[1];
// 给被指向的节点 to 节点,入度的度数加1
inDegree[to]++;
}
// 3.遍历入度数组,将入度为0的节点加入到队列中
Queue<Integer> queue = new LinkedList();
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) {
queue.add(i);
}
}
// 4.弹出队列中的元素,记录被弹出的节点个数,并根据被弹出的节点的依赖关系,进行入度数组的修改,将入度为0的节点重新加入到队列中
int count = 0;
while (!queue.isEmpty()) {
int poll = queue.poll();
count++;
// 修改入度数组,根据图的节点的链表
for (int next : graph[poll]) {
inDegree[next]--;
// 如果入度为0,需要加入到队列中
if (inDegree[next] == 0) {
queue.add(next);
}
}
}
// 5.当被弹出的元素个数和课程数相等时,证明没有环,就可以完成所有课程的学习
return count == numCourses;
}
/**
* 根据课程和先修课程的关系,构建课程图(有向图)
* graph【i】:代表的是第i个节点中指向的节点链表
*
* @param numCourses:课程数
* @param prerequisites:先修课程依赖关系
* @return
*/
List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
List<Integer>[] graph = new LinkedList[numCourses];
// 创建链表数组
for (int i = 0; i < numCourses; i++) {
graph[i] = new LinkedList();
}
// 根据课程关系遍历数组,将依赖关系放入节点中
for (int[] matrix : prerequisites) {
int from = matrix[0];
int to = matrix[1];
graph[from].add(to);// from 指向 to,节点from中的链表加入to节点
}
return graph;
}
}
//leetcode submit region end(Prohibit modification and deletion)
4、拓扑排序算法(BFS 版本)
力扣第210题,课程表 II
[210]课程表 II
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public int[] findOrder(int numCourses, int[][] prerequisites) {
List<Integer>[] graph = buildGraph(numCourses, prerequisites);
int[] inDegree = new int[numCourses];
for (int[] matrix : prerequisites) {
int from = matrix[0];
int to = matrix[1];
inDegree[to]++;
}
Queue<Integer> queue = new LinkedList();
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) {
queue.add(i);
}
}
int count = 0;
int[] res = new int[numCourses];
while (!queue.isEmpty()) {
int poll = queue.poll();
count++;
res[numCourses - count] = poll;
for (int next : graph[poll]) {
inDegree[next]--;
if (inDegree[next] == 0) {
queue.add(next);
}
}
}
if (count == numCourses) {
return res;
}
return new int[]{};
}
/**
* 创建邻接表作为图
*
* @param numCourses
* @param prerequisites
* @return
*/
List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
List<Integer>[] graph = new LinkedList[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new LinkedList();
}
for (int[] matrix : prerequisites) {
int from = matrix[0];
int to = matrix[1];
graph[from].add(to);
}
return graph;
}
}
//leetcode submit region end(Prohibit modification and deletion)
三、二分图判定算法
基于labuladong的算法网站,二分图判定算法
1、基本介绍
二分图的顶点集可分割为两个互不相交的子集,图中每条边依附的两个顶点都分属于这两个子集,且两个子集内的顶点不相邻。
核心思路:
- 是将有向图相连的两个节点,上不同的颜色;
- 可以DFS,也可以BFS;
- DFS的代码框架如下:
/* 图遍历框架 */
void traverse(Graph graph, boolean[] visited, int v) {
visited[v] = true;
// 遍历节点 v 的所有相邻节点 neighbor
for (int neighbor : graph.neighbors(v)) {
if (!visited[neighbor]) {
// 相邻节点 neighbor 没有被访问过
// 那么应该给节点 neighbor 涂上和节点 v 不同的颜色
traverse(graph, visited, neighbor);
} else {
// 相邻节点 neighbor 已经被访问过
// 那么应该比较节点 neighbor 和节点 v 的颜色
// 若相同,则此图不是二分图
}
}
}
2、判断二分图
力扣第785题,判断二分图
思路:
- 一边遍历,一遍给节点上色,不停的比较;
[785]判断二分图
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public boolean isBipartite(int[][] graph) {
// 根据dfs遍历数组,判断是否是二分图
int len = graph.length;
visited = new boolean[len];
color = new boolean[len];
for (int i = 0; i < len; i++) {
// 对于无向图,只有没有走过的节点才需要遍历
if (!visited[i]) {
traverse(graph, i);
}
}
return isOk;
}
boolean isOk = true;// 是二分图
boolean[] visited;// 是否访问过
boolean[] color;// 着色数组,true和false为两个颜色
/**
* 遍历图
*
* @param graph:邻接表
* @param index:此时节点处的位置
*/
void traverse(int[][] graph, int index) {
// 判断是否是二分图
if (!isOk) {
return;
}
// 标记该节点
visited[index] = true;
// 找到与节点相邻的节点
for (int neighbor : graph[index]) {
// 判断该邻居是否被标记过
if (!visited[neighbor]) {
// 未被标记过,就将颜色和index的颜色不同
color[neighbor] = !color[index];
traverse(graph, neighbor);// 递归遍历邻居
} else {
// 已经遍历过,比较邻居颜色和index的颜色
if (color[index] == color[neighbor]) {
isOk = false;
return;// 不需要遍历了
}
}
}
}
}
//leetcode submit region end(Prohibit modification and deletion)
如果采用BFS的逻辑:
[785]判断二分图
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public boolean isBipartite(int[][] graph) {
int len = graph.length;
visited = new boolean[len];
color = new boolean[len];
for (int i = 0; i < len; i++) {
if (!visited[i]) {
bfs(graph, i);
}
}
return isOk;
}
boolean isOk = true;
boolean[] visited;
boolean[] color;
/**
* 宽度优先遍历
*
* @param graph:图,邻接表
* @param start:从strat节点开始宽度优先遍历
*/
void bfs(int[][] graph, int start) {
if (!isOk) {
return;
}
// 利用队列
Queue<Integer> queue = new LinkedList();
queue.add(start);
visited[start] = true;
while (!queue.isEmpty()) {
// 弹出元素,将邻居加入
int poll = queue.poll();
// 比较邻居是否访问过
for (int next : graph[poll]) {
if (visited[next]) {
if (color[next] == color[poll]) {
isOk = false;
return;
}
} else {
// 没有被访问过
visited[next] = true;
queue.add(next);
color[next] = !color[poll];
}
}
}
}
}
//leetcode submit region end(Prohibit modification and deletion)
3、可能的二分法
力扣第886,可能的二分法;
思路:
- 需要生成图,邻接表,无向图;
- 再去用二分法判断;
[886]可能的二分法
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public boolean possibleBipartition(int n, int[][] dislikes) {
visited = new boolean[n + 1];
color = new boolean[n + 1];
List<Integer>[] graph = buildGraph(n + 1, dislikes);
for (int i = 0; i < n + 1; i++) {
if (!visited[i]) {
dfs(graph, i);
}
}
return can;
}
boolean can = true;// 可以把互相不喜欢的人分成两组
boolean[] visited;
boolean[] color;
/**
* 深度优先遍历
*
* @param graph:图
* @param index:当前节点所处的位置
*/
void dfs(List<Integer>[] graph, int index) {
if (!can) {
return;
}
visited[index] = true;
for (int next : graph[index]) {
// 判断邻居是否被遍历过
if (visited[next]) {
// 判断颜色
if (color[index] == color[next]) {
can = false;
return;
}
} else {
color[next] = !color[index];
dfs(graph, next);
}
}
}
/**
* 根据不喜欢的人数组,构建出邻接表
* graph【i】:表示编号为i的人,她不喜欢的人用链表存储起来
*
* @param n:一组n个人,编号为1....n
* @param dislikes
* @return
*/
List<Integer>[] buildGraph(int n, int[][] dislikes) {
List<Integer>[] graph = new LinkedList[n];
// 数组初始化
for (int i = 0; i < n; i++) {
graph[i] = new LinkedList();
}
// 遍历给定的数组 dislikes,生成无向图
for (int[] matrix : dislikes) {
int from = matrix[0];
int to = matrix[1];
graph[from].add(to);
graph[to].add(from);
}
return graph;
}
}
//leetcode submit region end(Prohibit modification and deletion)
四、并查集(Union-Find)算法
基于labuladong的算法网站,并查集(Union-Find)算法;
1、动态连通性
Union-Find算法主要需要实现两个API,如下:
class UF {
/* 将 p 和 q 连接 */
public void union(int p, int q);
/* 判断 p 和 q 是否连通 */
public boolean connected(int p, int q);
/* 返回图中有多少个连通分量 */
public int count();
}
该算法的关键在于,union和connected的效率。用什么模型来表示该图的联通状态?用什么数据结构来实现代码?是主要解决的问题。
2、基本思路
利用森林表示连通性,设定树的每个节点都有一个指针指向父节点,如果是根节点的话,指针指向自己,如下图表示:
代码结构如下:
class UF {
// 记录连通分量
private int count;
// 节点 x 的父节点是 parent[x]
private int[] parent;
/* 构造函数,n 为图的节点总数 */
public UF(int n) {
// 一开始互不连通
this.count = n;
// 父节点指针初始指向自己
parent = new int[n];
for (int i = 0; i < n; i++)
parent[i] = i;
}
/* 其他函数 */
}
如果想要让两个节点被连通,则让其中的任意一个节点的根节点接到另一个节点的根节点上,如下:
代码如下:
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ)
return;
// 将两棵树合并为一棵
parent[rootP] = rootQ;
// parent[rootQ] = rootP 也一样
count--; // 两个分量合二为一
}
/* 返回某个节点 x 的根节点 */
private int find(int x) {
// 根节点的 parent[x] == x
while (parent[x] != x)
x = parent[x];
return x;
}
/* 返回当前的连通分量个数 */
public int count() {
return count;
}
/* 判断节点p和q是否被连通 */
public boolean connected(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
}
该算法中最主要的两个函数union和connected都是find函数造成的,find主要的功能是从某个节点开始找到根节点,但是极端情况下可能出现不平衡的情况。
现在问题的关键是,如何去优化,避免树的不平衡现象呢?
3、平衡性优化
在union的过程中,如果只是简单的将p节点所在的树接到q所在的树根节点下面,可能出现不平衡的情况,如下:
理想情况是:将小树接到大树下;解决办法是使用一个size数组,记录每棵树包含的节点数,比如size[3]=5表示,以3为根节点的树,总共包含5个节点,就可以通过比较树的大小进行优化合并过程:
class UF {
private int count;
private int[] parent;
// 新增一个数组记录树的“重量”
private int[] size;
public UF(int n) {
this.count = n;
parent = new int[n];
// 最初每棵树只有一个节点
// 重量应该初始化 1
size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
/* 其他函数 */
}
4、路径压缩
可以进一步压缩,使树的高度始终保持为常数,find函数就可以以O(1)的时间找到某一个节点的根节点:
// 第二种路径压缩的 find 方法
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
就可以将一整条树枝压平,如果使用路径压缩就不需要借助size数组了,代码如下:
class UF {
// 连通分量个数
private int count;
// 存储每个节点的父节点
private int[] parent;
// n 为图中节点的个数
public UF(int n) {
this.count = n;
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
// 将节点 p 和节点 q 连通
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ)
return;
parent[rootQ] = rootP;
// 两个连通分量合并成一个连通分量
count--;
}
// 判断节点 p 和节点 q 是否连通
public boolean connected(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
// 返回图中的连通分量个数
public int count() {
return count;
}
}
5、题目实践
(1)无向图中连通分量的数目
力扣第323题,无向图中连通分量的数目;
题目照片如下:
代码如下:
class Solution {
public int countComponents(int n, int[][] edges) {
UF uf = new UF(n);
// 遍历数组边,将连通域连接起来,之后返回连通量
for (int[] edge : edges) {
// 将a和b连通
uf.union(edge[0], edge[1]);
}
return uf.count();
}
}
// 生成union-find类
class UF {
private int[] parent;// 定义一个parent数组,记录父节点
private int count;// 记录此时图中的连通量
// 类初始化
public UF(int n) {
this.count = n;
this.parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
// 将节点p和q合并到一个区域中
public void union(int p, int q) {
int pRoot = findRoot(p);
int qRoot = findRoot(q);
if (pRoot == qRoot) {
return;
}
parent[pRoot] = qRoot;
count--;
}
// 判断p和q是否在一个连通域中
public boolean isOne(int p, int q) {
int pRoot = findRoot(p);
int qRoot = findRoot(q);
return pRoot == qRoot;
}
// 返回连通量个数
public int count() {
return this.count;
}
// 找到x的根节点,顺便优化
public int findRoot(int x) {
if (parent[x] != x) {
parent[x] = findRoot(parent[x]);
}
return parent[x];
}
}
五、KRUSKAL 最小生成树算法
基于labuladong的算法网站,KRUSKAL 最小生成树算法;
1、基本介绍
Kruskal(克鲁斯卡尔算法),树和图的最大区别为树不会成环,而图可能成环,一幅图可以有很多不同的生成树(生成树:一棵包含图中所有节点的树),但图中节点每条边可能有权重,在所有可能生成树中,权重最小的那颗生成树就叫最小生成树。
一般情况下是在无向加权图中计算最小生成树,图的边权重一般代表成本、距离这样的标量,需要用到前面的并查集算法(union-find);
难点:
Kruskal算法的一个难点是保证生成树的合法性,保证那是棵树,不能包含环;
2、以图判树
思路:
- 本题采用并查集做;
- 对于要添加的边,这条边相连的两个节点,需要判断是否已经在一个连通域中;
- 添加完所有的边,需要验证此时的连通域数量是否为1;
class Solution {
public boolean validTree(int n, int[][] edges) {
// 创建并查集
UF uf = new UF(n);
// 将每条边相连的两个节点合并
for (int[] edge : edges) {
int a = edge[0];
int b = edge[1];
// 判断a和b是否是一个连通域
if (uf.isOne(a, b)) {
return false;
}
// 合并a和b到一个连通域中
uf.union(a, b);
}
// 并且需要判断此时连通域的数量
return uf.count == 1;
}
}
// 并查集
class UF {
private int size;// 并查集的大小
private int[] parent;// 节点的父节点
int count;// 连通域的数量
// 构造方法初始化
public UF(int size) {
this.size = size;
this.parent = new int[size];
this.count = size;
// 最开始每个节点的父节点都是自己
for (int i = 0; i < size; i++) {
parent[i] = i;
}
}
// 将两个节点合并到一个连通域中
public void union(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
parent[pRoot] = qRoot;
count--;
}
// 判断两个节点是否在同一个连通域
public boolean isOne(int p, int q) {
return find(p) == find(q);
}
// 找到节点的父节点
public int find(int node) {
if (parent[node] != node) {
parent[node] = find(parent[node]);
}
return parent[node];
}
}
3、Kruskal算法
所谓最小生成树:
- 包含图中所有的节点;
- 形成的结构是树结构,不存在环;
- 权重和最小;
前面的并查集可以轻松实现上面的前两点,关于权重和最小,可以按照每条边的权重从小到大进行排序,从最小权重的边开始遍历,生成树结构。
4、最低成本联通所有城市
思路:
- 本题采用并查集,连通域的问题;
- 需要将成本按照从小到大进行排序;
class Solution {
/**
* @param n:地图上有n座城市
* @param connections:表示将城市x和城市y连接所需要的成本
* @return
*/
public int minimumCost(int n, int[][] connections) {
// 结果
int res = 0;
// 按照成本将connections数组进行从小到大的排序
Arrays.sort(connections, (int[] o1, int[] o2) -> {
return o1[2] - o2[2];
});
// 创建并查集
UF uf = new UF(n + 1);// 因为有n座城市,但是城市是从1到n,故城市0永远是单独的
for (int[] connection : connections) {
int x = connection[0];
int y = connection[1];
int cost = connection[2];
// 判断x和y是否在一个连通域中
if (uf.isOne(x, y)) {
continue;
}
// 将x和y连通
uf.union(x, y);
res += cost;
}
return uf.count() == 2 ? res : -1;
}
}
// 并查集
class UF {
private int size;
private int[] parent;
private int count;
public UF(int size) {
this.size = size;
this.parent = new int[size];
this.count = size;
for (int i = 0; i < size; i++) {
parent[i] = i;
}
}
void union(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
parent[pRoot] = qRoot;
count--;
}
boolean isOne(int p, int q) {
return find(p) == find(q);
}
int count() {
return this.count;
}
int find(int node) {
if (parent[node] != node) {
parent[node] = find(parent[node]);
}
return parent[node];
}
}
5、连接所有点的最小费用
力扣第1584题,连接所有点的最小费用;
[1584]连接所有点的最小费用
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public int minCostConnectPoints(int[][] points) {
// 根据数组的顶点生成全部的边
List<int[]> list = new LinkedList();
int len = points.length;
for (int i = 0; i < len; i++) {
for (int j = i + 1; j < len; j++) {
int[] res = new int[3];
res[0] = i;
res[1] = j;
res[2] = Math.abs(points[i][0] - points[j][0]) + Math.abs(points[i][1] - points[j][1]);
list.add(res);
}
}
// 将数组顶点生成全部的边,按照边的权重从小到大排序
Collections.sort(list, (int[] a, int[] b) -> {
return a[2] - b[2];
});
// 并查集
int res = 0;
UF uf = new UF(len);
for (int[] matrix : list) {
int x = matrix[0];
int y = matrix[1];
int count = matrix[2];
if (uf.isOne(x, y)) {
continue;
}
uf.union(x, y);
res += count;
}
return res;
}
}
// 并查集
class UF {
private int size;
private int[] parent;
private int count;
public UF(int size) {
this.size = size;
this.parent = new int[size];
this.count = size;
for (int i = 0; i < size; i++) {
parent[i] = i;
}
}
void union(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
parent[pRoot] = qRoot;
count--;
}
boolean isOne(int p, int q) {
return find(p) == find(q);
}
int count() {
return this.count;
}
int find(int node) {
if (parent[node] != node) {
parent[node] = find(parent[node]);
}
return parent[node];
}
}
//leetcode submit region end(Prohibit modification and deletion)
六、PRIM 最小生成树算法
基于labuladong的算法网站,PRIM 最小生成树算法;
1、切分定理
切分:将一幅图分为两个不重叠且非空的节点集合;
横切边:切分时候被切割的边,叫做横切边;
对于任意一种切分,其中权重最小的那条边一定是构成最小生成树的那一条边;
2、Prim算法实现
算法原理:
- 利用切分定理;
- 每一次切分都可以找出最小生成树中的一条边,每次都把权重最小的横切边加入到最小生成树,直到把构成最小生成树的所有边都切出来为止;
代码实现:
// 最小生成树Prim算法
class Prim {
private PriorityQueue<int[]> queue;// 存储横切边的优先级队列
private boolean[] inMST;// 在最小生成树上的节点
private int weightSum = 0;// 最小权重和
private List<int[]>[] graph;// 图上的边,graph[i]代表第i个节点包含的链表(存储边),边由数组构成[from,to,weight]节点from、to、和权重
// 构造器初始化
public Prim(List<int[]>[] graph) {
// 1、变量初始化
this.graph = graph;
// 优先级队列初始化(按照横切边的权重从小到大排序)
queue = new PriorityQueue<>((int[] a, int[] b) -> {
return a[2] - b[2];
});
// 图中节点个数
int size = graph.length;
this.inMST = new boolean[size];
// 2、随便找一个节点,假设从节点开始
// 将节点加入到最小生成树中
inMST[0] = true;
// 将该节点未被切分过的边加入到存储横切边的优先级队列中
add(0);
// 3、不断进行生成最小生成树的节点
while (!queue.isEmpty()) {
// 弹出最小横切边
int[] edge = queue.poll();
int to = edge[1];
int weight = edge[2];
// 判断to点是否已在最小生成树上
if (inMST[to]) {
continue;
}
// 更新结果
weightSum += weight;
inMST[to] = true;
// 将to节点的边加入到优先级队列中
add(to);
}
}
// 将节点为index处的所有边加入到优先级队列中
void add(int index) {
// 遍历index的邻边
for (int[] matrix : graph[index]) {
int to = matrix[1];
// 判断邻边是否已经在最小生成树上
if (inMST[to]) {
continue;
}
// 加入横切边
queue.add(matrix);
}
}
// 判断最小生成树是否包含图中的所有节点
boolean allConnected() {
for (int i = 0; i < inMST.length; i++) {
if (!inMST[i]) {
return false;
}
}
return true;
}
// 最小权重和
int weightSum() {
return this.weightSum;
}
}
七、DIJKSTRA 算法模板及应用
基于labuladong的算法网站,DIJKSTRA 算法模板及应用;
1、算法实现
// Dijkstra 算法
class Dijkstra {
/**
* @param start:起始节点
* @param graph:图(邻接表)
* @return:返回从起始节点到各个节点的最小距离
*/
int[] dijkstra(int start, List<Integer>[] graph) {
// 返回值
int len = graph.length;
int[] res = new int[len];
// 首先将结果集的距离全部设为最大
Arrays.fill(res, Integer.MAX_VALUE);
// base case
res[start] = 0;
// 创建优先级队列
Queue<State> queue = new PriorityQueue<>((State a, State b) -> {
return a.distanceFromStart - b.distanceFromStart;
});
queue.add(new State(start, 0));
// 开始遍历所有的节点
while (!queue.isEmpty()) {
State curState = queue.poll();// 当前的节点
int curIndex = curState.id;
int curDistanceFromStart = curState.distanceFromStart;
// 如果已经有一条很短的路径到达了该节点,那么就不需要再去遍历了
if (curDistanceFromStart > res[curIndex]) {
continue;
}
// 找出该节点的邻居节点
for (int next : adj(curIndex)) {
// 加入邻居节点到优先级队列中
int distance = curDistanceFromStart + weight(curIndex, next);
if (distance < res[next]) {
res[next] = distance;
queue.add(new State(next, distance));
}
}
}
return res;
}
/**
* @param index
* @return:返回index位置的邻居节点
*/
List<Integer> adj(int index);
/**
* @param from
* @param to
* @return from到to节点的边的权重
*/
int weight(int from, int to);
}
// 辅助类
class State {
int id;// 节点位置
int distanceFromStart;// 从起始节点到该节点的最小距离
public State(int id, int distance) {
this.id = id;
this.distanceFromStart = distance;
}
}
如果需要计算的是指定end节点的距离,只需要修改代码为:
// 输入起点 start 和终点 end,计算起点到终点的最短距离
int dijkstra(int start, int end, List<Integer>[] graph) {
// ...
while (!pq.isEmpty()) {
State curState = pq.poll();
int curNodeID = curState.id;
int curDistFromStart = curState.distFromStart;
// 在这里加一个判断就行了,其他代码不用改
if (curNodeID == end) {
return curDistFromStart;
}
if (curDistFromStart > distTo[curNodeID]) {
continue;
}
// ...
}
// 如果运行到这里,说明从 start 无法走到 end
return Integer.MAX_VALUE;
}
2、秒杀三道题目
(1)网络延迟时间
力扣第743题,网络延迟时间;
思路:
- 利用Dijkstra算法
[743]网络延迟时间
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public int networkDelayTime(int[][] times, int n, int k) {
// 遍历 times 生成图(邻接表)
List<int[]>[] graph = new LinkedList[n + 1];
// 有n个节点,编号为1到n,那么我图的数组大小为n+1
for (int i = 0; i <= n; i++) {
graph[i] = new LinkedList();
}
// 遍历 times数组
for (int[] matrix : times) {
// 有向边 from--->to,权重为weight
int from = matrix[0];
int to = matrix[1];
int weight = matrix[2];
// 将边加入到图中
graph[from].add(new int[]{to, weight});
}
// Dijkstra
Dijkstra dijkstra = new Dijkstra();
int[] result = dijkstra.dijkstra(k, graph);
// 找出最大
int res = Integer.MIN_VALUE;
for (int ans : result) {
if (ans > res) {
res = ans;
}
}
// 判断一下最小生成树上是否包含所有的节点
return res == Integer.MAX_VALUE ? -1 : res;
}
}
// Dijkstra 算法
class Dijkstra {
List<int[]>[] graph;// 图 graph[i]
public int[] dijkstra(int start, List<int[]>[] graph) {
// 优先级队列,存储辅助类State
Queue<State> queue = new PriorityQueue<>((State a, State b) -> {
return a.distanceFromStart - b.distanceFromStart;
});
// 结果集
int len = graph.length;
int[] result = new int[len];
Arrays.fill(result, Integer.MAX_VALUE);
// 从第1个节点开始
result[0] = 0;
result[start] = 0;
queue.add(new State(start, result[start]));
// 开始遍历
while (!queue.isEmpty()) {
State curState = queue.poll();
int from = curState.id;
int distanceFromStart = curState.distanceFromStart;
if (distanceFromStart > result[from]) {
continue;
}
for (int[] matrix : graph[from]) {
int to = matrix[0];
int weight = matrix[1];
// 更新to节点
int distanceTo = distanceFromStart + weight;
if (distanceTo < result[to]) {
// 更新result并将该节点加入到队列中
result[to] = distanceTo;
queue.add(new State(to, distanceTo));
}
}
}
return result;
}
}
// 辅助类State
class State {
int id;
int distanceFromStart;
public State(int id, int distanceFromStart) {
this.id = id;
this.distanceFromStart = distanceFromStart;
}
}
//leetcode submit region end(Prohibit modification and deletion)
(2)最小体力消耗路径
力扣第1631题,最小体力消耗路径
[1631]最小体力消耗路径
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public int minimumEffortPath(int[][] heights) {
int m = heights.length;
int n = heights[0].length;
// 创建一个表记录最小体力消耗值
int[][] memo = new int[m][n];
// 给备忘录初始化
for (int i = 0; i < m; i++) {
Arrays.fill(memo[i], Integer.MAX_VALUE);
}
// 利用优先级队列
Queue<State> queue = new PriorityQueue<State>((State a, State b) -> {
return a.distanceFromStart - b.distanceFromStart;
});
// 设置起点
memo[0][0] = 0;
queue.add(new State(0, 0, 0));
// 记录返回值
// 遍历优先级队列
while (!queue.isEmpty()) {
State state = queue.poll();// 优先级队列中最小的元素
int x = state.x;// 最小元素的x坐标
int y = state.y;// 最小元素的y坐标
int distance = state.distanceFromStart;// 起始位置到当前最小元素的体力消耗值
// 判断是否需要终止
if (x == m - 1 && y == n - 1) {
return distance;
}
// 先判断memo中是否已经记录了(x,y)处的最小体力消耗值,并判断是否有必要验证
if (distance > memo[x][y]) {
continue;
}
// 队列中的体力消耗值小于备忘录中,那么就需要借助该元素算出邻居
List<int[]> neighbors = getNeighbors(x, y, heights);// 找到邻居
for (int[] matrix : neighbors) {
int toX = matrix[0];
int toY = matrix[1];
// 计算从(x,y)到(toX,toY)的消耗,题目是相邻格子的最大消耗值
int distanceTo = Math.max(distance, Math.abs(heights[x][y] - heights[toX][toY]));
// 判断是否有必要更新memo和将其加入到队列中
if (distanceTo < memo[toX][toY]) {
memo[toX][toY] = distanceTo;
queue.add(new State(toX, toY, distanceTo));
}
}
}
return -1;// 按道理来说不会走到这一步
}
/**
* @param x:x方向的坐标
* @param y:y方向的坐标
* @param heights:地图数组
* @return 返回(x,y)位置的邻居(上下左右)
*/
List<int[]> getNeighbors(int x, int y, int[][] heights) {
List<int[]> neighbors = new LinkedList();
int m = heights.length;
int n = heights[0].length;
// 以(x,y)为中心,位移矩阵为:
int[][] dis = new int[][]{{0, -1}, {0, 1}, {-1, 0}, {1, 0}};// 上下左右
for (int[] matrix : dis) {
int newX = x + matrix[0];
int newY = y + matrix[1];
// 判断 newX 和 newY
if (newX < 0 || newX >= m || newY < 0 || newY >= n) {
continue;
}
neighbors.add(new int[]{newX, newY});
}
return neighbors;
}
}
// 辅助类
class State {
int x;
int y;
int distanceFromStart;
public State(int x, int y, int distanceFromStart) {
this.x = x;
this.y = y;
this.distanceFromStart = distanceFromStart;
}
}
//leetcode submit region end(Prohibit modification and deletion)
(3)概率最大的路径
力扣第1514题,概率最大的路径;
[1514]概率最大的路径
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public double maxProbability(int n, int[][] edges, double[] succProb, int start, int end) {
// 图(邻接表)
List<double[]>[] graph = new LinkedList[n];
// 遍历生成图
for (int i = 0; i < n; i++) {
graph[i] = new LinkedList();
}
// 遍历生成图
for (int i = 0; i < edges.length; i++) {
int from = edges[i][0];
int to = edges[i][1];
double prob = succProb[i];
// 加入到图中,双向图
graph[from].add(new double[]{(double) to, prob});
graph[to].add(new double[]{(double) from, prob});
}
return max(start, end, graph);
}
/**
* @param start:起始节点
* @param end:目标接二点
* @param graph:图
* @return 返回最大可能性
*/
double max(int start, int end, List<double[]>[] graph) {
int len = graph.length;
// 创建结果数组
double[] result = new double[len];
Arrays.fill(result, -1);
// 优先级队列(大的在前面)
Queue<State> queue = new PriorityQueue<State>((State a, State b) -> {
return Double.compare(b.possibility, a.possibility);
});
queue.add(new State(start, 1));
result[start] = 1;
while (!queue.isEmpty()) {
State state = queue.poll();
int id = state.id;
double possibility = state.possibility;
if (id == end) {
return possibility;
}
if (possibility < result[id]) {
continue;
}
for (double[] matrix : graph[id]) {
int to = (int) matrix[0];
double poss = matrix[1];
// 取最大值
double res = possibility * poss;
if (res > result[to]) {
result[to] = res;
queue.add(new State(to, res));
}
}
}
return 0.0;
}
}
// 辅助类
class State {
int id;// 节点编号
double possibility;// 可能性
public State(int id, double possibility) {
this.id = id;
this.possibility = possibility;
}
}
//leetcode submit region end(Prohibit modification and deletion)