行程沿「马路−时刻」同余的对角线前进:按时间做一维 DP,对角线前缀和 O(1) 求行程金币,每条对角线单调队列维护长度不超过 p 的窗口最大值,总复杂度 O(nm)。
OJ: roj
题目 ID: 3571
难度:提高
标签:动态规划单调队列前缀和python
创建: 2026-10-02 08:13
更新: 2026-10-10 11:30
形式化题目
环上有
机器人在工厂瞬时购买并设定行走次数(
样例(
| 马路 \ 时刻 | 1 | 2 | 3 |
|---|---|---|---|
| 1 号马路 | 1 | 2 | 3 |
| 2 号马路 | 2 | 3 | 4 |
这张表里一次行程同时向右、向下移动:
正解
思路
朴素做法:按时间分段 DP。 由题意,任何合法玩法都恰好把时间轴
三重枚举是
瓶颈在哪。 转移里的行程金币和:一段行程同时沿「马路编号」和「时刻」两个方向前进,两个下标一起平移,一维前缀和套不上,朴素求和把
关键观察:一次行程沿「对角线」前进。 时刻
- 时刻
在对角线 上走的马路唯一确定: ; - 段起点工厂也由
、 决定: ( ,机器人出发时走的第一段马路就是它所在的工厂)。
对角线前缀和拆分转移。 对每条对角线按时刻建前缀和
滑动窗口 + 单调队列。 固定对角线
样例推演。 对角线前缀和为
| 窗口 |
各对角线候选 |
||
|---|---|---|---|
| 1 | 0 | ||
| 2 | 3 | ||
| 3 | 5 |
表中
边界情况。
代码
prefix[c] 在一轮循环里扮演两个角色:算 value 时它还是 queues[c] 就是对角线 value 即
/**
* 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-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int NEG = -1000000000;
int n, m, p;
int coin[1005][1005]; // coin[i][t]:第 i 条马路在第 t 个时间单位出现的金币
int cost[1005]; // 每个工厂买机器人的花费
ll prefix[1005];
deque<pair<int, ll> > queues[1005]; // (k, dp[k] - prefix - cost)
ll dp[1005];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> p;
for (int i = 0; i < n; i++)
for (int t = 1; t <= m; t++)
cin >> coin[i][t];
for (int i = 0; i < n; i++) cin >> cost[i];
for (int i = 0; i < n; i++) prefix[i] = 0;
dp[0] = 0;
for (int j = 1; j <= m; j++) {
ll best = NEG;
for (int c = 0; c < n; c++) {
int k = j - 1;
ll value = dp[k] - prefix[c] - cost[(k + c) % n];
deque<pair<int, ll> >& q = queues[c];
while (!q.empty() && q.back().second <= value) q.pop_back();
q.push_back(make_pair(k, value));
while (q.front().first < j - p) q.pop_front();
prefix[c] += coin[(j + c - 1) % n][j];
best = max(best, prefix[c] + q.front().second);
}
dp[j] = best;
}
cout << dp[m] << "\n";
return 0;
}#!/usr/bin/env python3
# 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-02 08:13
# update_at: 2026-10-02 08:13
import sys
from collections import deque
NEG = -10**9 # dp 候选初值:净收益下界远大于此
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m, p = next(data), next(data), next(data)
# coin[i][t]:第 i 条马路在第 t 个单位时间出现的金币(t 从 1 起)
coin = [[0] + [next(data) for _ in range(m)] for _ in range(n)] # n 行 × m 列
cost = [next(data) for _ in range(n)] # 每个工厂买机器人的花费
# 对角线 c:时刻 t 经过的马路是 (t + c - 1) % n,一次行程恰好沿一条对角线前进。
# prefix[c] 滚动维护该对角线的前缀和;队列里存 (k, dp[k] - 前缀和 - 购买花费)。
prefix = [0] * n
queues = [deque() for _ in range(n)] # 每条对角线一个单调队列,窗口最大值 O(1)
dp = [0] * (m + 1) # dp[j]:前 j 个单位时间能拿到的最大净收益
for j in range(1, m + 1):
best = NEG
for c in range(n):
k = j - 1 # 上一段恰好覆盖前 k 个单位时间,本段从 k+1 时刻起步
# 此刻 prefix[c] 正是 P_c[k];起步工厂即 (k+1) 时刻所在的工厂
value = dp[k] - prefix[c] - cost[(k + c) % n]
q = queues[c]
while q and q[-1][1] <= value:
q.pop() # 队尾收益不比新值大,之后永远轮不到它
q.append((k, value))
while q[0][0] < j - p: # 机器人最多走 p 步,本段长度 j-k ≤ p
q.popleft()
prefix[c] += coin[(j + c - 1) % n][j] # 前缀和推进到时刻 j
best = max(best, prefix[c] + q[0][1])
dp[j] = best
print(dp[m])
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:
。外层推进 个时刻,内层遍历 条对角线,单调队列操作均摊 ;恰好消掉了朴素转移里枚举段长的那一维。 时约 轮主循环。 - 空间复杂度:
存金币矩阵(输入本身),除输入外只用 :滚动前缀和、 个单调队列与 dp数组。
总结
- 先把「环上调度机器人」翻译成时间轴分段模型:段长不超过
、段间彼此独立,一维 就够,不必记录机器人当前在哪个工厂。 - 行程中「马路 − 时刻」模
不变,故收益沿对角线成段出现:对角线前缀和把行程金币和变成 区间和,还把「起点工厂」的选择一并吸收进 的转移里。 - 转移拆成只含
的 与只含 的 后,内层就是长度不超过 的滑动窗口最大值, 条对角线各配一个单调队列,总复杂度 。 - 凡是「收益的两个下标一起平移」的求和,先找沿程不变量(这里是两下标之差),再沿该方向做前缀和,通常都能把内层求和消掉。