zoukankan      html  css  js  c++  java
  • 点分治(学习笔记)

    淀粉质点分治最经典最基本的应用就是在一棵树上,对具有某些限定条件的路径静态地进行统计(比如,求一棵树上,有多少个点对满足距离小于等于k;或者一棵树上,距离为k的点对是否存在)

    点分治的题目中,树一般都是无根树,所以我们要自己找一个根,这个根不能rand,根的选择会很大地影响时间效率.它只有是树的重心(树的重心:找到一个点,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心,删去重心后,生成的多棵树尽可能平衡)时,才能保证点分治的时间效率是我们希望的nlogn.

    举个例子,当一棵有n个节点的树是一条链时,如果我们指定的根在链的端点,则它要递归访问到叶节点(链的另一个端点)的时间是O(n),而如果我们指定的根在链的正中间,则它要递归访问到叶节点的时间是O(logn).根据上面树的重心的定义,这条链的正中间的点就是这棵树的重心.

    void get_root(int u,int father){
        size[u]=1;f[u]=0;
    //size[u]记录u的所有子树的节点个数之和
    //通俗地讲就是u下面有多少个点
    //初始化size[u]=1,把u点自己也算进去了.
    //f[u]表示以u为根时,其最大的子树中的节点个数
        for(int i=head[u];i;i=nxt[i]){
    		int v=to[i];
    		if(v==father||visit[v])continue;
    //visit[]标记该点是否作为根节点处理过
    //(在第一次找根,即找整棵树的根时用不上)
    		get_root(v,u);//递归下去
    		size[u]+=size[v];
    //v是u的一个子节点,以v为根的树就是u的子树
    		f[u]=max(f[u],size[v]);
    //根据f数组的定义,这个还是很好理解的
        }
        f[u]=max(f[u],sum-size[u]);
    //这里是最难理解的,它用到了容斥原理(可以不管)
    //对于当前处理的节点u,它一定是从它的父节点递归来的
    //sum表示当前递归到的这棵树的大小
    //根据上面的代码,
    //size[u]只记录了以u的子节点为根的子树大小
    //sum-size[u]就表示以u的父节点为根的子树的大小
    //因为这是一棵无根树,我们现在把u当做根
    //则它的父节点就成了它的子节点
        if(f[u]<f[root])root=u;
    //找重心,也就是找到一个root,满足f[root]最小
    }
    

    继续,对于以root为根的一棵树(假设现在我们已经找到了根root),树上的路径只有两类:

    1、路径两端点在root的不同子树中,即经过根节点root

    2、路径两端点在root的一棵子树中,即不经过根节点root

    而根据分治的思想,对于case2,我们又可以把root的每棵子树作为子问题,递归下去处理就行了.

    所以我们只要求解case1,直接结合模板题来看代码.

    传送门

    题意:给定一棵有n个点的树,询问树上距离为k的点对是否存在.

    int n,m,tot,root,sum,cnt;
    //n个点,m个询问(k),tot用于存图,root当前找到的根
    //sum当前递归到的这棵树的大小,cnt不好直接解释
    int query[105];//离线存下m个询问
    int pd[105];//判断当前询问的结果,1为存在,0为不存在
    int size[10005];
    //size[i]记录i下面有多少个点
    int f[10005];
    //f[i]表示以i为根节点时,最大子树的大小(节点个数)
    int visit[10005];
    //visit[i]判断该点是否作为根节点处理过
    int dis[10005];
    //dis[i]表示i到根节点的距离
    //(不是深度,因为树上每条边都有权值)
    int have_dis[1000005];//看代码注释会懂
    int pd_dis[1000005];
    //pd_dis[i]表示到根距离为i的路径是否存在(相当于桶)
    //桶为什么开1000005,其实就是随便开的.
    int q[1000005];//辅助数组,等下代码会有注释
    int head[10005],nxt[20005],to[20005],w[20005];
    //这几个数组用于存图
    void add(int a,int b,int c){
        nxt[++tot]=head[a];
        head[a]=tot;
        to[tot]=b;
        w[tot]=c;
    }//存图
    void get_root(int u,int father){
        size[u]=1;f[u]=0;
        for(int i=head[u];i;i=nxt[i]){
    		int v=to[i];
    		if(v==father||visit[v])continue;
    		get_root(v,u);
    		size[u]+=size[v];
    		f[u]=max(f[u],size[v]);
        }
        f[u]=max(f[u],sum-size[u]);
        if(f[u]<f[root])root=u;
    }//上面解释了
    void get_dis(int u,int father){
        have_dis[++cnt]=dis[u];
    //以当前节点为根时,
    //它的子树中到它的第cnt个距离是dis[u],
    //其实就是把它的子树中,存在的所有到它的距离都存下来
    //以便于我们下面的查询操作
        for(int i=head[u];i;i=nxt[i]){
    		int v=to[i];
    		if(v==father||visit[v])continue;
    		dis[v]=dis[u]+w[i];
    		get_dis(v,u);
        }
    }//求have_dis[]数组
    void work(int u){//处理以u为根时它的子树
        int num=0;/与q数组一起起辅助作用,看下面注释
        for(int i=head[u];i;i=nxt[i]){
    		int v=to[i];
    		if(visit[v])continue;
    		cnt=0;dis[v]=w[i];
    //每次get_dis前cnt要记得清零重置
    //因为u是根,v是根的子节点,所以可以直接dis[v]=w[i]
    		get_dis(v,u);
    		for(int j=1;j<=cnt;j++)//枚举出所有距离
    	    	for(int k=1;k<=m;k++){//枚举询问
    				if(pd[k])continue;//小优化
    				if(query[k]>=have_dis[j])
    		    		if(pd_dis[query[k]-have_dis[j]])pd[k]=1;
    //因为我们处理的是第一种路径,即经过根节点
    //所以我们枚举 其中一个点到根节点的距离have_dis[j]
    //那么如果要对答案产生贡献,则
    //另一个点到根节点的距离是query[k]-have_dis[j]
    //然后判断这个距离是否存在,如果存在则产生了贡献.
    //因为当前这棵子树对pd_dis[]产生的贡献,下面才更新
    //所以pd_dis[]中的距离都是别的子树上的点到根节点的距离
    //所以这种写法不用考虑去重了
    	    }
    		for(int j=1;j<=cnt;j++){
    	    	q[++num]=have_dis[j];//看下面注释
    	    	pd_dis[have_dis[j]]=1;
    		}
        }
        for(int i=1;i<=num;i++)pd_dis[q[i]]=0;
    //用memset清空会超时,如果直接1-1000005(数组大小)清空也会超时
    //have_dis数组反正会被重新覆盖掉,没必要清空
    //所以借助数组q来清空.
    }
    void solve(int u){
        visit[u]=1;pd_dis[0]=1;
    //标记u点作为根节点处理过了
    //pd_dis[0]=1表示到当前根u距离为0的点存在,即它本身
    //因为可能存在根节点到某个点的距离恰好为k的情况
        work(u);//处理u的子树
        for(int i=head[u];i;i=nxt[i]){
    //对u的子树分治处理
    		int v=to[i];
    		if(visit[v])continue;
    //和第一次对整棵树处理的操作都一样
    		sum=size[v];root=0;f[root]=sum;
    		get_root(v,0);solve(v);
        }
    }
    int main(){
        n=read();m=read();
        for(int i=1;i<n;i++){
    		int a=read(),b=read(),c=read();
    		add(a,b,c);add(b,a,c);
        }//注意无根树的边是无向边
        for(int i=1;i<=m;i++)query[i]=read();
    //把所有询问离线
        f[root]=sum=n;
    //此时root是0,根据定义应该好理解.
        get_root(1,root);//找到整棵树的重心
        solve(root);//从根开始对整棵树点分治
        for(int i=1;i<=m;i++){
    		if(pd[i])puts("AYE");//如果存在
    		else puts("NAY");
        }
        return 0;
    }
    
    

    传送门

    题意:给你一棵树,求有多少点对它们两者间的距离小于等于K?

    分析:上题是等于k的点对是否存在?本题是小于等于k的点对的个数.总体的框架,即模板还是不变的.搞懂了上题,这题就很容易懂了.就只讲一下不同的地方了.其实只有work函数和solve函数的一点点不同.

    int work(int u,int dist){
        dis[u]=dist;
        cnt=0;get_dis(u,0);
        sort(have_dis+1,have_dis+1+cnt);
        int l=1,r=cnt,Ans=0;
        while(l<r){//此时序列具有单调性
    		if(have_dis[l]+have_dis[r]<=k)Ans+=r-l,l++;
    		else r--;
        }
        return Ans;
    }
    void solve(int u){
        visit[u]=1;
        ans+=work(u,0);
    //当前处理的这棵树的根节点是u
    //work(u,0)计算经过根节点u的合法路径的点对个数
        for(int i=head[u];i;i=nxt[i]){
    		int v=to[i];
    		if(visit[v])continue;
    		ans-=work(v,w[i]);//去重
    //为什么要去重呢?因为上面我们ans+=work(u,0)时,
    //考虑这样一种情况,就是两个点x,y在u的同一棵子树vi上
    //但是dis[x]+dis[y]<=k,对于这种情况我们统计了一次
    //而这种情况在以u的子节点vi为根时又会被算一次
    		sum=size[v];root=0;f[root]=sum;
    		get_root(v,0);solve(v);
        }
    }
    
    

    传送门

    题意:题意:给你一棵树,求有多少点对(可以是同一个点)它们两者间的距离是3的倍数?(注意:0也是3的倍数)

    我们之前两道题的have_dis数组存的是到根节点的距离,本题中我们让它存到根节点的距离mod 3后分别为0,1,2的个数.每换了一个根节点,就要清空.

    对于每个根节点,它对答案的贡献都是0的个数乘0的个数+2乘1的个数乘2的个数.

    为什么是这个式子呢?其实我们是在n个点中选1个点,然后又在n个点中再选一个点(因为可以是同一个点),所以总方案有(n^2)种.两次都选0的话,我们是在一个集合中选两个数,没有先后之分,直接乘法原理.一次选1,一次选2的话,我们是分别在不同的集合中选一个数,是有先后之分的,所以乘法原理之后还要乘上2.

    本题卡常了,可能是码风太丑,吸了一口氧.

    inline void get_dis(int u,int father){
        have_dis[dis[u]%3]++;//不同之处
        for(rg int i=head[u];i;i=nxt[i]){
    		int v=to[i];
    		if(visit[v]||v==father)continue;
    		dis[v]=dis[u]+w[i];
    		get_dis(v,u);
        }
    }
    inline int work(int u,int dist){
        have_dis[0]=have_dis[1]=have_dis[2]=0;
    //记得初始化为0
        dis[u]=dist;get_dis(u,0);
        return have_dis[0]*have_dis[0]+2*have_dis[1]*have_dis[2];
    }
    inline void solve(int u){
        visit[u]=1;
        ans+=work(u,0);
        for(rg int i=head[u];i;i=nxt[i]){
    		int v=to[i];
    		if(visit[v])continue;
    		ans-=work(v,w[i]);
    		sum=size[v];root=0;f[root]=sum;
    		get_root(v,0);solve(v);
        }
    }
    int main(){
        n=read();
        for(rg int i=1;i<n;i++){
    		rg int a,b,c;
    		a=read();b=read();c=read();
    		add(a,b,c);add(b,a,c);
        }
        sum=f[root]=n;
        get_root(1,0);
        solve(root);
    //注意一下输出格式,题目要求输出概率
        int ppx=__gcd(ans,fm);
        printf("%d/%d
    ",ans/ppx,fm/ppx);
        return 0;
    }
    
    
  • 相关阅读:
    Expedition---POJ
    LIS的优化算法O(n log n)
    Super Jumping! Jumping! Jumping! ---HDU
    数据库连接判断
    android stuido控件
    sql查询语句
    c# 字符串操作
    windows操作
    C# sql操作
    datagridview
  • 原文地址:https://www.cnblogs.com/PPXppx/p/10386680.html
Copyright © 2011-2022 走看看