二维数点学习笔记

定义

二维数点又称二维偏序,主要解决这样一类问题:给出一个二维平面內的若干个点,多次询问某个矩形区域內包含多少个点。或者,给定一个长为 $n$ 的序列,多次询问区间 $[ l , r ]$ 中值在 $[ x , y ]$ 内的元素个数。范围大概在 $10^6$ 级别,or 离散化后 $10^6$ 级别,时间复杂度可以做到 $\mathcal{O}(n\log n)$。

能解决这一问题的数据结构较多,包括树状数组、线段树、K-D Tree、可持久化线段树等,运用CDQ分治可解决更高维的偏序问题。

例题

P2163 [SHOI2007] 园丁的烦恼

题意

给定平面内 $n$ 个点,$q$ 次询问以 $(x_1,y_1)$ 为左下角,$(x_2,y_2)$ 为右上角的矩形内部(包括边界)有多少个点。

Solution

二维数点板子题。将询问 $(x_1,y_1,x_2,y_2)$ 矩形转化为求四个以 $(0,0)$ 为左下角的矩形的答案。定义 $f(a,b,c,d)$ 为以 $(a, b)$ 为左下角,$(c, d)$ 为右上角的矩形内部(包括边界)的点数。则 $f(x_1,y_1,x_2,y_2)=f(0,0,x_2,y_2)-f(0,0,x_1-1,y_2)-f(0,0,x_2,y_1-1)+f(0,0,x_1-1,y_1-1)$,$f$ 使用离线+扫描线+树状数组即可在 $\mathcal{O}(n\log n)$ 时间内求出。

Code

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
    int res=0,flag=1;
    char ch=getchar();
    while(!isalnum(ch)) (ch=='-')?flag=-1:1,ch=getchar();
    while(isalnum(ch)) res=res*10+ch-'0',ch=getchar();
    return res*flag;
}
struct postion
{
    int x,y;
    bool operator <(const struct postion &other)const
    {
        if(this->x==other.x)
            return this->y<other.y;
        return this->x<other.x;
    }
};
struct question
{
    int id,x,y,val;
    bool operator <(const struct question &other)const
    {
        if(this->x==other.x)
            return this->y<other.y;
        return this->x<other.x;
    }
};
struct postion pos[500010];
struct question que[2000010];
int cnt;
int data[500010],ans[500010];
int lowbit(int x)
{
    return x&(-x);
}
void modify(int pos)
{
    for(int i=pos;i<=500000;i+=lowbit(i))
        data[i]++;
    return ;
}
int query(int pos)
{
    if(pos==0)
        return 0;
    int res=0;
    for(int i=pos;i!=0;i-=lowbit(i))
        res+=data[i];
    return res;
}
int main(int argc,const char *argv[])
{
    int n=read(),q=read();
    for(int i=1;i<=n;i++)
        pos[i].x=read()+1,pos[i].y=read()+1;
    for(int i=1;i<=q;i++)
    {
        int a=read()+1,b=read()+1,c=read()+1,d=read()+1;
        que[++cnt]=(question){i,c,d,1};
        que[++cnt]=(question){i,a-1,d,-1};
        que[++cnt]=(question){i,c,b-1,-1};
        que[++cnt]=(question){i,a-1,b-1,1};
    }
    std::sort(pos+1,pos+n+1);
    std::sort(que+1,que+cnt+1);
    for(int i=1,now=1;i<=cnt;i++)
    {
        while(pos[now].x<=que[i].x&&now<=n)
            modify(pos[now].y),now++;
        ans[que[i].id]+=query(que[i].y)*que[i].val;
    }
    for(int i=1;i<=q;i++)
        printf("%d\n",ans[i]);
    return 0;
}

<div STYLE="page-break-after: always;"></div>

P3755 [CQOI2017] 老C的任务

题意

给定平面内 $n$ 个点,每个点有点权,$q$ 次询问以 $(x_1,y_1)$ 为左下角,$(x_2,y_2)$ 为右上角的矩形内部(包括边界)点权之和。

Solution

二维数点板子题 $\times 2$。 离散化后在 insert 点的时候多加一维权值,然后在 insert 进树状数组的时候把权值 insert 进去即可。

Code

