• 【算法】数据结构


    【平衡树】★平衡树 by onion_cyc

    【莫队算法】

    问题:给定长度为n的序列和m个区间询问,支持快速增减相邻元素维护区间信息。

    将询问按左端点分块,块大小为$Q=frac{n}{sqrt m}$,块内按右端点排序。

    然后依次回答询问,需要O(1)从(l,r)转移到(l,r+1),(l,r-1),(l-1,r),(l+1,r)。

    复杂度分析:

    左端点的移动,每个询问至多移动Q次,复杂度O(mQ)。

    右端点的移动,每个块内至多移动n次,复杂度O(n*n/Q)。

    平衡之后可以得到最佳块大小,复杂度$O(nsqrt m)$。

     

    【堆】

    二叉堆

    #include<cstdio>
    #include<algorithm>
    using namespace std;
    const int maxn=20010;
    int n,heap[maxn],sz;
    void heap_push(int x)
    {
        heap[++sz]=x;//新数入堆底 
        int now=sz;//以堆底为起点 
        while(now>1&&heap[now]<heap[now>>1])//非根节点的父亲>儿子时------注意非根判断 
         {
             swap(heap[now],heap[now>>1]);//交换即上推 
             now>>=1;//转移到父亲 
         }
    }
    int heap_pop()
    {
        int ans=heap[1];//取出答案 
        heap[1]=heap[sz--];//将堆底最后一个元素调上来 
        int now=1;//以堆顶为起点 
        while(now<=(sz>>1))//若now有儿子------儿子存在判断 
         {
             int next=now<<1;//令next为now的左儿子------儿子赋变量 
             if(next<sz&&heap[next]>heap[next|1])next++;//now有右儿子且右儿子更小时,令next为右儿子------左右儿子判断---注意右儿子存在判断 
             if(heap[next]>heap[now])return ans;//若根比儿子小,满足条件,退出 
              else
               {
                   swap(heap[now],heap[next]);//交换即下推 
                   now=next;//转移到儿子 
               }
         }
        return ans; 
    }
    int main()
    {
        scanf("%d",&n);
        for(int i=1;i<=n;i++)
         {
             int u;
             scanf("%d",&u);
             heap_push(u);
         }
        long long ans=0;
        for(int i=1;i<n;i++)
         {
             int u=heap_pop(),v=heap_pop();
             heap_push(u+v);
             ans+=u+v;
         }
        printf("%lld",ans);
        return 0;
    }
    View Code

    可并堆:左偏树(左偏树:定义沿右子节点往下到叶子的距离为深度,当x的左子节点深度小时交换,维护左子节点深度大的左偏性质)

    #include<cstdio>
    #include<algorithm>
    using namespace std;
    const int maxn=1000010;
    int l[maxn],r[maxn],fa[maxn],d[maxn],a[maxn],n,m;
    bool die[maxn];//0生1死
    int find(int x)
    {return fa[x]==x?x:fa[x]=find(fa[x]);}
    int merge(int x,int y)//返回x和y合并后子树的根
    {
        if(!x)return y;
        if(!y)return x;//遇到一边为空节点则把另一边剩余的子树整颗接上去(返回) 
        if(a[x]>a[y])swap(x,y);//将根节点更小的树放在左边 
        r[x]=merge(r[x],y);//递归合并左树右孩子和右树 
        if(d[l[x]]<d[r[x]])swap(l[x],r[x]);//维护左偏性质 
        d[x]=d[r[x]]+1;//更新节点距离 
        return x;//返回新树根
    } 
    int main()
    {
        scanf("%d",&n);
        for(int i=1;i<=n;i++)scanf("%d",&a[i]);
        for(int i=1;i<=n;i++)fa[i]=i;
        d[0]=-1;//因为后面的空节点都表示为0,因此会多次调用0。 
        scanf("%d",&m);
        for(int i=1;i<=m;i++)
         {
             char c=getchar();
             while(c!='M'&&c!='K')c=getchar();
             if(c=='M')
              {
                  int x,y;
                  scanf("%d%d",&x,&y);
                  if(die[x]||die[y])continue;
                  int p=find(x),q=find(y);
                  if(p!=q)
                   {
                       int t=merge(p,q);//t是新根,可能是fa[x]或fa[y] 
                       fa[p]=fa[q]=t;//p,q的父亲变为新根,其他点父亲均不变 
                   }
              }
             else
              {
                  int x;
                  scanf("%d",&x);
                  if(die[x]){printf("0
    ");continue;}
                  int p=find(x);die[p]=1;
                  printf("%d
    ",a[p]);
                  fa[p]=merge(l[p],r[p]);//返回新根(l[p]或r[p]),令原根的父亲为新根,由于并查集,不需要再修改 
                  fa[fa[p]]=fa[p];//注意改变新根的父亲
                 //为什么不能直接加个if判断新根左右然后修改左右父亲啊?改完交了RE,存疑…… 
              }
         }
        return 0;
    }
    View Code

    可并堆支持整体标记,详见【CodeForces】D. Roads in Yusland

    斜堆:在左偏树的基础上,每次直接暴力交换swap,可以证明复杂度均摊O(n log n),但是单次有可能爆栈。

     【区间和点】BZOJ1828 BZOJ1707

    题意:有一些点和区间的限制,求最多选择区间。

    核心思想:双关键字排序实现扫描线,按关键字顺序不同有两种角度:

    从区间角度出发:按区间右端点排序(消除右点对区间影响),从而当前堆里的区间只考虑向左。

    从点的角度出发:按区间左端点排序(消除点对左区间影响),从而当前堆里的区间只考虑向右。

    【反悔元素】Buy Low Sell High股票买卖 BZOJ1572

    排序,直接选择所有元素然后把反悔元素加入堆中,每次超限就从堆中弹出。

    可以反悔多次就加多个。

    【一种套路】利用题目自带优劣情况,每次只考虑少量最优状态后拓展一些情况入堆。

    求前k优问题:如果是满足每次取出一个元素,然后拓展出少量个元素,并且满足取出的元素不劣于拓展出的元素的问题,都可以用以上套路解决。

    eg.给一个非负序列,输出前k小的子区间和。先把所有[i,i]加入堆,然后取最小拓展[i-1,i]&[i,i+1]入堆,因为本身满足大区间包含小区间,所以一次只需要考虑n个区间。

    eg.超级钢琴

    【线段树】标记的维护技巧和平衡树通用

    推荐:夜深人静写算法(七)[2016 贺岁版] - 线段树

    特点:线段树又称“区间树”,对区间问题有强大的处理能力。

    只要满足可并性(可以从左右子区间O(1)合并信息)和可标记性(区间可以仅根据标记修改参数),就可以使用线段树。(单点操作可以不用可标记性)

    懒标记:打lazy标记的时候顺便把子树的其它参数都修改完毕,方便直接调用。

    访问到有lazy标记的子树时若需要继续往下访问(即该子树区间不完全在规定区间内)就把标记下传给左右子树并修改左右子树的其他参数。

    传递修改时要下传和上传,查询时要下传。

    例题:【BZOJ】2243 [SDOI2011]染色

    技巧:

    1.子树收缩:下传的逆过程,当两棵子树信息相同时合并存放在根节点处,减少访问量。

    2.标记永久化:与顺序无关的标记(满足交换律),如区间叠最小,区间加等。

    特别地,只有单点查询时不需要维护区间信息,都可以标记永久化。

    3.矩阵面积并:一维差分并留下差分标记,一维维护线段树询问和根据差分标记修改。

    http://hzwer.com/879.html

    4.维护幂和:要求查询区间数字x次幂的和(不是和幂),支持区间加值和区间覆盖。

    线段树维护0~x次幂和,区间加值利用二项式定理,例如加y并维护二次幂和:

    Σ(x+y)^2=Σ(x^2+2*x*y+y^2),其中Σx^2就是维护的二次幂和,Σx就是维护的幂和。

    5.区间对一个数取max:无法维护区间信息

    ①单点查询,可以直接标记永久化或者传递修改都可以。

    ②区间查询,在序列满足单调性的前提下转化为区间覆盖。

    例题:【CodeForces】671 C. Ultimate Weirdness of an Array

    6.多标记相互影响时:假设B标记影响A标记,那么做modify(B)时要修改A标记,下传先传B标记,就可以了。(比如乘法把加法也乘了,覆盖把加值变0)

    7.线段树上二分:先根遍历

    ①判断当前区间是否符合(一般用区间最右端点),否则返回r+1

    ②若l=r,返回

    ③依次查询左区间、中间、右区间,查到停止。

    例题:【BZOJ】4293: [PA2015]Siano 线段树上二分

    线段树上倍增:中根遍历(问一个端点L开始往左信息累加达到x的第一个位置)

    ①若l=r,返回(只进入一定有信息的区间,故能到叶子的一定是需要累加的)

    ②若L>=mid,直接访问右子树

    ③先访问左子树,返回累加到的位置y。

    如果y不是左子树最后一位或者右子数第一位不满足, 那么直接返回y

    如果右子树可以整棵都满足,那么直接加。

    否则进入右子树。

    #include<cstdio>
    #include<cstring>
    #include<cctype>
    #include<algorithm>
    using namespace std;
    int read(){
        int s=0,t=1;char c;
        while(!isdigit(c=getchar()))if(c=='-')t=-1;
        do{s=s*10+c-'0';}while(isdigit(c=getchar()));
        return s*t;
    }
    const int maxn=500010;
    int n,m,a[maxn],ll,rr,L,R;
    char s[maxn];
    struct tree{int l,r,L,R;}t[maxn*4];
    void merge(int x,int y,int a,int b,int &p,int &q){
        p=x,q=b;
        if(a>y)p+=a-y;else q+=y-a;
    }
    void up(int k){merge(t[k<<1].L,t[k<<1].R,t[k<<1|1].L,t[k<<1|1].R,t[k].L,t[k].R);}
    void build(int k,int l,int r){
        t[k].l=l;t[k].r=r;
        if(l==r){t[k].L=a[l];t[k].R=!a[l];return;}
        int mid=(l+r)>>1;
        build(k<<1,l,mid);build(k<<1|1,mid+1,r);
        up(k);
    }
    void modify(int k,int x){
        if(t[k].l==t[k].r){t[k].L^=1;t[k].R^=1;return;}
        int mid=(t[k].l+t[k].r)>>1;
        if(x<=mid)modify(k<<1,x);else modify(k<<1|1,x);
        up(k);
    }
    void query(int k,int l,int r){
        if(l<=t[k].l&&t[k].r<=r){merge(ll,rr,t[k].L,t[k].R,ll,rr);return;}
        int mid=(t[k].l+t[k].r)>>1;
        if(l<=mid)query(k<<1,l,r);
        if(r>mid)query(k<<1|1,l,r);
    }
    int findr(int k,int pos,int x){
        if(t[k].l==t[k].r)return merge(L,R,t[k].L,t[k].R,L,R),t[k].l;
        int mid=(t[k].l+t[k].r)>>1;
        if(pos>mid)return findr(k<<1|1,pos,x);else{
            int y=findr(k<<1,pos,x);
            if(L==x)return y;
            int l,r;
            merge(L,R,t[k<<1|1].L,t[k<<1|1].R,l,r);
            if(l>=x)return findr(k<<1|1,pos,x);
            else return L=l,R=r,t[k].r;
        }
    }
    int findl(int k,int pos,int x){
        if(t[k].l==t[k].r)return merge(t[k].L,t[k].R,L,R,L,R),t[k].l;
        int mid=(t[k].l+t[k].r)>>1;
        if(pos<=mid)return findl(k<<1,pos,x);else{
            int y=findl(k<<1|1,pos,x);
            if(R==x)return y;
            int l,r;
            merge(t[k<<1].L,t[k<<1].R,L,R,l,r);
            if(r>=x)return findl(k<<1,pos,x);
            else return L=l,R=r,t[k].l;
        }
    }
    int main(){
        freopen("grancrevasse.in","r",stdin);
        freopen("grancrevasse.in","r",stdin);
        n=read();m=read();
        scanf("%s",s+1);
        for(int i=1;i<=n;i++)a[i]=s[i]-'0';
        build(1,1,n);
        while(m--){
            int k=read();
            if(k==1)modify(1,read());
            else{
                int l=read(),r=read(),x=read();
                ll=0,rr=0;query(1,l,r);L=0;R=0;
                if(x>ll+rr)printf("-1
    ");
                else if(x<=ll)printf("%d
    ",findr(1,l,x));
                else printf("%d
    ",findl(1,r,ll+rr-x+1));
            }
        }
        return 0;
    }
    View Code

    留坑:zkw线段树 统计的力量 

    【树状数组】

    推荐:搞懂树状数组(只要耐心读就能明白了)

    特点:树状数组利用二进制分组规律,主要用于维护动态前缀和。

    树状数组本质是将数字按二进制的1进行分组,每个1统领一部分。

    c[i]只统领i的二进制中最低位的1代表的部分。

    如c[100]统领a[001].a[010].a[011].a[100]

    c[1100]统领a[1001].[1010].[1011].[1100]

    求和时,将一个数字拆成各个1统领的分组求和,如:

    sum(1110)=c(1110)+c(1100)+c(1000)

    c(1110)统领1101-1110(2个数字)

    c(1100)统领1001-1100(4个数字)

    c(1000)统领1-1000(8个数字)

    显然,1110---1100---1000可以通过每次消去最低位的1(获取次低位的1)来推进

    此时就需要lowbit(k)=k&(-k)表示k最低位的1代表的数字,lowbit(1110)=10,lowbit(1100)=100等。

     

    而每个数字(设初始数组或插入或改变本质上都是一样的)对c数组会影响统领它的1,如1010会影响1100.10000

    1010归1100直接统领,1010也就会直接影响1100。

    1100(显然不是归1000统领)归10000直接统领,1100也就会直接影响10000。

    c[1010]改变,影响了c[1100];c[1100]改变,又影响了c[10000]。

    显然这种推进可以用+lowbit(k),得到比当前最低位1上一位的1完成。

    int lowbit(int x){return x&(-x);}
    int query(int x){int ss=0;while(x<=n){ss+=c[x];x-=lowbit(x);}return ss;}
    void modify(int x){while(x<=n){c[x]+=k;x+=lowbit(x);}}
    View Code

     

    应用:

    1.可以O(n log n)地查询前缀最小值(因为不需要可差分性)

    2.可以O(n log n)地寻找前缀和为k的最小位置,也就是可以代替平衡树的排名功能。

    【BZOJ】3173: [Tjoi2013]最长上升子序列(树状数组)

    3.可以线性建树,1~n每个数字对自身+1,再对父亲贡献,具体可以见上面链接。

    4.树状数组求逆序对:离散化后按顺序将对应位置+1,每次ans+=i-getsum(i)

     

    【扫描线】链接

    【并查集】fa[i]=i;

    用于合并集合。用于维护图的连通(支持加边)。

    int getfa(int x)
    {return fa[x]==x?x:(fa[x]=getfa(fa[x]));}
    
    for(int i=1;i<=n;i++)fa[i]=i;
    View Code

    带权并查集要注意父亲顺序问题,即先用原父亲计算距离再更换为新父亲。

    ★<支持删除地查找前驱后继>合并几个空的点和一个满的点,思想是将处理过的合并起来,如花神的游历

    ★<排序+并查集>

    经典套路:找树上所有路径的边最值(对于每条路径,求出路径上的最大边)。

    排序后,按顺序对边两端并起来,这样路径的最值就会自然在最后并,在每次合并时这条边就是两端集合互通的所有路径的最值。

    同时,并查集保证每个点刚好和其它所有点配对一次,这样的套路是:

    【把一部分点和另一部分点配对,然后并起来,重复到只剩一个集合为止,此时保证两两配对完毕】

    eg.Codeforces From Y to Y

    经典套路:利用无后效性,将处理过的点并起来

    MST的kruskal算法就是这样的思想,处理过的边就最优了且两集合可以视为整体,于是并起来。

    还有bzoj安全路径也是,排序后将最小的处理后并起来,下次处理就可以直接跳过(因为不可能更优了,于是不可能处理到已经并起来的点)。

    无用并之,是排序并查集思想的核心,排序就是为了满足扫过的都无用了。

    <倒序>将删边改为加边,从而变成并查集。

    【可持久化权值线段树(主席树)】解决区间权值相关的查询问题

    可持久化原本是指保存历史版本的经典手法——只赋值修改部分,对于线段树而言就是只复制一条链。

    后来这种手法不仅用于保存历史版本,还大量用于可以基于原线段树直接构造线段树的情境,这之中重要的应用就是可持久化权值线段树。

    可持久化权值线段树,一般也称之为主席树,线段树中存放每个权值相关的变量(一般为出现次数和),建树时旧树传递变量作为依据,新树传地址,新树作为被修改的链需要赋初值(或改)。y=++sz;

    不带修改时,第i棵表示1~i的前缀和,第i棵在第i-1棵的基础上建树,差分查询区间,复杂度O(n log n)。

    例题:【BZOJ】3524: [Poi2014]Couriers

    带修改时,树状数组套可持久化权值线段树,第i棵表示树状数组中的第i个,基于本身建树(只建一条链),基于本身修改,复杂度O(n log2n)。

    例题:【BZOJ】1901: Zju2112 Dynamic Rankings

    特别注意,

    1.空间开大

    2.分清tot(权值范围)和n(数组范围)的区别

    一般在结构体中开左右孩子,左右区间直接传参。

    应用:

    通过可持久化很容易取出指定区间的权值线段树,那么一个区间能被解决关键看能否在其权值线段树上询问。

    而且可持久化权值线段树只支持单点修改。

    查询区间不同数字的个数:记录每个数字i上一次出现的位置lasti,维护不带修改可持久化权值线段树,权值为lasti,对于区间找lasti<L的和。

    区间第k小:找到sum<=k的最靠左的权值(位置)。

    树上区间第k小:count on a tree。对于每个点在其父亲的基础上可持久化,然后查询ans=ask(l)+ask(r)-ask(lca(l,r))-ask(fa[lca(l,r)]),这样刚好不重不漏一条链。

    【启发式合并】

    普通的启发式合并就是把size偏小的数据结构依次弹出后插入size较大的。

    这样将n个合并成一个(假设一次合并复杂度为O(size))的均摊复杂度是O(n log n)。

    【线段树合并】

    例题:【BZOJ】4756: [Usaco2017 Jan]Promotion Counting

    用到线段树合并的题目通常有个特点:很容易想出一种DP方法,每个点的状态是一个数组,状态转移需要考虑数组合并。

    然后把数组换成线段树就可以了www。

    merge(x,y)的三步骤:

    1.x和y有空,返回x^y。

    2.叶子结点信息直接合并返回(可能不需要)

    2.左右儿子合并x.l=merge(x.l,y.l) x.r=merge(x.r,y.r)

    3.信息上传x.sum=calc(x.l,x.r)

    显然需要动态开点。

    n棵单链树合并n-1次的复杂度为O(n log n),证明:每次合并等价于消除两棵线段树的交集,n棵单链树总共n log n个结点。

    带标记的线段树合并:

    1.动态开点:下传的时候开新点!因为每次操作至多log n次,所以复杂度正确。

    2.合并:合并的过程切忌下传,因为下传开新点最终会遍历整棵树。

    正确方法是直接连同标记一起合并,记得标记也要一起合并,这样也不需要上传。

    #include<stdio.h>
    #include<string.h>
    #include<stdlib.h>
    #include<algorithm>
    //#include<iostream>
    //#include<assert.h>
    #include<ctime>
    using namespace std;
    
    int n;
    #define maxn 100011
    struct Edge{int to,next;}edge[maxn<<1]; int first[maxn],le=2,val[maxn];
    bool in(int x,int y) {Edge &e=edge[le]; e.to=y; e.next=first[x]; first[x]=le++; return 0;}
    int lisa[maxn],li=0;
    
    int root[maxn];
    struct SMT
    {
        struct Node
        {
            int ls,rs;
            int Min,Max,be,add;
        }a[maxn*40];
        int size,n;
        void clear(int m) {size=0; n=m;}
        void New(int &x) {x=++size; a[x].ls=a[x].rs=0; a[x].Min=0; a[x].Max=0; a[x].be=-1; a[x].add=0;}
        void up(int x)
        {
            a[x].Min=min(a[a[x].ls].Min,a[a[x].rs].Min);
            a[x].Max=max(a[a[x].ls].Max,a[a[x].rs].Max);
        }
        void besingle(int &x,int v)
        {
            if (!x) New(x);
            a[x].Min=a[x].Max=a[x].be=v; a[x].add=0;
        }
        void addsingle(int &x,int v)
        {
            if (!x) New(x);
            a[x].Min+=v; a[x].Max+=v;
            if (a[x].be==-1) a[x].add+=v; else a[x].be+=v;
        }
        void down(int x)
        {
            if (a[x].be!=-1) besingle(a[x].ls,a[x].be),besingle(a[x].rs,a[x].be),a[x].be=-1;
            if (a[x].add) addsingle(a[x].ls,a[x].add),addsingle(a[x].rs,a[x].add),a[x].add=0;
        }
        void combine(int &x,int y,int L,int R)
        {
            if (!x || !y) {x=x^y; return;}
            if (a[y].be!=-1) {addsingle(x,a[y].be); return;}
            if (a[x].be!=-1) {addsingle(y,a[x].be); x=y; return;}
            a[x].Max+=a[y].Max-a[y].add; a[x].Min+=a[y].Min-a[y].add; addsingle(x,a[y].add);
            if (L==R) return;
            int mid=(L+R)>>1;
            combine(a[x].ls,a[y].ls,L,mid);
            combine(a[x].rs,a[y].rs,mid+1,R);
        }
        void combine(int &x,int y) {combine(x,y,1,n);}
        int mo(int &x,int L,int R,int pos,int v){
            if(!x)New(x);
            if(L==R){besingle(x,v); return R;}
            int mid=(L+R)>>1, y;
            down(x);
            if(pos<=L && a[x].Max<=v) {besingle(x,v); return R;}
            if(pos>mid) y=mo(a[x].rs, mid+1, R, pos, v);else{
                y=mo(a[x].ls, L, mid, pos, v);
                if(y!=mid || a[a[x].rs].Min>=v) return up(x), y;
                if(a[x].Max<=v) besingle(a[x].rs, v),y=R;
                else y=mo(a[x].rs, mid+1, R, pos, v);
            }
            up(x); return y;
        }
        void modify(int &rt,int pos,int v) {
        mo(rt,1,n,pos,v);}
        int query(int &x,int L,int R,int pos)
        {
            if (!x) New(x);
            if (L==R) return a[x].Min;
            down(x);
            int mid=(L+R)>>1;
            if (pos<=mid) return query(a[x].ls,L,mid,pos);
            else return query(a[x].rs,mid+1,R,pos);
        }
        int query(int &rt,int pos) {return query(rt,1,n,pos);}
    }t;
    
    void dfs(int x,int dep)
    {
        int v=1;
        for (int i=first[x];i;i=edge[i].next)
        {
            Edge &e=edge[i]; dfs(e.to,dep+1);
            v+=t.query(root[e.to],val[x]-1);
            t.combine(root[x],root[e.to]);
        }
        t.modify(root[x],val[x],v);
    }
    
    int main()
    {
        //freopen("tree.in","r",stdin);
        //freopen("tree.out","w",stdout);
        int o1=clock();
        scanf("%d",&n);
        for (int i=1,x;i<=n;i++) scanf("%d%d",&val[i],&x),(x && in(x,i)),lisa[++li]=val[i];
        lisa[++li]=0; sort(lisa+1,lisa+1+li); li=unique(lisa+1,lisa+1+li)-lisa-1;
        for (int i=1;i<=n;i++) val[i]=lower_bound(lisa+1,lisa+1+li,val[i])-lisa;
        
        t.clear(li);
        dfs(1,1);
        printf("%d
    ",t.query(root[1],li));
        int o2=clock();
        printf("time=%d
    ",o2-o1);
        return 0;
    }
    View Code

    【CDQ分治】时间分治算法

    论文:从《Cash》谈一类分治算法的应用

    推荐课件:(Day1)cdq分治相关

    CDQ分治适用于 不单调的斜率优化 和 在偏序问题中代替一维数据结构。

    CDQ分治的核心思想是对时间分治,每次只统计时间维左边的修改对时间维右边的询问的影响。CDQ分治的结构类似线段树,将对询问x有影响的修改分成log n次处理(祖先),也就是每个询问和修改只在其LCA处处理,这样复杂度O(n log n)。

    所以CDQ分治解决问题仅限于:离线,修改影响可拆分,单点修改,偏序询问。

    三维偏序问题:t维CDQ分治(离散化),x维排序扫描线,y维树状数组。

    按操作顺序分治,每次只计算时间维左区间的修改对时间维右区间的影响,递归进行,一般有以下步骤:

    ★先全部按x,y,t顺序从小到大排序。

    ①按x维顺序计算t左区间的修改和t右区间的影响。

    ②消除t左区间的修改。

    ③将数组按t维分成左区间和右区间(子区间内仍为x维顺序)。

    ④递归处理两个子区间。

    ★经典例题:【BZOJ】3262: 陌上花开

    矩阵和点:只能在[矩阵修改单点查询]和[单点修改矩阵查询]中二选一。

    ①前缀和表示单点信息,矩阵修改转化为四个单点修改,单点查询转化为前缀和查询。

    ②单点表示单点信息,单点修改,矩阵差分成四次查询前缀和。

    ★经典例题:【BZOJ】1176: [Balkan2007]Mokia

    CDQ分治优化DP:辅助斜率优化

    当$j<k,x_j<x_k,ans_k>ans_j$时,存在两种方程:

    $$frac{y_j-y_k}{x_j-x_k}>k_i$$

    此时$k_i$从大到小排序,维护斜率从大到小的上凸包

    $$frac{y_j-y_k}{x_j-x_k}<k_i$$

    此时$k_i$从小到大排序,维护斜率从小到大的下凸包

    列出决策比较式(y[j]-y[k])/(x[j]-x[k])>k[i],如果不满足x[]和k[]均单调,就使用CDQ分治优化。

    第 i 阶段决策实际上是在前i-1个点的上凸包中找到斜率最接近的k[i]的边,然后将第i个点加入维护动态上凸包(平衡树)。

    这实际上是二维偏序,阶段一维默认排序,斜率一维用平衡树动态维护。

    现在我们考虑用CDQ分治离线代替平衡树,需要改变之前CDQ分治的写法。

    初始按斜率$k_i$排序,分配区间按编号$i$分治,退出时按横坐标$x_i$排序,这样每次处理左子区间按$x_i$构造凸包,右子区间按$k_i$顺序决策。

    ★按斜率排序(ki),对阶段分治,每次:

    1.按阶段分配左右子区间。

    2.递归分治左子区间

    3.左子区间用栈构造凸包(已按x[]排序),右子区间顺序决策(已按k[]排序)

    4.递归分治右子区间

    5.按x[]归并排序整个区间

    阶段分配左右子区间,左子区间按x[]排序,右子区间按k[]排序,最终按x[]排序。

    ★经典例题:【BZOJ】1492: [NOI2007]货币兑换Cash

    【Link-Cut Tree】

    Link-Cut Tree简称lct,是解决动态树问题的常用数据结构。

    lct=树链剖分+splay。

    一、lct和树链剖分一样将树分成若干重链,对每条重链维护一棵按深度排序的splay。

    二、轻边x-y(y深度大)表现为y所在spaly的根的父亲设为x,但是x不记y这个儿子(因为lct的唯一核心操作access是从下往上,所以不用担心父亲变更的问题)

    三、一棵splay只能有一个父亲,记为根的父亲(可以随时替换根,父亲不变),表示这棵splay的最左端节点和根的父亲之间有一条轻边。

    每颗splay的根没有意义,而最左端节点是重链最小深度的点,最右端节点是重链最大深度的点。

    lct的根是变动的,是主链splay(深度最小的splay)的最左端节点。

    <splay(x)>将x旋转到所在splay的根,旋转前先整链下传,所以rotate就不需要下传了。

    <isroot(x)>判断x是否splay的根,只须判断x的父亲的儿子是否为x。(还有!x的情况也是根)

    <access(x)>将根到x的路径变成一条重链。方法是每跳到一条重链的位置x,将x旋转到根后,右节点设为上一棵splay的根,这样根到x的路径就会接成一棵完整的splay。

    而每次x和原来的右节点断开后,其父亲仍指向x而x不指向它,就变成了一条轻边。结束后x为主链splay的最右端结点,一般后面加splay(x)来定位到根(切记不能在access里最后来个splay,因为x已经变了)。

    void access(int x)
    {
            int y=0;
            while(x)
            {
                splay(x);
                t[x][1]=y;
                y=x;x=f[x];
            }       
    }
    access(x)

    <reserve>access(x);splay(x);g[x]^=1;

    将x变成主链splay的根,翻转后x就是主链的根,即所在树lct的根。

    <link>reserve(x);fa[x]=y;

    将x变成所在lct的根,然后作为y的轻儿子连入。

    <cut>reserve(x);access(y);splay(y);t[y][0]=f[x]=0;

    reserve(x)使x成为该树根节点,access(y)使y接到主链上,splay(y)使y成为splay的根,此时x是y的左子节点(原树根),断连即可。

    <findroot>将x变成主链splay的根之后,不断往左就能找到,一般用于判断两点是否连通(在同一棵树上)。

    ★例题:【BZOJ】2049: [Sdoi2008]Cave 洞穴勘测 LCT

    一道神奇的题:[WerKeyTom_FTD的模拟赛]Sone0,下面几点比较有趣:

    1.在原树形态的基础上,换根求新子树:分类讨论新根root和查询位置x的位置即可,不用真正换根。(重组病毒)

    2.开方到区间全1即可返回。(花神游历各国)

    3.动态树的链splay轮换:分别建形态splay和权值splay,改权只改权,改形一起改。

    4.动态维护子树大小:sz记录每个节点的虚子树节点总数。

    【点分治】

    每次找到一个区域的重心,以重心为根划分成若干子区域。点分治中每个点都会作为重心一次,一条路径只会作为跨越重心的路径被访问一次,因此主要用于处理树上所有路径的询问问题。

    这样至多log n层,所以总复杂度O(n log n)。需要特别注意点分治的常数很大。还需要注意点分治过程中的所有操作必须和点数相关(不能和权值相关),否则复杂度不对。

    三种统计方法:

    1.加所有子树信息,依次删除一棵子树进去统计后再加回。(4)

    特点:最万能的方法,复杂度也最高,要求信息支持删除。

    例题:【CodeForces】914 E. Palindromes in a Tree 点分治

    2.加所有树的信息,然后进入每棵子树统计,然后再进入每棵子树删除来自同一棵子树的路径。(3)

    特点:要求答案支持删除。

    3.每棵子树 i 和前1~i-1棵子树的信息合并后加入。(2)

    特点:路径只能单向统计。

    例题:【BZOJ】2599: [IOI2011]Race 点分治

    4.将所有子树的信息取出来单独处理。(1)

    特点:适用于特殊的题目,例如排序双指针(n log2n),但是注意取出来后复杂度依然要保证O(点数)。

    例题:【BZOJ】1468: Tree(POJ1741) 点分治

    【dsu on tree】

    例题:

    未完待续——

    【点分治】

    其它:树的重心及动态维护

    《分治算法在树的路径问题中的应用》

    浅谈对点分治的一些理解

    关于点分治的理解

    例题一:树中点对

    【树套树】

    一个数据结构里附加了另一个数据结构的根节点,如树状数组套线段树、线段数套线段数,都是O(log2n)。

    动态逆序对:一维序号一维大小,一行为一颗线段树,列为树状数组。

  • 相关阅读:
    多态
    封装
    继承
    面向对象
    2.机器学习相关数学基础
    作业1 机器学习概述
    作业15 语法制导的语义翻译
    作业14 算符优先分析
    作业13 自下而上语法分析
    作业12 实验二 递归下降语法分析
  • 原文地址:https://www.cnblogs.com/onioncyc/p/6617685.html
Copyright © 2020-2023  润新知