文章目录
- 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>
:
这说明在不使用动态数组的情况下,固定大小的静态数组使用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)中找到从一个起点到终点的最长路径。
解决方案:
- 使用拓扑排序确定处理节点的顺序。
- 按照拓扑序进行动态规划,计算每个节点的最长路径长度。
2. 最短路径问题(DAG)
问题描述 :
在一个有向无环图(DAG)中找到从一个起点到终点的最短路径。
解决方案:
- 使用拓扑排序确定处理节点的顺序。
- 按照拓扑序进行动态规划,计算每个节点的最短路径长度。
3. 最大路径和问题
问题描述 :
在一个有向无环图(DAG)中找到从起点到终点的路径中权重总和最大的路径。
解决方案:
- 使用拓扑排序确定处理节点的顺序。
- 按拓扑序进行动态规划,计算每个节点的路径权重总和。
4. 路径计数问题
问题描述 :
计算从起始点到终点的所有可能路径的数量。
解决方案:
- 使用拓扑排序确定处理节点的顺序。
- 按拓扑序计算从起始点到每个节点的路径数量。
5. 关键路径法(Critical Path Method, CPM)
问题描述 :
在项目管理中,给定一组任务及其依赖关系,找出项目的关键路径和项目的最短完成时间。
解决方案:
- 使用拓扑排序确定任务的处理顺序。
- 按拓扑序进行动态规划,计算每个任务的最早开始时间和最晚开始时间,从而确定关键路径。
6. DAG上的单源最短路径(Single Source Shortest Path in DAG)
问题描述 :
在一个有向无环图(DAG)中找到从一个起点到所有其他节点的最短路径。
解决方案:
- 使用拓扑排序确定处理节点的顺序。
- 按拓扑序进行动态规划,计算从起点到每个节点的最短路径长度。
7. 有向无环图中的最大子序列和问题
问题描述 :
在一个有向无环图(DAG)中找到从起点到终点的最大子序列和。
解决方案:
- 使用拓扑排序确定处理节点的顺序。
- 按拓扑序进行动态规划,计算每个节点的最大子序列和。
8. DAG中的最长递增子序列问题
问题描述 :
在一个有向无环图(DAG)中找到从起点到终点的最长递增子序列。
解决方案:
- 使用拓扑排序确定处理节点的顺序。
- 按拓扑序进行动态规划,计算每个节点的最长递增子序列长度。
9. 资源分配问题(DAG)
问题描述 :
在一个有向无环图(DAG)中,给定每个节点的资源需求和资源量,计算从起点到终点的最大资源分配路径。
解决方案:
- 使用拓扑排序确定处理节点的顺序。
- 按拓扑序进行动态规划,计算每个节点的最大资源分配路径。