最基础的方法
有用的信息无非是 "权值" 和 "长度"。于是可以固定权值,求最长长度(树状数组);或者固定长度 \(j\),求最小权值 \(g_j\)。
方案数?
延续上文的方法:可以用树状数组维护最值时同时维护方案数;或者,将 \(g\) 开成一个 \(\text{vector}\),放置所有 \(\text{LIS}\) 为 \(j\) 的数字及其方案数,显然这些数字是递减的。设位置 \(i\) 的方案数为 \(f_i\),它的 \(\text{LIS}\) 为 \(p\),显然有如下转移:
因为存储了历史值,所以显然不是所有属于 \(g(p-1)\) 的数字都能转移到 \(i\)。所以需要做一个前缀和 \(+\) 二分。
\(\text{Dilworth}\) 定理
不上升子序列最小个数 \(=\) \(\text{LIS}\) 的长度。
树上 \(\text{LIS}\)
例 1.
\(\text{BZOJ }\)大根堆
还是和 基础方法 一样:对于固定权值,此时发现儿子之间是互不影响的,于是可以使用线段树合并:维护以 \(u\) 为根的小于等于权值 \(v\) 的最长长度,支持区间加和区间取 \(\max\);对于固定长度,定长的数组无法处理,所以可以开一个 \(\text{multiset}\)。
例 2.
\(\text{CF490F Treeland Tour}\)
对于固定权值,用线段树分别维护 \(\text{LIS}\) 与 \(\text{LDS}\)。具体合并两棵线段树时,可以以 \(\text{mid}\) 为界进行 \(\text{LIS}\) 与 \(\text{LDS}\) 的合并。
极长 \(\text{LIS}\)
例 1.
\(\text{BZOJ - 2957 }\)楼房重建
将斜率作为权值,题目要求的就是 能选则选 的极长 \(\text{LIS}\)。
考虑用线段树维护,节点 \([l,r]\) 维护区间最大斜率 \(k\) 和 从 \(l\) 开始 的极长 \(\text{LIS}\),令其为 \(L\)。定义 \(\textbf{calc}(o,k)\) 为区间 \(o\) 中,且起点大于 \(k\) 的极长 \(\text{LIS}\)。
那么更新节点 \(o\) 的答案时,就只用左儿子的答案加上 \(\textbf{calc}(\text{rson},k_{\text{lson}})\) 即可。
关于 \(\textbf{calc}()\) 的内部实现,当左儿子的最大斜率不大于 \(k\) 时直接递归右儿子;反之递归左儿子,加上右半部分已经算好的值(因为左半部分的斜率不会变化)。注意右半部分算好的值不是 \(L_{\text{rson}}\),这个没有考虑左半部分的斜率。
时间复杂度 \(\mathcal O(n\log^2 n)\)。
#include <cstdio>
#define print(x,y) write(x),putchar(y)
template <class T>
inline T read(const T sample) {
T x=0; char s; bool f=0;
while((s=getchar())>'9' or s<'0')
f |= (s=='-');
while(s>='0' and s<='9')
x = (x<<1)+(x<<3)+(s^48),
s = getchar();
return f?-x:x;
}
template <class T>
inline void write(T x) {
static int writ[40],w_tp=0;
if(x<0) putchar('-'),x=-x;
do writ[++w_tp]=(x-x/10*10),x/=10; while(x);
while(putchar(writ[w_tp--]^48),w_tp);
}
#include <iostream>
using namespace std;
const int maxn = 1e5+5;
int n,m;
struct node {
double k; int ans;
} t[maxn<<2];
int calc(int o,int l,int r,const double k) {
if(l==r) return t[o].k>k;
int mid=l+r>>1;
if(t[o<<1].k<=k)
return calc(o<<1|1,mid+1,r,k);
return calc(o<<1,l,mid,k)+t[o].ans-t[o<<1].ans;
}
void ins(int o,int l,int r,int p,const double k) {
if(l==r) return t[o].k=k,t[o].ans=1,void();
int mid=l+r>>1;
if(p<=mid) ins(o<<1,l,mid,p,k);
else ins(o<<1|1,mid+1,r,p,k);
t[o].k = max(t[o<<1].k,t[o<<1|1].k);
t[o].ans = t[o<<1].ans+calc(o<<1|1,mid+1,r,t[o<<1].k);
}
int main() {
n=read(9),m=read(9);
for(int i=1;i<=m;++i) {
int x=read(9),y=read(9);
ins(1,1,n,x,1.0*y/x);
print(t[1].ans,'\n');
}
return 0;
}
例 2.
\(\text{Nowcoder - 7615D }\)牛半仙的妹子序列
题目大意:给定一个长度为 \(n\) 的排列,求出包含极长上升子序列的个数。\(n\le 2\cdot 10^5\)。
首先想到 \(\mathtt{dp}\),发现 \(j\) 能向 \(i\) 转移的条件是:只考虑 \(<a_i\) 的数,\(a_j\) 是区间 \([j,i)\) 中最大的数,且 \(a_j<a_i\)。
其实这可以转化为 \(\text{cdq}\) 分治,我们可以按照 \(a\) 来分治,转移时用前半部分贡献后半部分即可。
问题是后面的条件如何满足?事实上它可以转化为双向的条件:在 \([l,\text{mid}]\) 之中求出第一个满足在 原序列 上下标大于 \(j\) 且值大于 \(a_j\) 的数字在原序列上的下标 \(r_j\);在 \((\text{mid},r]\) 之中求出第一个满足在 原序列 上下标小于 \(i\) 且值小于 \(a_i\) 的数字在原序列上的下标 \(l_i\)。
由于 \(r_j,l_i\) 均满足权值在 \((a_j,a_i)\) 之间,那么显然上面的条件可以这样被描述:
这个问题就很简单了 —— 将 \(i\) 按照 \(l_i\) 从大到小排序,依次在树状数组中添加 \(j\) 的 \(\mathtt{dp}\) 值,拿 \(r_i\) 来查询即可。
时间复杂度 \(\mathcal O(n\log^2 n)\)。
其实这道题目也可以转化为上一道例题:我们发现,对于每个 \(i\),合法的 \(j\) 其实就是从 \(i\) 开始能选则选的单增序列(当然从左往右就是单减的)!转移时从小到大枚举权值,这样就能丢掉 最大值小于 \(a_i\) 的限制。维护时直接套板子,\(\textbf{calc}(o,k)\) 的含义就是大于 \(k\) 的方案数,所以需要维护最大值。时间复杂度仍然是 \(\mathcal O(n\log^2 n)\) 的。
看看代码吧:
#include <cstdio>
#define print(x,y) write(x),putchar(y)
template <class T>
inline T read(const T sample) {
T x=0; char s; bool f=0;
while((s=getchar())>'9' or s<'0')
f |= (s=='-');
while(s>='0' and s<='9')
x = (x<<1)+(x<<3)+(s^48),
s = getchar();
return f?-x:x;
}
template <class T>
inline void write(T x) {
static int writ[40],w_tp=0;
if(x<0) putchar('-'),x=-x;
do writ[++w_tp]=(x-x/10*10),x/=10; while(x);
while(putchar(writ[w_tp--]^48),w_tp);
}
#include <iostream>
using namespace std;
const int maxn = 2e5+5;
const int mod = 998244353;
int n,pos[maxn],lim;
int a[maxn],dp[maxn];
struct node {
int mx,lans;
} t[maxn<<2];
inline int inc(int x,int y) {
return x+y>=mod?x+y-mod:x+y;
}
int calc(int o,int l,int r,int k) {
if(l==r) return t[o].mx>k?dp[t[o].mx]:0;
int mid=l+r>>1;
if(t[o<<1|1].mx<=k) return calc(o<<1,l,mid,k);
return inc(calc(o<<1|1,mid+1,r,k),t[o].lans);
}
int ask(int o,int l,int r,int L,int R) {
if(l>=L && r<=R) {
int v=lim; lim = max(lim,t[o].mx);
return calc(o,l,r,v);
}
int mid=l+r>>1,ret=0;
if(R>mid) ret=ask(o<<1|1,mid+1,r,L,R);
if(L<=mid) ret=inc(ret,ask(o<<1,l,mid,L,R));
return ret;
}
void ins(int o,int l,int r,int p,int k) {
if(l==r) return t[o].mx=k,void();
int mid=l+r>>1;
if(p<=mid) ins(o<<1,l,mid,p,k);
else ins(o<<1|1,mid+1,r,p,k);
t[o].mx = max(t[o<<1].mx,t[o<<1|1].mx);
t[o].lans = calc(o<<1,l,mid,t[o<<1|1].mx);
}
int main() {
n=read(9);
for(int i=1;i<=n;++i)
pos[a[i]=read(9)]=i;
for(int i=1;i<=n;++i) {
int wh = pos[i]; lim=0;
dp[i] = ask(1,1,n,1,wh);
if(!dp[i]) dp[i]=1;
ins(1,1,n,wh,i);
}
int ans=0; lim=0;
for(int i=n;i;--i)
if(lim<a[i]) {
ans = inc(ans,dp[a[i]]);
lim = a[i];
}
print(ans,'\n');
return 0;
}