通过 Tarjan 缩点将强连通分量内的边转化为完全采摘的点权,跨分量的边化为单次通行的有向边,再在 DAG 上利用拓扑排序求最长路。
OJ: luogu
题目 ID: P2656
难度:普及+/提高-
标签:图论Tarjan强连通分量拓扑排序动态规划
创建: 2026-10-23 23:04
更新: 2026-10-23 23:27
形式化题目
给定一个包含
- 获得当前边上所有的蘑菇收益
; - 该边的蘑菇数更新为
。
求从起点
暴力解法
思路
针对小规模数据,可以直接采用带回溯的深度优先搜索(DFS)在图上模拟游走。
每次尝试走一条出边,累加采摘到的蘑菇,将该边当前的蘑菇数衰减,并递归到下一节点。
因为
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-23 23:20
* update_at: 2026-10-23 23:20
*/
// brute.cpp:小数据暴力解法
// 针对小规模图,利用带剪枝的深度优先搜索在图上模拟采蘑菇过程。
// 只适用于点数极小(N <= 5)且边数极少的情况,步数上限设为 25。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 10;
const int MAXM = 30;
int n, m, s;
struct Edge {
int u, v;
ll w;
int k;
};
Edge edges[MAXM];
vector<int> g[MAXN];
ll ans = 0;
void dfs(int u, ll sum, int step, int zero_gain_steps) {
if (sum > ans) {
ans = sum;
}
if (step >= 25) return;
if (zero_gain_steps >= 4) return;
for (int e_idx : g[u]) {
int v = edges[e_idx].v;
ll old_w = edges[e_idx].w;
ll gain = old_w;
edges[e_idx].w = (old_w * edges[e_idx].k) / 10;
dfs(v, sum + gain, step + 1, (gain == 0 ? zero_gain_steps + 1 : 0));
edges[e_idx].w = old_w;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
for (int i = 1; i <= m; ++i) {
int u, v;
ll w;
double coef;
cin >> u >> v >> w >> coef;
edges[i] = {u, v, w, (int)round(coef * 10)};
g[u].push_back(i);
}
cin >> s;
dfs(s, 0, 0, 0);
cout << ans << "\n";
return 0;
}复杂度
- 时间复杂度:搜索树的分支因子取决于顶点的出度,状态空间随搜索步数呈指数级爆炸,最坏情况下为
(其中 为平均出度, 为搜索截断步数)。 - 空间复杂度:递归栈及图存储占用
空间。
瓶颈
暴力的核心瓶颈在于:由于有向图中存在环路,且走同一条边多次会有连续衰减收益,搜索树深度大、状态数量庞大,无法处理
正解
思路
我们可以抓住图的结构特征与收益规律:
-
单条边的全收益: 若一条初始蘑菇数为
、衰减系数为 的边被反复经过,其最大可能获得的收益是确定的: 因为,衰减极快,直接循环数次即可求得该和式。 -
强连通分量(SCC)内的边: 在同一个强连通分量内,任意两点双向可达。因此只要进入了某个强连通分量,我们总可以通过在环路上往返走动,将该分量内部所有边的蘑菇完全采光(全部采到 0),并且最后停留在该分量内的任意一个出口节点。 因此,我们可以用 Tarjan 算法 进行缩点,将每个强连通分量缩成一个“超级点”,其点权等于该分量内部所有边的全收益之和
。 -
跨强连通分量的边: 对于连接两个不同强连通分量
的边,由于缩点后生成的图是有向无环图(DAG),一旦穿过此边到达后续分量,就绝对不可能再返回原分量。因此,跨分量的边至多只能被经过一次,其贡献为且仅为其初始权值 。 -
DAG 上的最长路: 缩点建图后,我们在 DAG 上求以包含起点
的超级点为起点的最长路。 设 表示从起点超级点出发到达超级点 所能获得的最大蘑菇数: - 初始化:
,其余节点设为不可达( ); - 按拓扑排序的顺序进行状态转移:
- 最终全局最大采蘑菇数即为
。
- 初始化:
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-23 23:20
* update_at: 2026-10-23 23:20
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 80005;
const int MAXM = 200005;
// 原图与缩点后图的链式前向星
int head[MAXN], edge_cnt;
struct Edge {
int to;
int next;
ll w;
int coef; // 衰减系数 * 10
} edges[MAXM];
void add_edge(int u, int v, ll w, int coef) {
edge_cnt++;
edges[edge_cnt].to = v;
edges[edge_cnt].next = head[u];
edges[edge_cnt].w = w;
edges[edge_cnt].coef = coef;
head[u] = edge_cnt;
}
// 原始输入的边,供缩点后重构图使用
struct RawEdge {
int u, v;
ll w;
int coef;
} raw_edges[MAXM];
// Tarjan 缩点相关变量
int dfn[MAXN], low[MAXN], timer;
int stk[MAXN], top;
bool in_stack[MAXN];
int scc[MAXN], scc_cnt;
ll scc_weight[MAXN]; // 每个强连通分量内部所有边的全收益之和
// 计算一条边反复经过直至为 0 的总蘑菇数
ll calc_all_gain(ll w, int coef) {
ll sum = 0;
while (w > 0) {
sum += w;
w = (w * coef) / 10;
}
return sum;
}
void tarjan(int u) {
dfn[u] = low[u] = ++timer;
stk[++top] = u;
in_stack[u] = true;
for (int e = head[u]; e; e = edges[e].next) {
int v = edges[e].to;
if (!dfn[v]) {
tarjan(v);
if (low[v] < low[u]) low[u] = low[v];
} else if (in_stack[v]) {
if (dfn[v] < low[u]) low[u] = dfn[v];
}
}
if (low[u] == dfn[u]) {
scc_cnt++;
while (true) {
int node = stk[top--];
in_stack[node] = false;
scc[node] = scc_cnt;
if (node == u) break;
}
}
}
// 缩点后 DAG 的链式前向星
int dag_head[MAXN], dag_edge_cnt;
struct DagEdge {
int to;
int next;
ll w;
} dag_edges[MAXM];
int in_degree[MAXN];
void add_dag_edge(int u, int v, ll w) {
dag_edge_cnt++;
dag_edges[dag_edge_cnt].to = v;
dag_edges[dag_edge_cnt].next = dag_head[u];
dag_edges[dag_edge_cnt].w = w;
dag_head[u] = dag_edge_cnt;
in_degree[v]++;
}
// DAG 拓扑排序求最长路
ll dist[MAXN];
int q[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
for (int i = 1; i <= m; ++i) {
int u, v;
ll w;
double c;
cin >> u >> v >> w >> c;
int coef = (int)round(c * 10);
raw_edges[i] = {u, v, w, coef};
add_edge(u, v, w, coef);
}
int start_node;
cin >> start_node;
// 运行 Tarjan 缩点
for (int i = 1; i <= n; ++i) {
if (!dfn[i]) {
tarjan(i);
}
}
// 统计强连通分量内部边的贡献,并建立 DAG 边
for (int i = 1; i <= m; ++i) {
int u = raw_edges[i].u;
int v = raw_edges[i].v;
int su = scc[u];
int sv = scc[v];
if (su == sv) {
scc_weight[su] += calc_all_gain(raw_edges[i].w, raw_edges[i].coef);
} else {
// 跨分量的单向边,只能采一次(因为 DAG 无法走回路)
add_dag_edge(su, sv, raw_edges[i].w);
}
}
// 在 DAG 上求从 scc[start_node] 出发的最长路
int start_scc = scc[start_node];
for (int i = 1; i <= scc_cnt; ++i) {
dist[i] = -1; // -1 表示从起点不可达
}
dist[start_scc] = scc_weight[start_scc];
// 拓扑排序
int front = 0, rear = 0;
for (int i = 1; i <= scc_cnt; ++i) {
if (in_degree[i] == 0) {
q[rear++] = i;
}
}
ll ans = dist[start_scc];
while (front < rear) {
int u = q[front++];
for (int e = dag_head[u]; e; e = dag_edges[e].next) {
int v = dag_edges[e].to;
if (dist[u] != -1) {
ll cand = dist[u] + dag_edges[e].w + scc_weight[v];
if (cand > dist[v]) {
dist[v] = cand;
}
}
in_degree[v]--;
if (in_degree[v] == 0) {
q[rear++] = v;
}
}
}
for (int i = 1; i <= scc_cnt; ++i) {
if (dist[i] > ans) {
ans = dist[i];
}
}
cout << ans << "\n";
return 0;
}复杂度
- 时间复杂度:
- Tarjan 缩点遍历原图点和边,时间复杂度为
; - 预处理每条边的全收益,由于每次衰减至少减少
,每条边最多计算 次,复杂度为 ; - 缩点后在新图上拓扑排序和动态规划,耗时为
; - 总体时间复杂度为
,在 规模下只需几十毫秒,运行极为充裕。
- Tarjan 缩点遍历原图点和边,时间复杂度为
- 空间复杂度:
- 原图和缩点后的 DAG 均使用全局静态链式前向星存储,额外维护 Tarjan 栈与拓扑队列;
- 总体空间复杂度为
,占用内存约 15MB 左右,远在空间限制内。
总结
本题的关键在于利用强连通分量内部可无限循环的特点,将环内的无穷衰减收益一次性全部结算为超级点的静态点权;再利用**缩点后图无环(DAG)**的性质,将环外边的遍历次数锁定为至多一次,从而将复杂的有环图最值路径问题转化为了经典的 DAG 最长路模型。