跳转至

摘抄学习笔记 | 二维线段树&树状数组


01线段树

二维信息维护

二维线段树

这是一个维护多维度学习的数据结构。如果没有轻质在线的要求,那么我们可以使用CDQ分治来代替。或者外面也可以使用K-D树来维护。

二维线段树——我们在主线段树的每一个节点上都建立一个子线段树。我们考虑下面的问题

给定一个 n×m 的矩阵,初始时元素均为 0,你需要维护一下操作:

1. `1 x y k`,将矩阵中 (x,y) 位置的元素加上 k。

2. `2 x1 y1 x2 y2`,查询左上角为 (x1,y1),右下角为 (x2,y2) 的矩阵中的元素和。

那么如果使用二维线段树——第一维维护行,第二维维护列,对于修改操作,我们找到对应行的节点,然后在其子线段树上找到对应列即可。对应查询,我们挑选出可以拼凑出询问行的节点,然后在这些节点中分别查询对应列的和信息。时间复杂度\(O(n\log^2 n)\)

如果是区间修改,我们则需要维护些许tag了。在主线段树中每次下传标记时,都要对某个子线段树进行修改。这种修改会修改子线段树中的所有点,因此时间复杂度最差为\(O(n\log n)\)无法接受。所以我们考虑**标记永久化**。

大概来说,就是标记不下传,不删除。我们在查询时,遇到了一个标记就直接修改\(res\)的值。但是标记永久化会有很多限制,局限性很大,不推荐使用。

二维树状数组

树状数组本是用来单点修改,区间求和的。这里我们让它支持区间修改,区间查询。

  • L a b c d delta —— 代表将 \((a,b),(c,d)\) 为顶点的矩形区域内的所有数字加上 \(delta\)

  • k a b c d —— 代表求 \((a,b),(c,d)\) 为顶点的矩形区域内所有数字的和。

一维树状数组的区间修改&区间查询

首先我们考虑树状数组怎样实现一维的区间修改和区间查询。对应一维的区间修改,因为树状数组本身只支持单点修改但是会自动维护前缀和,所以我们的区间修改就使用差分的形式。

如果我们要把[l,r]都+k,那么我们修改树状数组\(c_l\)加上\(k,c_{r+1}\)减去k。此时树状数组上正是原数组。

那么现在如果我们要求[l,r]之和,那么我们可以把\(query(l)\sim query(r)\)都加起来。

位置p的值\(=\sum_{j = 1}^{p} c[j]\)

位置p的前缀和 $\sum_{i = 1}^{p} a[i] = \sum_{i = 1}^{p} \sum_{j = 1}^{i} c[j] $

在等式最右侧的式子\((\sum_{i = 1}^{p} \sum_{j = 1}^{i} c[j])\)中,(\(c[1]\)) 被用了(\(p\))次,(\(c[2]\))被用了(p - 1)次……那么我们可以写出:

位置p的前缀和 =

$\sum_{i = 1}^{p} \sum_{j = 1}^{i} d[j] = \sum_{i = 1}^{p} d[i] * (p - i + 1) = (p + 1) \times \sum_{i = 1}^{p}c[i] - \sum_{i = 1}^{p}c[i]\times i $

那么我们可以维护两个数组的前缀和: 一个数组是 (\(sum1[i] = c[i]\)), 另一个数组是 (\(sum2[i] = c[i] \times i\))。

查询

位置p的前缀和即: (p + 1) * sum1数组中p的前缀和 - sum2数组中p的前缀和。

区间[l, r]的和即:位置r的前缀和 - 位置l的前缀和。

修改

对于sum1数组的修改同问题2中对d数组的修改。

对于sum2数组的修改也类似,我们给 sum2[l] 加上 l * x,给 sum2[r + 1] 减去 (r + 1) * x。

扩展到二维情况

在一维树状数组中,我们用c[x]记录右端点为x,长度为lowbit(x)的区间的区间和。我们同样可以类似地定义c[x][y]为右下端点为\((x,y)\),高为lowbit(x),宽为lowbit(y)的区间的区间和。

区间修改

