yet LIS
经典 LIS 贪心表的变体:同一位置的候选值先统一评估再统一写回,避免同一位置的候选值互相接龙。
OJ: roj
题目 ID: 19998
难度:普及+/提高-
标签:动态规划贪心二分
日期: 2026-08-28 19:47
形式化题目
有
思路
一句话本质:把经典 LIS 贪心表的"逐数更新"改为"同位置候选先统一评估、再统一写回",避免同一位置的候选值互相接龙。
直接暴力枚举会怎样?
枚举每个位置选哪个候选值,共
/**
* 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-08-28 19:06
* update_at: 2026-08-28 19:06
*/
// brute.cpp:小数据暴力解,使用选择序列递归枚举所有可能。
// 每一层递归在为"第 dep 个位置"选择候选值下标(1..k),
// 生成完整的 choose[] 后,在叶子节点按选出的序列跑标准 O(n^2) LIS,
// 所有方案里取最大的 LIS 长度就是答案。
// 只能处理很小的数据(k^n 种方案),用于对拍验证 main.cpp。
#include <bits/stdc++.h>
using namespace std;
const int MAXK = 10;
const int MAXN = 15;
int k, n;
int cand[MAXN][MAXK]; // cand[i][j] = 第 i 个位置的第 j 个候选值
int choose[MAXN]; // choose[i] = 第 i 个位置选的候选值下标(1..k)
int seq[MAXN]; // 由 choose 生成的完整序列
int ans;
// 对当前完整选择序列 seq[1..n] 跑标准 O(n^2) 的 LIS,返回长度。
int calc_answer() {
int dp[MAXN] = {0};
int best = 0;
for (int i = 1; i <= n; i++) {
dp[i] = 1;
for (int t = 1; t < i; t++) {
if (seq[t] < seq[i]) dp[i] = max(dp[i], dp[t] + 1);
}
best = max(best, dp[i]);
}
return best;
}
// dfs(dep) 为第 dep 个位置选择候选值下标,生成完整选择序列后在叶子统计。
void dfs(int dep) {
if (dep == n + 1) {
for (int i = 1; i <= n; i++) seq[i] = cand[i][choose[i]];
ans = max(ans, calc_answer());
return;
}
// 这一层在选择第 dep 个位置用哪个候选值(共 k 种选择)。
for (int c = 1; c <= k; c++) {
choose[dep] = c;
dfs(dep + 1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> k >> n; // 注意输入顺序:k 在前,n 在后
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= k; j++) cin >> cand[i][j];
}
dfs(1);
cout << ans << '\n';
return 0;
}这个暴力把每一层递归看作"第 dep 个位置在 k 个候选值里选一个"的选择序列,生成完整选择后到叶子节点跑标准
每个位置只有一个数时,LIS 怎么求?
经典 LIS 贪心:维护
现在每个位置有
会非法接龙:候选
怎样阻止同位置接龙?
把一轮拆成两步:第一步用"上一阶段结束时的
为什么每个长度只保留一个最小结尾值就够?
对每个候选值都从表头找"最大可接长度",能更快吗?
候选行已排序、单调不降,while (p+1 <= n && v[p+1] < a[j]) p++;,则
样例推演(DP 表)
这张表展示样例
| 阶段 | 候选值 |
评估用的旧 |
写回后的 |
|
|---|---|---|---|---|
| 初始 | — | — | — | |
| 位置 1 | 1 | 1 | ||
| 位置 1 | 3 | 1 | ||
| 位置 2 | 1 | 1 | ||
| 位置 2 | 2 | 2 |
观察点:候选值
正确性归纳:处理完前
代码
/**
* 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-08-28 19:06
* update_at: 2026-08-28 19:06
*/
/**
* C. yet LIS
* 每个位置 i 有 k 个已排序的候选值 a[i][1] <= ... <= a[i][k],
* 每个位置恰好选一个数构成序列,求所有可能序列的最长严格上升子序列的最大长度。
*
* 做法(官方双指针,O(n^2 + n*k)):
* v[L] = 处理完已走过的位置后,长度为 L 的严格上升子序列的最小结尾值。
* v[1..] 严格递增,v[L] = INF 表示长度 L 目前不可达。
* 对当前位置的 k 个候选值(已排序):
* 1) 先用"上一阶段"的 v 一次性算出每个候选值的 dp[j]:
* 双指针 p 表示最大的下标满足 v[p] < a[j],则 dp[j] = p + 1。
* 2) 再统一更新 v[dp[j]] = min(v[dp[j]], a[j])。
* 分两步是为了避免同一位置的候选值互相"接龙"(同一个位置用了两个数)。
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXK = 5005; // k 的最大值
const int MAXN = 1005; // n <= 1000,留几个空位
const int INF = 1000000010; // 比所有值(<=1000)都大,表示"不可达"
int k, n; // 每个位置 k 个候选值,共 n 个位置
int a[MAXK]; // 当前这一行的 k 个候选值(题目保证已排序)
int v[MAXN]; // v[L] = 长度为 L 的严格上升子序列的最小结尾值,INF 表示不可达
int dp[MAXK]; // dp[j] = 本轮选择候选值 a[j] 能得到的最长上升子序列长度
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> k >> n; // 注意输入顺序:k 在前,n 在后
for (int L = 1; L <= n; L++) v[L] = INF;
for (int i = 1; i <= n; i++) { // 依次处理每个位置
for (int j = 1; j <= k; j++) cin >> a[j];
// 第一步:用上一阶段结束时的 v,一次性算出所有 dp[j]。
// v 严格递增、a[j] 单调不降,所以 p 单调不降,可以双指针。
int p = 0; // 最大的下标满足 v[p] < a[j]
for (int j = 1; j <= k; j++) {
while (p + 1 <= n && v[p + 1] < a[j]) p++;
dp[j] = p + 1; // 长度 dp[j]-1 的链都能接上 a[j]
}
// 第二步:统一应用更新。同一长度取更小的结尾值(min 自动完成)。
for (int j = 1; j <= k; j++) {
v[dp[j]] = min(v[dp[j]], a[j]);
}
}
// 答案 = 最大的可达长度
for (int L = n; L >= 1; L--) {
if (v[L] != INF) {
cout << L << '\n';
return 0;
}
}
return 0;
}复杂度
每轮双指针
总结
这题是经典 LIS 贪心的直接扩展,卡点不在"找 LIS",而在"同一位置有多个互斥候选值":必须把"评估"和"写回"分两步,否则候选值会自我接龙。理解