浅谈二维线段树的几种不同的写法

目录

  • 参考文献
  • 参考文献
  • 暴力写法
  • 二叉树
  • 四叉树
  • 树套树写法1

参考文献

四叉树
树套树
以及和zhoufangyuan巨佬的激烈讨论

参考文献

大家好我口糊大师又回来了。

给你一个\(n*n\)矩阵,然后让你支持两种操作,对子矩阵加值和对子矩阵查和。

暴力写法

对于每一行开一个线段树,然后跑,时间复杂度\(n^2logn\)。

优点:

  1. 代码较短
  2. 较为灵活

缺点:

  1. 常数大
  2. 容易卡

二叉树

我们对于平面如此处理,一层维护横切,一层竖切。

当然,这个做法也是\(n^2logn\)的,卡法就是任意一行的全加值。

优点:

  1. 时间复杂度比较平均。

缺点:

  1. 时间复杂度还是这么垃圾。

四叉树

那么不能两两分就四四分!

即把一个区域分成四份。


当然还有这两种特殊情况。

很可惜,一样的卡法,还是那个\(n^2logn\)的味道。

来自参考文献的代码

//神奇的码风,等我哪一天码了一个类似的代码,就把这个换了吧,但是最近没时间。
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<map>
#include<cmath>
#define ll long long
#define max(x,y) ((x)>(y)?(x):(y))
#define min(x,y) ((x)<(y)?(x):(y))
#define fur(i,x,y) for(i=x;i<=y;i++)
#define fdr(i,x,y) for(i=x;i>=y;i--)
#define Fur(i,x,y) for(ll i=x;i<=y;i++)
#define Fdr(x,y) for(ll i=x;i>=y;i--)
#define in2(x,y) in(x);in(y)
#define in3(x,y,z) in2(x,y);in(z)
#define in4(a,b,c,d) in2(a,b);in2(c,d)
#define clr(x,y) memset(x,y,sizeof(x))
#define cpy(x,y) memcpy(x,y,sizeof(x))
using namespace std;
/*---------------------------------------*/
namespace fib{char b[300000]= {},*f=b;}
#define gc ((*fib::f)?(*(fib ::f++)):(fgets(fib::b,sizeof(fib::b),stdin)?(fib::f=fib::b,*(fib::f++)):-1))
inline void in(ll &x){x=0;char c;bool f=0;while((c=gc)>'9'||c<'0')if(c=='-')f=!f;x=c-48;while((c=gc)<='9'&&c>='0')x=x*10+c-48;if(f)x=-x;}
namespace fob{char b[300000]= {},*f=b,*g=b+300000-2;}
#define pob (fwrite(fob::b,sizeof(char),fob::f-fob::b,stdout),fob::f=fob::b,0)
#define pc(x) (*(fob::f++)=(x),(fob::f==fob::g)?pob:0)
struct foce{~foce(){pob;fflush(stdout);}} _foce;
namespace ib{char b[100];}
inline void out(ll x){if(x==0){pc(48);return;}if(x<0){pc('-');x=-x;}char *s=ib::b;while(x) *(++s)=x%10,x/=10;while(s!=ib::b) pc((*(s--))+48);}
inline void outn(ll x){out(x);pc('\n');}//快速输出
inline ll jdz(ll x){return x>0?x:-x;}//绝对值
/*------------------------------------------------------------------------------------------------*/

