扫描线

1 扫描线

1.1 研究对象

在一个B维直角坐标系下,第i维坐标在一个整数范围[li,ri]间,内部的点集称为一个B维正交范围。
一般1维正交范围简称区间,2维正交范围简称矩形,3维正交范围简称立方体
对于B维正交范围,每一维都有两个限制,即有两条边(side),这样是一个2B-side的B维正交范围
如果部分维只有一个限制,则是一个A-side的B维正交范围
如果有一维没有限制,则这一维是平凡的,没有任何意义,可以简化到一个(B-1)维的问题
A-side的B维正交范围不能确定出是哪些维,如果维不对称的话需要特殊说明

1.2 模型

扫描线有两种模型:

  1. 对于一个静态的二维问题,我们可以使用扫描线扫一维,数据结构维护另一维
    在扫描线从左到右扫的过程中,会在数据结构维护的那一维上产生一些修改与查询
    如果查询的信息可差分的话直接使用差分,否则需要使用分治
  2. 另一种看待问题的角度是站在序列角度,而不站在二维平面角度
    如果我们这样看待问题,则扫描线实际上是枚举了右端点r=1…n,维护一个数据结构,支持查询对于当前的r,给定一个值l,l到r的答案是什么
    即扫描线扫询问右端点,数据结构维护所有左端点的答案

Notice:
其实看到任何范围修改查询问题,如果能差分的话,想都不想就差分是不会有问题的,我推荐直接这样做
典型的差分方法:
序列区间[l,r]差分为[1,r]-[1,l-1]的前缀
树上差分
二维前缀和的差分

1.3 处理二维正交范围的扫描线

问题可差分的时候,我们通过差分可以将一个4-side矩形查询问题变为两个3-side矩形查询问题的差
将第一维的1-side的区间(即前缀)扫描线扫掉,数据结构维护2-side的区间查询(这里两条边是相对的而不是相邻的),支持:
1.单点修改,区间查询
2.区间修改,单点查询
3.区间修改,区间查询
中的一种
(不要忘记了差分)

两大基础问题:

问题 1. 给一个长为n的序列,有m次查询,每次查区间[l,r]中值在[x,y]内的元素个数

我们考虑 \(x\) 轴表示序列,\(y\) 轴表示权值。

image

先差分,去掉序列维度的左端,然后对序列维度扫描线,维护竖着的数据结构,支持单点加点和区间查询和。

能使用树状数组尽量使用树状数组,这里可以差分(加法有逆)所以可以使用树状数组维护。

问题 2. 给一个二维平面,上面有n个矩形,每个矩形坐标[1,n]
有m次查询,每次查询一个二维平面上的点被多少矩形包含
image

对某一维从左到右扫描线并且维护每个点被多少个矩形包含。有区间修改,单点查询,把区间修改差分掉变两个前缀修改,单点查询差分掉变两个前缀查询,可以用树状数组维护。

1.4 几个例子

一维数颜色

【题意】
给定长度为 \(n\) 的序列,\(m\) 次查询区间中不同的数个数。
【分析】
思路 1:
我只想计算第一次在区间内出现的数的个数。考虑记 \(pre_i\) 表示上一个和自己相同的数的位置,那么只统计 \(l \le i \le r, pre_i < l\)\(i\) 个数。两个维度分别是 \(i, pre_i\),是二维数点问题。可以扫描线树状数组。(直接用二维前缀和处理是 \(O(nm)\) 的,不适合本题,适合稠密的矩形)。
思路 2:
考虑扫描线扫右端点,维护所有左端点的答案。我们只对区间内第一次出现的数统计答案。也就是对 \(l > pre_r\) 的位置的答案加上 \(1\)。(也有些像 dp 数组一起进行递推,然后用数据结构维护)
思路 3:
扫描线的时候同时维护一个数组表示每个颜色最后一次出现的位置。然后修改是单点赋值为 \(r\),查询是询问数组里面比 \(k\) 大的数有多少个。可以用 set 啥的做。而且插入都是从末尾插入。
思路 4:
分块,考虑每一块维护一个 bitset,线段树维护。
单次询问时间 \(O(\cfrac{n \log(\sqrt n)}{\omega})\)。这个不需要打乱询问次序。
思路 5:
莫队。

支持单点修改和区间查询颜色数

现在操作中存在单点修改。怎么做:
思路 1:
依然考虑 pre 数组。
考虑一次修改造成的变化。
image
有三个点的 \(pre\) 发生了改变。
考虑查询是二维平面上的数点,这些改变可以拆成一个单点 \(-1\)\(+1\),会作为基本单位对后续的询问产生影响。

对于每一个修改-询问二元组,都有一个这样的影响。但修改是不能快速找到位置的,所以我们考虑先要化简时间维度。

考虑 CDQ 分治。如果我们能够在 \(O(n~\mathrm{polylog})\) 的时间完成所有时间位于 \(mid\) 之前的修改和之后的询问,那么就可以解决这个问题。

注意到,这时候之前的修改和之后的询问变成了静态的。我们可以先花费 \(O(n)\) 时间扫一遍修改,把每一次修改具体修改了什么点确定了。然后考虑问题变成了:平面上有 \(O(n)\) 个点,每个点权值为 \(1\) 或者 \(-1\),有若干个矩形,求每个矩形内权值和。

这显然还是二维数点问题。把 3-side 的矩形差分为两个 2-side 的矩形,然后扫描线扫 \(l\),树状数组维护前缀和即可。

支持区间覆盖和区间查询颜色数

操作中出现了区间覆盖(P4690)。
思路 1:
我们考虑和单点修改版本差距是什么。是修改操作,也就是 \(pre\) 的修改换了。

考虑颜色段均摊。删除颜色段个数是有保障的。那么我们可以方便地维护颜色段。最后可以方便地计算 pre。(如果在段内但不在段首,\(pre_i = i - 1\),否则找上个颜色)但是有这个还不够。只有这个性质的话,cdq 分治的正确性依然无法保障,因为不是和分治的段数相关而是和 \(n\) 相关了。

