问题引入
给定 n n n 个整数 a 1 , a 2 , a 3 , . . . , a n a_1,a_2,a_3,...,a_n a1,a2,a3,...,an,按从左到右的顺序选出尽量多的整数,组成一个严格上升子序列,求可以选出序列最长的长度。
动态规划
设 d ( i ) d(i) d(i) 为以 i i i 结尾的最长上升子序列的长度,则 d ( i ) = m a x { d ( i ) , d ( j ) + 1 } ( 1 ≤ j < i & & a [ j ] < a [ i ] ) d(i) = max\{ d(i), d(j)+1\}(1 \leq j<i~~ \&\&~~ a[j]<a[i]) d(i)=max{d(i),d(j)+1}(1≤j<i && a[j]<a[i])
显然最终答案是 m a x { d ( 1 ) , d ( 2 ) , . . . , d ( i ) } max\{ d(1),d(2),...,d(i) \} max{d(1),d(2),...,d(i)}
时间复杂度 O ( n 2 ) O(n^2) O(n2)
PS:如果需要求最长不下降子序列,改为 a [ j ] ≤ a [ i ] a[j] \leq a[i] a[j]≤a[i]即可
int a[maxn], d[maxn];
int n;
int LIS(int n) {
int ans = 1;
for (int i = 1; i <= n; i++) {
d[i] = 1;
for (int j = 1; j < i; j++) {
if (a[j] < a[i]) d[i] = max(d[i], d[j] + 1);
ans = max(ans, d[i]);
}
}
return ans;
}
二分
对于我们现在所求出的一个上升子序列,显然如果结尾的元素越小,越有可能得到更长的上升子序列
因此接下来我们考虑如何去拼接出使每个结尾尽量小的上升子序列
使用一个新的数组保存当前得到的上升子序列:
-
如果当前的 a [ i ] a[i] a[i] 大于该数组的末尾元素,那么接上去即可
-
否则我们直接从当前的数组中找到第一个大于等于 a [ i ] a[i] a[i] 的元素,将它替换为 a [ i ] a[i] a[i]
注意到在维护这个序列的过程中,我们将数组中第一个大于等于 a [ i ] a[i] a[i] 的元素替换为 a [ i ] a[i] a[i],有可能导致这个序列是错误的。但是它的长度仍然符合预期,因此我们可以理解为在当前子序列长度不变的情况下,这种替换会使后面出现更多的序列接入该序列从而得到更长的长度。
int a[maxn], d[maxn];
int n;
int LIS(int n) {
int len = 0;
//d[0] = -inf //如果序列出现负数需要初始化
for (int i = 1; i <= n; i++) {
if (a[i] > d[len]) {
d[++len] = a[i];
continue;
}
int pos = lower_bound(d + 1, d + 1 + len, a[i]) - d;
d[pos] = a[i];
}
return len;
}
树状数组优化
根据前面递推式:
d ( i ) = m a x { d ( i ) , d ( j ) + 1 } ( 1 ≤ j < i & & a [ j ] < a [ i ] ) d(i) = max\{ d(i), d(j)+1 \}(1 \leq j<i ~~\&\&~~ a[j]<a[i]) d(i)=max{d(i),d(j)+1}(1≤j<i && a[j]<a[i])
不难发现对于原序列每个元素,我们只需要将比它小的所有符合 1 ≤ j < i & & a [ j ] < a [ i ] 1 \leq j < i ~~ \&\& ~~ a[j]<a[i] 1≤j<i && a[j]<a[i] 的 d [ j ] d[j] d[j] 的最大值求出来,因此:
最长上升子序列实际上就是求最多有多少元素它们的下标和权值同时满足单调递增
于是我们将数组的每一个元素保存下标,然后按照权值从小到大排序。接着按从小到大的顺序枚举排序后的数组,我们的转移也就变成从之前的标号比它小的状态转移过来(当前是第 i i i 个元素,那么求 [ 1 , i − 1 ] [1,i-1] [1,i−1]的最大的 d [ i ] d[i] d[i])。因为每次只操作每个元素的前缀,因此只需要建立一个以下标为维护长度最大值的树状数组。
PS:如果出现相同权值的情况,按照序号从大到小可以保证所求为上升子序列,因为相同权值的数,前面的状态不能转移给后面,从大到小枚举就不会出现这种情况。
struct node {
int id, v;
bool operator<(const node &p) const {
if (v == p.v) return id > p.id;
return v < p.v;
}
} a[maxn];
int t[maxn];
int n;
void update(int i, int k) {
while (i <= n) {
t[i] = max(t[i], k);
i += lowbit(i);
}
}
int ask(int i) {
int ans = 0;
for (; i; i -= lowbit(i)) {
ans = max(ans, t[i]);
}
return ans;
}
int solve() {
for (int i = 1, x; i <= n; i++) {
cin >> x;
a[i] = node{i, x};
}
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; i++) {
update(a[i].id, ask(a[i].id) + 1);
}
return ask(n);
}
扩展一:求LIS的个数
设置数组 f f f 表示以当前数结尾形成最长的上升子序列的个数,转移前先计算出此时最长上升子序列的长度,则:
f [ i ] = m a x ( ∑ j = 1 i − 1 f [ j ] , 1 ) , j < i a n d a [ j ] < a [ i ] a n d d [ j ] + 1 = d [ i ] f[i] = max(\sum_{j = 1}^{i - 1}f[j], 1),j < i ~~and~~ a[j] < a[i] ~~and~~d[j] + 1 = d[i] f[i]=max(∑j=1i−1f[j],1),j<i and a[j]<a[i] and d[j]+1=d[i]
时间复杂度 O ( n 2 ) O(n^2) O(n2)。
int n;
int a[maxn], d[maxn];
ll f[maxn];
void solve() {
int ans = 0;
for(int i = 1; i <= n; i++) {
d[i] = 1;
for(int j = 1; j < i; j++) {
if(a[j] < a[i]) d[i] = max(d[i], d[j] + 1);
}
ans = max(ans, d[i]);
for(int j = 1; j < i; j++) {
if(a[j] < a[i] && d[j] + 1 == d[i]) {
f[i] += f[j];
}
}
if(!f[i]) f[i] = 1;
}
ll sum = 0;
for(int i = 1; i <= n; i++)
if(d[i] == ans) sum += f[i];
cout << ans << " " << sum << endl;
}
扩展二:Dilworth定理
定理
在有穷偏序集中,任何反链最大元素数目等于任何将集合到链的划分中链的最小数目。
偏序集的两个定理
定理一:令(
X
,
≤
X,\leq
X,≤)是一个有限偏序集,并令
r
r
r 是其最大链的大小。则
X
X
X 可以被划分成
r
r
r 个但不能再少的反链。
其对偶定理称为 Dilworth 定理。
定理二:令( X , ≤ X, \leq X,≤)是一个有限偏序集,并令 m m m 是反链的最大的大小。则 X X X 可以被划分成 m m m 个但不能再少的链。
上述两个定理可以简写为:链的最少划分数等于反链的最长长度
问题引入
给出一个序列,求出最少将这个序列划分为多少个上升的子序列。
结论
根据狄尔沃斯定理,最少的上升子序列的个数等于最长不升子序列的长度。其中的对应关系为:
- 最少的上升子序列( < < <)的个数等于最长不升子序列( ≥ \geq ≥)的长度
- 最少的不升子序列( ≥ \geq ≥)的个数等于最长上升子序列( < < <)的长度
- 最少的下降子序列( > > >)的个数等于最长不降子序列( ≤ \leq ≤)的长度
- 最少的不降子序列( ≤ \leq ≤)的个数等于最长下降子序列( > > >)的长度
暴力
设置一个数组存起来所有上升子序列的最后一个数,从左到右考虑每个数,然后枚举已经保存的上升子序列的结尾,如果存在一个上升子序列的结尾后面可以接上该数,则接上;否则就需要以该数为起始重新开一个上升子序列。这个做法得到的上升子序列一定是最少的。
int a[maxn], b[maxn], f[maxn];
int solve() {
int cnt = 0;
for(int i = 1; i <= n; i++) {
bool flag = 0;
for(int j = 1; j <= cnt; j++) {
if(f[j] <= a[i]) {
f[j] = a[i];
flag = 1;
break;
}
}
if(!flag) f[++cnt] = a[i];
}
return cnt;
}
证明
不难发现上述暴力做法中,保存的上升子序列的最后一个数一定是单调递增的,因为如果不是单调的那么它一定会接在前面的一个序列上。因此在找可以接上的第一个序列时,可以通过二分去找。
这样优化之后的代码就和找最长不升子序列的代码一模一样,严谨的证明如下:
从第 i i i 组中任取一个数,在第 i + 1 i+1 i+1 组一定能找到一个不大于它的数,因为如果找不到,那么第 i + 1 i + 1 i+1 组的数应该接在第 i i i 组上,从而矛盾。因此从第一组开始一定可以找到一个单调不升的子序列 K K K。设最长不升子序列长度为 P P P,则有 K ≤ P K \leq P K≤P ;又因为最长不升子序列中任意两个不在同一组内,则有 P ≥ K P \geq K P≥K,所以 K = P K=P K=P。
int a[maxn], b[maxn], f[maxn];
int solve() {
int cnt = 0;
for(int i = 1; i <= n; i++) {
int l = 1, r = cnt, pos = 0;
while(l <= r) {
int mid = (l + r) >> 1;
if(f[mid] <= a[i]) {
pos = mid;
r = mid - 1;
} else l = mid + 1;
}
if(!pos) f[++cnt] = a[i];
else f[pos] = a[i];
}
return cnt;
}