反向建边 + dfs
按题目来每次考虑每个点可以到达点编号最大的点,不如考虑较大的点可以反向到达哪些点
循环从N到1,则每个点i能访问到的结点的A值都是i
每个点访问一次,这个A值就是最优的,因为之后如果再访问到这个结点那么答案肯定没当前大了
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
#define MAXL 100010 // 最大节点数
int N, M; // 节点数和边数
int A[MAXL]; // 结果数组,A[x] 表示从节点 x 出发能到达的编号最大的点
vector<int> G[MAXL]; // 邻接表,用于存储图
// 深度优先搜索函数
void dfs(int x, int d) {
if (A[x]) return; // 如果节点 x 已经访问过,直接返回
A[x] = d; // 更新节点 x 的结果为 d,表示从 d 出发可以到达 x
for (int i = 0; i < G[x].size(); i++) { // 遍历节点 x 的所有邻接点 反向
dfs(G[x][i], d); // 递归处理邻接点
}
}
int main() {
int u, v; // 边的两个端点
scanf("%d%d", &N, &M); // 输入节点数 N 和边数 M
// 读入边并构建反向图
for (int i = 1; i <= M; i++) {
scanf("%d%d", &u, &v); // 输入边 (u, v)
G[v].push_back(u); // 反向建边,将 v 指向 u
}
// 从编号最大的节点依次进行 DFS
for (int i = N; i > 0; i--) {
dfs(i, i); // 从节点 i 出发,更新所有能到达的节点
}
// 输出结果
for (int i = 1; i <= N; i++) {
printf("%d ", A[i]); // 输出从 1 到 N 的结果
}
printf("\n"); // 换行
return 0;
}
这是一个经典的 拓扑排序 + 动态规划 问题。可以将每个杂务视为一个节点,准备工作视为有向边,然后通过拓扑排序确定每个节点的完成顺序,最后使用动态规划求出完成所有工作的最短时间。以下是用 C++ 实现该问题的完整代码。
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
// 定义最大节点数
const int MAXN = 10000;
vector<int> adj[MAXN + 1]; // 邻接表存储依赖关系,adj[i] 表示 i 的所有后继任务
int in_degree[MAXN + 1]; // 每个节点的入度,表示还有多少依赖任务未完成
int time_needed[MAXN + 1]; // 每个任务所需的完成时间
int dp[MAXN + 1]; // dp[i] 表示完成到第 i 个任务的最短时间
int main() {
int n;
cin >> n; // 输入任务数
// 输入每个任务的信息
for (int i = 1; i <= n; ++i) {
int task, time, dep;
cin >> task >> time; // 任务编号和所需时间
time_needed[task] = time; // 保存任务所需时间
// 输入当前任务的所有依赖任务
while (cin >> dep && dep != 0) {
adj[dep].push_back(task); // 从依赖任务到当前任务的有向边
++in_degree[task]; // 当前任务的入度加 1
}
}
// 使用队列进行拓扑排序
queue<int> q;
for (int i = 1; i <= n; ++i) {
// 将所有入度为 0 的任务加入队列(可以直接开始的任务)
if (in_degree[i] == 0) {
q.push(i);
dp[i] = time_needed[i]; // 初始任务的最短时间就是自身所需时间
}
}
// 拓扑排序处理任务
while (!q.empty()) {
int cur = q.front(); // 当前处理的任务
q.pop();
// 遍历当前任务的所有后继任务
for (int next : adj[cur]) {
// 更新后继任务的最短完成时间
dp[next] = max(dp[next], dp[cur] + time_needed[next]);
// 将后继任务的入度减 1
if (--in_degree[next] == 0) {
// 如果后继任务的入度变为 0,加入队列
q.push(next);
}
}
}
// 最后计算所有任务的最短完成时间
int result = 0;
for (int i = 1; i <= n; ++i) {
result = max(result, dp[i]); // 找到最大的 dp 值
}
cout << result << endl; // 输出结果
return 0;
}
关键逻辑详解
-
任务依赖建图
- 使用邻接表
adj
存储任务的依赖关系,每个任务指向它的后续任务。 - 入度数组
in_degree
记录每个任务的依赖任务数。
- 使用邻接表
-
拓扑排序
- 使用队列处理入度为 0 的任务,表示当前可以直接完成的任务。
- 完成一个任务后,将其后续任务的入度减 1;如果后续任务的入度变为 0,则加入队列。
-
动态规划更新最短完成时间
dp[i]
表示完成任务 i 的最短时间。- 更新公式:
dp[next] = max(dp[next], dp[cur] + time_needed[next])
,即后续任务的完成时间至少是当前任务完成时间加上后续任务所需时间。
-
结果计算
- 最后取所有
dp[i]
的最大值,表示完成所有任务的最短时间。
- 最后取所有
样例输入
7
1 5 0
2 2 1 0
3 3 2 0
4 6 1 0
5 1 2 4 0
6 8 2 4 0
7 4 3 5 6 0
算法逻辑
第 1 步:初始化队列
将入度为 0 的任务加入队列(这些任务可以直接开始完成):
初始队列:[1]
初始 dp:dp[1] = 5
(任务 1 自身时间)
第 2 步:拓扑排序和动态规划更新
依次从队列中取出任务,更新其后续任务的完成时间。
处理过程:
-
取出任务
1
:- 后续任务:
2
,4
- 更新后续任务的 dp:
dp[2] = max(dp[2], dp[1] + time_needed[2]) = 5 + 2 = 7
dp[4] = max(dp[4], dp[1] + time_needed[4]) = 5 + 6 = 11
- 入度更新:
in_degree[2] = 0
→ 加入队列in_degree[4] = 0
→ 加入队列
队列:
[2, 4]
- 后续任务:
-
取出任务
2
:- 后续任务:
3
,5
,6
- 更新后续任务的 dp:
dp[3] = max(dp[3], dp[2] + time_needed[3]) = 7 + 3 = 10
dp[5] = max(dp[5], dp[2] + time_needed[5]) = 7 + 1 = 8
dp[6] = max(dp[6], dp[2] + time_needed[6]) = 7 + 8 = 15
- 入度更新:
in_degree[3] = 0
→ 加入队列in_degree[5] = 1
in_degree[6] = 1
队列:
[4, 3]
- 后续任务:
-
取出任务
4
:- 后续任务:
5
,6
- 更新后续任务的 dp:
dp[5] = max(dp[5], dp[4] + time_needed[5]) = 11 + 1 = 12
dp[6] = max(dp[6], dp[4] + time_needed[6]) = 11 + 8 = 19
- 入度更新:
in_degree[5] = 0
→ 加入队列in_degree[6] = 0
→ 加入队列
队列:
[3, 5, 6]
- 后续任务:
-
后面的类似
解题思路
-
反向建图:将所有有向边反向,变成从目标节点指向源节点的边。这样可以方便地从每个奶牛所在的牧场出发,找到所有可以到达的节点。
-
多源 BFS:对于每头奶牛所在的牧场,利用 BFS 或 DFS 遍历所有可到达的节点,同时记录每个节点被多少头奶牛访问过。
-
统计结果:如果某个节点被所有奶牛访问过(即计数等于奶牛数量 K),则该节点为可行的聚集地点。
#include <iostream>
#include <vector>
#include <queue>
#include <cstring>
using namespace std;
const int MAXN = 1000; // 最大牧场数
const int MAXK = 100; // 最大奶牛数
// 反向图的邻接表
vector<int> reverse_graph[MAXN + 1];
// 每个牧场被访问的奶牛数量
int visit_count[MAXN + 1];
// 使用 BFS 遍历从某个起始点可以到达的所有节点
void bfs(int start) {
// 记录当前奶牛访问时是否已经到达某节点
bool visited[MAXN + 1] = {false};
queue<int> q;
// 初始化 BFS 队列
q.push(start);
visited[start] = true;
// 开始 BFS
while (!q.empty()) {
int cur = q.front();
q.pop();
// 更新当前节点被访问的次数
visit_count[cur]++;
// 遍历反向图中从当前节点可以到达的所有邻居节点
for (int next : reverse_graph[cur]) {
if (!visited[next]) { // 如果邻居节点未被访问
visited[next] = true;
q.push(next);
}
}
}
}
int main() {
int K, N, M; // K:奶牛数, N:牧场数, M:路径数
cin >> K >> N >> M;
vector<int> cow_start(K); // 每头奶牛所在的初始牧场
// 输入奶牛的初始位置
for (int i = 0; i < K; ++i) {
cin >> cow_start[i];
}
// 输入路径,并反向建图
for (int i = 0; i < M; ++i) {
int A, B;
cin >> A >> B;
reverse_graph[B].push_back(A); // 反向建图:从 B 指向 A
}
// 初始化访问计数
memset(visit_count, 0, sizeof(visit_count));
// 对每头奶牛的初始牧场执行 BFS
for (int cow : cow_start) {
bfs(cow);
}
// 统计被所有奶牛访问的牧场数
int result = 0;
for (int i = 1; i <= N; ++i) { // 遍历所有牧场
if (visit_count[i] == K) { // 如果某牧场被 K 头奶牛访问
result++;
}
}
// 输出结果
cout << result << endl;
return 0;
}
输入
2 4 4
2
3
1 2
1 4
2 3
3 4
数据解读
-
奶牛初始位置:
- 奶牛 1 在牧场 2
- 奶牛 2 在牧场 3
-
路径(反向建图):
- 原始路径 1→2,1→4,2→3,3→4
- 反向图:
- 2→1
- 4→1
- 3→2
- 4→3
运行过程
-
奶牛 1 的 BFS:
- 从节点 2 出发,访问节点:2 → 3 → 4。
-
奶牛 2 的 BFS:
- 从节点 3 出发,访问节点:3 → 4。
-
统计结果:从每头牛开始遍历,把他经过的牧场访问+1
- 节点 3 和节点 4 被两头奶牛访问,因此是可行的聚集地点。