O(n²) DP 递推每个位置结尾的最长不下降子序列的长度与元素和,同长时取编号字典序最小的序列。
OJ: luogu
题目 ID: P2008
难度:普及-
标签:dp枚举
日期: 2026-06-14 15:57
题意
有 n 个人,每人手上有一个一位数字(0~9)。对于第 i 个人,找到以他结尾的最长不下降子序列,计算该子序列中所有数字的和,作为他的分数。
如果存在多个最长不下降子序列(长度相同),选择编号字典序最小的那个——即把子序列中各元素在原数组中的下标按顺序排列,比较这些下标序列的字典序。
思路
对于每个位置 i,扫描所有 j < i,当 a[j] ≤ a[i] 时尝试转移:
dp[i]记录以 i 结尾的最长不下降子序列的长度sum[i]记录该序列的元素和pre[i]记录该序列中 i 的前驱下标(0 表示自己就是起点)
转移时优先取长度更长的序列;长度相同时保留编号字典序更小的序列(即之前已找到的、下标更靠前的前驱)。
DP 公式
设
当
若出现相同长度,保留字典序更小的前驱方案。最终从
先看朴素解法的代码:
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; ++i) cin >> a[i];
vector<int> len(n + 1), sum(n + 1), pre(n + 1);
auto seq_less = [&](int x, int y) -> bool {
if (x == y) return false;
vector<int> px, py;
for (int p = x; p; p = pre[p]) px.push_back(p);
for (int p = y; p; p = pre[p]) py.push_back(p);
reverse(px.begin(), px.end());
reverse(py.begin(), py.end());
return px < py;
};
for (int i = 1; i <= n; ++i) {
len[i] = 1;
sum[i] = a[i];
for (int j = 1; j < i; ++j) {
if (a[j] > a[i]) continue;
int clen = len[j] + 1;
if (clen > len[i]) {
len[i] = clen;
sum[i] = sum[j] + a[i];
pre[i] = j;
} else if (clen == len[i]) {
if (seq_less(j, pre[i])) {
sum[i] = sum[j] + a[i];
pre[i] = j;
}
}
}
cout << sum[i] << " \n"[i == n];
}
return 0;
}下面是另一种「01 序列」风格的暴力写法。它对每个结尾位置 i,递归枚举前面每个位置“选 / 不选”。递归先生成完整选择,叶子节点再检查序列是否不下降,并统计以 i 结尾的最优答案:
另一种暴力写法:01 序列
// brute_01_style.cpp:01 序列风格暴力,对每个结尾 i 枚举前面位置选或不选。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
int n;
int a[MAXN];
int target_pos;
int choose_pos[MAXN]; // choose_pos[i] = 0/1,表示位置 i 不选/选
vector<int> best_path;
bool path_less(const vector<int> &x, const vector<int> &y) {
if (y.empty()) {
return true;
}
return x < y;
}
int path_sum(const vector<int> &path) {
int sum = 0;
for (int i = 0; i < (int)path.size(); i++) {
sum += a[path[i]];
}
return sum;
}
void update_answer() {
vector<int> path;
for (int i = 1; i < target_pos; i++) {
if (choose_pos[i] == 1) path.push_back(i);
}
path.push_back(target_pos);
for (int i = 1; i < (int)path.size(); i++) {
if (a[path[i - 1]] > a[path[i]]) {
return;
}
}
if ((int)path.size() > (int)best_path.size()) {
best_path = path;
return;
}
if ((int)path.size() == (int)best_path.size() && path_less(path, best_path)) {
best_path = path;
}
}
void dfs_choose(int dep) {
if (dep == target_pos) {
update_answer();
return;
}
for (int i = 0; i <= 1; i++) {
choose_pos[dep] = i;
dfs_choose(dep + 1);
}
}
int solve_one_position(int pos) {
target_pos = pos;
best_path.clear();
dfs_choose(1);
return path_sum(best_path);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= n; i++) {
if (i > 1) {
cout << ' ';
}
cout << solve_one_position(i);
}
cout << '\n';
return 0;
}brute.cpp 实现了完整的 O(n²) 枚举,同长时通过 seq_less 回溯整个 pre 链来正确比较编号字典序,适合小数据验证。
最终的 main.cpp 同样采用 O(n²) 枚举所有前驱,但做了简化:当两个候选序列长度相同时,保留下标更小的前驱。这是因为对于相同长度的序列,较小的前驱下标使整个编号序列的字典序更小。
公式解释:dp_i 表示最后一个数固定为 a_i 时能得到的最长长度。只有 a_j <= a_i 的前驱才能接到 i,所以用 dp_j + 1 更新;长度相同时保留更靠前、更小字典序的前驱,是为了恢复出题目要求的那条序列。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e4 + 5;
ll a[maxn]; // 输入的数字 (0~9)
ll dp[maxn]; // dp[i] = 以 i 结尾的 LNDS 长度
ll sum[maxn]; // sum[i] = 该序列的元素和
int pre[maxn]; // pre[i] = 该序列中 i 的前驱下标 (0 表示无前驱)
int n;
void init() {
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
init();
// 第一个位置:序列只有自己
dp[1] = 1;
sum[1] = a[1];
// 递推位置 2..n
for (int i = 2; i <= n; ++i) {
dp[i] = 1; // 至少是长度为 1 的序列(只包含自己)
sum[i] = a[i];
int pre_j = 0; // 当前最优前驱
for (int j = 1; j < i; ++j) {
if (a[j] > a[i]) continue; // 不满足不下降
int clen = dp[j] + 1;
if (clen > dp[i]) {
// 长度更优,直接替换
dp[i] = clen;
sum[i] = sum[j] + a[i];
pre_j = j;
} else if (clen == dp[i]) {
// 长度相同:保留编号字典序更小的序列
// 因为 pre_j 一定是之前找到的编号更小的前驱,
// 而 j 是更大的编号,默认保留 pre_j 即可得到字典序更小的序列
}
}
pre[i] = pre_j;
}
// 输出所有结果
for (int i = 1; i <= n; ++i) {
cout << sum[i] << " \n"[i == n];
}
return 0;
}复杂度
- 时间复杂度:O(n²)。每个位置 i 扫描 i-1 个前驱,总比较次数约 n²/2。
- 空间复杂度:O(n)。
总结
本题是最长不下降子序列(LNDS)的变体,需要在求长度的同时维护元素和。核心注意点:
- DP 状态需要分别记录长度
dp[i]和元素和sum[i] - 长度优先,同长时按编号字典序比较
- 字典序的比较可以通过
pre链回溯实现,也可以在长度相同时直接保留下标更小的前驱(二者等效)
对于 n ≤ 10^4,O(n²) 的算法即可通过,编码简单直观。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
