【算法-刷题】图论专题

1. 图论

1.1 分类

  • 按表现形式

    • 邻接表
    • 邻接矩阵
  • 按图的种类

    • 无向图
      • 无向无环图
      • 无向有环图
    • 有向图
      • 有向有环图
      • 有向有环图
    • 加权图

1.2 代码框架

class Tranverse:
    def __init__(self):
        self.res = []
        self.visited = {
    
    }

    def tranverse(self, _graph, _s, _path):
        """
        :param _graph: 邻接表或邻接矩阵
        :param _s: 节点
        :param _path: 记录的路径
        :return:
        """
        if self.visited[_s]:
            return

        # 经过节点
        self.visited[_s] = True
        _path.append(_s)
        if ...:
            pass

        # 遍历节点
        for v in _graph[_s]:
            self.tranverse(_graph, v, _path)

        # 离开节点
        self.visited[_s] = False
        _path.pop()

1.3 二叉树、多叉树、图遍历框架

/* 二叉树遍历框架 */
void traverse(TreeNode root) {
    
    
    if (root == null) return;
    traverse(root.left);
    traverse(root.right);
}

/* 多叉树遍历框架 */
void traverse(Node root) {
    
    
    if (root == null) return;
    for (Node child : root.children)
        traverse(child);
}

/* 图遍历框架 */
boolean[] visited;
void traverse(Graph graph, int v) {
    
    
    // 防止走回头路进入死循环
    if (visited[v]) return;
    // 前序遍历位置,标记节点 v 已访问
    visited[v] = true;
    for (TreeNode neighbor : graph.neighbors(v))
        traverse(graph, neighbor);
}

/* 图遍历框架 */
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 的颜色
            // 若相同,则此图不是二分图
        }
    }
}

1.4 并查集代码

class UnionFind:
    def __init__(self, n):
        self._count = n
        self.parent = [i for i in range(n)]
        self.size = [1 for _ in range(n)]

        for i in range(n):
            self.parent[i] = i
            self.size[i] = 1

    def union(self, p, q):
        root_p = self.find(p)
        root_q = self.find(q)
        if root_q == root_p:
            return
        # 小树接到大树下面,较平衡
        if self.size[root_p] > self.size[root_q]:
            self.parent[root_q] = root_p
            self.size[root_p] += self.size[root_q]
        else:
            self.parent[root_p] = root_q
            self.size[root_q] += self.size[root_p]
        self._count -= 1

    def find(self, x):
        while self.parent[x] != x:
            self.parent[x] = self.parent[self.parent[x]]
            x = self.parent[x]
        return x

    def connected(self, p, q):
        root_p = self.find(p)
        root_q = self.find(q)
        return root_p == root_q

    @property
    def count(self):
        return self._count

1.5 kruskal算法

一般来说,我们都是在无向加权图中计算最小生成树的,
所以使用最小生成树算法的现实场景中,图的边权重一般代表成本、距离这样的标量

1.6 Dijkstra 解决最短路径问题

Dijkstra 模板

  1. 维护一个State类
    1). 记录图节点的id:id
    2). 从 start 节点到当前节点的距离:distFromStart
  2. 添加一个返回相邻节点的api:adj(s)
  3. 使用优先队列来节省遍历时间:distFromStart 较小的排在前面
  4. 带备忘录记录节点start到节点i的最短路径权重
  5. BFS,缺少的层序for循环遍历利用State类来补充


// 返回节点 from 到节点 to 之间的边的权重
int weight(int from, int to);

// 输入节点 s 返回 s 的相邻节点
List<Integer> adj(int s);

