当前位置: 首页 > news >正文

神秘数据结构手法之区间 LIS

给定 \(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\) 有两个关键性质:

  1. \(S_{i+1,j} \subseteq S_{i,j}\)

  2. \(||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)\) 的做法,可以通过。

http://www.jsqmd.com/news/27948/

相关文章:

  • 软考九
  • [该退役了]
  • 逆向基础--汇编语言介绍(003)
  • 文档抽取技术的实现原理及其在法律行业的应用价值分析
  • 【算法导论】2分治法
  • c++写得多不如写得少,同样的逻辑写的多报错逆天
  • 整理数学数据结构
  • viewerjs+vue3 using typescript
  • 题解:B4207 [常州市赛 2021] 战士
  • 最小二乘问题详解7:正则化最小二乘
  • 什么是重组蛋白?
  • 代码大全2{3}
  • work3
  • 25.10.31
  • 关于计数
  • 游记2
  • WebRTC实时音视频通信核心原理
  • Python高阶和匿名函数 _ 脱了马甲也要认识
  • 第11天(中等题 滑动窗口)
  • 麒麟 V10系统中离线安装python的setuptools和pip,并使用python代码查询达梦数据库,并上传文件到minio
  • 如何选择陶瓷放电管
  • 10.31每日总结
  • 对称密钥算法 非对称密钥算法 Hash函数 公钥和私钥在网络安全中的应用流程超超超详细,清楚,简单!!!
  • 读《代码大全2》读后感3
  • revit api楼梯创建
  • 《代码大全2》初读有感
  • 代码大全2{2}
  • revit api 几何图元连接
  • 读《代码大全2》读后感2
  • 公众号排版工具实测报告:为什么有一云AI编辑器成为全能高效的“排版专家”?