数据结构手法之区间 LIS 和区间 LCS

题目 link

P1: gym101237G
P2: P9109 [PA 2020] Tekstówka
P3: P2075 区间 LIS

区间 LCS

P1

给定两个序列 \(s,t\)\(q\) 次询问,每次查询 \(s\)\(t[l\ \mathrm{:}\ r]\) 的最长公共子序列长度。

\(|s|,|t| \leq 10^3,q \leq 10^5\)

先考虑怎么算两个序列的 LCS。有一个朴素的 dp,令 \(f_{i,j}\) 表示 \(s[1\ \mathrm{:}\ i]\)\(t[1\ \mathrm{:}\ j]\) 的 LCS。有如下转移:

\(s_{i+1}=t_{j+1}\),有 \(f_{i,j}+1 \to f_{i+1,j+1}\)

\(f_{i,j} \to f_{i+1,j}\)

\(f_{i,j} \to f_{i,j+1}\)

于是我们把所有 \((i,j)\) 画到网格图上,那么就相当 \((i,j)\)\((i+1,j),(i,j+1)\) 连出两条边权为 \(0\) 的有向边,若 \(s_{i}=t_{j}\),则有一条斜着的 \((i-1,j-1)\) 连向 \((i,j)\) 的边权为 \(1\) 的边。那么 \(f_{i,j}\) 的值即为 \((0,0)\)\((i,j)\) 的最长路。

推广到区间问题上,我们发现这个匹配的转移模式是不变的,所以依然可以视为图上的最段路问题,\(s\)\(t[l\ \mathrm{:}\ r]\) 的最长公共子序列长度即为 \((0,l-1)\)\((n,r)\) 的最长路。我们在之后用 \(\mathrm{dis}((x_1,y_1),(x_2,y_2))\) 表示 \((x_1,y_1)\)\((x_2,y_2)\) 的最长路。在问题里不妨将 \(l\) 减一计算 \(\mathrm{dis}((0,l),(n,r))\)

然而现在变成了两点间的最长路问题,并没有什么简化,因为跑一次最长路也是 \(O(n^2)\) 的。于是关于这个图有下列几个性质:

  • \(\mathrm{dis}((0,y),(i,j))-\mathrm{dis}((0,y),(i-1,j)) \in \{0,1\}\)

  • \(\mathrm{dis}((0,y),(i,j))-\mathrm{dis}((0,y),(i,j-1)) \in \{0,1\}\)

  • 对于所有 \((i,j)\),总存在一个 \(h(i,j)\),使得 \(\mathrm{dis}((0,y),(i,j))-\mathrm{dis}((0,y),(i,j-1))=[y \geq h(i,j)]\)

  • 对于所有 \((i,j)\),总存在一个 \(v(i,j)\),使得 \(\mathrm{dis}((0,y),(i,j))-\mathrm{dis}((0,y),(i-1,j))=[y < v(i,j)]\)

1,2 对称,3,4 对称。故这里只给出 1,3 的证明。

性质 1 证明

首先有 $\mathrm{dis}((0,y),(i,j)) \geq \mathrm{dis}((0,y),(i-1,j))$,不然可以直接从 $(i-1,j)$ 走到 $(i,j)$ 更新,并且若 $\mathrm{dis}((0,y),(i,j))-\mathrm{dis}((0,y),(i-1,j)) \geq 2$,那么我们总可以把 $(0,y)$ 到 $(i,j)$ 的一条最长路上走的 **最后一个斜边** 替换成向右走使得最终到达 $(i-1,j)$,并且因为它们相差二,所以这条替换得到的路径显然更优,这点矛盾了,得证。

这里为什么是最后一个斜边呢?因为这样的话它前面的所有斜边不会被影响,仍然都能走。

性质 3 证明