// 输入一幅图和一个起点 start,计算 start 到其他节点的最短距离
int[] dijkstra(int start, List<Integer>[] graph) {
    
    
    // 图中节点的个数
    int V = graph.length;
    // 记录最短路径的权重,你可以理解为 dp table
    // 定义:distTo[i] 的值就是节点 start 到达节点 i 的最短路径权重
    int[] distTo = new int[V];
    // 求最小值,所以 dp table 初始化为正无穷
    Arrays.fill(distTo, Integer.MAX_VALUE);
    // base case,start 到 start 的最短距离就是 0
    distTo[start] = 0;

    // 优先级队列,distFromStart 较小的排在前面
    Queue<State> pq = new PriorityQueue<>((a, b) -> {
    
    
        return a.distFromStart - b.distFromStart;
    });

    // 从起点 start 开始进行 BFS
    pq.offer(new State(start, 0));

    while (!pq.isEmpty()) {
    
    
        State curState = pq.poll();
        int curNodeID = curState.id;
        int curDistFromStart = curState.distFromStart;

        if (curDistFromStart > distTo[curNodeID]) {
    
    
            // 已经有一条更短的路径到达 curNode 节点了
            continue;
        }
        // 将 curNode 的相邻节点装入队列
        for (int nextNodeID : adj(curNodeID)) {
    
    
            // 看看从 curNode 达到 nextNode 的距离是否会更短
            int distToNextNode = distTo[curNodeID] + weight(curNodeID, nextNodeID);
            if (distTo[nextNodeID] > distToNextNode) {
    
    
                // 更新 dp table
                distTo[nextNodeID] = distToNextNode;
                // 将这个节点以及距离放入队列
                pq.offer(new State(nextNodeID, distToNextNode));
            }
        }
    }
    return distTo;
}

1.7 例题汇总

  • 797 所有可能的路径
  • 785 判断⼆分图
  • 886 可能的二分法
  • 207 课程表
  • 210 课程表 II
  • 130 被围绕的区域
  • 90 等式⽅程的可满⾜性
  • 261 以图判树
  • 1135 最低成本联通所有城市
  • 1584 连接所有点的最⼩费⽤
  • 743 ⽹络延迟时间
  • 514 概率最⼤的路径
  • 1631 最⼩体⼒消耗路径

2. 题目

【797】 所有可能的路径

def allPathsSourceTarget(graph):
    '''
    给你⼀个有 n 个节点的有向⽆环图(DAG),graph[i] 记录着第节点 i 能够到达的节点。
    请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序)。
    leetcode: 797. 所有可能的路径
    input:graph = [[1,2],[3],[3],[]]
    output:[[0,1,3],[0,2,3]]
    思路:
        1.
        2.
        3.
    '''
    import copy

    class Tranverse:
        def __init__(self):
            self.res = []

        def traverse(self, _graph, s, _path):
            _path.append(s)

            n = len(_graph)
            # 终止条件是遍历到最后一个节点
            if s == n - 1:
                p = copy.deepcopy(_path)
                self.res.append(p)
                _path.pop()
                return
            # 递归遍历每个节点
            for v in _graph[s]:
                self.traverse(_graph, v, _path)
            _path.pop()

    path = []
    t = Tranverse()
    t.traverse(graph, 0, path)
    print(t.res)
    return t.res

【785】 判断⼆分图