#include<bits/stdc++.h>
using namespace std;
inline long long read()
{
    long long res=0,flag=1;
    char ch=getchar();
    while(!isalnum(ch)) (ch=='-')?flag=-1:1,ch=getchar();
    while(isalnum(ch)) res=res*10+ch-'0',ch=getchar();
    return res*flag;
}
struct postion
{
    int x;
    long long y,val;
    bool operator <(const struct postion &other)const
    {
        if(this->x==other.x)
            return this->y<other.y;
        return this->x<other.x;
    }
};
struct question
{
    int id,x,y,val;
    bool operator <(const struct question &other)const
    {
        if(this->x==other.x)
            return this->y<other.y;
        return this->x<other.x;
    }
};
struct postion pos[100010];
struct question que[400010];
int cnt,tot;
long long data[1000010],ans[100010];
priority_queue<long long,vector<long long>,greater<long long> > Queue;
map<long long,int> mp;
int lowbit(int x)
{
    return x&(-x);
}
void modify(int pos,long long val)
{
    for(int i=pos;i<=1000000;i+=lowbit(i))
        data[i]+=val;
    return ;
}
long long query(int pos)
{
    if(pos==0)
        return 0;
    long long res=0;
    for(int i=pos;i!=0;i-=lowbit(i))
        res+=data[i];
    return res;
}
int main(int argc,const char *argv[])
{
    int n=read(),q=read();
    for(int i=1;i<=n;i++)
    {
        pos[i].x=read();
        pos[i].y=read();
        pos[i].val=read();
        Queue.push(pos[i].y);
    }
    for(int i=1;i<=q;i++)
    {
        int a=read(),b=read(),c=read(),d=read();
        que[++cnt]=(question){i,c,d,1};
        que[++cnt]=(question){i,a-1,d,-1};
        que[++cnt]=(question){i,c,b-1,-1};
        que[++cnt]=(question){i,a-1,b-1,1};
        Queue.push(b-1),Queue.push(d);
    }
    while(Queue.empty()==false)
    {
        long long val=Queue.top();
        if(mp[val]==0)
            mp[val]=++tot;
        Queue.pop();
    }
    std::sort(pos+1,pos+n+1);
    std::sort(que+1,que+cnt+1);
    for(int i=1,now=1;i<=cnt;i++)
    {
        while(pos[now].x<=que[i].x&&now<=n)
            modify(mp[pos[now].y],pos[now].val),now++;
        ans[que[i].id]+=query(mp[que[i].y])*que[i].val;
    }
    for(int i=1;i<=q;i++)
        printf("%lld\n",ans[i]);
    return 0;
}

<div STYLE="page-break-after: always;"></div>

P9196 [JOI Open 2016] 销售基因链

题意

给定 $n$ 个由 AGUC 组成的字符串 $s_1,s_2,\dots,s_n$。$q$ 次查询以 $pre_i$ 为前缀,以 $suc_i$ 为后缀的字符串个数。

Solution

显然地,用 trie 树来处理前后缀,发现关键问题在于如何判断同时满足前后缀要求。发现将 $s_1,s_2,\dots,s_n$ 排序后,满足前缀为 $pre_i$ 的字符串编号连续,设其为 $[a_l,a_r]$。将 $s_1,s_2,\dots,s_n$ 翻转后排序,满足后缀为 $suc_i$ 的字符串编号同样连续,设其为 $[b_l,b_r]$。设字符串 $s_i$ 原来排序后的编号为 $u_i$,翻转排序后的编号为 $v_i$。则问题转化为同时满足 $u_i \in [a_l,a_r]$,$v_i\in [b_l,b_r]$ 的字符串个数。

考虑离线后二维数点解决,$[a_l,a_r]$ 和 $[b_l,b_r]$ 可用 trie 树快速维护。设 $\sum{\mid s_i\mid}=m$,字符串平均长度为 $len$,显然地,对于两个字符串,比较的最坏复杂度为 $\mathcal{O}(len)$,则排序复杂度为 $\mathcal{O}((\frac{m}{len}\log\frac{m}{len})\times len)$ 即 $\mathcal{O}(m \log \frac{m}{len})$,最坏情况下 $len=1$,即最坏复杂度为 $\mathcal{O}(m \log m)$。故总时间复杂度为 $\mathcal{O}(m\log m+\sum{\mid pre_i\mid}+\sum{\mid suc_i\mid})$。