/*------------------------------------------------------------------------------------------------*/
#define N 2334
#define c1 (rt*4-2)
#define c2 (rt*4-1)
#define c3 (rt*4)
#define c4 (rt*4+1)
#define z1 x1,y1,mx,my
#define z2 x1,my+1,mx,y2
#define z3 mx+1,y1,x2,my
#define z4 mx+1,my+1,x2,y2
#define Z ll mx=(x1+x2)>>1,my=(y1+y2)>>1
#define U ll x1,ll y1,ll x2,ll y2,ll rt
#define pu s[rt]=s[c1]+s[c2]+s[c3]+s[c4]//push_up
ll s[N*N*4],laz[N*N*4],a[N][N],n,m,q,g[N*N*4];
bool b[N*N*4];
inline ll gs(ll x1,ll y1,ll x2,ll y2){return (jdz(x1-x2)+1)*(jdz(y1-y2)+1);}
inline void pd(ll rt,ll n1,ll n2,ll n3,ll n4){
    if(laz[rt]){ll &t=laz[rt];
        s[c1]+=n1*t;s[c2]+=n2*t;s[c3]+=n3*t;s[c4]+=n4*t;
        laz[c1]+=t;laz[c2]+=t;laz[c3]+=t;laz[c4]+=t;
        t=0;
    }
}
inline void build(U){
    g[rt]=gs(x1,y1,x2,y2);
    if(x1==x2&&y1==y2){s[rt]=a[x1][y1];return;}
    Z;
    build(z1,c1);if(y1!=y2)build(z2,c2);else b[c2]=1;
    if(x1!=x2)build(z3,c3);else b[c3]=1;if(x1!=x2&&y1!=y2)build(z4,c4);else b[c4]=1;
    pu;
}
inline void upd(ll X1,ll Y1,ll X2,ll Y2,ll v,U){
    if(b[rt])return;
    if(X1<=x1&&Y1<=y1&&x2<=X2&&y2<=Y2){s[rt]+=gs(x1,y1,x2,y2)*v;laz[rt]+=v;return;}
    Z;pd(rt,g[c1],g[c2],g[c3],g[c4]);
    if(X1<=mx&&Y1<=my)upd(X1,Y1,X2,Y2,v,z1,c1);
    if(X1<=mx&&Y2>my)upd(X1,Y1,X2,Y2,v,z2,c2);
    if(X2>mx&&Y1<=my)upd(X1,Y1,X2,Y2,v,z3,c3);
    if(X2>mx&&Y2>my)upd(X1,Y1,X2,Y2,v,z4,c4);
    pu;
}
inline ll qh(ll X1,ll Y1,ll X2,ll Y2,U){
    if(b[rt])return 0;
    if(X1<=x1&&Y1<=y1&&x2<=X2&&y2<=Y2)return s[rt];
    Z,ans=0;pd(rt,g[c1],g[c2],g[c3],g[c4]);
    if(X1<=mx&&Y1<=my)ans+=qh(X1,Y1,X2,Y2,z1,c1);
    if(X1<=mx&&Y2>my)ans+=qh(X1,Y1,X2,Y2,z2,c2);
    if(X2>mx&&Y1<=my)ans+=qh(X1,Y1,X2,Y2,z3,c3);
    if(X2>mx&&Y2>my)ans+=qh(X1,Y1,X2,Y2,z4,c4);
    return ans;
}
int main(){
    in3(n,m,q);
    Fur(i,1,n)Fur(j,1,m)in(a[i][j]);
    build(1,1,n,m,1);
    ll p,x1,y1,x2,y2,v;
    while(q--){
        in(p);in4(x1,y1,x2,y2);
        if(p==1)outn(qh(x1,y1,x2,y2,1,1,n,m,1));
        else{in(v);upd(x1,y1,x2,y2,v,1,1,n,m,1);}
    }
}

优点:

  1. 常数更小了。

缺点:

  1. 代码更长了。
  2. 时间复杂度吃屎般的大。

当然,最后被吐槽说上面两种都是KDT,其实个人也觉得挺像的。

树套树写法1

这才是重头戏。

论如何树套树搞?

当然这个一般只能维护区间赋值和区间加值,或者偶尔某些神奇的维护也可以用这个。

当然一般也可以用二维树状数组来搞,以后再学吧。

我们需要理解一个永久化标记的东西。就是曾经我以为自己是第一个YY出这个的东西,上网一查,啪啪打脸的那玩意。

当一个点赋上了永久化标记,这个标记不会下传,而是在递归的时候,在记录标记。

对于外层线段树,维护的是行,对于内层线段树,维护的是列,不过代码是两棵,一棵是找。

如果一个外层节点维护的是\(1,2\)行,那么他里面维护的就是\([(1,l),(2,r)]\)的矩阵的信息。

对于外层叶子结点的内层线段树,仅仅维护这一行的和,但是非叶子结点的内层线段树的节点就是非叶子节点的左右儿子节点的内层线段树的相应节点之和。