# 二分图
def isBipartite(graph):
    '''
    存在⼀个 ⽆向图,图中有 n 个节点。其中每个节点都有⼀个介于 0 到 n - 1 之间的唯⼀编号。给你⼀个⼆
    维数组 graph,其中 graph[u] 是⼀个节点数组,由节点 u 的邻接节点组成。形式上,对于 graph[u] 中
    的每个 v,都存在⼀条位于节点 u 和节点 v 之间的⽆向边。该⽆向图同时具有以下属性:
        1、不存在⾃环(graph[u] 不包含 u)。
        2、不存在平⾏边(graph[u] 不包含重复值)。
        3、如果 v 在 graph[u] 内,那么 u 也应该在 graph[v] 内(该图是⽆向图)
        4、这个图可能不是连通图,也就是说两个节点 u 和 v 之间可能不存在⼀条连通彼此的路径。
    ⼆分图 定义:如果能将⼀个图的节点集合分割成两个独⽴的⼦集 A 和 B,并使图中的每⼀条边的两个节点⼀
    个来⾃ A 集合,⼀个来⾃ B 集合,就将这个图称为 ⼆分图。
    如果图是⼆分图,返回 true;否则,返回 false。
    0 -- 1
    | \  |
    3 -- 2
    leetcode: 785. 判断⼆分图
    input:graph = [[1,2,3],[0,2],[0,1,3],[0,2]]
    output:false
    思路:
        1.
        2.
        3.
    '''

    class DFS:
        def __init__(self, _graph):
            self.graph = _graph
            self.ok = True
            self.n = len(_graph)
            self.color = [False for _ in range(self.n)]
            self.visited = [False for _ in range(self.n)]

        def traverse(self, v):
            if not self.ok:
                #  如果已经确定不是二分图了,就不用浪费时间再递归遍历了
                return
            self.visited[v] = True
            for w in self.graph[v]:
                if not self.visited[w]:
                    # 相邻节点 w 没有被访问过
                    self.color[w] = not self.color[v]
                    self.traverse(w)
                else:
                    # 相邻节点 w 已经被访问过
                    # 根据 v 和 w 的颜色判断是否是二分图
                    if self.color[w] == self.color[v]:
                        #  若相同,则此图不是二分图
                        self.ok = False

        def run(self):
            for v in range(self.n):
                if not self.visited[v]:
                    self.traverse(v)
            print("result: ", self.ok)
            return self.ok

    class BFS:
        def __init__(self, _graph):
            self.graph = _graph
            self.ok = True
            self.n = len(_graph)
            self.color = [False for _ in range(self.n)]
            self.visited = [False for _ in range(self.n)]

        def traverse(self, start):
            # 从 start 节点开始进行 BFS 遍历
            self.visited[start] = True
            q = [start]
            # 退出条件:队列为空或者找到不是二分图的证据
            while len(q) and self.ok:
                v = q[0]
                q.remove(v)
                # 从节点 v 向所有相邻节点扩散
                for w in self.graph[v]:
                    if not self.visited[w]:
                        self.color[w] = not self.color[v]
                        # 标记 w 节点,并放入队列
                        self.visited[w] = True
                        q.append(w)
                    else:
                        if self.color[w] == self.color[v]:
                            self.ok = False

        def run(self):
            for v in range(self.n):
                if not self.visited[v]:
                    self.traverse(v)
            print(self.ok)
            return self.ok

    dfs = DFS(graph)
    dfs.run()

    bfs = BFS(graph)
    bfs.run()

【886】 可能的二分法

# 二分图问题
def possibleBipartition(n, dislikes):
    '''
    给定⼀组 N ⼈(编号为 1, 2, ..., N),我们想把每个⼈分进任意⼤⼩的两组。
    每个⼈都可能不喜欢其他⼈,那么他们不应该属于同⼀组。
    形式上,如果 dislikes[i] = [a, b],表示不允许将编号为 a 和 b 的⼈归⼊同⼀组。
    当可以⽤这种⽅法将所有⼈分进两组时,返回 true;否则返回 false。
    leetcode: 886 可能的二分法
    input: N = 4, dislikes = [[1,2],[1,3],[2,4]]
    output:true

    input: N = 4, dislikes = [[1,2],[1,3],[2,3]]
    output:false
    思路:
        1.
        2.
        3.
    '''

    def build_graph(_n, _dislicks):
        graph = [[] for _ in range(_n + 1)]
        for edge in _dislicks:
            w = edge[0]
            v = edge[1]
            graph[w].append(v)
            graph[v].append(w)
        return graph

    class DFS:
        def __init__(self, graph):
            self.n = len(graph)
            self.graph = graph
            self.visited = [False for _ in range(self.n)]
            self.color = [False for _ in range(self.n)]
            self.ok = True

        def traverse(self, v):
            if not self.ok:
                return

            self.visited[v] = True
            for w in self.graph[v]:
                if not self.visited[w]:
                    self.color[w] = not self.color[v]
                    self.traverse(w)
                else:
                    if self.color[w] == self.color[v]:
                        self.ok = False

        def run(self):
            for i in range(1, self.n):
                if not self.visited[i]:
                    self.traverse(i)
            print(self.ok)
            return self.ok

    graph = build_graph(n, dislikes)
    dfs = DFS(graph)
    dfs.run()

【207】 课程表

