主席树(Lights,HDU 5820)

一开始考虑用二维树状数组,写到一半发现开不下空间。

全局数组变量最多能开5e8个int的空间。

题目限制大概5e7个int的空间。

点的坐标范围是[1,5e4],如果用一个二维树状数组去维护的话需要开5e4*5e4=2.5e9的空间,开不下来,也远超了题目限制。

点的个数为5e5,如果空间能够开下来的话时间复杂度是O(nlog^2x),可以接受的。

平时没怎么关注空间复杂度,一般MLE都是写错了导致的。

如果n=1e4级别的,那么时间复杂度O(n^2)是可以承受的,但是空间复杂度O(n^2)是不能承受的。

一般题目的空间复杂度都远小于时间复杂度,所以平时没怎么关注这方面的限制,直到写代码的时候才发现。


然后就考虑别的办法,然后觉得找不到什么办法能够做到不询问区域内点的个数,因为节点之间的关系是二维的,如果不考虑具体的坐标和区域分布情况,没有办法仅凭横纵方向的关系判断任意两个节点之间的关系是否合法。


然后就考虑到只有5e5个节点,而位置却有2.5e9个,所以整个盘面一定是非常稀疏的,所以考虑离散化。

当时就决定对每一行(或者每一列)进行离散化,这样总的位置就只有5e5个了,然后我们再对每一列(或者每一行)用树状数组维护,然后再把多个树状数组用树状数组维护起来,就像模仿二维树状数组那样弄,更新和询问也是模仿着做。当时就考虑如果这个点恰好是在这一行(或者这一列),那就是正常的更新,否则的话就可能找不到位置。我就考虑使用二分来找到大于等于它的第一个位置,然后照常更新。后来思考了一段时间以后觉得这样做是错的。因为树状数组和线段树本质上都是一样的,都是维护区间的性质,都是有层级关系的,都是更新了以后要通知上级,都是如果上级覆盖了这个区域,那我们就不去下级访问了。唯一的区别就是区间设计以及上下层关系不一样,线段树是左区间右区间一合并就是当前区间,而树状数组是用lowbit来完成这个区间设计以及区间合并的。如果我们使用二分去找位置,那就意味着很多个坐标融合在了一起,这种融合在最下层的更新时一定不存在的,因为最下层一定有这个坐标。但是往上层通知更新的时候,上层不一定有这个坐标,我们就把多个坐标融合在了一起,以后也无法区分,除非访问下层。但是树状数组正是通过正确更新和访问上层来保证正确性以及时间复杂度的。如果这样做的话,那我们在询问一个区间的时候,要么就没法区分被融合的坐标,要么就只能访问下层,前者会得到错误的答案,而后者就是一个暴力。主要是二维树状数组有些抽象,思考了一段时间才想明白,如果是一维的话正确性是很好看出来的。

比如说下左图就是一个4*4的二维树状数组的模拟图。每一个圆代表一个位置。每一个圆都有且仅有一个以它为右上角的矩形,表示这个节点维护的区域。

下面我们考虑离散化后的情况,假设打叉的节点是因为离散化所以删掉的节点,当我们需要询问第一行第二个矩形的情况时,由于没有这个节点,所以我们只能二分到第一行最后边的那个节点,这时候的询问是没有办法区分两个打问号的节点的,而且还可能会包含到其他多余的节点。


后来觉得还是没办法,又继续考虑区域询问,尝试使用动态开点二维线段树,当时觉得5e5个灯,每个点开2logx个节点来维护,最终空间为2e7应该没有问题,但是事实上二维线段树的更新是每个子树都要进去更新一次的,即每个灯要用log^2x个节点来维护,所以这样做最差情况就等价于直接使用二维线段树。事实上动态开点能解决的问题一定可以用离散化解决,如果离散化解决不了,那么动态开点也解决不了。

考虑一维的情况帮助理解:

动态开点相当于为每一个更新打通一条到底的路,但是如果每个点都更新一次,那就相当于开了一个满二叉树。

离散化是通过离线,只保留将会使用的坐标。

动态开点就是用到我就开一个空间给你,不用到就不开,因此空间复杂度是和离散化一样的。

因为离散化后使用二维树状数组无法解决本题,因此动态开点二维线段树也无法解决本题。


后来就学习了可持久化数据结构。

