用 O(n^2) 动态规划维护每个位置结尾的最长不下降子序列长度和方案数,最后汇总最优结尾。
OJ: luogu
题目 ID: P2362
难度:普及-
标签:dplis动态规划
创建: 2026-05-31 15:31
更新: 2026-10-07 10:45
后置题目
luogu P1020[NOIP 1999 提高组] 导弹拦截B 的第一问直接复用 A 教的“以 i 结尾定义子序列长度、按前后元素大小关系从 j 转移”这一步(brute.cpp 的 if (a[j] >= a[i]) dp1[i]=max(dp1[i],dp1[j]+1),正式解则写成对 -a[i] 求最长不下降子序列),只是把大小关系反向、再叠加二分维护尾值的 O(n log n) 优化与第二问的 Dilworth 结论。luogu P1233[ICPC 2001 Taejon R] 木棍加工B 的 main.cpp 沿用 A 教的「dp[i] 记为以 i 结尾的 LIS 长度、用 dp[j]+1 转移」这一步,只把不下降条件换成严格上升 a[j].w<a[i].w,并在前面叠加 A 未教的排序降维和 Dilworth 最少不升链覆盖,因而比普及- 的 A 更难。
题意
给出多组木桩高度序列。
每组都要从中按原顺序选出若干根木桩,使高度构成不下降序列。
要求输出:
- 最长不下降子序列长度;
- 达到这个长度的方案数。
思路
最直接的教学版做法是枚举所有子序列:
cpp
// brute.cpp:枚举所有子序列,统计最长不下降子序列长度和方案数,作为教学版和对拍基准程序。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
int T;
int n;
int a[MAXN];
int best_len;
long long ways;
void dfs(int pos, int last_value, int cur_len) {
if (pos > n) {
if (cur_len > best_len) {
best_len = cur_len;
ways = 1;
}
else if (cur_len == best_len) {
ways++;
}
return;
}
// 不选当前位置
dfs(pos + 1, last_value, cur_len);
// 选当前位置,必须保持不下降
if (a[pos] >= last_value) {
dfs(pos + 1, a[pos], cur_len + 1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> T;
while (T--) {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
best_len = 0;
ways = 0;
dfs(1, -1000000000, 0);
cout << best_len << ' ' << ways << '\n';
}
return 0;
}但真正提交时,用
设:
dp[i]:以i结尾的最长不下降子序列长度cnt[i]:达到dp[i]的方案数
初始化:
dp[i] = 1cnt[i] = 1
DP 公式
把序列记为
当
最后令
然后枚举 j < i:
如果 a[j] <= a[i],就可以从 j 转移到 i。
- 若
dp[j] + 1 > dp[i],说明找到更优解,直接覆盖长度和方案数; - 若
dp[j] + 1 == dp[i],说明又找到一种同样优的接法,把方案数累加。
最后先求出全局最长长度,再把所有达到这个长度的结尾方案数加起来。
公式解释:dp_i 只关心最后选中的木桩是 i,因为前面的合法序列只需要满足高度不超过 a_i。当从 j 接到 i 得到更长长度时,旧方案全部失效,所以方案数改成 cnt_j;如果长度相同,就说明多了一批同样最优的结尾方案,要把方案数累加。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
int T;
int n;
int a[MAXN];
int dp[MAXN]; // dp[i]:以 i 结尾的最长不下降子序列长度
long long cnt[MAXN]; // cnt[i]:达到 dp[i] 的方案数
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> T;
while (T--) {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
dp[i] = 1;
cnt[i] = 1;
}
int best_len = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j < i; j++) {
if (a[j] <= a[i]) {
if (dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
cnt[i] = cnt[j];
}
else if (dp[j] + 1 == dp[i]) {
cnt[i] += cnt[j];
}
}
}
best_len = max(best_len, dp[i]);
}
long long ways = 0;
for (int i = 1; i <= n; i++) {
if (dp[i] == best_len) {
ways += cnt[i];
}
}
cout << best_len << ' ' << ways << '\n';
}
return 0;
}复杂度
- 时间复杂度:
- 空间复杂度:
总结
这题是经典的最长不下降子序列 DP 扩展:
在维护长度的同时,再维护“达到这个最优长度的方案数”即可。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