def findOrder(numCourses, prerequisites):
    '''
    你这个学期必须选修 numCourses ⻔课程,记为 0 到 numCourses - 1。
    在选修某些课程之前需要⼀些先修课程。先修课程按数组 prerequisites 给出,其
    中 prerequisites[i] = [ai, bi],表示如果要学习课程 ai 则必须先学习课程 bi。
    请你判断是否可能完成所有课程的学习?如果可以,返回 true;否则,返回 false。
    leetcode: 207. 课程表
    input:numCourses = 2, prerequisites = [[1,0]]
    output:true

    input:numCourses = 2, prerequisites = [[1,0],[0,1]]
    output:false
    思路:
        1. 图常见的存储方式是使用邻接表
        2. 拓扑排序
        3.
    '''

    def build_graph(n, relationship):
        '''
        建立一个有向图
        :param n:
        :param relationship:
        :return:
        '''
        graph = [[] for _ in range(n)]
        for v in relationship:
            _from = v[1]
            _to = v[0]
            graph[_from].append(_to)
        return graph

    class DFS:
        def __init__(self, graph):
            self.graph = graph
            self.n = len(self.graph)
            self.visited = [False for _ in range(self.n)]
            self.on_path = [False for _ in range(self.n)]
            self.has_cycle = False

        def traverse(self, v):
            if self.on_path[v]:
                self.has_cycle = True

            if self.visited[v] or self.has_cycle:
                return

            self.visited[v] = True
            self.on_path[v] = True
            for w in self.graph[v]:
                self.traverse(w)
            self.on_path[v] = False

        def run(self):
            for i in range(self.n):
                self.traverse(i)
            print(self.has_cycle)
            return False if self.has_cycle else True

    graph = build_graph(numCourses, prerequisites)
    dfs = DFS(graph)
    dfs.run()

【210】 课程表 II

def findOrder2(numCourses, prerequisites):
    '''
    现在你总共有 numCourses ⻔课需要选,记为 0 到 numCourses - 1。
    给你⼀个数组 prerequisites,其中 prerequisites[i] = [ai, bi],表示在选修课程 ai 前必须先选修 bi。
    返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回任意⼀种就可以了。
    如果不可能完成所有课程,返回⼀个空数组。
    leetcode: 210. 课程表 II
    input: numCourses = 2, prerequisites = [[1,0]]
    output: [0,1]
    思路:
        1.拓扑排序
        2.拓扑排序就是后序遍历反转之后的结果
        3.且拓扑排序只能针对有向无环图,进行拓扑排序之前要进行环检测
    '''

    def build_graph(n, relationship):
        graph = [[] for _ in range(n)]
        for v in relationship:
            _from = v[1]
            _to = v[0]
            graph[_from].append(_to)
        return graph

    class DFS:
        def __init__(self, graph):
            self.graph = graph
            self.n = len(self.graph)
            self.visited = [False for _ in range(self.n)]
            self.on_path = [False for _ in range(self.n)]
            self.postorder = []
            self.has_cycle = False

        def traverse(self, v):
            if self.on_path[v]:
                self.has_cycle = True
                return

            if self.visited[v] or self.has_cycle:
                return

            self.visited[v] = True
            self.on_path[v] = True
            for w in self.graph[v]:
                self.traverse(w)

            self.on_path[v] = False
            self.postorder.append(v)

        def run(self):
            for i in range(self.n):
                self.traverse(i)

            if self.has_cycle:
                return []
            else:
                return self.postorder[::-1]

    graph = build_graph(numCourses, prerequisites)
    dfs = DFS(graph)
    res = dfs.run()
    print(res)
    return res

【130】 被围绕的区域

