LeetCode 1857. Largest Color Value in a Directed Graph【拓扑排序,动态规划】困难

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

给你一个 有向图 ,它含有 n 个节点和 m 条边。节点编号从 0 到 n - 1 。

给你一个字符串 colors ,其中 colors[i] 是小写英文字母,表示图中第 i 个节点的 颜色 (下标从 0 开始)。同时给你一个二维数组 edges ,其中 edges[j] = [aj, bj] 表示从节点 aj 到节点 bj 有一条 有向边 。

图中一条有效 路径 是一个点序列 x1 -> x2 -> x3 -> ... -> xk ,对于所有 1 <= i < k ,从 xi 到 xi+1 在图中有一条有向边。路径的 颜色值 是路径中 出现次数最多 颜色的节点数目。

请你返回给定图中有效路径里面的 最大颜色值  如果图中含有环,请返回 -1 。

示例 1:
LeetCode 1857. Largest Color Value in a Directed Graph【拓扑排序,动态规划】困难_第1张图片

输入:colors = "abaca", edges = [[0,1],[0,2],[2,3],[3,4]]
输出:3
解释:路径 0 -> 2 -> 3 -> 4 含有 3 个颜色为 "a" 的节点(上图中的红色节点)。

示例 2:
LeetCode 1857. Largest Color Value in a Directed Graph【拓扑排序,动态规划】困难_第2张图片

输入:colors = "a", edges = [[0,0]]
输出:-1
解释:从 00 有一个环。

提示:

  • n == colors.length
  • m == edges.length
  • 1 <= n <= 10^5
  • 0 <= m <= 10^5
  • colors 只含有小写英文字母。
  • 0 <= aj, bj < n

解法 拓扑排序 + 动态规划

我们需要求出的答案等价于:对于一种颜色 c c c ,以及一条路径 path \textit{path} path ,其中颜色为 c c c 的节点有 path c \textit{path}_c pathc。我们希望挑选 c c c 以及 p a t h path path ,使得 path c \textit{path}_c pathc 的值最大。

我们可以枚举颜色 c c c ,随后选出可以使得 path c \textit{path}_c pathc 达到最大值的 path \textit{path} path 。这些 path c \textit{path}_c pathc 中的最大值即为答案。

  • 如果给定的有向图包含环,那么它不存在拓扑排序。
  • 如果给定的有向图不包含环,那么这个有向图是一个「有向无环图」,它一定存在拓扑排序。
    根据拓扑排序的性质,如果节点 a a a 有一条有向边指向节点 b b b ,那么 b b b 在拓扑排序中一定出现在 a a a 之后。因此,一条有效路径上点的顺序与它们在拓扑排序中出现的顺序是一致的

我们可以根据拓扑排序来进行动态规划。设 d p [ v ] [ c ] dp[v][c] dp[v][c] 表示以节点 v v v 为终点的所有有效路径中,包含颜色 c c c 的节点数量的最大值。在进行状态转移时,我们考虑所有 v v v 的前驱节点(即有一条有向边指向 v v v 的节点) prev v \textit{prev}_v prevv
d p [ v ] [ c ] = ( max ⁡ u ∈ prev j d p [ u ] [ c ] ) + I ( v , c ) dp[v] [c] = \left( \max_{u \in \textit{prev}_j} dp [u][c] \right) + \mathbb{I}(v, c) dp[v][c]=(uprevjmaxdp[u][c])+I(v,c)
即找出前驱节点中包含颜色 c c c 的节点数量最多的那个节点进行转移,并且如果 v v v 本身的颜色为 c c c d p [ v ] [ c ] dp[v][c] dp[v][c] 的值就增加 1 1 1 。这里 I ( v , c ) \mathbb{I}(v, c) I(v,c) 为示性函数,当节点 v v v 的颜色为 c c c 时,函数值为 1 1 1 ,否则为 0 0 0 。那么 path c \textit{path}_c pathc 的最大值即为 d p [ v ] [ c ] dp[v][ c] dp[v][c] 中的最大值。