我们回忆一下我们是如何对一维树状数组进行区间修改的。我们对其进行差分操作,是为了使得到的差分数组前缀和就等于对应位置元素的值。那么我们可以运用类比思想,设计一个二维的差分呢?

我们来看一下二维的前缀和。

\(sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+a[i][j]\)

查询左上角点\((x1,y1)\),右下角点\((x2,y2)\)的区间和则是:\(sum[x2][y2]-sum[x1-1][y2]-sum[x2][y1-1]+sum[x1-1][y1-1]\)

那么我们可以使差分数组\(d[i][j]\)等于\(a[i][j]\)\(a[i-1][j]+a[i][j-1]-a[i-1][j-1]\)的差。

这些都是差分基本操作了。

区间查询

根据差分数组的定义,我们不难发现,对于点(x,y),它的二维前缀和就是:

\(\sum_{i=1}^x\sum_{j=1}^y\sum_{h=1}^i\sum_{k=1}^j d[h][k]\)

但我们类比一下一维树状数组的区间求和,我们亦可以统计每个\(d[h][k]\)出现的次数,我们就可以发现\(d[1][1]\)出现了\((x\times y)\)次,\(d[1][2]\)出现了\(x\times(y-1)\)次……\(d[h][k]\)出现了\((x-h+1)\times (y-k+1)\)次。

则原式整理得:

\(\sum_{i=1}^x\sum_{j=1}^y d[i][j]\times (x-i+1)\times (y-j+1)\)

分解得:

\(\sum_{i=1}^x\sum_{j=1}^y d[i][j]\times [(xy-xj+x)+(-yi+ij-i)+(y-j+1)]\)

最后得:

\(\sum_{i=1}^x\sum_{j=1}^y d[i][j]\times (xy+x+y+1)-d[i][j]\times i(y+1)-d[i][j]\times j(x+1)+d[i][j]\times i\times j\)

根据我们最后分解出来的公式,我们需要维护四个数组\(d[i][j],d[i][j]\times i,d[i][j]\times j,d[i][j]\times ij\),从而实现区间查询。

例题 #1

题目描述

输入数据的第一行为 X n m,代表矩阵大小为 \(n\times m\)。 从输入数据的第二行开始到文件尾的每一行会出现以下两种操作:

  • L a b c d delta —— 代表将 \((a,b),(c,d)\) 为顶点的矩形区域内的所有数字加上 \(delta\)

  • k a b c d —— 代表求 \((a,b),(c,d)\) 为顶点的矩形区域内所有数字的和。

请注意,\(k\) 为小写。

思路

二维树状数组模板。

/*
CB Ntsc
*/

#include<bits/stdc++.h>
using namespace std;
// #define int long long
#define ull unsigned long long
#define pii pair<int, int>
#define pf first
#define ps second

#define rd read()
// #define nl putc('\n')
#define ot write
#define nl putchar('\n')
inline int rd
{
    int xx=0,ff=1;
    char ch=getchar();
    while(ch<'0'||ch>'9') {if(ch=='-') ff=-1;ch=getchar();}
    while(ch>='0'&&ch<='9') xx=xx*10+(ch-'0'),ch=getchar();
    return xx*ff;
}
inline void write(int out)
{
    if(out<0) putchar('-'),out=-out;
    if(out>9) write(out/10);
    putchar(out%10+'0');
}

bool f1;
const int INF = 1e9;
const int N = 2e3+50;
const int M = 10;
int MOD = 10;

int n,m;
int ans = INF;