这等价于 $\mathrm{dis}((0,y),(i,j))-\mathrm{dis}((0,y),(i,j-1)) \leq \mathrm{dis}((0,y+1),(i,j))-\mathrm{dis}((0,y+1),(i,j-1))$。考虑取 $(0,y)$ 到 $(i,j)$ 和 $(0,y+1)$ 到 $(i,j-1)$ 的两条最长路,它们必然存在交点。我们把这两条路径交点之后走的路径交换,那么就得到 $(0,y)$ 到 $(i,j-1)$ 和 $(0,y+1)$ 到 $(i,j)$ 的两条路径,并且路径长度和不变,然而这个长度和显然小于 $\mathrm{dis}((0,y),(i,j-1))+\mathrm{dis}((0,y+1),(i,j))$,即 $\mathrm{dis}((0,y),(i,j))+\mathrm{dis}((0,y+1),(i,j-1)) \leq \mathrm{dis}((0,y),(i,j-1))+\mathrm{dis}((0,y+1),(i,j))$,移项得到原不等式。

可以发现根据上面的性质,我们有了相邻位置的 \(\mathrm{dis}\) 之间的差值关系,这意味着我们可以通过对差值的求和,得到最长路的另一种形式。
根据上面的性质,可以得到 \(\mathrm{dis}((0,l),(n,r))=\mathrm{dis}(0,l),(n,l)+\sum_{i=l+1}^r \mathrm{dis}((0,l),(n,i))-\mathrm{dis}((0,l),(n,i-1))=\sum_{i=l+1}^r [h(n,i) \leq l]\)。接下来只要求出 \(h\) 就行了,然而我们需要通过 \(v\) 配合求解,下面是求解的过程:

  • 首先有边界情况:\(h(0,i)=i,v(i,0)=0\)
  • 对于 \(h(i,j)\),若 \(s_i=t_j\),则 \(h(i,j)=v(i,j-1)\),否则 \(h(i,j)=\mathrm{max}(h(i-1,j),v(i,j-1))\)
  • 对于 \(v(i,j)\),对称的,有 \(s_i=t_j\)\(v(i,j)=h(i-1,j)\),否则 \(v(i,j)=\mathrm{min}(h(i-1,j),v(i,j-1))\)

证明参考 dp 转移的式子在纸上画画就行了,比较懒这里就不放了,读者可自行思考。

P2

给定两个序列 \(s,t\)\(q\) 次询问,每次查询 \(t[l_1\ \mathrm{:}\ r_1]\)\(t[l_2\ \mathrm{:}\ r_2]\) 的最长公共子序列长度。

\(|s|,|t| \leq 10^3,q \leq 10^5\)

现在是两个区间来匹了,如果直接对每个询问用上面的做法的话根本没有利用到性质,因为上面的题是多次询问,预处理以后每次询问复杂度降低,这个题不能直接套做法。

然而我们继续使用上面的那张 \(s,t\) 的转移图,可以发现现在其实就是要求 \((l_1-1,l_2-1)\)\((r_1,r_2)\) 的最长路,可以用经典猫树分治处理。具体的,我们考虑对 \(s\) 建出来线段树,我们考虑对于每一个 \([l_1,r_1]\)\(([l_1,r_1],[l_2,r_2])\) 挂到对应的线段树区间上,然后对于每一个线段树区间,把它的所有 \([l_2,r_2]\) 的贡献加上即可。

区间 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\) 有两个关键性质:

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

  • \(||S_{i+1,j}|-|S_{i,j}|| \leq 1\)

性质 2 证明

考虑从 $j$ 往前扫,$|S_{i,j}|$ 和 $|S_{i+1,j}|$ 唯一可能产生变化的地方在于 $S_{i,j}$ 拥有在最后插入的一个 $a_i$,然而 $a_i$ 最多使得 LIS 长度变化 1。

性质 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)\) 的做法,可以通过。