Code

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
    int res=0,flag=1;
    char ch=getchar();
    while(!isalnum(ch)) (ch=='-')?flag=-1:1,ch=getchar();
    while(isalnum(ch)) res=res*10+ch-'0',ch=getchar();
    return res*flag;
}
struct node 
{
    int data;
    int son[5];
};
int tot;
struct node nd[2000010];
int l[2000010],r[2000010];
int cnt[2000010],ans[2000010];
string s[100010];
vector<tuple<string,int,int> > info[2000010];
int helper(char ch)
{
    switch(ch)
    {
        case 'A': return 1;
        case 'U': return 2;
        case 'G': return 3;
        case 'C': return 4;
    }
    return -1;
}
void insert(string s,int id)
{
    int fr=0;
    for(int i=0;i<s.length();i++)
    {
        int tmp=helper(s[i]);
        int to=nd[fr].son[tmp];
        if(to==0)
        {
            nd[fr].son[tmp]=++tot;
            to=tot;
            l[to]=id,r[to]=id;
        }
        l[to]=min(l[to],id),r[to]=max(r[to],id);
        fr=to;
    }
    return ;
}
void modify(string s)
{
    int fr=0;
    for(int i=s.length()-1;i>=0;i--)
    {
        int tmp=helper(s[i]);
        int to=nd[fr].son[tmp];
        if(to==0)
            nd[fr].son[tmp]=++tot,to=tot;
        cnt[to]++;
        fr=to;
    }
    return ;
}
int query(string s)
{
    int fr=0;
    for(int i=0;i<s.length();i++)
    {
        int tmp=helper(s[i]);
        int to=nd[fr].son[tmp];
        if(to==0)
            return -1;
        fr=to;
    }
    return fr;
}
int main(int argc,const char *argv[])
{
    int n=read(),q=read();
    for(int i=1;i<=n;i++)
        cin>>s[i];
    sort(s+1,s+n+1);
    for(int i=1;i<=n;i++)
        insert(s[i],i);
    for(int i=1;i<=q;i++)
    {
        string pre,suc;
        cin>>pre>>suc;
        int pos=query(pre);
        if(pos==-1)
            continue;
        reverse(suc.begin(),suc.end());
        info[r[pos]].push_back(make_tuple(suc,i,1));
        info[l[pos]-1].push_back(make_tuple(suc,i,-1));
    }
    memset(nd,0,sizeof nd);
    tot=0;
    for(int i=1;i<=n;i++)
    {
        modify(s[i]);
        for(auto j:info[i])
        {
            int pos=query(get<0>(j));
            ans[get<1>(j)]+=cnt[pos]*get<2>(j);
        }
    }
    for(int i=1;i<=q;i++)
        printf("%d\n",ans[i]);
    return 0;
}

<div STYLE="page-break-after: always;"></div>

CF983E NN country

题意

给定一棵树和若干条路线,每条路线相当于 $a,b$ 之间的路径,途径路径上的每个点。

给出若干个询问,每次询问从 $x$ 到 $y$ 至少需要利用几条路线。

Solution

考虑子问题,两个询问点在同一条链上,这样问题就类似 [SCOI2015] 国旗计划,只不过这道题是环上问题,但思路相同。

对于链,考虑贪心,对于每一个点 $fr_i$ 跳到能到达的最远的点 $to_i$,容易想到下一步应当是跳到 $to_{to_i}$,故考虑倍增优化这个不断往前跳的过程。定义 $jump_{i,k}$ 为点 $i$ 跳 $2^k$ 步能到达的最远节点,可以用 $\mathcal{O}(n\log n)$ 复杂度的时间来处理出 $jump$ 数组。

考虑树上的两个点,对于 $x$ 是 $y$ 的祖先节点($y$ 为 $x$ 祖先节点时同理)的情况,同链上情况处理。

对于两个点分别不是对方父亲节点的情况,考虑将问题拆分为 $x$ 到 $lca$ 和 $y$ 到 $lca$ 两个问题处理。令 $ans_x$ 为 $x$ 跳到 $lca$ 的最小步数,$ans_y$ 为 $y$ 跳到 $lca$ 的最小步数,$pre_x$ 为 $x$ 向上跳 $ans_x-1$ 步到达的深度最浅的节点,即跳到 $lca$ 的前一个节点,$pre_y$ 同理。考虑两种情况:

  • 有一条路线同时经过 $pre_x$ 和 $pre_y$;
  • 不存在一条路线同时经过 $pre_x$ 和 $pre_y$。

