关于 LIS,它没死,
0. 前言
LIS(最长上升子序列)为 DP(动态规划)的经典题型,也经常最为初学者们最先接触的 DP 题目。本文将详细介绍有关 LIS 的内容及拓展。
让我们从这一个简单问题开始:
给定一个长度为
的序列 ,请你求出它的最长上升子序列长度。
1. 最初的 DP
我们设
对于转移,我们可以找到最后一个不同的地方。由于最后一个元素一定是以
答案显然为
for (int i = 1; i <= n; ++ i )
{
f[i] = 1; // 因为 1 个元素也是上升子序列
for (int j = 1; j < i; ++ j )
if (a[j] < a[i])
f[i] = max(f[i], f[j] + 1);
}
时间复杂度
2. 优化
2.1. 贪心 + 二分
对于
不难看出应当留下
一般化这个问题。若有多个相同长度的上升子序列,那么我们只需要留下最后一个元素最小的子序列即可。
这是思路。考虑实现这个想法。
我们定义
如果仍然暴力去寻找这个子序列,复杂度仍为
这启发我们可以二分查找这个合法的上升子序列。具体的,我们要二分找到最后一个(因为越靠后下标越大,即上升子序列长度越大)小于
最后的答案就是
int len = 0;
for (int i = 1; i <= n; ++ i )
{
int l = 1, r = len, pos = 0;
while (l <= r)
{
int mid = l + r >> 1;
if (q[mid] < a[i]) pos = mid, l = mid + 1;
else r = mid - 1;
}
q[pos + 1] = a[i];
if (pos == len) ++ len;
}
cout << len;
2.2. 树状数组
如果我们将权值作为下标,那么对于一个元素
计算完后,我们在下标为
上述操作用到了“求前缀最大值”和“单点修改”,因此树状数组维护即可。
注意由于 map
维护树状数组。下面是用的离散化。
void modify(int u, int k)
{
for (int i = u; i <= n; i += lowbit(i))
tr[i] = max(tr[i], k);
}
int query(int u)
{
int res = 0;
for (int i = u; i; i -= lowbit(i))
res = max(res, tr[i]);
return res;
}
cin >> n;
for (int i = 1; i <= n; ++ i ) cin >> a[i], b[i] = a[i];
sort(b + 1, b + n + 1);
int len = unique(b + 1, b + n + 1) - b - 1;
for (int i = 1; i <= n; ++ i )
{
int k = lower_bound(b + 1, b + len + 1, a[i]) - b;
int q = query(k - 1) + 1;
modify(k, q);
res = max(res, q);
}
cout << res;
3. 例题
3.1
给定一个长度为
的序列,其中第 个元素有两个属性 。请你从中选择一个子序列,使得 递增。求最大的子序列的 属性之和。
, 。
设状态
答案为
首先注意到,
具体的,我们定义
此时可以发现,上面的
时间复杂度
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 200010;
int n, a[N], b[N], f[N];
int tr[N];
int lowbit(int x)
{
return x & -x;
}
void modify(int u, int x)
{
for (int i = u; i <= n; i += lowbit(i))
tr[i] = max(tr[i], x);
}
int query(int u)
{
int res = 0;
for (int i = u; i; i -= lowbit(i))
res = max(res, tr[i]);
return res;
}
main()
{
cin >> n;
for (int i = 1; i <= n; ++ i ) cin >> a[i];
for (int i = 1; i <= n; ++ i ) cin >> b[i];
for (int i = 1; i <= n; ++ i )
{
f[i] = query(a[i] - 1) + b[i];
modify(a[i], f[i]);
}
cout << *max_element(f + 1, f + n + 1);
return 0;
}
3.2
简化题意:有上下两条直线,每条直线上有
个点。已知有 条线段,第 条线段连接第一条直线的 和第二条直线的 。请你选出一些线段,使得两两之间没有交叉。求最多的选择的线段数。
。
考虑这样实现:首先我们将每条线段按
例如下图:
*其中蓝字
那么我们显然不能选择
避免这种情况就是需要找到随着
时间复杂度
#include <iostream>
#include <algorithm>
const int N = 2e5 + 10;
using namespace std;
int n, res, q[N], len;
pair<int, int> a[N];
int main()
{
cin >> n;
for (int i = 1; i <= n; ++ i )
cin >> a[i].first >> a[i].second;
sort(a + 1, a + n + 1);
int len = 0;
for (int i = 1; i <= n; ++ i )
{
int l = 1, r = len, pos = 0;
while (l <= r)
{
int mid = l + r >> 1;
if (q[mid] < a[i].second) pos = mid, l = mid + 1;
else r = mid - 1;
}
q[pos + 1] = a[i].second;
if (pos == len) ++ len;
}
cout << len;
return 0;
}
3.3
给定
的两个排列 和 ,求它们的最长公共子序列。
。
没错,这是最长公共子序列的模板题。
在这道题中,由于保证了给定的是两个排列,也就是不存在重复元素,所以我们可以尝试将最长公共子序列问题转化成最长上升子序列问题。
分析这样一组数据。两个序列分别是:
现在尝试把
这样的重新标号使得
此时,显然
这是一条十分重要的性质。那么我们就可以找重新标号后
最长上升子序列问题可以使用上面的任意一种方法优化,因此整道题的时间复杂度降到了
3.4
给定一颗
个点的树,点有点权。请对于每个 ,求出 号点到 号点的路径上最长上升子序列的长度。
, 。
我们不妨设
注意需要提前将
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, w[N], p[N], a, b;
int h[N], e[N * 2], ne[N * 2], idx = 1;
int len;
int f[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
int tr[N];
int lowbit(int x)
{
return x & -x;
}
void modify(int u, int x)
{
for (int i = u; i <= n; i += lowbit(i))
tr[i] = max(tr[i], x);
}
int query(int u)
{
int res = 0;
for (int i = u; i; i -= lowbit(i))
res = max(res, tr[i]);
return res;
}
int gett(int x)
{
return lower_bound(p + 1, p + len + 1, w[x]) - p;
}
void dfs(int u, int fa)
{
for (int i = h[u]; i; i = ne[i])
{
int j = e[i];
if (j == fa) continue;
int k = gett(j);
queue<int> v; // 用一个队列存储变化过的值,省去不必要的空间
for (int q = k; q <= n; q += lowbit(q))
v.push(tr[q]);
f[j] = query(k - 1) + 1;
modify(k, f[j]);
dfs(j, u);
for (int q = k; q <= n; q += lowbit(q))
tr[q] = v.front(), v.pop();
}
}
void getv(int u, int fa)
{
for (int i = h[u]; i; i = ne[i])
{
int j = e[i];
if (j == fa) continue;
f[j] = max(f[j], f[u]);
getv(j, u);
}
}
int main()
{
cin >> n;
for (int i = 1; i <= n; ++ i ) cin >> w[i], p[i] = w[i];
sort(p + 1, p + n + 1);
len = unique(p + 1, p + n + 1) - p - 1;
for (int i = 1; i < n; ++ i )
{
cin >> a >> b;
add(a, b), add(b, a);
}
modify(gett(1), 1);
f[1] = 1;
dfs(1, -1);
getv(1, -1);
for (int i = 1; i <= n; ++ i ) cout << f[i] << '\n';
return 0;
}
3.5
给定
个操作,每次操作给出 ,并在 序列里依次添加 。 求最后
的最长上升子序列。
, 。
设计初始 DP:设
-
若
,也就是两个区间没有交集,那么我们可以直接把 接到 后面。即 ; -
若
,也就是两个区间存在交集,那么直接把把 接到 后面是不可取的,因为这会导致 这一段区间重复。所以我们需要将这一段区间去掉。即 。
因此总转移为:
直接转移复杂度是
我们维护两颗线段树,均以
计算完后需要在当前下标位置更新。
注意计算前需要离散化。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 400010;
int n, l[N], r[N];
int L[N], R[N], b[N], cnt, len;
int f[N];
struct Tree
{
struct Node
{
int l, r, v;
}tr[N << 2];
#define ls (u << 1)
#define rs (u << 1 | 1)
void pushup(int u)
{
tr[u].v = max(tr[ls].v, tr[rs].v);
}
void build(int u, int l, int r)
{
tr[u] = {l, r, (int)-2e9};
if (l == r) return;
int mid = l + r >> 1;
build(ls, l, mid), build(rs, mid + 1, r);
}
void modify(int u, int x, int d)
{
if (tr[u].l > x || tr[u].r < x) return;
if (tr[u].l == tr[u].r) tr[u].v = max(tr[u].v, d);
else
{
int mid = tr[u].l + tr[u].r >> 1;
if (x <= mid) modify(ls, x, d);
else modify(rs, x, d);
pushup(u);
}
}
int query(int u, int l, int r)
{
if (l > r || tr[u].l > r || tr[u].r < l) return -2e9;
if (tr[u].l >= l && tr[u].r <= r) return tr[u].v;
int mid = tr[u].l + tr[u].r >> 1, res = -2e9;
if (l <= mid) res = query(ls, l, r);
if (r > mid) res = max(res, query(rs, l, r));
return res;
}
}A, B;
main()
{
cin >> n;
for (int i = 1; i <= n; ++ i )
cin >> l[i] >> r[i],
b[ ++ cnt] = l[i], b[ ++ cnt] = r[i];
sort(b + 1, b + cnt + 1);
len = unique(b + 1, b + cnt + 1) - b - 1;
for (int i = 1; i <= n; ++ i )
L[i] = lower_bound(b + 1, b + len + 1, l[i]) - b,
R[i] = lower_bound(b + 1, b + len + 1, r[i]) - b;
A.build(1, 1, len), B.build(1, 1, len);
for (int i = 1; i <= n; ++ i )
{
f[i] = max({
r[i] - l[i] + 1,
A.query(1, 1, L[i] - 1) + r[i] - l[i] + 1,
B.query(1, L[i] + 1, R[i] - 1) + r[i]
});
A.modify(1, R[i], f[i]);
B.modify(1, R[i], f[i] - r[i]);
}
cout << *max_element(f + 1, f + n + 1);
return 0;
}
3.6
给定长度为
的序列 ,其中有一些数是未知的,这些未知的数为 。 请你为每个未知的数赋值,使得
的严格最长上升子序列最长。求最长的长度。 例如:
。如果把 ,那么 的严格最长上升子序列为 。
采用“贪心 + 二分优化 LIS”的方法。
从前往后考虑每个
- 如果
是确定的:- 根据上面讲的,二分找到最后一个小于
的 ,并将 。
- 根据上面讲的,二分找到最后一个小于
- 如果
是不确定的:- 考虑如果将它放在某个
的后面,那么将 改为多少比较好呢? - 我们希望仍然上升,但又不想对后面造成太大影响。所以我们应该将其改成
。 - 此时我们并不知道
是多少。但是我们可以在后续用到它的时候,再决定此时的 。否则在没用到它的时候, 是多少都无所谓。 - 所以我们可以对于每个
都计算。也就是对于所有的 ,将 。
- 考虑如果将它放在某个
这个东西是很好做的。第一种情况二分查找,第二种情况只需要记录一下全局加的次数即可。
4. 后言
参考文献:
- 动态规划基础 - OI Wiki;
- stO @gyh 的题解 AcWing 896. 最长上升子序列 II;
- stO @RNTBW 的题解 ARC159D-solution。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】