# 并查集算法
def connectedArea(board):
    '''
    给你⼀个 m x n 的矩阵 board,由若⼲字符 'X' 和 'O' 组成,找到所有被 'X' 围绕的区域,并将这些区域⾥所有的 'O' ⽤ 'X' 填充。
    leetcode: 130. 被围绕的区域
    input:board = [
                    ["X","X","X","X"],
                    ["X","O","O","X"],
                    ["X","X","O","X"],
                    ["X","O","X","X"]
                  ]
    output: [
                ["X","X","X","X"],
                ["X","X","X","X"],
                ["X","X","X","X"],
                ["X","O","X","X"]
            ]
            被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。
            任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。
            如果两个元素在⽔平或垂直⽅向相邻,则称它们是“相连”的
    思路:
        1.
        2.
        3.
    '''
    if len(board) == 0:
        return 0
    m, n = len(board), len(board[0])
    uf = UnionFind(m * n + 1)
    dummy = m * n
    # 首列和末列的O与dummy相连
    for i in range(m):
        if board[i][0] == "O":
            uf.union(i * n, dummy)
        if board[i][n - 1] == "O":
            uf.union(i * n + n - 1, dummy)

    # 首行和末行的O与dummy相连
    for j in range(n):
        if board[0][j] == "O":
            uf.union(j, dummy)
        if board[m - 1][j] == "O":
            uf.union(n * (m - 1) + j, dummy)

    direct = [[1, 0], [0, 1], [0, -1], [-1, 0]]
    for i in range(1, m - 1):
        for j in range(1, n - 1):
            if board[i][j] == "O":
                for k in range(0, 4):
                    x = i + direct[k][0]
                    y = j + direct[k][1]
                    if board[x][y] == "O":
                        uf.union(x * n + y, i * n + j)

    for i in range(1, m - 1):
        for j in range(1, n - 1):
            if not uf.connected(dummy, i * n + j):
                board[i][j] = 'X'

    for b in board:
        print(b)
    print('\n')

【90】 等式⽅程的可满⾜性

# 并查集算法
def equationsPossible(equations):
    '''
    给定⼀个由表示变量之间关系的字符串⽅程组成的数组,每个字符串⽅程 equations[i] 的⻓度为 4,并采
    ⽤两种不同的形式之⼀:"a==b" 或 "a!=b"。在这⾥,a 和 b 是⼩写字⺟(不⼀定不同),表示单字⺟变量名。
    只有当可以将整数分配给变量名,以便满⾜所有给定的⽅程时才返回 true,否则返回 false。
    leetcode:990. 等式⽅程的可满⾜性
    input:["a==b","b!=a"]
    output:false
    思路:
        1.将equations中的算式根据==和!=分成两部分,先处理==算式,使得他们通过相等关系各自勾结成门派;然后处理!=算式,检查不等关系是否破坏了相等关系的连通性
        2.
        3.
    '''
    uf = UnionFind(26)
    for e in equations:
        if "==" in e:
            x = ord(e[0]) - ord('a')
            y = ord(e[3]) - ord('a')
            uf.union(x, y)

    for e in equations:
        if "!=" in e:
            x = ord(e[0]) - ord('a')
            y = ord(e[3]) - ord('a')
            if uf.connected(x, y):
                print(False)
                return False
    print(True)
    return True

【261】 以图判树

# 生成树
def validTree(n, edges):
    '''
    给定从 0 到 n-1 标号的 n 个结点,和⼀个⽆向边列表(每条边以结点对来表示),请编写⼀个函数⽤来判断这些边是否能够形成⼀个合法有效的树结构。
    leetcode: 261. 以图判树
    input1: n = 5, 边列表 edges = [[0,1], [0,2], [0,3], [1,4]]
    output1: true

    input2: n = 5, 边列表 edges = [[0,1], [1,2], [2,3], [1,3], [1,4]]
    output2: false
    思路:
        1. 树不会包含环,图可以包含环
        2. 对于添加的这条边,如果该边的两个节点本来就在同一连通分量里,那么添加这条边会产生环;
        3. 反之,如果该边的两个节点不在同一连通分量里,则添加这条边不会产生环。
    '''
    uf = UnionFind(n)
    for edge in edges:
        x = edge[0]
        y = edge[1]
        if uf.connected(x, y):
            return False
        uf.union(x, y)

    # 只有一个
    return uf.count == 1

【1135】 最低成本联通所有城市