对于第二种情况,答案即为 $ans_x+ans_y$,对于第一种情况,答案为 $ans_x+ans_y-1$。问题转化为如何维护是否存在一条路线经过两个点。

发现对于一个节点 $u$,只要 $u$ 的子树中存在一个点,使得存在一条从其出发的路径在 $v$ 的子树中结束,则存在一条路径同时经过 $u$ 和 $v$。考虑通过 dfs 序转化为区间问题,令 $size_i$ 为节点 $i$ 的子树大小,则问题进一步转化为询问是否存在一条路径 $(fr,to)$ 使得 $dfn_{fr}\in[dfn_x,dfn_x+size_x-1]$ ,$dfn_{to}\in[dfn_y,dfn_y+size_y-1]$。考虑二维数点,即查询平面上矩形 $[(dfn_x,dfn_y),(dfn_x+size_x-1,dfn_y+size_y-1)]$ 是否有点。将询问离线排序并用树状数组维护即可。

有个小细节:由于 $(fr,to)$ 和 $(to,fr)$ 在此题中是等价的,故在插入点时都应插入,否则可能会统计不到这个点。

其他具体实现细节见代码,以及由于我不会倍增求 $lca$,所以写了个树剖。

总时间复杂度应该是 $\mathcal{O}(n\log n)$ 的,但是有巨大常数。

code

