通过将边权不超过 5 的有向图拆点为 5n 个状态,把按天动态规划转化为 (max, +) 矩阵乘法,并利用二进制预处理矩阵幂与向量乘法在 O((5n)^3 log T + k(5n)^2 log T) 内解决。
OJ: luogu
题目 ID: P6772
难度:提高+/省选-
标签:动态规划矩阵快速幂拆点倍增
创建: 2026-10-23 23:04
更新: 2026-10-26 14:16
形式化题目
给定一张包含
暴力解法
思路
不考虑
定义
- 初始状态:
,其余均为 。 - 空间优化:因为
,第 天的状态仅依赖于 天的状态,可以通过对天数模 进行滚动数组压缩,空间复杂度仅为 。
代码
/**
* brute.cpp:小数据/低天数暴力解(按天数动态规划),用于帮助理解题意与对拍。
* 为什么只适合小数据:
* 朴素 DP 外层需要枚举每一天 t 从 1 到 T,内层枚举所有有向边,
* 时间复杂度为 O(T * m)。
* 当 T <= 50000 时能在 1 秒内运行完毕(覆盖前 40% 数据),
* 但当 T 达到 10^9 时,循环次数高达 10^9 * 500 = 5 * 10^11,会严重超时(TLE)。
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 55;
const int MAXM = 505;
const int MAXK = 205;
const ll INF = 0x3f3f3f3f3f3f3f3fLL;
int n, m, k;
ll T;
ll c[MAXN];
struct Edge {
int u, v;
int w;
} edges[MAXM];
struct Festival {
ll t;
int x;
ll y;
} fest[MAXK];
// dp[t % 6][u]:到达城市 u,距离恰好还剩多久到达的记录;
// 这里直接用二维 vector dp[t][u] 支持 T <= 100000 的情况
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m >> T >> k)) return 0;
for (int i = 1; i <= n; ++i) {
cin >> c[i];
}
for (int i = 1; i <= m; ++i) {
cin >> edges[i].u >> edges[i].v >> edges[i].w;
}
for (int i = 1; i <= k; ++i) {
cin >> fest[i].t >> fest[i].x >> fest[i].y;
}
// 预处理美食节哈希或查找表:小数据下可以用 map<(t, x), y>
map<pair<ll, int>, ll> fest_map;
for (int i = 1; i <= k; ++i) {
fest_map[{fest[i].t, fest[i].x}] += fest[i].y;
}
// dp[t][u]:第 t 天恰好到达城市 u 的最大愉悦值
// 为节省空间,只记录最近 6 天的状态 dp[0..5][u]
vector<vector<ll>> dp(6, vector<ll>(n + 1, -INF));
dp[0][1] = c[1];
for (ll t = 1; t <= T; ++t) {
int cur = t % 6;
for (int i = 1; i <= n; ++i) {
dp[cur][i] = -INF;
}
for (int i = 1; i <= m; ++i) {
int u = edges[i].u;
int v = edges[i].v;
int w = edges[i].w;
if (t >= w) {
int prev = (t - w) % 6;
if (dp[prev][u] >= 0) {
ll val = dp[prev][u] + c[v];
if (val > dp[cur][v]) {
dp[cur][v] = val;
}
}
}
}
// 检查第 t 天是否有美食节
for (int i = 1; i <= n; ++i) {
if (dp[cur][i] >= 0) {
auto it = fest_map.find({t, i});
if (it != fest_map.end()) {
dp[cur][i] += it->second;
}
}
}
}
ll ans = dp[T % 6][1];
if (ans < 0) {
cout << -1 << "\n";
} else {
cout << ans << "\n";
}
return 0;
}复杂度
- 时间复杂度:每一天需要遍历所有
条边,共转移 天,时间复杂度为 。 - 空间复杂度:由于采用模
的滚动数组保存最近 天的状态,空间复杂度为 。
瓶颈
在题目中
瓶颈在于:虽然总天数
正解
思路
1. 广义矩阵乘法与 半环
在没有美食节时,考虑转移方程的形式:
- 矩阵乘法定义为:
; - 该矩阵乘法同样满足结合律
; - 零元为
,乘法单位元 为对角线上全为 、非对角线全为 的对角矩阵。
下表逐项对照
| 符号角色 | 经典矩阵乘法 | |
|---|---|---|
| 求和号 |
加法 |
取最大值 |
| 相乘处的标量运算 | 标量乘法 |
标量加法 |
| 零元 | ||
| 乘法单位元 |
对角线全 |
对角线全 |
整张表其实只有两个替换: 求和号处的
2. 边权 的拆点技巧
由于有向边的通行时间
- 内部连边:对
,建立转移 ,转移增益为 。这表示每过一天,距离到达城市 就少一天; - 原图有向边:对于原图中的有向边
,小 W 在到达城市 (即状态 )时决定沿该边出发,耗时 天到达 。这相当于经过 天后,状态进入 ,并在最终到达时收获 愉悦值。因此建立转移 ,转移增益为 (若存在重边,取最大的 即可)。
下图把城市
flowchart LR
subgraph GU["城市 u 的 5 个虚点"]
direction TB
U4["(u, 4) 距到达 u 还剩 4 天"] -->|"增益 0"| U3["(u, 3) 还剩 3 天"]
U3 -->|"增益 0"| U2["(u, 2) 还剩 2 天"]
U2 -->|"增益 0"| U1["(u, 1) 还剩 1 天"]
U1 -->|"增益 0"| U0["(u, 0) 已到达 u"]
end
subgraph GV["城市 v 的 5 个虚点 (以 w = 3 为例)"]
direction TB
V4["(v, 4) 距到达 v 还剩 4 天"] -->|"增益 0"| V3["(v, 3) 还剩 3 天"]
V3 -->|"增益 0"| V2["(v, 2) 还剩 2 天"]
V2 -->|"增益 0"| V1["(v, 1) 还剩 1 天"]
V1 -->|"增益 0"| V0["(v, 0) 已到达 v"]
end
U0 -->|"原图边 (u, v, w = 3), 增益 c_v"| V2
每组内的链是内部边
拆点后,新图的点数变为
3. 美食节划分与向量乘矩阵优化
图中有
- 在区间
内,转移完全等价于乘以 (其中 ); - 到达第
天后,将对应美食节城市 的状态加上额外愉悦值 (若该状态当前可达)。
如果对每个区间都重新计算矩阵幂
关键优化:
我们并不需要维护完整的
- 在开头预处理出矩阵的倍增幂
,共需 次矩阵相乘,耗时 ; - 转移每一段
时,将 拆为二进制位,每一位对应一次行向量乘以预处理好的矩阵,耗时仅 ; - 总共只需要进行
次向量乘矩阵,计算量降至 次运算,配合非负剪枝后可以在时限内轻松通过。
代码
/**
* 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:04
* update_at: 2026-10-23 23:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 55;
const int MAX_STATES = 255;
const ll INF = 0x3f3f3f3f3f3f3f3fLL;
int n, m, k;
ll T;
ll c[MAXN];
int tot_states; // N = 5 * n
// 拆点编号映射:u in [1, n], j in [0, 4]
// j 表示距离到达城市 u 还要经过 j 天
inline int get_id(int u, int j) {
return j * n + u;
}
// (max, +) 矩阵
struct Matrix {
ll mat[MAX_STATES][MAX_STATES];
Matrix() {
for (int i = 1; i <= tot_states; ++i) {
for (int j = 1; j <= tot_states; ++j) {
mat[i][j] = -INF;
}
}
}
};
// 矩阵乘法 C = A * B 在 (max, +) 半环下
Matrix multiply(const Matrix &A, const Matrix &B) {
Matrix C;
for (int i = 1; i <= tot_states; ++i) {
for (int mid = 1; mid <= tot_states; ++mid) {
if (A.mat[i][mid] < 0) continue;
for (int j = 1; j <= tot_states; ++j) {
if (B.mat[mid][j] < 0) continue;
ll val = A.mat[i][mid] + B.mat[mid][j];
if (val > C.mat[i][j]) {
C.mat[i][j] = val;
}
}
}
}
return C;
}
// 行向量乘矩阵:vec = vec * A 在 (max, +) 半环下
void vec_multiply(vector<ll> &vec, const Matrix &A) {
vector<ll> res(tot_states + 1, -INF);
for (int mid = 1; mid <= tot_states; ++mid) {
if (vec[mid] < 0) continue;
for (int j = 1; j <= tot_states; ++j) {
if (A.mat[mid][j] < 0) continue;
ll val = vec[mid] + A.mat[mid][j];
if (val > res[j]) {
res[j] = val;
}
}
}
vec = std::move(res);
}
// 预处理矩阵倍增幂:pow_mat[p] = M^(2^p)
Matrix pow_mat[32];
struct Festival {
ll t;
int x;
ll y;
} fest[205];
bool cmp_fest(const Festival &a, const Festival &b) {
return a.t < b.t;
}
void solve() {
tot_states = 5 * n;
// 构建基础一步转移矩阵 M
Matrix M;
// 1. 虚点内部连边:距离到达 u 还剩 j 天,走 1 天变为还剩 j-1 天
for (int u = 1; u <= n; ++u) {
for (int j = 1; j <= 4; ++j) {
M.mat[get_id(u, j)][get_id(u, j - 1)] = 0;
}
}
// 2. 原图中的有向边 (u, v, w):从到达 u 的瞬间出发,耗时 w 天到达 v
// 相当于下一步处于状态 (v, w - 1),到达 v 时可获得 c[v] 愉悦值
for (int i = 1; i <= m; ++i) {
int u, v, w;
cin >> u >> v >> w;
int from_id = get_id(u, 0);
int to_id = get_id(v, w - 1);
if (c[v] > M.mat[from_id][to_id]) {
M.mat[from_id][to_id] = c[v];
}
}
for (int i = 1; i <= k; ++i) {
cin >> fest[i].t >> fest[i].x >> fest[i].y;
}
sort(fest + 1, fest + k + 1, cmp_fest);
// 预处理倍增矩阵幂
pow_mat[0] = M;
for (int p = 1; p <= 30; ++p) {
pow_mat[p] = multiply(pow_mat[p - 1], pow_mat[p - 1]);
}
// 初始状态向量:第 0 天位于城市 1(get_id(1, 0)),获得愉悦值 c[1]
vector<ll> F(tot_states + 1, -INF);
F[get_id(1, 0)] = c[1];
ll cur_t = 0;
for (int i = 1; i <= k; ++i) {
ll dt = fest[i].t - cur_t;
if (dt > 0) {
for (int p = 0; p <= 30; ++p) {
if ((dt >> p) & 1) {
vec_multiply(F, pow_mat[p]);
}
}
cur_t = fest[i].t;
}
int target_id = get_id(fest[i].x, 0);
if (F[target_id] >= 0) {
F[target_id] += fest[i].y;
}
}
// 跨越最后一段无美食节的时间直到 T
if (T > cur_t) {
ll dt = T - cur_t;
for (int p = 0; p <= 30; ++p) {
if ((dt >> p) & 1) {
vec_multiply(F, pow_mat[p]);
}
}
}
ll ans = F[get_id(1, 0)];
if (ans < 0) {
cout << -1 << "\n";
} else {
cout << ans << "\n";
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
while (cin >> n >> m >> T >> k) {
for (int i = 1; i <= n; ++i) {
cin >> c[i];
}
solve();
}
return 0;
}复杂度
- 时间复杂度:
- 预处理矩阵倍增幂:计算
次 矩阵相乘,耗时 ; - 美食节按时间排序:
; - 跨越
段区间的向量转移:共进行最多 次向量乘矩阵,每次耗时 ,总耗时 ; - 总时间复杂度为
。当 时, ,常数较小且稀疏剪枝,运行时间在 0.5 秒左右,完全满足时限要求。
- 预处理矩阵倍增幂:计算
- 空间复杂度:
- 存储
个 的 long long转移矩阵,空间占用约为; - 辅助向量和美食节数组空间为
; - 总空间复杂度为
,远小于题目限制的 512 MB。
- 存储
总结
本题的关键在于两个核心转化的有机结合:
- 拆点降维:利用边权
的限制,将每座城市拆成 5 个等待状态,将带权边转化为单位步长的状态机转移,从而使问题能够套用矩阵快速幂; - 向量乘法降阶:利用状态只需要从起点单向推进的特点,将
段区间的全矩阵快速幂优化为“预处理矩阵倍增幂 + 向量右乘”,把瓶颈项的复杂度从 降为 。