我们还需要一个关于 \(pre\) 值改变个数的结论。

考虑颜色段替代的时候有什么影响:
image
可以发现删除一个连续段最多对 \(pre\) 产生 \(3\) 的影响。因此 \(m\) 次覆盖好像是 \(O(n + m)\) 的?

确实是这样,但是你注意 CDQ 分治某一层的总共影响就是 \(O(n+m)\) 的,所以我们只需要模拟 \(O(n+m)\log n\) 个影响。

于是时间复杂度 \(O(n + m \log^2 n)\)

问题你怎么写 CDQ 分治,怎么维护 pre 的修改,怎么做到空间复杂度尽量好?

  • 对于 pre 的修改,虽然我们删掉一次颜色之后可以得知哪些点改变了,但是这样写会有很多分讨,不好。正确的做法是,把所有可能改变的地方丢到 set 里面,把所有颜色段修改维护完,然后重新对 set 里面的点求 pre。

  • 对于时间分治的 cdq 怎么写:考虑分三步走:

    1. 统计 \([l, mid]\) 内修改对 \((mid, r]\) 内查询的影响。并且撤销影响。
    2. 递归到 \([l, mid]\) 区间。
    3. 递归到 \((mid, r]\) 区间。
    4. \([l, r]\) 内所有修改造成实际影响。

    这样,递归入某一个区间的时候,\([1,l)\) 内的修改都已经造成实际影响,而其他的没有造成影响。

    一个修改最多被执行 \(2 \log\) 次,是对的。

  • 考虑维护什么。首先要维护颜色段,以及被修改的点的 set,然后要备份颜色段,没了。空间复杂度 \(O(n+m)\)

二维数颜色

(待填坑)

一些转化

  1. 给定一棵 \(n\) 个节点的树,点和边都有编号。\(m\) 次查询,每次给出 \(l,r\),查询如果只保留树上点编号在 \([l,r]\) 的点,边标号在 \([l,r]\) 的边,有多少连通块,此时 \(a\)\(b\) 连通等价于 \(l \le a,b \le r\) 并且 \(a,b\) 在树上简单路径中每个点和边编号都在 \([l,r]\) 内。
    转化:连通块个数转化为点数减边数,边 \(c: a\rightarrow b\) 有用转化为 \(l \le \min(a, b, c) \le \max(a, b, c)\)。转化为二维数点。

  2. 给定很多模式字符串,每次查询时给两个字符串s1,s2,问有多少模式字符串前缀是s1,后缀是s2
    转化:字符串“前缀是 x” 转化成“是 trie 树某个节点的子树”,每个串挂在 trie 的某点上;后缀就对反串再建一个。变成子树交问题。转化成 dfn 上区间,二维数点。

  3. 给一个长为n的序列,m次查询:
    如果将区间 [l,r] 中所有数都+1,那么整个序列有多少个不同的数?
    询问间独立,也就是说每次查询后这个修改都会被撤销
    转化:考虑 \(l,r\) 是两个维度。一般考虑利用不同值对答案贡献独立的性质,对每一个元素/值求贡献,然后用数据结构批处理。这题用出现的数算比较不好算,考虑算什么时候某个数不出现。
    什么情况下全局没有x这个值呢?
    所有原来为x的数都被加上了1;所有原来为x-1的数都没有被加上1。
    image
    转化为了,有多少个矩形包含某个点。矩形个数就是 \(\sum O(某个数出现次数) = O(n)\)

  4. 给定一个序列,每次查询区间中出现偶数次的数的异或和
    转化:如果是奇数次,等价于区间异或和。偶数次怎么办?
    可以转化为,区间所有出现过的颜色的异或和,异或上,奇数次颜色的异或和。
    可以转化为区间数颜色,但是某个点带权值,是带权值二维数点问题。依然可以扫描线+树状数组解决,维护异或和即可。

  5. 矩形面积并
    维护区间最小值和最小值出现次数,有区间覆盖。这个只能线段树维护,合并的时候,若二者min相等,则累计min出现次数,否则取一边的作为答案;如果出现次数是 \(0\),那么 min 出现次数就是 \(0\) 出现次数;否则出现次数是 \(0\)

1.5 区间子区间/历史和问题

给一个序列,每次查询区间有多少子区间满足某个条件

image

考虑区间看做二维平面上一个点,然后所有子区间转换为矩形内所有点。

由于区间 \([l,r]\) 满足 \(l \le r\),所以实际上是一个三角部分,可以看做一个 2-side 矩形。

有的题询问所有区间的信息,这时候可能就需要条件反射想一下第二种扫描线,一般会比第一种扫描线更好做,第二种扫描线扫描了右端点提供了更多局部信息。