#include<bits/stdc++.h>
inline int read()
{
    int res=0,flag=1;
    char ch=getchar();
    while(!isalnum(ch)) (ch=='-')?flag=-1:1,ch=getchar();
    while(isalnum(ch)) res=res*10+ch-'0',ch=getchar();
    return res*flag;
}
struct edge 
{
    int to,nxt;
};
int n,m,q,tot=0;
int head[200010];
struct edge ed[400010];
void add_edge(int fr,int to)
{
    ed[++tot]=(edge){to,head[fr]};
    head[fr]=tot;
    return ;
}
namespace TreePartition
{
    struct node
    {
        int fa,son;
        int top;
        int size;
        int dep;
    };
    const int root=1;
    struct node nd[200010];
    void set(int fa,int fr)
    {
        nd[fr].size=1;
        nd[fr].fa=fa;
        nd[fr].dep=nd[fa].dep+1;
        for(int i=head[fr];i!=0;i=ed[i].nxt)
        {
            int to=ed[i].to;
            if(to==fa)
                continue;
            set(fr,to);
            nd[fr].size+=nd[to].size;
            if(nd[to].size>nd[nd[fr].son].size)
                nd[fr].son=to;
        }
        return ;
    }
    void dfs(int fa,int fr)
    {
        if(nd[fr].son!=0)
        {
            int to=nd[fr].son;
            nd[to].top=nd[fr].top;
            dfs(fr,to);
        }
        for(int i=head[fr];i!=0;i=ed[i].nxt)
        {
            int to=ed[i].to;
            if(to==fa||to==nd[fr].son)
                continue;
            nd[to].top=to;
            dfs(fr,to);
        }
        return ;
    }
    void init()
    {
        nd[root].dep=1;
        nd[root].top=root;
        set(0,root);
        dfs(0,root);
        return ;
    }
    int lca(int x,int y)
    {
        int fx=nd[x].top;
        int fy=nd[y].top;
        while(fx!=fy)
        {
            if(nd[fx].dep<nd[fy].dep)
                std::swap(x,y),std::swap(fx,fy);
            if(x==root)
                return root;
            x=nd[fx].fa;
            fx=nd[nd[fx].fa].top;
        }
        if(nd[x].dep>nd[y].dep)
            std::swap(x,y);
        return x;
    }
}
namespace BIT
{
    struct point
    {
        int x,y;
        bool operator <(const struct point &other)const
        {
            if(this->x==other.x)
                return this->y<other.y;
            return this->x<other.x; 
        }
    };
    struct question
    {
        int id;
        int val;
        int x,y;
        bool operator <(const struct question &other)const
        {
            if(this->x==other.x)
                return this->y<other.y;
            return this->x<other.x; 
        }
    };
    int pts,cnt;
    struct point points[800010];
    struct question questions[1600010];
    int data[400010];
    int lowbit(int x)
    {
        return x&(-x);
    }
    void modify(int pos,int val)
    {
        if(pos==0)
            return ;
        for(int i=pos;i<=n;i+=lowbit(i))
            data[i]+=val;
        return ;
    }
    int query(int pos)
    {
        int res=0;
        for(int i=pos;i>=1;i-=lowbit(i))
            res+=data[i];
        return res;
    }
};
namespace solve
{
    int dfncnt;
    int ans[200010],tmp[200010];
    int dfn[200010];
    int jump[200010][21];
    std::vector<int> graph[200010];
    void dfs(int fa,int fr)
    {
        dfn[fr]=++dfncnt;
        for(int i=head[fr];i!=0;i=ed[i].nxt)
        {
            int to=ed[i].to;
            if(to==fa)
                continue;
            dfs(fr,to);
        }
        return ;
    } 
    void set(int fa,int fr) 
    {
        using namespace TreePartition;
        jump[fr][0]=fr;
        for(auto Lca:graph[fr])
            if(nd[jump[fr][0]].dep>nd[Lca].dep)
                jump[fr][0]=Lca;
        for(int i=head[fr];i!=0;i=ed[i].nxt)
        {
            int to=ed[i].to;
            if(to==fa)
                continue;
            set(fr,to);
            if(nd[jump[fr][0]].dep>nd[jump[to][0]].dep)
                jump[fr][0]=jump[to][0];
        }
        return ;
    }
    void input()
    {
        n=read(),m=read(),q=read();
        for(int to=2;to<=n;to++)
        {
            int fr=read();
            add_edge(fr,to);
            add_edge(to,fr);
        }
        TreePartition::init();
        dfs(0,1); 
        for(int i=1;i<=m;i++)
        {
            int fr=read(),to=read();
            int Lca=TreePartition::lca(fr,to);
            if(fr!=Lca)
                graph[fr].push_back(Lca);
            if(to!=Lca)
                graph[to].push_back(Lca);
            using namespace BIT;
            points[++pts]=(point){dfn[fr],dfn[to]};
            points[++pts]=(point){dfn[to],dfn[fr]};
        }
        set(0,1);
        return ;
    } 
    void init()
    {
        for(int i=1;i<=20;i++)
            for(int j=1;j<=n;j++)
                jump[j][i]=jump[jump[j][i-1]][i-1];
        return ; 
    } 
    void solve()
    {
        for(int i=1;i<=q;i++)
        {
            using namespace TreePartition;
            int fr=read(),to=read();
            int Lca=lca(fr,to);
            if(dfn[fr]>dfn[to])
                std::swap(fr,to); 
            for(int j=20;j>=0;j--)
                if(jump[fr][j]!=0&&nd[jump[fr][j]].dep>nd[Lca].dep)
                    fr=jump[fr][j],ans[i]+=(1<<j);
            for(int j=20;j>=0;j--)
                if(jump[to][j]!=0&&nd[jump[to][j]].dep>nd[Lca].dep)
                    to=jump[to][j],ans[i]+=(1<<j);
            if(nd[jump[fr][0]].dep>nd[Lca].dep||nd[jump[to][0]].dep>nd[Lca].dep) 
            {
                ans[i]=-1;
                continue;
            }
            if(fr==Lca)
            {
                ans[i]++;
                continue;
            };
            ans[i]+=2;
            int sizefr=nd[fr].size;
            int sizeto=nd[to].size;
            using namespace BIT;
            questions[++cnt]=(question){i,1,dfn[fr]+sizefr-1,dfn[to]+sizeto-1};
            questions[++cnt]=(question){i,-1,dfn[fr]+sizefr-1,dfn[to]-1};
            questions[++cnt]=(question){i,-1,dfn[fr]-1,dfn[to]+sizeto-1};
            questions[++cnt]=(question){i,1,dfn[fr]-1,dfn[to]-1};
        }
        using namespace BIT;
        std::sort(points+1,points+pts+1);
        std::sort(questions+1,questions+cnt+1);
        for(int i=1,now=1;i<=cnt;i++)
        {
            while(now<=pts&&points[now].x<=questions[i].x)
                modify(points[now].y,1),now++;
            tmp[questions[i].id]+=questions[i].val*query(questions[i].y);
        }
        for(int i=1;i<=q;i++)
        {
            if(tmp[i]>0)
                ans[i]--;
            printf("%d\n",ans[i]);
        }
        return ;
    }
}; 
int main(int argc,const char *argv[])
{
    solve::input();
    solve::init();
    solve::solve();
    return 0;
}
posted @   Che_001  阅读(310)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App
点击右上角即可分享
微信分享提示