线性 DP:dp[i][j] 表示第 i 朵花放花瓶 j 的最大美观值,前缀最大值 O(FV) 转移,再用后缀 DP + 贪心还原字典序最小的摆放方案。
OJ: roj
题目 ID: 3164
难度:普及+/提高-
标签:动态规划线性DP输出方案
创建: 2026-10-01 21:57
更新: 2026-10-10 13:00
形式化题目
给定
输出这个最大值,以及一组取到最大值的下标;若有多组方案,输出字典序最小的一组(即让
正解
思路
从朴素到线性 DP。 最直接的想法是给每朵花枚举花瓶,复杂度是指数级。但注意题目自带"顺序":花必须按
状态与转移。 设
第
还原字典序最小的方案。 直接用正向 dp 倒推 pre 数组会比较繁琐。更优雅的做法是把 dp 反过来算一遍——定义后缀状态
转移只有两个选择:第
边界
有了
的花瓶——等式成立说明"第
看样例。对样例数据算出后缀表
| 53 | 53 | 25 | |||
| 41 | 41 | 30 | 30 | ||
| 20 | 20 | 20 | 20 | 20 | |
| 0 | 0 | 0 | 0 | 0 |
观察重点:
- 第一行
就是答案; 、 为 ,因为后面的花瓶不够后面两朵花分。 - 第 3 行全是 20:只剩一朵花时它可以跳过前面任意花瓶、挑最好的花瓶 5(
),所以每个起点都同样优。 - 从左上角 53 出发,每一步要么"放在这里"(斜向右下,吃掉
),要么"跳过这个花瓶"(向右一格),53 就是这条路径上各 值之和的最大者。
贪心逐朵还原的过程:
| 花朵 |
从 |
判定 |
决策 |
|---|---|---|---|
| 1 | 放花瓶 2 | ||
| 2 | 放花瓶 4 | ||
| 3 | 放花瓶 5 |
每朵花都取"能取到最优值的最早花瓶",得到字典序最小方案 2 4 5。
代码
/**
* 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 13:00
* update_at: 2026-10-10 13:00
*/
// 花店橱窗布置:后缀 DP + 贪心还原字典序最小方案
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MAXF = 512;
const int MAXV = 512;
const ll NEG = -(1LL << 50); // 不可行状态哨兵
ll a[MAXF][MAXV]; // a[i][j]:第 i 朵花放进第 j 个花瓶的美观度(均 1 起编号)
// g[i][j]:第 i..F 朵花放进花瓶 j..V 的最大美观度(后缀 DP)
ll g[MAXF][MAXV];
ll pos[MAXF]; // 还原出来的方案:第 i 朵花的花瓶编号
int main() {
int f, v;
if (scanf("%d %d", &f, &v) != 2) {
return 0;
}
for (int i = 1; i <= f; i++) {
for (int j = 1; j <= v; j++) {
scanf("%lld", &a[i][j]);
}
}
for (int i = 1; i <= f + 1; i++) {
for (int j = 0; j <= v + 2; j++) {
g[i][j] = NEG;
}
}
for (int j = 0; j <= v + 2; j++) {
g[f + 1][j] = 0; // 没有花要放:任何花瓶区间都是 0
}
for (int i = f; i >= 1; i--) {
// 第 i 朵花放 j 时前面还剩 i-1 朵花,所以 j 至少是 v-f+i
for (int j = v - f + i; j >= 1; j--) {
g[i][j] = max(a[i][j] + g[i + 1][j + 1], g[i][j + 1]);
}
}
// 贪心还原字典序最小方案:第 i 朵花取最小的花瓶 j,使得「放 j + 后缀最优」仍等于整体最优
int j = 1;
for (int i = 1; i <= f; i++) {
while (j <= v && a[i][j] + g[i + 1][j + 1] != g[i][j]) {
j++;
}
pos[i] = j;
j++;
}
printf("%lld\n", g[1][1]);
for (int i = 1; i <= f; i++) {
if (i > 1) printf(" ");
printf("%lld", pos[i]);
}
printf("\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-01 21:57
# update_at: 2026-10-01 21:57
import sys
NEG = -10**100 # 不可行状态哨兵:花比可用花瓶还多时的空方案
def suffix_best(a: list[list[int]]) -> list[list[int]]:
"""g[i][j] = 第 i..F 朵花放进花瓶 j..V 的最大美观度(后缀 DP,用于贪心还原方案)。"""
f, v = len(a), len(a[0])
g = [[NEG] * (v + 2) for _ in range(f + 2)]
g[f + 1] = [0] * (v + 2) # 没有花要放:任何花瓶区间都是 0
for i in range(f, 0, -1):
# 第 i 朵花放 j 时,前面还剩 i-1 朵花,所以 j 至少是 i
for j in range(v - f + i, 0, -1):
g[i][j] = max(a[i - 1][j - 1] + g[i + 1][j + 1], g[i][j + 1])
return g
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
f, v = next(data), next(data)
a = [[next(data) for _ in range(v)] for _ in range(f)]
g = suffix_best(a)
# 贪心还原字典序最小方案:第 i 朵花取最小的花瓶 j,
# 使得「放 j + 后缀最优」仍等于整体最优 g[i][j]
j = 1
pos = []
for i in range(1, f + 1):
while a[i - 1][j - 1] + g[i + 1][j + 1] != g[i][j]:
j += 1
pos.append(j)
j += 1
print(g[1][1])
print(*pos)
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:
——后缀表 个状态,每个 转移;贪心还原每朵花至多扫 个花瓶,也是 。 ,总量约 。 - 空间复杂度:
存后缀表 。 - 实测:真实测试数据 20 组全部通过,单组约 0.02 s、峰值内存约 15 MB,远低于 1 s / 128 MB 限制。
总结
- 花按固定顺序放、位置必须递增 → "前
朵花 + 第 朵的位置"就是完整状态,天然适合线性 DP, 。 型转移用滚动前缀最大值优化,是"线性 DP + 前缀最值"的标准套路。 - 输出字典序最小方案的关键技巧:把 dp 反向后缀化,使"每一步取最早可行位置"成为合法贪心;判定条件是
a[i][j] + g[i+1][j+1] == g[i][j]。