038、图(labuladong)

手把手刷图算法

一、图论基础及遍历算法

基于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)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值