#include <bits/stdc++.h>
#define rep(i, l, r) for (int i = (l); i <= (r); i ++ )
#define per(i, r, l) for (int i = (r); i >= (l); i -- )
#define pb push_back
#define fi first
#define se second
using namespace std;

typedef pair<int, int> PII;

void tomin(int &x, int v) { x = (x < v? x: v); }
void tomax(int &x, int v) { x = (x > v? x: v); }

inline int read() {
    int x = 0, f = 1; char c = getchar();
    for(; (c < '0' || c > '9'); c = getchar()) { if (c == '-') f = -1; }
    for(; (c >= '0' && c <= '9'); c = getchar()) x = x * 10 + (c & 15);
    return x * f;
}


const int N = 1e5 + 5, B = 300, M = N / B + 5;
int n, Q, p[N], q[N], ans[N];
vector<PII> Qry[N];
priority_queue<int> val[N]; // val: 块内数  tag: 标记
priority_queue<int, vector<int>, greater<int> > tag[N];
int l[M], r[M], bl[N];              // l,r: 块left,right  bl: belong

struct BIT {
    int tr[N];
    void add(int x, int v) {
        x += 1;
        while (x <= n + 1) {
            tr[x] += v;
            x += x & (-x);
        }
    }
    int query(int x) {
        x += 1;
        int res = 0;
        while (x > 0) {
            res += tr[x];
            x -= x & (-x);
        }
        return res;
    }
    int query(int l, int r) {
        return query(r) - query(l - 1);
    }
} fenwick;

inline void rebuild(int x) {      //块内重构
    if (tag[x].empty()) return;
    rep(i, l[x], r[x]) if (q[i] > tag[x].top()) {
        int mn = tag[x].top(); tag[x].pop();
        tag[x].push(q[i]), q[i] = mn;
    }
    priority_queue<int, vector<int>, greater<int> >().swap(tag[x]);
}

inline void work(int left) {      //对 left~n 操作
    int now = 0; rebuild(bl[left]);
    rep(i, left, r[bl[left]]) if (q[i] > now) swap(q[i], now);
    priority_queue<int>().swap(val[bl[left]]);
    rep(i, l[bl[left]], r[bl[left]]) val[bl[left]].push(q[i]);
    rep(i, bl[left] + 1, bl[n]) if (val[i].top() > now) {
        int mx = val[i].top(); val[i].pop();
        val[i].push(now), tag[i].push(now), now = mx;
    }
    fenwick.add(now, -1), fenwick.add(0, 1);
}


int main() {

    n = read(), Q = read();
    rep(i, 1, n) p[i] = read();
    rep(i, 1, Q) {
        int x, y; x = read(), y = read();
        Qry[y].pb({x, i});
    }

    int len = sqrt(n + 0.5);
    rep(i, 1, (n - 1) / len + 1) l[i] = n;
    rep(i, 1, n) {
        bl[i] = (i - 1) / len + 1;
        tomin(l[bl[i]], i), tomax(r[bl[i]], i);
    }
    rep(i, 1, bl[n]) rep(j, 1, r[i] - l[i] + 1) val[i].push(0);
    fenwick.add(0, n);

    rep(i, 1, n) {
        int v = p[i];
        if (v < n) work(v + 1);   //pos{v+1~n}

        rebuild(bl[v]);
        fenwick.add(q[v], -1);
        q[v] = i;
        fenwick.add(q[v], 1);      //pos{v}

        priority_queue<int>().swap(val[bl[v]]);
        rep(j, l[bl[v]], r[bl[v]]) val[bl[v]].push(q[j]);

        for (auto x: Qry[i]) ans[x.se] = fenwick.query(x.fi, n); //ask Q
    }

    rep(i, 1, Q) cout << ans[i] << '\n';


    return 0;
}

注意,清空最好用代码中的方式(与空 pq 进行 \(\mathrm{swap}\)),不然会 T。

posted @ 2025-10-31 22:37  v1ne0qrs  阅读(39)  评论(0)    收藏  举报