# Kruskal 算法
# 一般来说,我们都是在无向加权图中计算最小生成树的,
# 所以使用最小生成树算法的现实场景中,图的边权重一般代表成本、距离这样的标量
def minimumCost(n, connections):
    '''
    想象⼀下你是个城市基建规划者,地图上有 N 座城市,它们按以 1 到 N 的次序编号。
    给你⼀些可连接的选项 conections,其中每个选项 conections[i] = [city1, city2, cost] 表示
    将城市 city1 和城市 city2 连接所要的成本为 cost(连接是双向的,也就是说城市 city1 和城
    市 city2 相连也同样意味着城市 city2 和城市 city1 相连)。
    计算使得每对城市都连通的最⼩成本。如果根据已知条件⽆法完成该项任务,则请你返回 -1。
    leetcode: 1135. 最低成本联通所有城市
    input: N = 3, conections = [[1,2,5],[1,3,6],[2,3,1]]
    output: 6
    思路:
        1. 最小生成树属性:
            1). 包含图中的所有节点。
            2). 形成的结构是树结构(即不存在环)。
            3). 权重和最小
        2. 将所有边按照权重从小到大排序,从权重最小的边开始遍历
            1). 如果这条边和mst中的其它边不会形成环,则这条边是最小生成树的一部分,将它加入mst集合
            2). 否则,这条边不是最小生成树的一部分,不要把它加入mst集合。
        3. 贪心思路:
            1). 先按照权重排序
            2). 按照权重由小到大遍历,选择没有成环的节点,加入到mst中
    '''
    uf = UnionFind(n + 1)
    connections = sorted(connections, key=lambda x: x[2])
    mst = 0
    for edge in connections:
        u = edge[0]
        v = edge[1]
        weight = edge[2]
        if uf.connected(u, v):
            continue

        mst += weight
        uf.union(u, v)

    # 保证所有节点都被连通 按理说 uf.count == 1 说明所有节点被连通
    # 但因为节点 0 没有被使用,所以 0 会额外占用一个连通分量
    return mst if uf.count == 2 else -1

【1584】 连接所有点的最⼩费⽤

def minCostConnectPoints(points):
    '''
    给你⼀个 points 数组,表示 2D 平⾯上的⼀些点,其中 points[i] = [xi, yi] 。
    连接点 [xi, yi] 和点 [xj, yj] 的费⽤为它们之间的曼哈顿距离:|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。
    请你返回将所有点连接的最⼩总费⽤。只有任意两点之间有且仅有⼀条简单路径时,才认为所有点都已连接。
    leetcode: 1584. 连接所有点的最⼩费⽤
    input: points = [[0,0],[2,2],[3,10],[5,2],[7,0]]
    output: 20
    思路:
        1. 根据曼哈顿距离生成所有的边和权重
        2. 利用kruskal算法求得最小权重
        3.
    '''
    edges = []

    # 生成edges
    for i in range(len(points)):
        for j in range(i, len(points)):
            # 城市与城市之间两两建立关系
            xi, yi = points[i][0], points[i][1]
            xj, yj = points[j][0], points[j][1]
            weight = abs(xi - xj) + abs(yi - yj)
            edges.append([i, j, weight])

    edges = sorted(edges, key=lambda x: x[2])

    uf = UnionFind(len(edges))
    mst = 0
    # kruskal 算法
    for edge in edges:
        u = edge[0]
        v = edge[1]
        weight = edge[2]
        if uf.connected(u, v):
            continue
        mst += weight
        uf.union(u, v)

    return mst

【743】 ⽹络延迟时间

def networkDelayTime(times, n, k):
    '''
    有 n 个⽹络节点,标记为 1 到 n,给你⼀个列表 times,表示信号经过有向边的传递时间。
    times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是⽬标节点,wi 是⼀个信号从源节点传递到⽬标节点的时间。
    现在,从某个节点 K 发出⼀个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1。
    leetcode: 743. ⽹络延迟时间
    input: times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2
    output: 2
    思路:
        1. 问题转换为从节点k到其他所有节点的最短路径中,最长的那条最短路径距离是多少
        2. 为什么是最长的那条路径,因为要到达所有其他节点
        3. 说白了就是算从节点k出发到其他所有节点的最短路径的最大值
    '''
    import math

    class State:
        def __init__(self, _id, dist_from_start):
            self.id = _id
            self.dist_from_start = dist_from_start

        def __lt__(self, other):
            return self.dist_from_start < other.dist_from_start

    def build_graph(n, relationship):
        # 网络节点是从1开始标记的
        graph = [[] for _ in range(n + 1)]

        for r in relationship:
            _from = r[0]
            _to = r[1]
            weight = r[2]
            graph[_from].append([_to, weight])
        return graph

    def dijkstra(start, graph):
        # dist_to[i]的值是起点start到达节点i的最短路径权重
        dist_to = [math.inf for _ in range(len(graph))]
        # base case: start 到 start的值就是0
        dist_to[start] = 0
        # 优先级队列,dist_from_start排在前面
        pq = PriorityQueue()

        # 从起点开始bfs
        pq.put(State(start, 0))

        while not pq.empty():
            cur_state = pq.get_nowait()
            cur_id = cur_state.id
            cur_dist_from_start = cur_state.dist_from_start
            if cur_dist_from_start > dist_to[cur_id]:
                continue

            # 将相邻节点装入队列
            for neighbor in graph[cur_id]:
                next_id = neighbor[0]
                dist_to_next_node = dist_to[cur_id] + neighbor[1]
                # 更新备忘录
                if dist_to[next_id] > dist_to_next_node:
                    dist_to[next_id] = dist_to_next_node
                    pq.put(State(next_id, dist_to_next_node))
        return dist_to

    graph = build_graph(n, times)
    dist_to = dijkstra(k, graph)
    for i in range(1, len(dist_to)):
        if dist_to[i] == math.inf:
            print(-1, dist_to)
            return -1
    #  去掉第0个元素,因为网络是从1开始计数的
    dist_to.pop(0)
    res = max(dist_to)
    print(res)