那么我们来看一个例子:

图片解释:对于每个橙色节点,他都维护了一棵绿色的线段树,那么黄色就是我们加值的点,设加\(k\)

那么因为加值在第\(1\)行,而蓝色框住的节点维护的行范围包括但不是等于第\(1\)行,我们就统计他的影响,很明显在第\(2\)列总共加了\(k\)(如果是竖着的两格就是\(2k\),不过第二个蓝色框框是全覆盖了),那么对于维护第\(1\)行的叶子结点,我们再修改统计和的内层线段树的同时,我们也要对于永久化标记的线段树,给第\(2\)列打上\(k\)的永久化标记。

而对于下面的进阶情况:

我们需要对于外层根节点的统计和的内层线段树在[2,3]列加上\(3k\)的值,因为是统计和,所以我们可以统计出修改对于上层节点的内层线段树的影响。(最大值就不能直接统计)

当然,外层节点\([3,4]\)行的\([2,3]\)列的和也是加\(k\),对于维护\([1,2]\)行的节点,我们需要在\([2,3]\)列打上永久化标记\(k\),这时候你问了,为什么不是\(2k\),完全覆盖一列\(2k\)呀?因为我们以后到了这个节点,我们可以查出在\([1,2]\)行的任意一行的\([2,3]\)列打的标记,然后根据询问的行数,再乘以行数,不然标记会局限于当查询行数完全覆盖才能查标记导致TLE或者WA,打完标记后退出。

内层\(log\),外层\(log\),\(log^2\)时间

查询就是对于沿路经过的外层节点,查询列的标记,乘以查询的行数,并在查询行数覆盖的节点直接查和。

\(log^2\)

来自参考文献的代码:

//这个代码还算友善
#include<cstdio>
#define gc getchar
int gi(){int x=0,f=0;char c=gc();while(c<'0'||'9'<c){if(c=='-')f=!f;c=gc();}while('0'<=c&&c<='9'){x=x*10+c-48;c=gc();}return f?(-x):x;}
using namespace std;
#define N 2010
int D,S,q;
struct xds{//内层(标记永久化)
    #define Z int m=(l+r)>>1
    #define ls rt<<1
    #define rs rt<<1|1
    int s[N*4],tag[N*4]/*内层也用永久化标记。。。*/;
    void build(int l,int r,int rt){//内层建树
        if(l==r){s[rt]=gi();return;}
        Z;build(l,m,ls);build(m+1,r,rs);
        s[rt]=s[ls]+s[rs];
    }
    void upd(int L,int R,int v,int l,int r,int rt){//内层修改
        s[rt]+=v*(R-L+1);
        if(L==l&&r==R){
            tag[rt]+=v;
            return;
        }
        Z;
        if(R<=m)upd(L,R,v,l,m,ls);
        else{
            if(L>m)upd(L,R,v,m+1,r,rs);
            else upd(L,m,v,l,m,ls),upd(m+1,R,v,m+1,r,rs);
        }
    }
    int qh(int L,int R,int l,int r,int rt,int ad){
        if(L==l&&r==R)return s[rt]+ad*(r-l+1);
        Z;ad+=tag[rt];
        if(R<=m)return qh(L,R,l,m,ls,ad);
        else{
            if(L>m)return qh(L,R,m+1,r,rs,ad);
            else return qh(L,m,l,m,ls,ad)+qh(m+1,R,m+1,r,rs,ad);
        }
    }
}s[N*4]/*维护和*/,tag[N*4]/*维护永久化标记*/;
void mg(xds& o,xds& lc,xds& rc,int l,int r,int rt){//外层节点更新(pushup)
    o.s[rt]=lc.s[rt]+rc.s[rt];
    if(l==r)return;
    Z;mg(o,lc,rc,l,m,ls);mg(o,lc,rc,m+1,r,rs);
}
void build(int l,int r,int rt){//外层建树
    if(l==r){
        s[rt].build(1,S,1);
        return;
    }
    Z;build(l,m,ls);build(m+1,r,rs);
    mg(s[rt],s[ls],s[rs],1,S,1);
}
void upd(int x,int y,int xx,int yy,int v,int l,int r,int rt){//外层修改
    s[rt].upd(y,yy,v*(xx-x+1),1,S,1);//沿路改和
    if(x==l&&r==xx){
        tag[rt].upd(y,yy,v,1,S,1);//改标记
        return;
    }
    Z;
    if(xx<=m)upd(x,y,xx,yy,v,l,m,ls);
    else{
        if(x>m)upd(x,y,xx,yy,v,m+1,r,rs);
        else upd(x,y,m,yy,v,l,m,ls),upd(m+1,y,xx,yy,v,m+1,r,rs);
    }
}
int qh(int x,int y,int xx,int yy,int l,int r,int rt,int ad){//查询(求和)
    if(x==l&&r==xx)return s[rt].qh(y,yy,1,S,1,0)+ad*(r-l+1);
    Z;ad+=tag[rt].qh(y,yy,1,S,1,0);//查标记
    if(xx<=m)return qh(x,y,xx,yy,l,m,ls,ad);
    else{
        if(x>m)return qh(x,y,xx,yy,m+1,r,rs,ad);
        else return qh(x,y,m,yy,l,m,ls,ad)+qh(m+1,y,xx,yy,m+1,r,rs,ad);
    }
}
int main(){
    D=gi();S=gi();q=gi();
    build(1,D,1);
    int p,x,y,xx,yy;
    while(q--){
        p=gi();x=gi();y=gi();xx=gi();yy=gi();
        if(p==1)printf("%d\n",qh(x,y,xx,yy,1,D,1,0));
        else upd(x,y,xx,yy,gi(),1,D,1);
    }
}