大白书P398上面的那个图挺直观的,讲解也比较好懂,配合模板题和代码可以比较快的理解。

模板题链接:https://vjudge.net/problem/HDU-2665

我的题解:http://blog.csdn.net/xl2015190026/article/details/76275477


静态(不带修改)树套树(比如二维线段树或者二维树状数组)可以用主席树优化时空复杂度。


代码

#include<stdio.h>
#include<algorithm>
#define m ((l+r)>>1)
using namespace std;
const int maxn = 500010;
const int maxp = 50010;
const int maxs = 20*maxn;
const int n = 50000;

struct Point
{
    int r,c;
    void read()
    {
        scanf("%d %d",&r,&c);
    }
}p[maxn];
int N;
int row[maxp];
int col[maxp];

int cmp1(const Point& A,const Point& B)
{
    if(A.r!=B.r) return A.r<B.r;
    else return A.c<B.c;
}

int cmp2(const Point& A,const Point& B)
{
    if(A.r!=B.r) return A.r<B.r;
    else return A.c>B.c;
}

//可持久化线段树
int rt[maxp];
int tree[maxs],ls[maxs],rs[maxs],ver[maxs],tot;
void init()
{
    tot=0;
    for(int i=0;i<=n;i++) rt[i]=0;
}
void build(int l,int r,int& now)
{
    now=++tot;
    tree[now]=0;
    ver[now]=0;
    if(l==r) return;
    build(l,m,ls[now]);
    build(m+1,r,rs[now]);
}
void update(int last,int l,int r,int& now,int pos,int v)
{
    if(!now||ver[now]!=v)
    {
        now=++tot;
        ver[now]=v;
        ls[now]=ls[last];
        rs[now]=rs[last];
        tree[now]=tree[last]+1;
    }
    else tree[now]++;
    if(l==r) return;
    if(pos<=m) update(ls[last],l,m,ls[now],pos,v);
    else update(rs[last],m+1,r,rs[now],pos,v);
}
void newversion(int last,int& now,int v)
{
    now=++tot;
    ver[now]=v;
    ls[now]=ls[last];
    rs[now]=rs[last];
    tree[now]=tree[last];
}
int qry(int l,int r,int now,int ql,int qr)
{
    if(!now) return 0;
    if(l>qr||r<ql) return 0;
    if(ql<=l&&r<=qr) return tree[now];
    return qry(l,m,ls[now],ql,qr)+qry(m+1,r,rs[now],ql,qr);
}
//可持久化线段树

bool ok(int r1,int r2,int c1,int c2)
{
    return qry(1,n,rt[r2],c1,c2)-qry(1,n,rt[r1-1],c1,c2)==0;
}

int solve()
{
    for(int i=1;i<=N;i++)
        p[i].read();

    init();
    build(1,n,rt[0]);
    sort(p+1,p+1+N,cmp1);
    for(int i=0;i<=n;i++)
        row[i]=col[i]=0;
    int last=0;
    for(int i=1;i<=N;i++)
    {
        if(last<p[i].r)
        {
            for(int j=last+1;j<=p[i].r;j++)
                newversion(rt[j-1],rt[j],j);
            last=p[i].r;
        }
        if(!ok(col[p[i].c]+1,p[i].r,row[p[i].r]+1,p[i].c)) return 0*puts("NO");
        update(rt[p[i].r-1],1,n,rt[p[i].r],p[i].c,p[i].r);
        row[p[i].r]=p[i].c;
        col[p[i].c]=p[i].r;
    }

    init();
    build(1,n,rt[0]);
    sort(p+1,p+1+N,cmp2);
    for(int i=0;i<=n;i++)
    {
        row[i]=n+1;
        col[i]=0;
    }
    last=0;
    for(int i=1;i<=N;i++)
    {
        if(last<p[i].r)
        {
            for(int j=last+1;j<=p[i].r;j++)
                newversion(rt[j-1],rt[j],j);
            last=p[i].r;
        }
        if(!ok(col[p[i].c]+1,p[i].r,p[i].c,row[p[i].r]-1)) return 0*puts("NO");
        update(rt[p[i].r-1],1,n,rt[p[i].r],p[i].c,p[i].r);
        row[p[i].r]=p[i].c;
        col[p[i].c]=p[i].r;
    }

    return 0*puts("YES");
}

int main()
{
    while(scanf("%d",&N)!=EOF&&N) solve();
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值