一些转化

  1. 有两个数列 \(a_{1, ..., n}, b_{1, ..., n}\)\(q\) 次询问每次给定 \(l,r\),求 \(\sum \limits_{l \le x \le y \le r} \max(a_x, ..., a_y) \times \max(b_x, ..., b_y)\) 的值。(NOIP2022 T4)
    考虑最值分治,然后转化为一些矩形赋值 \(amax, bmax\)。不妨看成矩形乘法。然后询问是 2-side 的,所以不妨从右往左扫。
    考虑可以把这个矩形乘法:\([l, r]\) 时刻内 \([x,y]\) 乘以 \(k\)。可以在扫描线上拆成:\(r\) 时刻 \([x,y]\) 乘以 \(k\)\(l-1\) 时刻 \([x,y]\) 除以 \(k\)
    对于线段树部分:我们有两种标记,一种 \(A \times = u\),一种 \(B+=A\)\(B,A\) 分别是当前和和历史和)注意标记合并的时候是某个节点原本的标记先于父亲节点的标记。然后这两个标记都是可以下传的。这题标记比较少,如果多的话,可以看成矩阵乘法再去传。
    注意到这个除法有点难搞。怎么办呢?有人说改成区间赋值,那样的话标记会更多。注意到答案范围 \(10^{23}\) 以下,一种方法是用 __float128 去做,这个相对精度是 \(2^{-113}\) 的,最多不会 \(-0.1\),所以我们每个数 \(+0.1\) 然后向 0 舍入即可。但是这样会非常的慢。第二种方法是三模合并,取三个质数做模数,求出答案之后 crt 合并。这种方法一定要确保取模没问题,以及 crt 合并的时候要先 mod \(\prod p_i\),否则会寄!
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
#define double __float128
const int inf = 1e9;
//#define cerr if(false)cerr
//#define freopen if(false)freopen
#define watch(x) cerr  << (#x) << ' '<<'i'<<'s'<<' ' << x << endl
void pofe(int number, int bitnum) {
    string s; f(i, 0, bitnum) {s += char(number & 1) + '0'; number >>= 1; } 
    reverse(s.begin(), s.end()); cerr << s << endl; 
    return;
}
template <typename TYP> void cmax(TYP &x, TYP y) {if(x < y) x = y;}
template <typename TYP> void cmin(TYP &x, TYP y) {if(x > y) x = y;}
//调不出来给我对拍!
//use std::array.
int ql[250010], qr[250010], qa[250010], qb[250010];
struct line{int l, r; int k;};
vector<line> li[250010]; //时间节点,从后往前做
    //扫描线的时候
    //在第一维是 x 的时候,第二维度 [l, r] 区间乘 k
struct rect{int l, r, x, y; int k;};
vector<rect> re;
struct query{int l, r, id;}; 
vector<query> qu[250010];
uint64_t ys[3][250010];
int mod;
    //矩形乘 k
