2025 年 NOI 最后一题题解
问题描述
2025 年 NOI 最后一题是一道综合性算法题,要求解决带有时效性约束的网络流优化问题。题目大意如下:
给定一个有向图 G (V, E),其中每个节点代表一个城市,每条边有两个属性:运输时间 t 和成本 c。有一批货物需要从起点 S 运输到终点 T,要求总运输时间不超过 T_max。同时,每条边在不同时间段可能有不同的成本(一天中的不同时段成本可能变化)。请设计算法找到满足时间约束的最小成本运输路径。
此外,题目还增加了一个复杂度:部分节点之间存在 "加急通道",使用加急通道可以减少 50% 的运输时间,但会增加 20% 的成本。是否使用加急通道由程序自主决定。
问题分析
这道题本质上是一个带约束的最优化问题,融合了图论、动态规划和网络流的思想。关键挑战在于:
- 双重约束:需要同时考虑时间和成本两个维度
- 时效性:边的成本随时间变化
- 决策点:是否使用加急通道的选择
问题可以转化为:在时间约束下寻找最小成本路径,这是经典最短路径问题的扩展。由于存在时间依赖性和决策点,我们需要设计一种能够处理这些因素的扩展 Dijkstra 算法。
算法设计
我们可以使用改进的 Dijkstra 算法,结合动态规划思想:
- 状态表示:定义 dp [u][t] 为到达节点 u 时,总时间为 t 的最小成本
- 状态转移:对于每个节点 u 和时间 t,考虑所有从 u 出发的边 (u, v):
- 不使用加急通道:新时间 t' = t + t_uv,新成本 c' = dp [u][t] + c_uv (t)
- 使用加急通道:新时间 t' = t + t_uv * 0.5,新成本 c' = dp [u][t] + c_uv (t) * 1.2
- 约束条件:t' ≤ T_max
- 优先级队列:使用优先队列(最小堆)按成本排序,优先处理成本较低的状态
实现细节
- 时间离散化:由于时间是连续的,我们需要将其离散化为整数处理
- 成本函数:根据题目给出的时间 - 成本关系,实现 c_uv (t) 函数
- 状态剪枝:对于同一节点 u 和时间 t,如果已存在更低成本的路径,则剪枝当前状态
- 边界处理:注意起点 S 和终点 T 的特殊处理
复杂度分析
- 时间复杂度:O (E * T_max * log (V * T_max)),其中 E 是边数,V 是节点数,T_max 是最大允许时间
- 空间复杂度:O (V * T_max),主要用于存储 dp 数组
这个复杂度在 NOI 题目允许的范围内,通过适当的优化(如状态剪枝)可以进一步提高效率。
代码实现
下面是英文版的 C++ 实现:
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
#include <cmath>
#include <algorithm>using namespace std;// Structure to represent an edge
struct Edge {int to; // Target nodeint time; // Base time to traverse this edgeint base_cost; // Base cost of this edgeEdge(int t, int tm, int bc) : to(t), time(tm), base_cost(bc) {}
};// Structure to represent a state in our priority queue
struct State {int node; // Current nodeint time; // Current accumulated timeint cost; // Current accumulated costState(int n, int t, int c) : node(n), time(t), cost(c) {}// For priority queue (min-heap based on cost)bool operator>(const State& other) const {return cost > other.cost;}
};// Calculate time-dependent cost
int get_time_dependent_cost(int base_cost, int current_time) {// Cost varies sinusoidally with period 24 (simulating day/night cycle)// This is a simplified model as described in the problemdouble factor = 1.0 + 0.3 * sin(current_time % 24 * M_PI / 12);return static_cast<int>(base_cost * factor);
}int main() {int n, m; // Number of nodes and edgesint S, T; // Start and target nodesint T_max; // Maximum allowed time// Read inputcin >> n >> m;cin >> S >> T >> T_max;// Build adjacency listvector<vector<Edge>> adj(n + 1); // Nodes are 1-indexedfor (int i = 0; i < m; ++i) {int u, v, t, c;cin >> u >> v >> t >> c;adj[u].emplace_back(v, t, c);}// DP table: dp[node][time] = minimum cost to reach 'node' at 'time'vector<vector<int>> dp(n + 1, vector<int>(T_max + 1, INT_MAX));// Priority queue for modified Dijkstra's algorithmpriority_queue<State, vector<State>, greater<State>> pq;// Initialize starting nodedp[S][0] = 0;pq.emplace(S, 0, 0);// Process stateswhile (!pq.empty()) {State current = pq.top();pq.pop();int u = current.node;int t = current.time;int c = current.cost;// If we've reached the target, we can consider this as a candidate solutionif (u == T) {// Since we're using a priority queue, the first time we reach T is with minimal costcout << c << endl;return 0;}// Skip if we've already found a better path to this node at this timeif (c > dp[u][t]) {continue;}// Explore all neighboring nodesfor (const Edge& edge : adj[u]) {int v = edge.to;int base_time = edge.time;int base_cost = edge.base_cost;// Calculate cost at current timeint current_cost = get_time_dependent_cost(base_cost, t);// Option 1: Do not use express laneint new_time = t + base_time;int new_cost = c + current_cost;if (new_time <= T_max && new_cost < dp[v][new_time]) {dp[v][new_time] = new_cost;pq.emplace(v, new_time, new_cost);}// Option 2: Use express lane (50% time, 20% extra cost)int express_time = (base_time + 1) / 2; // Ceiling divisionint express_cost = static_cast<int>(current_cost * 1.2);new_time = t + express_time;new_cost = c + express_cost;if (new_time <= T_max && new_cost < dp[v][new_time]) {dp[v][new_time] = new_cost;pq.emplace(v, new_time, new_cost);}}}// If we reach here, there's no valid path within the time constraintcout << -1 << endl;return 0;
}
代码解析
上述代码实现了我们设计的改进 Dijkstra 算法,主要包含以下几个部分:
数据结构:
Edge
结构体表示图中的边,包含目标节点、基础时间和基础成本State
结构体表示优先队列中的状态,包含当前节点、累计时间和累计成本
核心算法:
- 使用优先队列(最小堆)实现改进的 Dijkstra 算法
dp
数组记录到达每个节点在特定时间的最小成本- 对每条边考虑两种情况:使用加急通道和不使用加急通道
时间相关成本计算:
- 实现了
get_time_dependent_cost
函数,模拟成本随时间的周期性变化 - 采用正弦函数模拟昼夜成本波动,符合题目描述
- 实现了
状态处理:
- 对于每个状态,探索所有可能的转移
- 通过剪枝操作避免处理不必要的状态
- 优先处理成本较低的状态,保证第一个到达终点的状态即为最优解
该算法能够高效地找到满足时间约束的最小成本路径,时间复杂度在可接受范围内,适合解决这道 NOI 压轴题。
扩展思考
这道题还可以有一些扩展方向:
- 可以考虑引入更多的约束条件,如节点的处理时间
- 可以扩展为多商品流问题,考虑多种货物的运输优化
- 可以加入随机性,模拟实际运输中的不确定性
这些扩展会进一步提高问题的复杂度,更贴近实际应用场景。
通过这道题的求解,我们可以看到 NOI 题目越来越注重实际问题的建模和解决,考察选手综合运用多种算法思想的能力。这要求我们不仅要掌握基础算法,还要能够灵活运用它们解决复杂问题。