struct BIT{
    int c[N][N];
    void change(int x,int y,int v) {
        if(!x||!y)  return ;
        for(int i=x;i<=n;i+=i&-i)    
            for(int j=y;j<=m;j+=j&-j)    
                c[i][j]+=v;
    }
    int query(int x,int y) {
        int res=0,i,j;
        for(i=x;i;i-=i&-i) 
            for(j=y;j;j-=j&-j)  
                res+=c[i][j];
        return res;
    }
}c1,c2,c3,c4;
char str[2];
void change(int x,int y,int v){
    c1.change(x,y,v*x*y);
    c2.change(x,y,v*x);
    c3.change(x,y,v*y);
    c4.change(x,y,v);
}
int query(int x,int y){
    int res=c1.query(x,y);
    res+=y*(c2.query(x,m)-c2.query(x,y));
    res+=x*(c3.query(n,y)-c3.query(x,y));
    res+=x*y*(c4.query(n,m)-c4.query(x,m)-c4.query(n,y)+c4.query(x,y));
    return res;
}
signed main(){
    scanf("X %d %d",&n,&m);
    int i,a,b,c,d,e;
    while(scanf("%s%d%d%d%d",str,&a,&b,&c,&d)!=EOF){
        if(str[0]=='L') {
        // cerr<<"OK";
            scanf("%d",&e);
            change(a-1,b-1,e),change(a-1,d,-e),change(c,b-1,-e),change(c,d,e);
        }
        else printf("%d\n",query(a-1,b-1)-query(a-1,d)-query(c,b-1)+query(c,d));
    }
    return 0;
}
/*
2 5
0 1 1 1 1
0 1 1 2 4
0 2 1 2 1
0 2 1 1 4
*/

对于 \(100\%\) 的数据,\(1 \le n \le 2048\)\(1 \le m \le 2048\)\(-500 \le delta \le 500\),操作不超过 \(2\times 10^5\) 个,保证运算过程中及最终结果均不超过 \(32\) 位带符号整数类型的表示范围。

例题 #2 [NOI2012] 魔幻棋盘

题目描述

将要读二年级的小 Q 买了一款新型益智玩具——魔幻棋盘,它是一个 \(N\)\(M\) 列的网格棋盘,每个格子中均有一个正整数。棋盘守护者在棋盘的第 \(X\) 行第 \(Y\) 列(行与列均从 \(1\) 开始编号)并且始终不会移动。棋盘守护者会进行两种操作:

  • (a)询问:他会以自己所在位置为基础,向四周随机扩展出一块大小不定的矩形区域,向你询问这一区域内所有数的最大公约数是多少。

  • (b)修改:他会随意挑选棋盘上的一块矩形区域,将这一区域内的所有数同时加上一个给定的整数。

游戏说明书上附有这样一句话“聪明的小朋友,当你连续答对 \(19930324\) 次询问后会得到一个惊喜噢!”。小 Q 十分想得到这个惊喜,于是每天都在玩这个玩具。但由于他粗心大意,经常算错数,难以达到这个目标。于是他来向你寻求帮助,希望你帮他写一个程序来回答棋盘守护者的询问,并保证 \(100\%\) 的正确率。

为了简化问题,你的程序只需要完成棋盘守护者的 \(T\) 次操作,并且问题保证任何时刻棋盘上的数字均为不超过 \(2^{62} - 1\) 的正整数。

思路

简洁明了一道黑题。

  • 询问:询问一个矩形内的gcd

  • 修改:把一个矩形内的数+k

一维情况

二维问题,首先考虑一维的情况。如果是一维,那么我们想一想区间修改和区间gcd查询,很不好做。这里直接给出对策——我们发现gcd满足gcd(a,b)=gcd(a-b,b),同样的,对于一个区间\(a_i,i\in[l,r]\),我们有\(\gcd(a_l,\dots,a_r)=\gcd(a_i,a_{i+1}-a_i,a_{i+2}-a_{i+1},\dots)\),这说明一个区间的gcd=这个区间的差分数组的gcd。

所以我们维护差分数组即可。现在就是单点、修改查询区间gcd了,是可行的。

但是要注意,如果是在原数组中截取的区间[l,r],那么其差分数组的第一项是\(a_l-a_{l-1}\),但是我们是不允许出现\(a_{l-1}\)的,所以对于\(a_l\)这一项,我们要单独维护其值。这个可以使用一个一维线段树(单点查询区间修改)完成。

好了,一维的情况解决了,现在我们考虑二维。

二维情况

我们设平面直角坐标系xOy中矩形左上角坐标为(x,y),长为a,宽为b。那么一种方法是建立N个线段树,对每个线段树的[x,x+b]区间求gcd后统计答案。计算得知复杂度\(O(TN\log M)\)无法通过极限数据。