//41, 53, 89
int qpow(int x, int k)  {
    x %= mod;
    int res = 1; 
    while(k) {
        if(k & 1) res = res * x % mod;
        x = x * x % mod;
        k >>= 1;
    }
    return res;
}
struct sgt {
    int sum[1000010], his[1000010], tag1[1000010], tag2[1000010]; 
        //A, B, A*=tag1, B+=tag2A(B 先加,A 后加)
    void circ1(int &a, int &b, int la, int lb, int ra, int rb) {a = la + ra; b = lb + rb; a %= mod; b %= mod;}
    void circ2(int &a, int &b, int tg1, int tg2) {b += tg2 * a; a *= tg1; b %= mod; a %= mod;}
    void circ3(int &t1, int &t2, int tg1, int tg2) {t2 += tg2 * t1; t1 *= tg1; t2 %= mod; t1 %= mod;}
    void build(int now, int x, int y) {
        //初始化为 1
        if(x == y) {sum[now] = y - x + 1; his[now] = 0; tag1[now] = 1; tag2[now] = 0;return;}
        int mid = (x + y) >> 1; 
        build(now * 2, x, mid); build(now * 2 + 1, mid + 1, y);
        circ1(sum[now], his[now], sum[now * 2], his[now * 2], sum[now * 2 + 1], his[now * 2 + 1]);
        tag1[now] = 1, tag2[now] = 0;
    }
    void pushdown(int now) {
        if(tag1[now] != 1 || tag2[now] != 0) {
            circ2(sum[now * 2], his[now * 2], tag1[now], tag2[now]);
            circ2(sum[now * 2 + 1], his[now * 2 + 1], tag1[now], tag2[now]);
            circ3(tag1[now * 2], tag2[now * 2], tag1[now], tag2[now]);
            circ3(tag1[now * 2 + 1], tag2[now * 2 + 1], tag1[now], tag2[now]);            
            tag1[now] = 1; tag2[now] = 0;
            return;
        }
    }
    void mul(int now, int l, int r, int x, int y, int k) {
        //区间 [l, r] *= k
        if(l >= x && r <= y) {
            // cerr << "before " << now << " " <<l << " " << r << " " <<x<<" "<<y<<" "<<k<<", sum, tag1 = " << sum[now] << ' ' << tag1[now] << endl;
            sum[now] *= k; sum[now] %= mod;
            tag1[now] *= k; tag1[now] %= mod;
            // cerr << "fix " << now << " " <<l << " " << r << " " <<x<<" "<<y<<" "<<k<<", sum, tag1 = " << sum[now] << ' ' << tag1[now] << endl;
            return;
        }
        if(l > y || r < x) return;
        int mid = (l + r) >> 1;
        pushdown(now);
        mul(now * 2, l, mid, x, y, k); 
        mul(now * 2 + 1, mid + 1, r, x, y, k);
        circ1(sum[now], his[now], sum[now * 2], his[now * 2], sum[now * 2 + 1], his[now * 2 + 1]);
    }
    void add(int now, int l, int r, int x, int y) {
        //B += A, (x, y) always be (1, n).
        if(l >= x && r <= y) {
            his[now] += sum[now]; if(his[now] >= mod) his[now] -= mod;
            tag2[now] += tag1[now]; if(tag2[now] >= mod) his[now] -= mod;
            return;
        }
        return;
    }
    int query(int now, int l, int r, int x, int y) {
        //不存在的部分都是 1, 需要减掉。
        //减掉多少:(n-l+1)*r - (r-l+2)*(r-l+1)/2
        if(l >= x && r <= y) {
            return his[now];
        }
        if(l > y || r < x) return 0;
        int mid = (l + r) >> 1;
        pushdown(now);
        return (query(now * 2, l, mid, x, y) + query(now * 2 + 1, mid + 1, r, x, y)) % mod;
    }
    void dfs(int now, int l, int r) {
        // cerr << "node number #" << now<<", (l, r) = " << l << " " << r << endl;
        // cerr << "sum, his: " << sum[now] << " " << his[now] << endl;
        // cerr << "tag1, tag2: " << tag1[now] << " " << tag2[now] << endl;
        if(l == r) return;
        pushdown(now);
        int mid = (l + r) >> 1;
        dfs(now * 2, l, mid); dfs(now * 2 + 1, mid + 1, r);
    }
}t;
int T, n; int q; 
int rnk[250010];
void zzfz(int* x) {
    set<pii> s; s.insert({n, 1}); f(i, 1, n) rnk[x[i]] = i;
    for(int i = n; i >= 1; i--) {
        int pos = rnk[i];
        auto it = s.lower_bound({pos, 0});
        int l = (*it).second, r = (*it).first;
        //[l, pos], [pos, r] *= i
        re.push_back({l, pos, pos, r, i});
        s.erase(it);
        if(pos - 1 >= l) s.insert({pos - 1, l});
        if(r >= pos + 1) s.insert({r, pos + 1});
    }
}
void unpack() {
    for(rect i : re) {
        li[i.r].push_back({i.x, i.y, i.k});
        li[i.l - 1].push_back({i.x, i.y, qpow(i.k, mod - 2)});
    }
}
void dealq() {
    f(i, 1, q) {qu[ql[i]].push_back({1, qr[i], i});}
}
void doit(uint64_t* ans) {
    re.clear(); f(i, 1, n) li[i].clear(), qu[i].clear(); 
    zzfz(qa); zzfz(qb); unpack(); dealq();
    t.build(1, 1, n);//t.dfs(1, 1, n);
    for(int i = n; i >= 1; i--) {
        // cerr << "lines of " << i << endl;
        // for(line it : li[i]) cerr << it.l  <<" " << it.r << " " << it.k << endl;
        for(line it : li[i]) {
            t.mul(1, 1, n, it.l, it.r, it.k); //t.dfs(1, 1, n);
        }
        t.add(1, 1, n, 1, n);
        // t.dfs(1, 1, n);
        for(query it : qu[i]) {
            __int128_t res = t.query(1, 1, n, it.l, it.r);
            // cerr << i << " " << res << endl;
            res -= (n - i + 1) * it.r - (it.r - i + 2) * (it.r - i + 1) / 2;
            res = (res % mod) + mod; if(res >= mod) res -= mod;
            // cerr << "minus: " <<(n - i + 1) * it.r <<" "<< (it.r - i + 2) * (it.r - i + 1) / 2 << endl;
            ans[it.id] = (uint64_t)res;
        }
    }
}
const __int128_t mt = ((__int128_t)~0ull + 1);
uint64_t crt(int id) {
    // cerr << qpow(1000, mod - 2)
    __int128_t res = 0; 
    __int128_t md[3] = {998244341, 998244353, 998244389};
    // cout << ys[0][id] << " " << ys[1][id] << " " << ys[2][id] << endl;
    __int128_t bm = md[0] * md[1] * md[2];
    // cout << (uint64_t)(bm*2) << endl;
   // cerr << (ll)(bm / md[0]) << endl;
    // res += (__int128_t)ys[0][id] * md[1] * md[2] % mt * qpow(md[1] * md[2], md[0] - 2) % mt;
    // res += (__int128_t)ys[1][id] * md[0] * md[1] % mt * qpow(md[0] * md[2], md[1] - 2) % mt;
    // res += (__int128_t)ys[2][id] * md[0] * md[2] % mt * qpow(md[0] * md[1], md[2] - 2) % mt;
    f(i, 0, 2) {
        mod = md[i]; 
        // cout << (ll)((bm / md[i])  * qpow(bm / md[i], md[i] - 2) % (md[i] )) << endl;
        res += (__int128_t) ys[i][id] * (bm / md[i]) % bm * qpow(bm / md[i], md[i] - 2) % bm;
    }
    res %= bm;
    res %= mt;
    return res;
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(NULL);
    cout.tie(NULL);
    freopen("match.in", "r", stdin);
    freopen("match.out", "w", stdout);
    //time_t start = clock();
    //think twice,code once.
    //think once,debug forever.
    cin >> T >> n;
    f(i, 1, n) cin >> qa[i];
    f(i, 1, n) cin >> qb[i];
    cin >> q;
    f(i, 1, q) cin >> ql[i] >> qr[i];
    
    // cerr << "rectangles: " << endl;
    // for(rect i : re) cerr << i.l << " " << i.r << " " << i.x << " " << i.y << " " << i.k << endl;
    mod = 998244341; doit(ys[0]);
    mod = 998244353; doit(ys[1]);
    mod = 998244389; doit(ys[2]);
    // return 0;
    f(i, 1, q) cout << crt(i) << endl;
   // t.build(1, 1, n);t.dfs(1, 1, n);
    // for(int i = n; i >= 1; i--) {
        // cerr << "lines of " << i << endl;
        // for(line it : li[i]) cerr << it.l  <<" " << it.r << " " << it.k << endl;
        // for(line it : li[i]) {
            // t.mul(1, 1, n, it.l, it.r, it.k); //t.dfs(1, 1, n);
        // }
        // t.add(1, 1, n, 1, n);
        // t.dfs(1, 1, n);
        // for(query it : qu[i]) {
            // __int128_t res = t.query(1, 1, n, it.l, it.r);
          //  cerr << i << " " << res << endl;
            // res -= (n - i + 1) * it.r - (it.r - i + 2) * (it.r - i + 1) / 2 ;
            // cerr << "minus: " <<(n - i + 1) * it.r <<" "<< (it.r - i + 2) * (it.r - i + 1) / 2 << endl;
            // ans[it.id] = (uint64_t)res;
        // }
    // }
    // cerr << "alive\n";
    // f(i, 1, q) cout << ans[i] << endl;
    
    //time_t finish = clock();
    //cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
    return 0;
}
/*
2023/x/xx
start thinking at h:mm
值域是 n
对区间进行最值分治,分治出来一堆矩形,拆成一些线,右边 *x, 左边 /x,放到 vector 里
从右到左去对 vector 里面的线进行操作,也就是线段树的区间 *x
线段树维护 区间和,tag(乘以多少),初始所有信息都是 1
标记:乘以多少,信息和标记:乘法,信息和信息:加法
区间内所有数乘以 k,区间和乘以 k
否则可以从下面到上面 pushup
除法直接当做 double 就好了,实际上乘完是 int,最后 +0.5,转为 int 即可。
(这就是四舍五入)
start coding at h:mm
finish debugging at h:mm
*/