【514】 概率最⼤的路径

def maxProbability(n, edges, succ_prob, start, end):
    '''
    给你⼀个由 n 个节点(下标从 0 开始)组成的⽆向加权图,该图由⼀个描述边的列表组成,其中 edges[i]
    = [a, b] 表示连接节点 a 和 b 的⼀条⽆向边,且该边遍历成功的概率为 succProb[i]。
    指定两个节点分别作为起点 start 和终点 end,请你找出从起点到终点成功概率最⼤的路径,并返回其成功概率。
    如果不存在从 start 到 end 的路径,请返回 0。只要答案与标准答案的误差不超过 1e-5,就会被视作正确答案。
    leetcode:1514. 概率最⼤的路径
    input:n = 3, edges = [[0,1],[1,2],[0,2]], succProb = [0.5,0.5,0.2], start = 0, end = 2
    output: 0.2500
    解释:从起点到终点有两条路径,其中⼀条的成功概率为 0.2,⽽另⼀条为 0.5 * 0.5 = 0.25
    思路:
        1. 无向图可以转换为双向图
        2. 求概率最大,即相当于求优先队列中最大的值,即定义优先队列的时候,重新定义比较函数
        3.
    '''

    class State:
        def __init__(self, id, prob_from_start):
            self.id = id
            self.prob_from_start = prob_from_start

        def __lt__(self, other):
            return self.prob_from_start > other.prob_from_start

    def build_graph(_n, _edges, _succ_prob):
        graph = [[] for _ in range(_n)]
        for i in range(len(_edges)):
            _from = _edges[i][0]
            _to = _edges[i][1]
            prob = _succ_prob[i]

            graph[_from].append([_to, prob])
            graph[_to].append([_from, prob])
        return graph

    def dijkstra(_start, _end, _graph):
        # probTo[i] 的值就是节点 start 到达节点 i 的最大概率
        prob_to = [-1 for _ in range(len(_graph))]
        # base case:start到start为1
        prob_to[start] = 1
        pq = PriorityQueue()
        pq.put(State(_start, 1))

        while not pq.empty():
            cur_state = pq.get()
            cur_id = cur_state.id
            cur_prob_from_start = cur_state.prob_from_start

            # 由于使用的是优先队列,且较大值在优先出队列,因此,当遇到end时,就可以直接退出
            if cur_id == _end:
                return cur_prob_from_start

            if cur_prob_from_start < prob_to[cur_id]:
                continue

            for neighbor in _graph[cur_id]:
                next_id = neighbor[0]
                prob_to_next_node = prob_to[cur_id] * neighbor[1]
                if prob_to[next_id] < prob_to_next_node:
                    prob_to[next_id] = prob_to_next_node
                    pq.put(State(next_id, prob_to_next_node))

        return 0

    graph = build_graph(n, edges, succ_prob)
    res = dijkstra(start, end, graph)
    print(res)
    return res

【1631】 最⼩体⼒消耗路径

