点分治+CDQ分治+整体二分全纪录

点分治

点分治讲解

解决树上路径问题

经典例题:点分治(长度小于m的路径计数)
经典例题:点分治(聪聪可可)
经典例题:点分治(多个定值路径计数)
经典例题:点分治(采药)
经典例题:点分治+ST表+优先队列
经典例题:点分治+FFT+概率期望
经典例题:点分治+01分数规划
舒老师推荐点分治(难)

注意:点分治的常数比较大
每个点都会作为重心,然而每个点的遍历次数不止一次

点分治有两种写法:

  • 无脑计算整棵子树中的路径,减去路径端点在同一棵子树中的情况
    (主要用于路径计数)

  • 依次遍历子树,记录当前子树和之前子树之间的路径
    (主要用于复杂的情况,或者说是不容易去重的情况:dp,采药人,点分治+ST+优先队列)

点分治中比较重要的就是 findroot f i n d r o o t
附赠长度 <=m <= m 的路径计数,长度 =m = m 的路径计数

int size[N],f[N],root,sz;
bool vis[N];

void findroot(int now,int fa) {
    f[now]=0;
    size[now]=1;
    for (int i=st[now];i;i=way[i].nxt)
        if (way[i].y!=fa&&!vis[way[i].y]) {
            findroot(way[i].y,now);
            size[now]+=size[way[i].y];
            f[now]=max(f[now],size[way[i].y]);
        }
    f[now]=max(f[now],sz-size[now]);
    if (f[now]int x) {
    root=0; f[0]=N;
    sz=size[x];            //当前子树的大小 
}

长度 <=m <= m 的路径计数
注意 i++,j i + + , j − −

int d[N],cnt,deep[N];

void dfs(int now,int fa) {
    d[++cnt]=deep[now];
    for (int i=st[now];i;i=way[i].nxt)
        if (way[i].y!=fa&&!vis[way[i].y]) {
            deep[way[i].y]=deep[now]+way[i].v;
            dfs(way[i].y,now);
        }
} 

int cal(int now,int dep,int opt) {
    cnt=0;
    deep[now]=dep;
    dfs(now,0);
    sort(d+1,d+1+cnt); 
    int i=1,j=cnt,ans=0;
    while (iif (d[i]+d[j]<=m) ans+=(j-i),i++;
        else j--;
    }
    return ans*opt;
}

长度 =m = m 的路径计数
注意 l++,r l + + , r − − if (l>=r) break;

int cal(int now,int dep,int opt) {
    cnt=0;
    deep[now]=dep;
    dfs(now,0);

    sort(d+1,d+1+cnt);
    int l=1,r=cnt,ans=0;
    while (lwhile (d[l]+d[r]>m&&lif (l>=r) break;
        int rr=r;
        while (d[l]+d[r]==m) {
            ans++; 
            r--;
            if (l>=r) break; 
        }
        r=rr;
        l++;
        if (l>=r) break; 
    }

    return ans*opt;
}

CDQ分治

CDQ分治简单讲解

CDQ分治是我们处理各类问题的重要武器
优势在于可以顶替复杂的高级数据结构,而且常数比较小
缺点在于必须离线操作

经典例题:CDQ(三位偏序入门)
经典例题:CDQ(矩阵和)
经典例题:CDQ(最近点对||KDtree)
经典例题:CDQ+dp(纸箱)
经典例题:CDQ+dp
经典例题:CDQ(动态逆序对)
经典例题:CDQ+dp+FFT(一)
经典例题:CDQ+dp+FFT(二)
经典例题:CDQ+dp(维护凸壳【难)

细节一

CDQ一句话总结:左区间影响右区间
CDQ分治有两种写法:

//----法一-----
CDQ(L,M)
CDQ(M+1,R)

solve

//----法二----
CDQ(L,M)

solve

CDQ(M+1,R)

感谢舒老师为我解答了困惑
点分治+CDQ分治+整体二分全纪录_第1张图片
仔细想了一下
例如陌上花开这种题,只需要统计,我们就可以归并
然而如果是dp需要CDQ分治辅助,我们在处理 [mid+1,r] [ m i d + 1 , r ] 的时候需要 [l,mid] [ l , m i d ] f f 值已经维护好了
所以我们先要 CDQ(l,mid) C D Q ( l , m i d ) ,处理好这一部分之后
把相应的贡献转移到 f[mid+1] f [ m i d + 1 ] 上,再 CDQ(mid+1,r) C D Q ( m i d + 1 , r )

CDQ分治的变化还是很多的,只能给出一部分情况的总结

//先归并后处理 

struct node{
    a,b,c,num;
}p[N],q[N];
//a 第一维
//b 需要归并的第二维 
//c 树状数组 
//num 按a排序时的位置 

void CDQ(int l,int r) {
    if (l==r) return;
    int mid=(l+r)>>1;
    CDQ(l,mid);
    CDQ(mid+1,r);

    int t1=l,t2=mid+1;
    for (int i=l;i<=r;i++)
        if ((t1<=mid&&p[t1].b<=p[t2].b)||t2>r)
            q[i]=p[t1++];
        else q[i]=p[t2++];

    for (int i=l;i<=r;i++) {
        p[i]=q[i];
        if (p[i].num<=mid) ...   //加值 
        else ...                 //维护ans 
    } 

    for (int i=l;i<=r;i++)
        if (p[i].num<=mid) ...   //清空 
}

//边归并边处理

struct node{
    a,b,c,type;
}p[N],q[N];

void CDQ(int l,int r) {
    if (l==r) return;
    int mid=(l+r)>>1;
    CDQ(l,mid);
    CDQ(mid+1,r);

    sort(p+l,p+mid+1,cmp);      //sort会比归并慢  按照第二维排序 
    sort(p+mid+1,p+r+1,cmp);
    //我之所以这么做是因为我们没有定义num,不能把两部分打乱 

    int t1=l,t2=mid+1,last=0;
    while (t2<=r) {
        while (t1<=mid&&p[t1].type!=1) t1++;    //左区间找修改
        while (t2<=r&&p[t2].type!=2) t2++;      //右区间找询问

        if (t1<=mid&&p[t1].b<=p[t2].b) {
            ...
            last=t1;                             //记录处理到的修改 
        } 
        else if (t2<=r) {
            ...
        }
    } 

    for (int i=l;i<=last;i++)
        if (p[i].type==1) ...                     //清除影响 
}

整体二分

感觉整体二分和CDQ分治比较像,所以就放到一起啦

整体二分简单讲解

所谓整体二分,需要数据结构题满足以下性质:
1. 询问的答案具有可二分性
2. 修改对判定答案的贡献相对独立,修改之间互不影响效果
3. 修改如果对判定答案有贡献,则贡献为一确定的与判定标准无关的值
4. 贡献满足交换律,结合律,具有可加性
5. 题目允许离线操作

询问的答案有可二分性质显然是前提,我们发现,因为修改对判定标准的贡献相对独立,且贡献的值(如果有的话)与判定标准无关,所以如果我们已经计算过某一些修改对询问的贡献,那么这个贡献永远不会改变,我们没有必要当判定标准改变时再次计算这部分修改的贡献,只要记录下当前的总贡献,再进一步二分时,直接加上新的贡献即可
这样的话,我们发现,处理的复杂度可以不再与序列总长度直接相关了,而可能只与当前待处理序列的长度相关

经典例题:整体二分(区间第k大)
经典例题:整体二分(区间第k大+单点修改)
经典例题:整体二分+线段树(区间第k大+区间添加)
经典例题:整体二分(二维矩阵第k大+奇技淫巧)

细节一

我们需要把题目中的信息都转化为修改信息或者询问信息
(初始信息转化为添加操作,修改信息转化为一个删除一个添加)

细节二

有的时候我们需要把修改和查询一起二分
例题

举个简单的例子:求区间第 k k
二分答案 M M
我们一般习惯 O(n) O ( n ) 扫一遍所有的操作,遇到修改就修改,遇到询问就询问

不过这样做有一个前提:对于每个询问,能够影响到ta的修改一定在ta之前就处理过了

显然值 <=M <= M 的修改需要处理,并且分到左区间 k<= k <= “ 当前区间内有的元素个数 ” 的询问分到左区间
其余的操作分到右区间

这里又要注意啦,分到右区间的询问一定会受左区间操作的影响
但是已经分到左区间的修改操作在继续二分的过程中是不会再在右区间出现了

所以这种情况下,对于分到右区间的询问我们需要记录下左区间操作对ta的影响
(在询问区间第 k k 大的题目中,就是: k=M k − = 值 小 于 M 的 元 素 个 数

有的时候我们不需要把修改二分,只需要一个变量记录处理到了哪个操作即可
例题

这种方法用于修改繁琐的情况
二分答案 M M
移动修改指针,使得只有小于M的操作处理了
按照 k k 和元素个数的关系,划分区间

不过这里要注意
因为我们不二分操作,所以对于右区间的询问我们保持原样

细节三

整体二分多和树状数组结合(例如记录区间内有多少小于M的元素)
但是不要忘了树状数组只支持两套操作(不能混用)

  • 单点修改+区间查询

  • 区间修改+单点查询

如果我们遇到了需要区间加并且区间查询的操作,可以考虑用线段树维护

细节四

流程:

if (l>r) return;
if (L==R) 记录答案

处理操作

最后不要忘了依次消除影响(不能无脑清零,复杂度会降低)

solve(L,M)
solve(M+1,R)

(同时二分修改和询问)

const int INF=1e9+7;
const int N=200010;
struct node{
    int x,y,k,id,type;
};
node q[N],q1[N],q2[N];
int n,m,tot,ans[N],mn,mx,c[N];

//在位置上change

void add(int x,int z) {for (int i=x;i<=n;i+=(i&(-i))) c[i]+=z;}
int ask(int x) {
    int ans=0;
    for (int i=x;i>0;i-=(i&(-i))) ans+=c[i];
    return ans;
}

//对于每个询问,能够影响到ta的修改一定在ta之前就处理过了 

void solve(int l,int r,int L,int R) {
    if (l>r) return;
    if (L==R) {
        for (int i=l;i<=r;i++)
            if (q[i].type==2)
                ans[q[i].id]=L;
        return;
    }

    int M=(L+R)>>1;
    int t1=0,t2=0;
    for (int i=l;i<=r;i++) 
        if (q[i].type==1) {
            if (q[i].y<=M) {
                add(q[i].x,1);
                q1[++t1]=q[i];
            }
            else q2[++t2]=q[i];
        }
        else {
            int sum=ask(q[i].y)-ask(q[i].x-1);
            if (sum>=q[i].k)
                q1[++t1]=q[i];
            else {
                q[i].k-=sum;            //记录影响 
                q2[++t2]=q[i];
            }
        }

    for (int i=1;i<=t1;i++) 
        if (q1[i].type==1) add(q1[i].x,-1);
    for (int i=1;i<=t1;i++) q[l+i-1]=q1[i];
    for (int i=1;i<=t2;i++) q[l+t1+i-1]=q2[i];

    solve(l,l+t1-1,L,M);
    solve(l+t1,r,M+1,R);
}

int main()
{
    tot=0; 
    mn=INF,mx=-INF;
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++) {        //修改 
        tot++;
        scanf("%d",&q[tot].y);
        q[tot].x=i; q[tot].type=1;
        mn=min(mn,q[tot].y);
        mx=max(mx,q[tot].y);
    }
    for (int i=1;i<=m;i++) {
        tot++;
        scanf("%d%d%d",&q[tot].x,&q[tot].y,&q[tot].k);
        q[tot].id=i; q[tot].type=2;
    }
    solve(1,tot,mn,mx);
    for (int i=1;i<=m;i++) printf("%d\n",ans[i]);
    return 0;
}

(二分修改)

const int INF=1e9+7;
const int N=200010;
struct node{
    int x,y,k,id;
}q[N],q1[N],q2[N],a[N];
int n,m,T,mn,mx;
int cnt_a,cnt_q,ans[N],c[N];

int cmp(const node &a,const node &b) {
    return a.yy;
}

void add(int x,int z) {for (int i=x;i<=n;i+=(i&(-i))) c[i]+=z;}
int ask(int x) {
    int ans=0;
    for (int i=x;i>0;i-=(i&(-i))) ans+=c[i];
    return ans;
}

void solve(int l,int r,int L,int R) {
    if (l>r) return;
    if (L==R) {
        for (int i=l;i<=r;i++)
            ans[q[i].id]=L;
        return;
    }

    int M=(L+R)>>1;
    while (a[T+1].y<=M&&Tx,1);
    while (a[T].y>M&&T>=1) add(a[T].x,-1),T--;

    int t1=0,t2=0;
    for (int i=l;i<=r;i++) {
        int sum=ask(q[i].y)-ask(q[i].x-1);
        if (q[i].k<=sum) q1[++t1]=q[i];
        else q2[++t2]=q[i];
    }

    for (int i=1;i<=t1;i++) q[l+i-1]=q1[i];
    for (int i=1;i<=t2;i++) q[l+t1+i-1]=q2[i];

    solve(l,l+t1-1,L,M);
    solve(l+t1,r,M+1,R);
}

int main()
{
    cnt_a=cnt_q=0; 
    mn=INF,mx=-INF;
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++) {        //修改 
        scanf("%d",&a[i].y);
        a[i].x=i; 
        mn=min(mn,a[i].y);
        mx=max(mx,a[i].y);
    }
    for (int i=1;i<=m;i++) {
        scanf("%d%d%d",&q[i].x,&q[i].y,&q[i].k);
        q[i].id=i;
    }
    sort(a+1,a+1+n,cmp);
    T=0;
    solve(1,m,mn,mx);
    for (int i=1;i<=m;i++) printf("%d\n",ans[i]);
    return 0;
} 

你可能感兴趣的:(全纪录系列)