CF1824D LuoTianyi and the Function

【题意】
给定序列 \(a_1, ..., a_n\),定义 \(g(l, r)\) 为:

  • 如果 \(l \le r\)\(g(l,r)\) 定义为最大的 \(x\) 使得 \(\{a_l, ..., a_r\} \subseteq \{a_x, ..., a_r\}\)
  • 否则,\(g(l,r) = 0\)
    给定 \(l, r, x, y\),求 \(\sum \limits_{i = l}^r \sum \limits_{j = x}^y g(i, j)\)

【分析】
这个问题一看是区间子区间问题,想到在二维平面上做:对于某一个数字 \(x\),如果其出现位置有 \(b_1, b_2, ..., b_k\),那么对于 \(\forall i \in [1, k - 1]\)\([1, b_i], [b_i, b_{i +1 })\) 这个矩形内取 \(\min\)

问题转化为区间取 \(\min\),查询历史和。这个问题可以用吉老师线段树解决,但是要维护的信息一看就多(而且会被凉心出题人卡)

其实这样做是繁了。正解是不需要这个的。

正解是第二种扫描线,扫描线扫右端点,维护所有左端点的答案,然后有一些历史和。

首先观察左端点的答案长什么样。

\[\begin{array} \mathrm {a_i}&1&1&2&1&3&2&3&3 \\ \mathrm {g(i,r)}&4&4&4&4&6&6&8&8 \\ (r = 8) \end{array} \]

即,每一个数的最后一次出现位置集合为 \(S = \{c_1, c_2, ..., c_k\}\) 的话,\(g(i,r)\) 的取值为 \(i\) 之后第一个 \(S\) 中元素。

\(r\) 往右边移动 \(1\) 的时候,可能有一个数会被覆盖。有两段 \(g\) 会改变,可以认为是区间加历史最值线段树:

\[\begin{array} \mathrm {a_i}&1&1&2&1&3&2&3&3&\color{blue}2 \\ \mathrm {g(i,r)}&4&4&4&4&6&6&8&8 \\ \mathrm {g(i,\color{blue}{r+1})}&4&4&4&4&\color{blue}8&\color{blue}8&8&8&\color{blue}9 \\ (r = 8 \rightarrow \color{blue}9)\\ \end{array} \]

从原题到“有一些区间加,查询历史最值”的转化,用一个 set 维护即可。

下面考虑区间加历史最值线段树的实现细节,由于若干次写的时候都犯了一些错,有必要讲细一些。

首先,我们线段树的每一个节点上有信息和标记两个部分。某一个节点的信息,等于其节点本身信息 \(\circ\) 上祖先所有节点标记;祖先标记反映了对整个区间已经做过的,但是还没有在下面体现的所有操作之和。

其次,考虑一个“修改”操作。可以认为对包含的区间做了一个 \(\circ2, \circ3\)(区间信息乘标记,区间标记乘标记),对象是“修改”操作代表的标记。对不是包含的区间,是在下面做完修改 pushup 的。所以 pushup 环节在修改里是很重要的不要漏掉。

考虑“pushdown”操作。可以认为是对 \(\mathrm{info}_{lson, rson}\)\(\mathrm{tag}_{lson, rson}\) 分别 \(\circ_2/ \circ_3\)\(\mathrm{tag}_{now}\)。因此这一部分和修改操作的“包含”部分完全一致,有高度的模板性。甚至是对于比较“另类”的 \(\circ\)(例如吉老师线段树的如果 \(se \ge x\) 那么直接往下递归)也是一致的,在 pushdown 函数里面长得一模一样。(“修改”向下递归到的是“修改”函数,“\(\circ\)”向下地递归到的是“\(\circ\)”函数,但是两者是统一的,因为一旦包含,往下在修改函数里也只是一直进入包含部分罢了。)

搞清楚这俩是同一个东西之后,我们需要实现:\(3\)\(\circ\),以及其他细枝末节的东西(调用这三个 \(\circ\) 的接口和 pushdown)。主要错误都出现在 \(3\)\(\circ\) 上。

现在考虑对于区间加历史和线段树,需要打什么标记?需要有什么标记不是人为规定出来的,而是我们看看若干个“修改”叠加会发生什么。

我们有这两种“修改”:\(A += x, B += A\)。我习惯令标记含义是先 \(B\)\(A\),所以考虑:

\[\begin{array} \left B += A, \mathrm{then}~ A += x \rightarrow (1, x) \\ (1, x_1) + (1, x_2) \rightarrow B += 2A + x_1, \mathrm{then}~A+=x_1+x_2 \\ \Rightarrow \mathrm{tag} :B += kA + b, \mathrm{then}~A += x \end{array} \]

所以我们规定标记由三元组 \((k, b, x)\) 构成。但是这里注意了:对于“加法”,我们只能考虑给区间内每个 \(B(A)\)都加上一个常数,也就是 \(b, x\) 的含义其实是“区间内每个单独的位置的 \(B(A)\) 加上 \(b/x\)”。而 \(k\) 的含义可以是每一个位置的 \(B\) 都加上该位置的 \(A\),也可以是整个 \(B\) 加上整个 \(A\),都一样(其实第一种理解方式应当是正统)。\(k\) 的定义不是关键,重点是 \(k, b\) 的定义需要很注意,对信息和标记的询问很有帮助(不注意这些看似简单的细节,就是会写错掉,所以要认真思考每一个标记的含义)

