【算法】数据结构

【平衡树】★平衡树 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)。

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

原文地址:https://www.cnblogs.com/onioncyc/p/6617685.html