NOI 2020 题解:美食家 (Delicacy)
核心考点:动态规划、拆点技巧、广义矩阵快速幂 (Max-Plus Algebra)、倍增预处理优化。
1. 题目大意简述
给出一个 n 个点 m 条边的有向图。每到达一个点 u 会获得 c_u 的愉悦值。走过每一条边需要花费 w \in [1,5] 天。总时长要求恰好为 T 天。初始(第 0 天)在点 1,并且已经获得了 c_1 的愉悦值。最后一天必须回到点 1。此外,有 k 个特殊的美食节:在第 t_i 天到达点 x_i,会额外获得 y_i 的愉悦值。求 T 天后恰好回到点 1 能获得的最大愉悦值。
数据范围:n \leq 50, m \leq 500, T \leq 10^9, k \leq 200, w \in \{1,2,3,4,5\}。
2. 状态转移与朴素 DP
如果不考虑 T 非常大,这是一个经典的图上 DP 模型:设 dp[t][u] 表示第 t 天走到点 u 的最大愉悦值。转移方程:
加上美食节的限制:如果当前天数 t 和地点 u 恰好匹配某个美食节,额外加上 y_i 即可。
但注意到 T \leq 10^9,显然不能逐天推导,这提示我们需要用到 矩阵快速幂。
3. 核心转化一:拆点(处理边权 w > 1)
标准的图上矩阵快速幂只能处理边权(时间花费)为 1 的情况。但题目中 w \in [1,5]。我们可以利用经典技巧:拆点。把每个点 u 拆分成 5 个状态:(u, 0), (u, 1), \dots, (u, 4)。
对于每个点 u,连边 (u, j) \to (u, j-1)(其中 j = 1,2,3,4)。这些边表示“在路上经过了一天”。对于原图的一条边 u \to v,权值为 w,我们在新图上连边:
这样,新图共有 5n 个节点(最多 250 个),并且每条新边的花费时间均为 1 天。
4. 核心转化二:广义矩阵乘法 (Max-Plus 代数)
在普通的矩阵乘法中,(AB)_{ij} = \sum_k A_{ik} B_{kj}。在图论求最长路的 DP 中,我们可以重新定义矩阵乘法为:
这种定义满足结合律,因此依然可以使用 快速幂。我们构造一个 S \times S 的转移矩阵 M(S = 5n)。M_{ij} 表示从新图的节点 i 走一步(1 天)到节点 j 的最大愉悦值增量。若不可达则设为 -\infty。
5. 核心转化三:处理美食节与“向量乘法优化”(满分关键!)
如果没有美食节,答案就是初始向量 V 乘上 M^T 后的结果。有 k 个美食节,我们可以把时间轴分为 k+1 段。将事件按发生时间 t_i 从小到大排序。假设当前状态向量为 V,时间处于 cur\_time,下一个事件在 t_i 发生:
- 计算步数 dt = t_i - cur\_time。
- 进行矩阵快速幂:V \leftarrow V \cdot M^{dt}。
- 把点 x_i 的对应状态值加上 y_i:V[(x_i, 0)] \mathrel{+}= y_i。
时间复杂度陷阱:如果是普通的实现,每次处理一个事件需要做一次矩阵快速幂。矩阵大小 S = 250。k 次矩阵快速幂复杂度为 O(k \cdot S^3 \log T)。这绝对会 TLE!
满分优化:倍增预处理(仅计算向量 × 矩阵) 注意到矩阵乘矩阵是 O(S^3),但向量乘矩阵是 O(S^2)!我们可以先花 O(S^3 \log T) 的时间预处理出转移矩阵 M 的所有 2^p 次方,即 base[p] = M^{2^p}。在处理每段事件间隔 dt 时,把 dt 二进制拆分,用当前的状态向量去依次乘以预处理好的矩阵:
由于每次都是向量乘矩阵,单次跨越的时间复杂度降为了 O(S^2 \log T)。总复杂度:预处理 O(S^3 \log T) + 事件跳跃 O(k S^2 \log T),可以轻松通过所有测试点!
6. C++ 核心代码实现
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = 4e18; // 注意防溢出,用正数 INF 配合负号
int n, m, T, k;
int S; // 矩阵大小 5n
// 获取拆点后的 ID
// 点下标从 1 开始,附加状态 c 范围 [0, 4]
int get_id(int u, int c) {
return (u - 1) + c * n + 1;
}
struct Matrix {
ll mat[255][255];
Matrix() {
for(int i=1; i<=S; i++)
for(int j=1; j<=S; j++)
mat[i][j] = -INF;
}
} base[35]; // base[p] 存 M^{2^p}
// 矩阵乘矩阵 (O(S^3))
Matrix mul(Matrix A, Matrix B) {
Matrix res;
for(int k_idx=1; k_idx<=S; k_idx++) {
for(int i=1; i<=S; i++) {
if(A.mat[i][k_idx] == -INF) continue;
for(int j=1; j<=S; j++) {
if(B.mat[k_idx][j] == -INF) continue;
res.mat[i][j] = max(res.mat[i][j], A.mat[i][k_idx] + B.mat[k_idx][j]);
}
}
}
return res;
}
struct Vector {
ll v[255];
Vector() {
for(int i=1; i<=S; i++) v[i] = -INF;
}
};
// 向量乘矩阵 (O(S^2)) 满分关键!
Vector mul_vec(Vector A, Matrix B) {
Vector res;
for(int j=1; j<=S; j++) {
for(int i=1; i<=S; i++) {
if(A.v[i] != -INF && B.mat[i][j] != -INF) {
res.v[j] = max(res.v[j], A.v[i] + B.mat[i][j]);
}
}
}
return res;
}
struct Event {
int t, x;
ll y;
bool operator<(const Event& rhs) const {
return t < rhs.t;
}
} ev[205];
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m >> T >> k;
S = 5 * n;
vector<ll> c(n + 1);
for(int i=1; i<=n; i++) cin >> c[i];
// 初始化转移矩阵
for(int i=1; i<=n; i++) {
for(int j=1; j<=4; j++) {
// (i, j) -> (i, j-1) 权值为 0
base[0].mat[get_id(i, j)][get_id(i, j-1)] = 0;
}
}
for(int i=1; i<=m; i++) {
int u, v, w;
cin >> u >> v >> w;
// (u, 0) -> (v, w-1) 权值为 c[v]
base[0].mat[get_id(u, 0)][get_id(v, w-1)] = c[v];
}
// 预处理倍增矩阵
for(int p=1; p<=30; p++) {
base[p] = mul(base[p-1], base[p-1]);
}
for(int i=1; i<=k; i++) {
cin >> ev[i].t >> ev[i].x >> ev[i].y;
}
sort(ev + 1, ev + 1 + k);
// 初始状态
Vector V;
V.v[get_id(1, 0)] = c[1];
int cur_time = 0;
for(int i=1; i<=k; i++) {
int dt = ev[i].t - cur_time;
if (dt < 0) continue; // 忽略无效事件
// 用向量乘法跳跃 dt 步
for(int p=30; p>=0; p--) {
if((dt >> p) & 1) {
V = mul_vec(V, base[p]);
}
}
cur_time = ev[i].t;
// 加上美食节的奖励
if (V.v[get_id(ev[i].x, 0)] != -INF) {
V.v[get_id(ev[i].x, 0)] += ev[i].y;
}
}
// 处理最后一个事件到 T 的剩余时间
if (cur_time < T) {
int dt = T - cur_time;
for(int p=30; p>=0; p--) {
if((dt >> p) & 1) {
V = mul_vec(V, base[p]);
}
}
}
if(V.v[get_id(1, 0)] < 0) {
cout << -1 << "\n";
} else {
cout << V.v[get_id(1, 0)] << "\n";
}
return 0;
}
7. 总结
- 遇到“图上行走找最大收益,并且 T 极大”时,条件反射写出 Max-Plus 代数下的矩阵乘法。
- 遇到“边权为极小常数但不是 1”的情况,利用拆点技巧把图的规模乘以边权上限。
- 重点:在需要多次查询/跳跃不同步数的矩阵乘法时,预处理矩阵的 2^p 的幂次,使用向量 × 矩阵能实现降维打击,是节省时间的黄金法则!