以上面的理论为依据可以写出一棵这样的线段树:

struct SGT{ 
    int A[4000200], B[4000200], k[4000200], X[4000200], b[4000200]; //B += kA + b, A += x
    void circ1(int now, int lc, int rc) {
        A[now] = A[lc] + A[rc]; B[now] = B[lc] + B[rc]; return; 
    }
    void circ2(int now, int l, int r, int k0, int x0, int b0){
        B[now] += k0 * A[now] + b0 * (r - l + 1); A[now] += (r - l + 1) * x0; 
    }
    void circ3(int now, int l __attribute__((unused)), int r __attribute__((unused)), int k0, int x0, int b0){
        k[now] += k0; b[now] += k0 * X[now] + b0; X[now] += x0; 
    }
    void build(int now, int l, int r) {
        if(l == r) {A[now] = 0; B[now] = 0; k[now] = X[now] = b[now] = 0; return; }
        int mid = (l + r) >> 1; build(now * 2, l, mid); build(now * 2 + 1, mid + 1, r); 
        circ1(now, now * 2, now * 2 + 1); 
    }
    void pushdown(int now, int l, int r) {
        if(k[now]!=0 || X[now]!=0 || b[now]!=0) {
            int mid=(l+r)>>1;
            circ2(now*2,l,mid,k[now],X[now],b[now]);circ2(now*2+1,mid+1,r,k[now],X[now],b[now]);
            circ3(now*2,l,mid,k[now],X[now],b[now]);circ3(now*2+1,mid+1,r,k[now],X[now],b[now]);
            k[now]=X[now]=b[now]=0;
        }
    }
    void add(int now, int l, int r, int x, int y, int t) {
        if(l>=x&&r<=y) {
            circ2(now, l, r, 0, t, 0); circ3(now, l, r, 0, t, 0); return;
        }
        if(l>y||r<x)return;
        int mid=(l+r)>>1; pushdown(now,l,r); add(now*2, l, mid, x, y, t);add(now*2+1, mid+1, r, x, y, t);
        circ1(now, now*2, now * 2 + 1); 
    }
    void his() {
        circ2(1, 1, n, 1, 0, 0); circ3(1, 1, n, 1, 0, 0); return;
    }
    int que(int now, int l, int r, int x, int y) {
        if(l >= x && r <= y) return B[now]; 
        if(l > y || r < x) return 0; 
        int mid = (l + r) >> 1; pushdown(now, l, r); circ1(now, now * 2, now * 2 + 1); 
        return que(now * 2, l, mid, x, y) + que(now * 2 + 1, mid + 1, r, x, y);  
    }
}sgt; 

那么这题就结束了。但是我依然想要说一件事情:线段树写挂怎么调。首先结构上的问题是好查的,一般来说难查的是有关标记含义的问题。这时候可以先重新理一下,看看标记含义有没有错漏。然后下一步是对线段树进行 dfs。dfs 过程可以选择实时 pushdown(效果就像每次修改直接到叶子)也可以不 pushdown(这时候每一个位置的信息不一定是正确的,但是可以观察标记和信息的运作情况)。可以选择 \(n=4\) 这样的线段树来模拟,有助于快速发现错误。

1.6 分治

如果信息不支持差分为 3-side,那么我们需要分治。

考虑某一个询问矩形 \((l, r), (x, y)\),将其在 \(x\) 轴上切开变成两个 3-side 矩形,转化为历史和询问。

考虑对时间轴建立分治结构 \(\{[L, R],mid\}\),每一个分治结构是一个独立的部分,处理方式是从 \(mid\) 往左扫,有一些区间修改,有一些区间查询历史信息。然后从 \(mid\) 往右扫,一样。询问有 \(id\),将这些信息合并到 \(id\) 的答案里面。

注意虽然是 \(mid\) 往左右边扫,但是这个处理过程属于 \([L,R]\) 这个区间,不要搞混了。

image

\((x,y)\) 是区间查询的范围我们不关心,\((l, r)\) 这个区间在分治到 \([L,R](mid)\) 的时候处理当且仅当 \((l, r)\) 跨过 \(mid\) 并且在 \([L,R]\) 内。

image

注意这个跨过的意义是可以认为没有其中一个边界,那么如果其右端点正好是 \(mid\)(认为两边是 \([L, mid], [mid + 1, R]\))那么依然将其划分到这个位置。

注意当 \(x\) 轴范围 \(\ge 2\) 的时候不会走到 \(L = R\) 的区间,如果特判 $ \ge 2$ 的情况回溯会有 \(\cfrac{1}{2}\) 的常数。(注意一定要特判,不然 \(n=1\) 就寄了)

这样划分之后,每一个区间刚好被分到一次,其答案就是 \(mid\) 两边两段历史和的信息合并。

考虑矩形修改,矩形询问。对于修改矩形,依然给它放在分治的区间上。

但是这个“放”的意义是对某一些区间上是一个区间修改。所以考虑其放的时间复杂度。对于完全被这个区间覆盖的区间,就直接打上标记,不需要处理了;对于部分覆盖的区间,需要处理。所以每一层每一个区间最多有 \(2\) 个要处理的位置,一个区间在整个分治结构上面会拆分为 \(O(\log )\) 个修改操作。

实现上,考虑下传的时候判断一下是否 \(l = L, r \ge mid\),如果是的话,就不下传到左儿子,而是放一个全局修改操作在左儿子。

