图论:1857. 有向图中最大颜色值(拓扑排序+动态规划)

文章目录

  • 1.问题分析
  • 2.代码解析
    • [2.1 代码步骤](#2.1 代码步骤)
      • [1. 初始化数据结构](#1. 初始化数据结构)
      • [2. 构建图和入度数组](#2. 构建图和入度数组)
      • [3. 初始化队列](#3. 初始化队列)
      • [4. 拓扑排序和动态规划](#4. 拓扑排序和动态规划)
      • [5. 检查是否存在环并返回结果](#5. 检查是否存在环并返回结果)
  • [3. 问题扩展](#3. 问题扩展)
      • [1. 最长路径问题(DAG)](#1. 最长路径问题(DAG))
      • [2. 最短路径问题(DAG)](#2. 最短路径问题(DAG))
      • [3. 最大路径和问题](#3. 最大路径和问题)
      • [4. 路径计数问题](#4. 路径计数问题)
      • [5. 关键路径法(Critical Path Method, CPM)](#5. 关键路径法(Critical Path Method, CPM))
      • [6. DAG上的单源最短路径(Single Source Shortest Path in DAG)](#6. DAG上的单源最短路径(Single Source Shortest Path in DAG))
      • [7. 有向无环图中的最大子序列和问题](#7. 有向无环图中的最大子序列和问题)
      • [8. DAG中的最长递增子序列问题](#8. DAG中的最长递增子序列问题)
      • [9. 资源分配问题(DAG)](#9. 资源分配问题(DAG))

LeetCode:1857. 有向图中最大颜色值

本题乍一看和求所有路径中的最长路径没啥区别,直接暴力枚举所有路径,但是时间复杂度不允许我们这样做。

1.问题分析

数据结构:图的拓扑排序与关键路径

其实关键路径就是使用了动态规划解法,它先将有向无环图进行拓扑排序,然后按照拓扑序进行动态规划。

  • 有向无环图一定存在拓扑排序
  • 按照拓扑序顺序遍历,每次更新该结点的后继,按照这个方法,遍历到某结点时一定能够保证其前驱都已经遍历过并且进行过更新。我们并不需要关心具体的顺序是什么,但一定有边 < u , v > <u,v> <u,v>, u u u的状态能够更新 v v v。

因此本题也可以使用拓扑排序+动态规划

2.代码解析

我们定义一个 d p [ i ] [ c ] dp[i][c] dp[i][c]表示第 i i i个节点的颜色 c c c。

则必有: d p [ i ] [ c ] = m a x ( d p [ i p r e ] [ c ] ) + I ( i p r e , i ) dp[i][c] = max(dp[i_{pre}][c]) + I(i_{pre},i) dp[i][c]=max(dp[ipre][c])+I(ipre,i)

  • 求到达第 i i i个节点时 能够包含的最多颜色 c c c的个数,等价于到达其前驱 i p r e i_{pre} ipre能够包含的最多颜色 c c c的个数再一步到达 i i i包含的个数。
cpp 复制代码
class Solution {
public:
    int largestPathValue(string colors, vector<vector<int>>& edges) {
        vector<vector<int>> dp(colors.size(), vector<int>(26, 0));
        vector<int> inDegrees(colors.size(), 0);
        vector<vector<int>> graph(colors.size());

        for(int i = 0; i < edges.size(); ++i){
            inDegrees[edges[i][1]] ++;
            graph[edges[i][0]].emplace_back(edges[i][1]);
        }

        vector<int> topo;//拓扑序
        queue<int> q;
        for(int i = 0; i < colors.size(); ++ i){
            if(inDegrees[i] == 0){
                q.push(i);
            }
        }

        while(!q.empty()){
            int u = q.front(); q.pop();
            topo.emplace_back(u);
            for(auto & v : graph[u]){
                -- inDegrees[v];
                if(inDegrees[v] == 0) {q.push(v);}
            }
        }

        int ans = 0;
        for(auto & u : topo){
            for(int i = 0; i < 26; ++ i){
                if(colors[u] == 'a' + i) dp[u][i] ++;
                ans = max(ans, dp[u][i]);
                for(auto & v : graph[u]){
                    dp[v][i] = max(dp[u][i], dp[v][i]);
                }
            }
        }

        if(topo.size() != colors.size()) return -1;
        return ans;
    }
};

可以进行进一步优化:

(1)拓扑排序的过程中更新,这样就不用求出拓扑序了,因为排序的过程中就是拓扑序了,所以边排序边更新状态。

(2)ans的求解使用 a n s = m a x ( a n s , d p [ u ] [ c o l o r s [ u ] − ′ a ′ ] ) ans = max(ans, dp[u][colors[u] - 'a']) ans=max(ans,dp[u][colors[u]−′a′]),原因在于,任何一个颜色最大路径,该颜色的最后一个结点都会被遍历到,用该结点就能求出最大值。

(3)由于是固定大小的数组,直接使用array即可。(这是加速的关键)

使用vector<int>(26, 0)

使用array<int, 26>

这说明在不使用动态数组的情况下,固定大小的静态数组使用arrayvector快很多。

cpp 复制代码
class Solution {
public:
    int largestPathValue(string colors, vector<vector<int>>& edges) {
        vector<array<int, 26>> dp(colors.size());
        vector<int> inDegrees(colors.size(), 0);
        vector<vector<int>> graph(colors.size());

        for(int i = 0; i < edges.size(); ++i){
            inDegrees[edges[i][1]] ++;
            graph[edges[i][0]].emplace_back(edges[i][1]);
        }

        queue<int> q;
        for(int i = 0; i < colors.size(); ++ i){
            if(inDegrees[i] == 0){
                q.push(i);
            }
        }

        int ans = 0;
        int topo = 0;
        while(!q.empty()){
            int u = q.front(); q.pop();
            topo ++;
            
            dp[u][colors[u] - 'a'] ++;
            ans = max(ans, dp[u][colors[u] - 'a']);
            for(auto & v : graph[u]){
                -- inDegrees[v];
                if(inDegrees[v] == 0) {q.push(v);}
                for(int i = 0; i < 26; ++ i){
                    dp[v][i] = max(dp[u][i], dp[v][i]);
                }
            }
        }

        if(topo != colors.size()) return -1;
        return ans;
    }
};

好的,让我们详细解释这段代码。该代码的目的是解决一个有向图中的最大路径值问题,其中每个节点都有一个颜色。目标是找到从图的起点到终点路径中某种颜色出现最多的次数。

2.1 代码步骤

1. 初始化数据结构

cpp 复制代码
vector<array<int, 26>> dp(colors.size());
vector<int> inDegrees(colors.size(), 0);
vector<vector<int>> graph(colors.size());
  • dp:一个二维数组,dp[i][j] 表示从起点到节点 i 的路径中颜色 j(用0到25表示)的最大出现次数。
  • inDegrees:记录每个节点的入度。
  • graph:表示图的邻接表。

2. 构建图和入度数组

cpp 复制代码
for(int i = 0; i < edges.size(); ++i){
    inDegrees[edges[i][1]]++;
    graph[edges[i][0]].emplace_back(edges[i][1]);
}
  • 遍历 edges,填充 inDegreesgraph
  • 对于每一条边 (u, v),增加 v 的入度,并在 graph[u] 中添加 v

3. 初始化队列

cpp 复制代码
queue<int> q;
for(int i = 0; i < colors.size(); ++i){
    if(inDegrees[i] == 0){
        q.push(i);
    }
}
  • 初始化一个队列 q,将所有入度为 0 的节点入队。这些节点作为拓扑排序的起点。

4. 拓扑排序和动态规划

cpp 复制代码
int ans = 0;
int topo = 0;
while(!q.empty()){
    int u = q.front(); q.pop();
    topo++;
    
    dp[u][colors[u] - 'a']++;
    ans = max(ans, dp[u][colors[u] - 'a']);
    for(auto & v : graph[u]){
        --inDegrees[v];
        if(inDegrees[v] == 0) { q.push(v); }
        for(int i = 0; i < 26; ++i){
            dp[v][i] = max(dp[u][i], dp[v][i]);
        }
    }
}
  • ans:记录路径上颜色出现的最大次数。
  • topo:记录拓扑排序的节点数量,用于检测是否存在环。

主要逻辑

  • 取队首节点 u,更新 topo
  • 更新 dp[u][colors[u] - 'a'],表示节点 u 的颜色出现次数增加。
  • 更新 ans 为当前颜色出现的最大次数。
  • 遍历 u 的邻接节点 v
    • 减少 v 的入度。
    • 如果 v 的入度为 0,入队 q
    • 更新 dp[v],根据从 uv 的路径更新 v 的颜色出现次数。

5. 检查是否存在环并返回结果

cpp 复制代码
if(topo != colors.size()) return -1;
return ans;
  • 如果拓扑排序遍历的节点数量不等于 colors 的长度,说明图中存在环,返回 -1。
  • 否则,返回 ans,即路径上某种颜色的最大出现次数。

3. 问题扩展

1. 最长路径问题(DAG)

问题描述

在一个有向无环图(DAG)中找到从一个起点到终点的最长路径。

解决方案

  1. 使用拓扑排序确定处理节点的顺序。
  2. 按照拓扑序进行动态规划,计算每个节点的最长路径长度。

2. 最短路径问题(DAG)

问题描述

在一个有向无环图(DAG)中找到从一个起点到终点的最短路径。

解决方案

  1. 使用拓扑排序确定处理节点的顺序。
  2. 按照拓扑序进行动态规划,计算每个节点的最短路径长度。

3. 最大路径和问题

问题描述

在一个有向无环图(DAG)中找到从起点到终点的路径中权重总和最大的路径。

解决方案

  1. 使用拓扑排序确定处理节点的顺序。
  2. 按拓扑序进行动态规划,计算每个节点的路径权重总和。

4. 路径计数问题

问题描述

计算从起始点到终点的所有可能路径的数量。

解决方案

  1. 使用拓扑排序确定处理节点的顺序。
  2. 按拓扑序计算从起始点到每个节点的路径数量。

5. 关键路径法(Critical Path Method, CPM)

问题描述

在项目管理中,给定一组任务及其依赖关系,找出项目的关键路径和项目的最短完成时间。

解决方案

  1. 使用拓扑排序确定任务的处理顺序。
  2. 按拓扑序进行动态规划,计算每个任务的最早开始时间和最晚开始时间,从而确定关键路径。

6. DAG上的单源最短路径(Single Source Shortest Path in DAG)

问题描述

在一个有向无环图(DAG)中找到从一个起点到所有其他节点的最短路径。

解决方案

  1. 使用拓扑排序确定处理节点的顺序。
  2. 按拓扑序进行动态规划,计算从起点到每个节点的最短路径长度。

7. 有向无环图中的最大子序列和问题

问题描述

在一个有向无环图(DAG)中找到从起点到终点的最大子序列和。

解决方案

  1. 使用拓扑排序确定处理节点的顺序。
  2. 按拓扑序进行动态规划,计算每个节点的最大子序列和。

8. DAG中的最长递增子序列问题

问题描述

在一个有向无环图(DAG)中找到从起点到终点的最长递增子序列。

解决方案

  1. 使用拓扑排序确定处理节点的顺序。
  2. 按拓扑序进行动态规划,计算每个节点的最长递增子序列长度。

9. 资源分配问题(DAG)

问题描述

在一个有向无环图(DAG)中,给定每个节点的资源需求和资源量,计算从起点到终点的最大资源分配路径。

解决方案

  1. 使用拓扑排序确定处理节点的顺序。
  2. 按拓扑序进行动态规划,计算每个节点的最大资源分配路径。
相关推荐
AIAdvocate13 小时前
力扣-96.不同的二叉搜索树 题目详解
python·算法·动态规划
GZK.16 小时前
【Leetcode】70. 爬楼梯
算法·leetcode·动态规划
会敲代码的小张1 天前
设计模式-观察者模式
java·开发语言·后端·观察者模式·设计模式·代理模式
源代码•宸2 天前
Leetcode—740. 删除并获得点数【中等】(unordered_map+set+sort)
c++·经验分享·算法·leetcode·动态规划·哈希表
闻缺陷则喜何志丹2 天前
【C++前后缀分解 动态规划】2100. 适合野炊的日子|1702
c++·算法·动态规划·力扣·前后缀分解·日子·适合
逝去的秋风2 天前
【代码随想录训练营第42期 Day57打卡 - 图论Part7 - Prim算法与Kruskal算法
算法·图论·prim算法
jingling5552 天前
后端开发刷题 | 最长上升子序列
java·开发语言·数据结构·后端·算法·动态规划
jingling5552 天前
后端开发刷题 | 打家劫舍
java·开发语言·数据结构·算法·动态规划
小陶 coding3 天前
代码随想录刷题day32丨动态规划理论基础,509. 斐波那契数, 70. 爬楼梯, 746. 使用最小花费爬楼梯
java·数据结构·算法·leetcode·动态规划
应茶茶3 天前
动态规划算法:05.路径问题_不同路径_C++
c++·算法·动态规划