优点:

  1. 时间复杂度优秀。
  2. 因为使用永久化标记,常数较小。以及用主席树支持可持久化

缺点:

  1. 不能支持最大值。
  2. 思想复杂。
  3. 空间变大

    树套树写法2

    如何支持求最大值呢。

比如最近这道题目

即使这道题目随便用四叉树爆叉。

但是时间复杂度还是太慢了!!!!

于是我就去请教机房巨佬之神,膜拜\(1e∞\)后,zhoufangyuan巨佬为之撼动,教了我一个做法,继续爆切黑题。

就是一样是对于行完全覆盖的外层节点的内层线段树,我们一样可以打个永久化标记,然后维护最大值,但是不一样的是,我们发现跑每个叶子节点内层线段树所经过的节点都是一样的,且是\(logn\)个的,我们可以先建个线段树,把列的范围扔进去,然后处理出那么会被访问节点。


就是蓝色框框中的点。(而且因为每层最多两个节点被访问,所以数量级为\(logn\))

然后我们一如既往的跑一遍外层修改,然后也是把行完全覆盖的外层节点的内层节点改一下,然后对于行未完全覆盖的外层节点,就是把自己内层线段树中的蓝色框住的点的最大值,去合并左右儿子蓝色框柱的点的最大值就是了,这样就可以用\(log^2\)代替原本\(log^2\)就可以计算的部分影响,无伤大雅,果然远哥一想就是一个神仙。

其实就是把部分影响的维护方法给了一下,但是却可以多维护大量的信息。

都是口糊的了,哪有代码。

优点:

  1. 维护信息多。
  2. 时间复杂度优秀。

缺点:

  1. 代码打起来有点抽。
  2. 大部分缺点与写法1类似。

原文地址:https://www.cnblogs.com/zhangjianjunab/p/11749115.html

时间: 2024-11-03 21:03:43

浅谈二维线段树的几种不同的写法的相关文章

浅谈二维线段树

一.定义 二维线段树,即用线段树维护一个矩阵 有两种实现方式: 1.原一维线段树的基础上,每一个节点都是一个线段树,代表第二维 下图是一个4*4矩阵 2.四分法转化为一维线段树 两种方法的空间复杂度都是n*n*log^2 第一种方法单次操作的时间复杂度是log^2,第二种方法最差可以退化到n 一维线段树的标记思想,在第一种方法中,可以用于二维线段树的第二维,不可以用于二维线段树的第一维 第二种方法本质上是四叉的一维线段树, 在此只介绍第一种方法 二.基本操作 1.单点修改+矩阵查询 单次访问一个