但是全局修改操作是不好合并的(因为每一个修改操作其实是对 y 轴的 \([x,y]\) 区间做一个修改操作)而且如果一个一个进行修改的话复杂度就不对了。所以我们考虑类似标记永久化的东西,就是走到某个点,把这个点上的全局修改做了,然后做自己的部分修改并且撤销(撤销的意思是对于一个区间修改拆成扫描线上两个单点修改,其加在一起效果对 \(A\) 这个量是 \(\epsilon\)),然后递归到儿子上,这时候儿子必然也需要这个全局修改,然后在线段树上只存在所有祖先的全局修改了,所以只需要对儿子这里第一次出现的区间修改做一遍,等所有儿子做完之后,递归回去的时候把所有全局修改撤销掉。

这样做就是普通的区间线段树操作,时间复杂度就是对的。

有修改也有询问的话,你可以事先把所有区间的 \([L,R], mid\) 处理一下,然后挂上询问,挂上修改,“挂”的意思是从根递归下去找它应该在的位置。然后所有东西做完之后,再从根节点开始 dfs 这个分治结构,dfs 到一个点的时候从 \(mid\) 向两边做扫描线,处理所有东西。

挂的时候,像正常扫描线一样把 \((x, y), (k/id)\) 挂在 \([L,R]\) 的某一个“重新标号的时间”上,重新标号是这样的:

image

这样标号之后,只需要处理 \([1, mid - l + 2]\) 的和 \([1, r - mid + 1]\) 时间上的操作(注意线段树的 \(n\) 是 y 轴值域),之所以 \(L\) 左边还有一个点是为了差分之后的撤回操作,这个操作是必须要做的,否则传下去的线段树有问题。

对于 \(x,y\) 轴长度是 \(n\),修改和询问个数分别是 \(m_1, m_2\) 的问题,时间复杂度:\(O(m_1 \log^2 n + m_2 \log n)\)

如果 \(m_1, m_2\) 同阶,可以利用多叉分治结构进行进一步平衡,但是我不会。

P6109 rprmq1

【题意】

矩形加,矩形查询最大值。

【分析】

分治的部分刚刚说过了,剩下线段树里面的东西。

考虑信息 \((A, B)\),标记 \((addtag, histag)\),表示区间内 \(cmax(B, A + histag)\) 然后 \(A += addtag\)

三种合并很好推导,只要抓住先后顺序。

但是要注意的是,很容易出现 typo,你检查的时候一定要检查清楚。

随机数据很强,但是样例绝对很弱

对拍显然是可以写的。