我们可以将状态转移,融入使用广度优先搜索的方法求解拓扑排序的过程中。当遍历到节点 u u u 时:

  • 如果 u u u 的颜色为 c c c ,那么将 d p [ u ] [ c ] dp[u] [c] dp[u][c] 的值增加 1 1 1
  • 枚举 u u u 的所有后继节点(即从 u u u 出发经过一条有向边可以到达的节点),对于后继节点 v v v ,将 d p [ v ] [ c ] dp[v][c] dp[v][c] 更新为其与 d p [ u ] [ c ] dp[u][c] dp[u][c] 的较大值。

这样的操作与上文描述的状态转移方程是一致的。它的好处在于,如果使用广度优先搜索的方法求解拓扑排序,那么我们需要使用邻接表存储所有的有向边,而上文的动态规划是通过「枚举 v → v → v 枚举前驱节点 u u u 」进行状态转移的,这就需要我们额外存储所有边的反向边,才能通过 v v v 找到所有的前驱节点。如果我们通过「枚举 u → u \to u 枚举后继节点 v v v 」进行状态转移,这样就与拓扑排序存储的边保持一致了。

class Solution {
public:
    int largestPathValue(string colors, vector<vector<int>>& edges) {
        int n = colors.size();
        vector<int> g[n];
        vector<int> ind(n); // 节点的入度统计,用于找出拓扑排序中最开始的节点
        for (vector<int>& e : edges) {
            g[e[0]].push_back(e[1]);
            ++ind[e[1]];
        }
        vector<array<int, 26>> dp(n);
        int found = 0, ans = 0;
        queue<int> q;
        for (int i = 0; i < n; ++i) if (ind[i] == 0) q.push(i); 
        while (!q.empty()) {
            int u = q.front(); q.pop();
            ++found;
            ++dp[u][colors[u] - 'a']; // 节点u对应的颜色+1
            ans = max(ans, dp[u][colors[u] - 'a']);
            for (int v : g[u]) {
                for (int c = 0; c < 26; ++c)
                    dp[v][c] = max(dp[u][c], dp[v][c]);
                if (--ind[v] == 0) q.push(v);
            } 
        } 
        return found != n ? -1 : ans;
    }
};

复杂度分析:

  • 时间复杂度: O ( n + m ∣ Σ ∣ ) O(n+m |\Sigma |) O(n+m∣Σ∣),其中 ∣ Σ ∣ |\Sigma | ∣Σ∣ 表示颜色的数量,在本题中 ∣ Σ ∣ = 26 |\Sigma |=26 ∣Σ∣=26
    一般的拓扑排序需要的时间为 O ( n + m ) O(n+m) O(n+m)。而在本题中在拓扑排序的过程中加入了状态转移,由于一条有向边对应着 ∣ Σ ∣ |\Sigma | ∣Σ∣ 次状态转移,因此拓扑排序的时间复杂度实际为 O ( n + m ∣ Σ ∣ ) O(n + m|\Sigma|) O(n+m∣Σ∣)
  • 空间复杂度: O ( n ∣ Σ ∣ + m ) O(n|\Sigma| + m) O(n∣Σ∣+m) 。我们需要 O ( n ∣ Σ ∣ ) O(n |\Sigma|) O(n∣Σ∣) 的空间存储对应数量的状态;我们需要 O ( n + m ) O(n+m) O(n+m) 的空间存储邻接表;我们需要 O ( n ) O(n) O(n) 的队列空间进行拓扑排序。将它们相加,即可得到总时间复杂度为 O ( n ∣ Σ ∣ + m ) O(n |\Sigma| + m) O(n∣Σ∣+m)

你可能感兴趣的:(#,拓扑排序,动态规划,leetcode,动态规划,算法)