所以我们抛弃取巧做法,老老实实的写我们的二维线段树吧。即我们把原图转化为二维差分,依旧按照之前一维的情况下的单点修改,区间查询来做。

别忘了,在一维中我们要特殊处理区间的第一个数即a_l,那么在二维情况下,我们需要特殊处理哪些呢?

\(\begin{aligned} &a_{x,y}\qquad\qquad\qquad\quad a_{x,2}-a_{x,1}\qquad\qquad\qquad\quad a_{x,3}-a_{x,2}\qquad\qquad\quad~~~ \dots \\\\ &a_{2,y}-a_{1,y}\qquad a_{2,2}-a_{2,1}-a_{1,2}+a_{1,1}\qquad a_{2,3}-a_{2,2}-a_{1,3}+a_{1,2}\qquad \dots \\\\ &a_{3,y}-a_{2,y}\qquad a_{3,2}-a_{3,1}-a_{2,2}+a_{2,1}\qquad a_{3,3}-a_{3,2}-a_{2,3}+a_{2,2}\qquad \dots \end{aligned}\)

很明显可以看出我们要特殊维护第1行和第1列。

这时要注意,在一维中,我们区间[l,r]的gcd是由差分线段树的[l+1,r]区间的答案(gcd)和a_l取gcd得到的,对于二维,我们的答案应该是以下四个数据取gcd得到的。

  • 二维差分线段树[(x+1,y+1)(x+b,y+a)]

  • 一维差分线段树(维护行差分,即形如\(a_{x,i}-a_{x,i-1}\)的数据)[y+1,y+a]

  • 一维差分线段树(维护列差分,即形如\(a_{x,i}-a_{x-1,i}\)的数据)[x+1,x+b]

  • 维护单点的线段树(维护\(a_{x,y}\))求出的\(a_{x,y}\)

二维线段树

这里再提一提二维线段树。因为本题线段树较多且树套树方法需要写内存分配,但是四分树本质是一维线段树,容易被卡(虽然本题不卡),我们写树套树。

四分树(下)

image.png

四分树写法可参考:

www.luogu.com.cn

线段树套线段树

动态开点线段树套动态开点线段树

例题 #1 雪豹、将军与叛乱

给定一棵 n 个点的树,你需要维护一个路径可重集 S,S 中的每个元素可以用 (a,b) 描述,表示在树上从 a 到 b 的简单路径。初始时,S 中有 m 个元素。

再给定 q 次操作,操作分两种,第一种是给定两个点 x,y,查询集合 S 中有多少条路径 (a,b) 满足,x 到 y 的路径包含 a 到 b 的路径,第二种是向路径集合中插入 (x,y) 代表的简单路径。

Input Format

第一行包括三个以空格分隔正整数 n,m,q,分别代表燕国地图的点数、初始叛军数与任务数。

接下来 n−1 行,每行两个以空格分隔的正整数 ui​,vi​,表示有一条连接 ui​ 和 vi​ 的道路。

接下来 m 行,每行两个以空格分隔正整数 ai​,bi​,表示初始时每队叛军的活动范围。

接下来 q 行,每行三个以空格分隔正整数 ti​,xi​,yi​:

  • 若 ti​=1,表示一组询问,xi​ 和 yi​ 的含义如上所述。

  • 若 ti​=2,则表示新增了一组活动范围为 xi​ 到 yi​ 的叛军。

Output Format

对于每组询问输出答案。

对于 100% 的数据,满足 1≤n,m,q≤10^5,ti​∈{1,2},1≤ai​,bi​,xi​,yi​≤n。保证 ai​≠bi​,xi​≠yi​。


将询问离线,考虑边E的加入对哪些询问有贡献。

先假设E(a,b)有拐点(即lca(a,b)≠a或者b),那么就相当于对于询问Q(x,y),如果x,y分别在子树a,b或者b,a中,那么这个询问的答案就+1.

我们把一个询问看成二维平面上的一个点,于是就是二维平面上的矩形加(因为子树的dfn连续),可以考虑线段树套线段树。

正解是cdq,因为线段树常数较大,但是时间复杂度是正确的。