四类爬行卡片的数量只决定用牌组合,用牌总数又唯一决定乌龟的位置,于是把「顺序枚举」压成四维计数 DP。
OJ: roj
题目 ID: 3583
难度:提高
标签:
创建: 2026-10-02 09:05
更新: 2026-10-10 11:30
形式化题目
棋盘是一排
用光全部
样例解释
样例中 1 3 1 2 1(三张 1、一张 2、一张 3),最优顺序 1,1,3,1,2 的行走路线是:
| 步骤 | 打出 | 从格子 | 到格子 | 到达格分数 |
|---|---|---|---|---|
| 起点 | — | — | 1 | |
| 1 | 1 | 1 | 2 | |
| 2 | 1 | 2 | 3 | |
| 3 | 3 | 3 | 6 | |
| 4 | 1 | 6 | 7 | |
| 5 | 2 | 7 | 9 |
合计
正解
思路
朴素做法及其瓶颈。 最直接的枚举是给
从枚举顺序到计数 DP。 进一步观察:某个组合
其中
flowchart LR
S["(0,0,0,0) 格1 得6"] -->|"打1→格2"| A["(1,0,0,0) 得16"]
S -->|"打2→格3"| B["(0,1,0,0) 得20"]
A -->|"打2→格4"| C["(1,1,0,0) 得22"]
B -->|"打1→格4"| C
从 (1,0,0,0) 打 2、和从 (0,1,0,0) 打 1,都会到达组合 (1,1,0,0),但得分不同(max 的意义,也说明得分依赖路径顺序,而组合本身不依赖,所以必须把"每个组合的最优值"存下来。
实现方式。 既可以用四维数组 f[c1][c2][c3][c4] 按 dict 以四元组为键直接从 cnt[k] 加一后得到的新组合的得分用 max 更新。这样状态里只存"实际可达"的组合,且无需写显式的四层逆序循环。需要注意两处剪枝:某类卡片用完就不能再打该类;下一步若走出棋盘(新位置
代码
/**
* 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;
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; if (!(cin >> n >> m)) return 0;
vector<ll> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
int cnt[5] = {0, 0, 0, 0, 0};
for (int i = 0; i < m; i++) { int x; cin >> x; cnt[x]++; }
static ll dp[45][45][45][45];
static bool ok[45][45][45][45];
ok[0][0][0][0] = true; dp[0][0][0][0] = a[0];
for (int c1 = 0; c1 <= cnt[1]; c1++)
for (int c2 = 0; c2 <= cnt[2]; c2++)
for (int c3 = 0; c3 <= cnt[3]; c3++)
for (int c4 = 0; c4 <= cnt[4]; c4++) {
if (!ok[c1][c2][c3][c4]) continue;
int pos = c1 + 2 * c2 + 3 * c3 + 4 * c4;
int cs[5] = {0, c1, c2, c3, c4};
for (int k = 1; k <= 4; k++) {
if (cs[k] == cnt[k] || pos + k >= n) continue;
int n1 = c1, n2 = c2, n3 = c3, n4 = c4;
if (k == 1) n1++; else if (k == 2) n2++; else if (k == 3) n3++; else n4++;
ll gain = dp[c1][c2][c3][c4] + a[pos + k];
if (!ok[n1][n2][n3][n4] || gain > dp[n1][n2][n3][n4]) {
ok[n1][n2][n3][n4] = true; dp[n1][n2][n3][n4] = gain;
}
}
}
cout << dp[cnt[1]][cnt[2]][cnt[3]][cnt[4]] << '\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 09:05
# update_at: 2026-10-02 09:05
import sys
from itertools import product
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m = next(data), next(data)
a = [next(data) for _ in range(n)] # a[i]:第 i+1 格的分数(0 起下标)
cnt = [0] * 5 # cnt[k]:数字为 k 的卡片张数,cnt[0] 不用
for _ in range(m):
cnt[next(data)] += 1
# dp[(c1,c2,c3,c4)]:四类卡片分别用了 c1..c4 张时的最大得分。
# 用牌总数唯一决定当前位置(第 1 格 + 已走步数),所以状态里不必再存位置。
# 题目保证用光 M 张牌恰好到终点,答案就是四类牌全部用完时的状态值。
dp = {(0, 0, 0, 0): a[0]} # 起点:还没出牌,已自动获得第 1 格分数
for c1, c2, c3, c4 in product(range(cnt[1] + 1), range(cnt[2] + 1),
range(cnt[3] + 1), range(cnt[4] + 1)):
state = (c1, c2, c3, c4)
cur = dp.get(state)
if cur is None:
continue # 这个用牌组合从起点走不到
pos = c1 + 2 * c2 + 3 * c3 + 4 * c4 # 已走步数 = 当前格子的 0 起下标
for k in range(1, 5):
nxt_pos = pos + k
if state[k - 1] == cnt[k] or nxt_pos >= n:
continue # 数字 k 的牌已用完,或这一步会跳出棋盘
nxt = state[:k - 1] + (state[k - 1] + 1,) + state[k:]
gain = cur + a[nxt_pos]
if gain > dp.get(nxt, -1):
dp[nxt] = gain
print(dp[(cnt[1], cnt[2], cnt[3], cnt[4])])
if __name__ == "__main__":
solve()复杂度
状态数是
总结
- 同类物品无区别时,“排列枚举"应压缩为"计数组合”:本题的关键观察是用牌总数唯一决定乌龟位置,从而把状态从「位置 + 剩余所有牌」砍成四维计数。
- 转移是"最后一动画牌"的逆推(或等价的前向扩展),每个状态最多
个前驱/后继,这是计数 DP 的典型形态。 - 用
dict+ 元组状态可以省掉四维数组的边界书写,代价是常数变大;Python 实现需留意nxt_pos < n与"该类牌是否用完"两个合法性检查。 - 得分依赖行走顺序、状态却与顺序无关,二者靠
max融合——凡是"过程有序、结果只看计数"的题都可以套用这个框架。
图示解析
下图把样例的 DP 状态按"已用牌数 - 表示该组合不可达,格子数 =
| 组合 |
格子 | 最大得分 | |
|---|---|---|---|
| 0 | (0,0,0) | 1 | 6 |
| 1 | (1,0,0) / (0,1,0) / (0,0,1) | 2 / 3 / 4 | 16 / 20 / 8 |
| 2 | (2,0,0) / (1,1,0) / (1,0,1) / (0,1,1) | 3 / 4 / 5 / 6 | 30 / 22 / 24 / 28 |
| 3 | (3,0,0) / (2,1,0) / (2,0,1) / (1,1,1) | 4 / 5 / 6 / 7 | 32 / 38 / 38 / 46 |
| 4 | (3,1,0) / (3,0,1) / (2,1,1) | 5 / 6 / 8 | 46 / 56 / 51 |
| 5 | (3,1,1) | 9 | 73 |
观察重点:
- 每一层的状态只从上一层转移而来(每打一张牌
加一),所以按 递增处理即是无后效性的拓扑序。 - 最优出牌顺序
1,1,3,1,2对应链(0,0,0)→(1,0,0)→(2,0,0)→(2,0,1)→(3,0,1)→(3,1,1),逐状态得分;例如 (1,1,0)得是从 (0,1,0)打 1 到达的(),比从 (1,0,0)打 2 到达的更优,体现取 max的必要性。 - 终态
(3,1,1)的正是样例答案,且它由 (3,0,1)(打 2)转移而来,与题面给出的最优出牌顺序一致。