def minimumEffortPath(heights):
    '''
    你准备参加⼀场远⾜活动。给你⼀个⼆维 rows x columns 的地图 heights,其中 heights[row][col] 表示格⼦ (row, col) 的⾼度。
    ⼀开始你在最左上⻆的格⼦ (0, 0),且你希望去最右下⻆的格⼦ (rows-1, columns-1)(注意下标从0开始编号)。
    你每次可以往上,下,左,右 四个⽅向之⼀移动,你想要找到耗费体⼒最⼩的⼀条路径。
    ⼀条路径耗费的体⼒值是路径上相邻格⼦之间⾼度差绝对值的最⼤值决定的。请你返回从左上⻆⾛到右下⻆的最⼩体⼒消耗值
    leetcode: 1631. 最⼩体⼒消耗路径
    input:heights = [[1,2,2],[3,8,2],[5,3,5]]
    output:2
    解释:路径 [1,3,5,3,5] 连续格⼦的差值绝对值最⼤为 2。
         这条路径⽐路径 [1,2,2,2,5] 更优,因为另⼀条路径差值最⼤值为 3。
    思路:
        1.
        2.
        3.
    '''
    dirs = [
        [0, 1],
        [1, 0],
        [0, -1],
        [-1, 0],
    ]

    class State:
        def __init__(self, x, y, effort):
            self.x = x
            self.y = y
            self.effort_from_start = effort

        def __lt__(self, other):
            return self.effort_from_start < other.effort_from_start

    def adjacent(matrix, x, y):
        m, n = len(matrix), len(matrix[0])  # row, col
        neighbors = []
        for _dir in dirs:
            new_x = x + _dir[0]
            new_y = y + _dir[1]
            if new_x >= m or new_x < 0 or new_y >= n or new_y < 0:
                continue
            neighbors.append([new_x, new_y])
        print(x, ", ",  y, ": ", neighbors)
        return neighbors

    def dijkstra(heights):
        import math
        m, n = len(heights), len(heights[0])
        effort_to = [[math.inf for _ in range(n)] for _ in range(m)]
        effort_to[0][0] = 0
        print(effort_to)
        pq = PriorityQueue()
        pq.put(State(0, 0, 0))

        while not pq.empty():
            cur_state = pq.get()
            cur_x = cur_state.x
            cur_y = cur_state.y
            cur_effort_from_start = cur_state.effort_from_start

            if cur_x == m - 1 and cur_y == n - 1:
                return cur_effort_from_start

            if cur_effort_from_start > effort_to[cur_x][cur_y]:
                continue

            for neighbor in adjacent(heights, cur_x, cur_y):
                next_x = neighbor[0]
                next_y = neighbor[1]
                effort_to_next = max(effort_to[cur_x][cur_y], abs(heights[cur_x][cur_y] - heights[next_x][next_y]))

                if effort_to[next_x][next_y] > effort_to_next:
                    effort_to[next_x][next_y] = effort_to_next
                    pq.put(State(next_x, next_y, effort_to_next))

        return -1

    res = dijkstra(heights)
    print(res)
    return res

【测试例】

if __name__ == "__main__":
    # allPathsSourceTarget([[1, 2], [3], [3], []])
    # isBipartite([[1, 2, 3], [0, 2], [0, 1, 3], [0, 2]])
    # isBipartite([[1, 3], [0, 2], [1, 3], [0, 2]])

    # possibleBipartition(4, [[1, 2], [1, 3], [2, 4]])
    # possibleBipartition(3, [[1, 2], [1, 3], [2, 3]])

    # findOrder2(2, [[1, 0]])
    # equationsPossible(["a==b", "b!=a"])

    # print(validTree(5, [[0, 1], [0, 2], [0, 3], [1, 4]]))
    # print(validTree(5, [[0, 1], [1, 2], [2, 3], [1, 3], [1, 4]]))
    # print(minimumCost(3, [[1, 2, 5], [1, 3, 6], [2, 3, 1]]))
    # networkDelayTime([[2, 1, 1], [2, 3, 1], [3, 4, 1]], 4, 2)

    # maxProbability(3, [[0, 1], [1, 2], [0, 2]], [0.5, 0.5, 0.2], 0, 2)
    minimumEffortPath([[1, 2, 2], [3, 8, 2], [5, 3, 5]])

猜你喜欢

转载自blog.csdn.net/u010859970/article/details/122986262
今日推荐