hdu1823 Luck and Love 二维RMQ(二维线段树)

题意:给定你二维范围,找这个范围里面的最大值 解题思路:二维线段树有两种实现方式,一种是  树套树  ,另一种则是将二维平面分成4块的  完全四叉树 我的代码: // File Name: 1823.cpp // Author: darkdream // Created Time: 2014年07月10日 星期四 09时38分05秒 #include<vector> #include<list> #include<map> #include<set> #in

浅谈二维中的树状数组与线段树

一般来说,树状数组可以实现的东西线段树均可胜任,实际应用中也是如此.但是在二维中,线段树的操作变得太过复杂,更新子矩阵时第一维的lazy标记更是麻烦到不行. 但是树状数组在某些询问中又无法胜任,如最值等不符合区间减法的询问.此时就需要根据线段树与树状数组的优缺点来选择了. 做一下基本操作的对比,如下图. 因为线段树为自上向下更新,从而可以使用lazy标记使得矩阵的更新变的高校起来,几个不足就是代码长,代码长和代码长. 对于将将矩阵内元素变为某个值,因为树状数组自下向上更新,且要满足区间加法等限制

HDU1832 二维线段树求最值(模板)

Luck and Love Time Limit: 10000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 50 Accepted Submission(s): 20   Problem Description 世界上上最远的距离不是相隔天涯海角而是我在你面前可你却不知道我爱你                ―― 张小娴 前段日子,枫冰叶子给Wiskey做了个征婚启事,聘

ZOJ 2859 二维线段树

思路:自己写的第二发二维线段树1A,哈哈,看来对二维的push操作比较了解了:但是还没遇到在两个线段树中同时进行push操作的,其实这题我是想在x维和y维同时进行push操作的,但是想了好久不会,然后看到这题又给出10秒,然后想想在x维线段直接单点查询肯定也过了,然后在第二维就只有pushup操作,在第一维线段树没有pushup操作.要是在第一维也有pushup操作的话,那就不用单点查询那么慢了.不过也A了,想找题即在二维同时进行pushup和pushdown操作的. #include<iost

[POJ2155] Matrix(二维线段树,树套树)

题目链接:http://poj.org/problem?id=2155 题意:给一个01矩阵,两个操作,翻转:子矩阵里每一个数都由0变1,1变0. 查询:查询某一点是0还是1. 一直以为二维线段树就是开一个线段树数组的我- 这题暴力更新每一个小矩形,翻转就+1,最后看看某点的奇偶. 写屎了,特别注意的是在外层查询的时候要先把当前层的内层query掉,接着再向下扩展.这样外层就不用lazy啦 1 #include <algorithm> 2 #include <iostream> 3

poj1195 Mobile phones 二维线段树入门

二维线段树就是树套树,线段树套线段树... #include<iostream> #include<cstdio> #include<cstring> #include<cstdlib> #include<algorithm> #define REP(i,a,b) for(int i=a;i<=b;i++) #define MS0(a) memset(a,0,sizeof(a)) #define lson l,m,rt<<1 #

UVA 11297 Census ——二维线段树

[题目分析] 二维线段树模板题目. 简直就是无比的暴力.时间复杂度为两个log. 标记的更新方式比较奇特,空间复杂度为N^2. 模板题目. [代码] #include <cstdio> #include <cstring> //#include <cmath> #include <cstdlib> #include <map> #include <set> #include <queue> #include <str

tyvj P1716 - 上帝造题的七分钟 二维树状数组区间查询及修改 二维线段树

P1716 - 上帝造题的七分钟 From Riatre    Normal (OI)总时限:50s    内存限制:128MB    代码长度限制:64KB 背景 Background 裸体就意味着身体. 描述 Description “第一分钟,X说,要有矩阵,于是便有了一个里面写满了0的n×m矩阵.第二分钟,L说,要能修改,于是便有了将左上角为(a,b),右下角为(c,d)的一个矩形区域内的全部数字加上一个值的操作.第三分钟,k说,要能查询,于是便有了求给定矩形区域内的全部数字和的操作.第