给定 \(1\sim n\) 的排列,\(q\) 次询问,每次查询区间 \([l,r]\) 内的最长上升子序列长度。
\(n \leq 10^5\)。
这里只讨论排列的情况,如果不是排列,也容易通过重新编号变成在 LIS 上等价的一排列。
\(O(n\sqrt{n}\ \mathrm{log}\ n+q\ \mathrm{log}\ n)\)
首先对于问题的关键在 LIS 的刻画方法上,常见的方法有 dp 和 二分贪心。dp 的做法看起来没有什么优化的空间,尝试使用 二分贪心 的方法。
首先回顾一下二分贪心求 LIS 的过程,它是维护一个集合 \(S\),每次在末尾新加入一个数时,在 \(S\) 中找到最小的大于它的数并替换,如果没有则直接插入。
代入到这个区间的问题上来,我们考虑令 \(S_{i,j}\) 表示从 \(i\) 顺序扫描到 \(j\) 结束后的集合 \(S\)。关于这个集合 \(S\) 有两个关键性质:
\(S_{i+1,j} \subseteq S_{i,j}\)
\(||S_{i+1,j}|-|S_{i,j}|| \leq 1\)。
第二条性质比较显然,可以考虑从 \(j\) 往前扫,\(|S_{i,j}|\) 和 \(|S_{i+1,j}|\) 唯一可能产生变化的地方在于 \(S_{i,j}\) 拥有在最后插入的一个 \(a_i\),然而 \(a_i\) 最多使得 LIS 长度变化 1。
对于第一条性质做一个说明,可以发现 \(S_{i,j}\) 和 \(S_{i+1,j}\) 的区别之处就是 \(S_{i,j}\) 在最开始比 \(S_{i+1,j}\) 多放一个 \(a_i\),\(S_{i+1,j}\) 初始则是空的,然后两个 \(S\) 都是正常插入 \(a_{i+1},a_{i+2},...,a_j\) 了。因此我们讨论 \(a_i\) 是否被替换了,如果 \(a_i\) 被替换了就毫无影响,那么就相当于是 \(a_{i+1},a_{i+2},...,a_j\) 按照上面的流程做一遍,因此 $S_{i,j}=S_{i+1,j}。如果 \(a_i\) 没被替换,那么显然对于 \(a_{i+1},a_{i+2},...,a_j\) 没有数小于 \(a_i\),即 \(a_i\) 最小,那么 \(a_i\) 显然影响不到后面的操作,因此 \(S_{i,j}={a_i} \cup S_{i+1,j}\)。
综上,两条性质均已被证明。我们继续回到原问题。
第一条性质说明了一件事情,就是我们可以认为 \(S_{i,j}\) 对于一个固定的 \(j\) 而言,从 \(S_{1,j}\) 扫到 \(S_{j,j}\) 的过程就是每次可以删除至多一个数,所以对于 \(a_1,a_2,...,a_j\) 每个数来说,它在 \(S_{i,j}\) 出现的都是一个 \(i\) 的前缀,尝试对于所有 \(j\),维护它的 \(a_i(i \leq j)\) 对应的前缀 \(p_{a_i}\)(注意:这里 \(p_x\) 表示值为 \(x\) 的位置对应的前缀,\(x\) 并非位置),那么如果我们把所有询问 \((l,r)\) 挂在 \(r\) 上,那么就是要求 \(j=r\) 时有多少个 \(p_i \geq l\)。
考虑对 \(j\) 做扫描线,当 \(j \to j+1\) 时维护 \(p\) 数组的变化,对于新加入的 \(v=a_{j+1}\),显然有 \(p_{v}=j+1\),因为不管前面的 \(S\) 长啥样,\(a_{j+1}\) 一定会替换掉其中一个使得 \(a_{j+1}\) 存在于 \(S\)。对于之前的 \(p_x\),若 \(x<v\),那么显然 \(v(a_{j+1})\) 的加入不会产生影响,因此我们只考虑 \(x>v\) 的 \(p_x\)。
对于 \(p_{v+1}\) 它不管之前在不在,一定会被 \(v\) 替换掉,所以直接 \(p_{v+1}=0\)。对于 \(p_{v+2}\),只有当插入 \(a_{j+1}\) 的时候 \(v+1\) 在 \(S\) 中出现,才能让 \(v+1\) 帮 \(v+2\) 挡一刀使得 \(v+2\) 活着,因此只有在 \(\mathrm{min}(p_{v+1},p_{v+2})\) 这个前缀 \(v+2\) 能活着,也就是当 \(p_{v+2}>p_{v+1}\) 时,会有 \(p_{v+2}=p_{v+1}\),否则不变。
模拟上面的过程,可以写出下面形式的代码:
p[v]=j+1,now=0;
for(int i=v+1;i<=n;i++)if(p[i]>now)swap(p[i],now);
首先考虑如果不是对 \(v+1\) 这个后缀操作,而是从 \(1\) 扫到 \(n\),即下面这个代码:
p[v]=j+1,now=0;
for(int i=1;i<=n;i++)if(p[i]>now)swap(p[i],now);
那么因为我们只关注有多少个 \(p_x \geq l\),因此我们只关系 \({p_i}\) 这个可重集。考虑执行上面那个代码对 \({p_i}\) 的影响,可以发现是若 \(now>\mathrm{max}\{p_i\}\),则不影响,否则会把最大值替换成 \(now\)。为什么是这样呢?考虑第一个满足 \(p_i>now\) 的位置,那么这个位置就会被换成 \(now\),然后 \(now\) 变成这个位置然后继续往后放,第二个也是如此,以此类推。如果我们设被替换的位置序列为 \(b_1,b_2,...,b_m\),那么最终它们的值就是 \(now,p_1,p_2,...,p_{m-1}\),对于整个可重集也就是把 \(a_m\) 替换成 \(now\),而 \(a_m\) 显然是整个数组的最大值,故此说明。这个过程可以用堆来维护。
现在思考 \(i=v+1,v+2,...,n\) 来操作时的做法。这里是一个类似于对区间操作的事情,如果我们还对于整个 \(\{p_i\}\) 的可重集考虑的话,因为可重集是个集合,关注的是整体的变化,没法记位置所以肯定不能再考虑整个了。但是我们可以分块,每个块内维护一个堆,记录整个块的可重集信息。对于 \(v+1 \sim n\) 的整块,从左到右扫描,每次维护一个当前的 \(now\)。对于当前扫到的块,如果 \(now<\mathrm{max}\{p_i\}\),那么就把 \(\mathrm{max}\{p_i\}\) 替换成 \(now\),并把 \(now=\mathrm{max}\{p_i\}\),否则不操作,做完之后扫下去。
整块知道怎么做了以后看散块,如果我们知道这个散块内每个元素是啥,那么就能直接模拟一遍了,然鹅我们对于块维护的都是整体信息,所以肯定还得弄出点新的东西来维护。我们考虑对于一个块,假设我们知道这个块被哪些 \(now\) 更新过(把整个块扫过一遍),能否还原出每个数呢?确定数的顺序应该和更新的顺序一致,从块内第一个元素开始还原。假设这个块是被 \(now:\{x_1,x_2,...,x_m\}\) 更新的(我们可以对于每个块记录标记,维护它被哪些 \(now\) 更新过),那么第一个元素一定是被最小的那个 \(x\) 更新,也就是说它的值就是 \(x\) 中的最小者,然后这个最小者在往后更新的时候因为和第一个元素 \(\mathrm{swap}\) 了,所以它在后面更新的时候值就是 \(p_{blk_1}\)(第一个元素原本值)了,也就是说要把 \(x\) 中最小值替换成 \(p_{blk_1}\),这个可以发现也是可以用堆维护的。当整个块处理完以后,因为 \(p\) 也更新了,所以得把标记清空。
这样就得到了一个 \(O(n\sqrt{n}\ \mathrm{log}\ n+q\ \mathrm{log}\ n)\) 的做法,可以通过。