图论: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 dpic dpic表示第 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 ) dpic = max(dpi_{pre}c) + I(i_{pre},i) dpic=max(dpiprec)+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, dpucolors\[u - 'a']) ans=max(ans,dpucolors\[u−′a′]),原因在于,任何一个颜色最大路径,该颜色的最后一个结点都会被遍历到,用该结点就能求出最大值。

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

使用vector<int>(26, 0):

使用array<int, 26>:

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

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,填充 inDegrees 和 graph。
  • 对于每一条边 (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],根据从 u 到 v 的路径更新 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. 按拓扑序进行动态规划,计算每个节点的最大资源分配路径。
相关推荐
一只小小的芙厨7 小时前
【线性DP】
学习·算法·动态规划
晒太羊的猫1 天前
hot100动态规划刷题
算法·动态规划
步行cgn1 天前
GoF 代理模式详解
代理模式
Logic1011 天前
C语言/数据结构动态规划题解:爬楼梯(含坏台阶)——避开障碍物的路径计数
c语言·数据结构·动态规划·时间复杂度·空间复杂度·算法题·爬楼梯
如何原谅奋力过但无声2 天前
【灵神高频面试题合集21-25】动态规划(下)
算法·动态规划
lvwangshu2 天前
CF813D Two Melodies 题解
动态规划·题解·技巧·性质题·思维好题
漂流瓶jz2 天前
UVA-11491 奖品的价值 题解答案代码 算法竞赛入门经典第二版
c++·算法·图论·题解·aoapc·算法竞赛入门经典·uva
longlongzihan2 天前
从LeetCode 70. 爬楼梯看动态规划:如何精准推导状态转移方程?
c++·算法·leetcode·动态规划
Coder-magician2 天前
《代码随想录》刷题打卡day47:并查集-part01
数据结构·算法·图论
Rnan-prince2 天前
LeetCode 股票买卖系列:一个动态规划状态机模板通吃四题
leetcode·动态规划