#include<bits/stdc++.h>
using namespace std;
#define int long long
//use ll instead of int.
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
const int inf = 1e16;
// #define cerr if(false)cerr
#define freopen if(false)freopen
#define watch(x) cerr  << (#x) << ' '<<'i'<<'s'<<' ' << x << endl
void pofe(int number, int bitnum) {
    string s; f(i, 0, bitnum) {s += char(number & 1) + '0'; number >>= 1; } 
    reverse(s.begin(), s.end()); cerr << s << endl; 
    return;
}
template <typename TYP> void cmax(TYP &x, TYP y) {if(x < y) x = y;}
template <typename TYP> void cmin(TYP &x, TYP y) {if(x > y) x = y;}
//调不出来给我对拍!
//use std::array.
struct qj {int l, r; int x, y; int k; bool operator< (qj op) {return l < op.l;}}; // 原始区间
struct xw {int l, r, x, y; }; //原始询问
int n,m1,m2;
int ans[500040];
struct line {int l, r, k; }; //y 轴上 [l, r] 区间加 k
struct query {int l, r; int id;}; //询问 [l, r] 的历史和,然后 ans[id] 对这个历史和取 max
//x: [l, mid] / [mid + 1, r]
vector<line> li[50040];
vector<query> qu[50040];
struct SGT {
    int A[200010], B[200010]; int k[200010], t[200200];
    void build(int now,int l,int r) {
        A[now]=B[now]=k[now]=0; t[now]=-inf;
        if(l == r) return;
        int mid=(l+r)>>1;
        build(now*2,l,mid); build(now*2+1,mid+1,r);
    }
    void circ1(int &a, int &b, int al, int bl, int ar, int br) {a = max(al, ar); b = max(bl, br);}
    void circ2(int &T, int &K, int t1, int k1, int t2, int k2) {T = max(t1, k1 + t2); K = k1 + k2;}
    void circ3(int &a, int &b, int ap, int bp, int T, int K) {a = ap + K; b = max(bp, ap + T);}
    void pushdown(int now) {
        if(k[now]!=0 || t[now] != -inf) {
            circ3(A[now*2], B[now*2], A[now*2], B[now*2], t[now], k[now]);
            circ3(A[now*2+1], B[now*2+1], A[now*2+1], B[now*2+1], t[now], k[now]);
            circ2(t[now*2], k[now*2], t[now*2], k[now*2], t[now], k[now]);
            circ2(t[now*2+1], k[now*2+1], t[now*2+1], k[now*2+1], t[now], k[now]);
            k[now]=0; t[now]=-inf;
        }
    }
    void add(int now, int l,int r,int x,int y,int K) {
        if(l>=x&&r<=y) {
            circ2(t[now], k[now], t[now], k[now], -inf, K);
            circ3(A[now], B[now], A[now], B[now], -inf, K);
            return;
        }
        if(l>y||r<x)return;
        int mid=(l+r)>>1;
        pushdown(now);
        add(now*2,l,mid,x,y,K); add(now*2+1,mid+1,r,x,y,K);
        circ1(A[now],B[now],A[now*2],B[now*2],A[now*2+1],B[now*2+1]);
    }
    void aft(int now) {
        //历史和行为
        circ2(t[now], k[now], t[now], k[now], 0, 0);
        circ3(A[now], B[now], A[now], B[now], 0, 0);
        return;
    }
    int que(int now,int l,int r,int x,int y){
        if(l>=x&&r<=y){return B[now];}
        if(l>y||r<x)return -inf;
        int mid=(l+r)>>1;
        pushdown(now);
        return max(que(now*2,l,mid,x,y),que(now*2+1,mid+1,r,x,y));
    }
}sgt;
struct node {
    int l, r; int mid;  //外层的 size 是 mid - l; r - mid + 1. 
    vector<vector<query>> lq, rq; //在左边的询问和右边的询问,得到的答案加上 tag 和 ans[id] 取 max
    vector<vector<line>> la, ra; //区间加
    vector<line> qjadd; // 全局 add
};
// int eee = 0;
struct timetree {
    node timeqj[200010];
    void build(int now, int l, int r) {
        timeqj[now].l = l; timeqj[now].r = r;
        timeqj[now].mid = (l + r) >> 1;
        timeqj[now].lq.resize(timeqj[now].mid - l + 2 + 1); timeqj[now].la.resize(timeqj[now].mid - l + 2 + 1);
        timeqj[now].rq.resize(r - timeqj[now].mid + 1 + 1); timeqj[now].ra.resize(r - timeqj[now].mid + 1 + 1);
        if(l == r) return;
        int mid = (l + r) >> 1;
        build(now * 2, l, mid); build(now * 2 + 1, mid + 1, r);
    }
    void addin(int now, int l, int r, qj x) {
        int mid = (l + r) >> 1; 
        int lo = mid, ro = mid + 1; // 时间为 t 的在左边的编号是 mid - t, 右边是 t - mid - 1
        if(x.l <= mid) {
            timeqj[now].la[lo - min(mid, x.r) + 1].push_back({x.x, x.y,x.k});
            timeqj[now].la[lo - (x.l - 1) + 1].push_back({x.x, x.y, -x.k});
            if(l != r) {
                if(x.l == l && x.r >= mid) timeqj[now * 2].qjadd.push_back({x.x, x.y, x.k});
                else addin(now * 2, l, mid, {x.l, min(mid, x.r), x.x, x.y, x.k});
            }
        }
        if(x.r >= mid + 1) {
            timeqj[now].ra[max(mid + 1, x.l) - ro + 1].push_back({x.x, x.y,x.k});
            timeqj[now].ra[x.r + 1 - ro + 1].push_back({x.x, x.y, -x.k});            
            if(l != r) {
                if(x.r == r && x.l <= mid + 1) timeqj[now * 2 + 1].qjadd.push_back({x.x, x.y, x.k});
                else addin(now * 2 + 1, mid + 1, r, {max(mid + 1, x.l), x.r, x.x, x.y, x.k});
            }
        }
    }
    void queryin(int now, int l, int r, xw x, int id) {
        int mid = (l + r) >> 1; assert(x.l >= l && x.r <= r);
        int lo = mid, ro = mid + 1;
        if(x.l >= l && x.r <= r && x.l <= mid + 1 && x.r >= mid) {
            if(x.l <= mid)timeqj[now].lq[lo - x.l + 1].push_back({x.x, x.y, id});
            if(x.r >= mid + 1) timeqj[now].rq[x.r - ro + 1].push_back({x.x, x.y, id});
            return;
        }
        else if(x.r <= mid) queryin(now * 2, l, mid, x, id);
        else queryin(now * 2 + 1, mid + 1, r, x, id);
    }
    void dfs(int now, int l, int r) {
        //注意要先 - 再 +,这里满足了
        //处理左半边区间,右半边区间
        if(l == r && n != 1) return; 
        int delta = sgt.B[1];
        sgt.add(1, 1, n, 1, n, delta);
        int llen = timeqj[now].mid - timeqj[now].l + 2; 
        for(line it : timeqj[now].qjadd) sgt.add(1, 1, n, it.l, it.r, it.k);
        f(i, 1, llen) {
            for(line it : timeqj[now].la[i]) {
                sgt.add(1, 1, n, it.l, it.r, it.k);
            }
            sgt.aft(1);
            for(query it : timeqj[now].lq[i]) {
                int tmp = sgt.que(1, 1, n, it.l, it.r);
                cmax(ans[it.id], tmp - delta);
            }
        }
        sgt.add(1, 1, n, 1, n, -delta);
        int rlen = timeqj[now].r - timeqj[now].mid + 1; 
        delta = sgt.B[1];
        sgt.add(1, 1, n, 1, n, delta);
        f(i, 1, rlen) {
            assert((int)timeqj[now].ra.size() > i); 
            assert((int)timeqj[now].rq.size() > i); 
            for(line it : timeqj[now].ra[i]) {
                sgt.add(1, 1, n, it.l, it.r, it.k);
            }
            sgt.aft(1);
            for(query it : timeqj[now].rq[i]) {
                int tmp = sgt.que(1, 1, n, it.l, it.r);
                cmax(ans[it.id], tmp - delta);
            }
        }
        sgt.add(1, 1, n, 1, n, -delta);   
        int mid = (l + r) >> 1;
        if(l != r) {dfs(now * 2, l, mid); dfs(now * 2 + 1, mid + 1, r);}
        for(line it : timeqj[now].qjadd) sgt.add(1, 1, n, it.l, it.r, -it.k);
    }
}tt;
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(NULL);
    cout.tie(NULL);
    cin >> n >> m1 >> m2;
    tt.build(1, 1, n);
    f(i,1,m1) {
        qj x; cin >>x.l>>x.x>>x.r>>x.y>>x.k; 
        tt.addin(1, 1, n, x);
    }
    f(i,1,m2){
        xw x; cin >>x.l>>x.x>>x.r>>x.y;
        tt.queryin(1, 1, n, x, i);
    }
    tt.dfs(1, 1, n);
    f(i,1,m2)cout<<ans[i]<<endl;
    //time_t finish = clock();
    //cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
    return 0;
}
/*
2023/x/xx
start thinking at h:mm


start coding at 16:51
finish debugging at h:mm
*/

分段检查:线段树先写好,然后检查完线段树再去写分治。

posted @ 2023-03-22 16:01  OIer某罗  阅读(78)  评论(0编辑  收藏  举报