• 点分治


    点分治

    1. 1 算法概述

    • 点分治,是一种针对可带权树上简单路径统计问题的算法。本质上是一种带优化的暴力,带上一点容斥的感觉。
    • 注意对于树上路径,并不要求这棵树有根,即我们只需要对无根树进行统计。接下来请把无根树这一关键点牢记于心。

    1.2 问题引入

    • 给定一棵树,树上的边有权值,给定一个阈值k,请统计这棵树上总长度小于等于k的路径个数。路径长度为路径路径上所有边的权值和。POJ 1741

    1.3 问题分析

    • 方法一:dfs一遍求出任一点到根的距离,枚举每一条路径u~v,通过LCA(u,v),求出路径的长度。时间效率为:(O(n^2log(n)))
    • 方法二:点分治。

    1.4 点分治原理

    • 假设一条满足条件的路径经过点 x ,那么这条路径在 x 的一个子树里(以 x 为端点)或者在 x 的两个不同的子树里,如图:

    • 假设我们选出一个根 Root ,那么答案路径存在两种情况:

      1. 被一个子树所包含;
      2. 跨过 Root ,在黑子树中选择一部分路径,在红子树中选择一部分路径,然后从 Root 处拼起来形成一条答案路径.

      • 仔细想一下,发现情况1(被一个子树包含)中,答案路径上的一点变为根 Root ,就成了情况2(在两棵子树中)。

      • 如图, Root 为根的子树中存在答案(蓝色实边路径),可以看成以 Root2 为根的两棵子树存在答案,所以只用处理情况2就行了,可以用分治的方法,这应该是点分治的基本原理。

    • 首先根不能随便选,选根不同会影下面遍历的效率的,如图:

      • 显然选x为根比选y为根更优,选 x 最多递归2层,选 y 最多递归4层,显然可以发现找树的重心(重心所有的子树的大小都不超过整个树大小的一半)是最优的。

    1.5 算法核心

    1. 对于这棵无根树,找到一个点,使得它在树的中心位置,满足如果以它为根,它的最大子树大小尽量小,这个点称为重心
    2. 以这个点为根,计算它的答案。
    3. 把以这个点为根的树的所有子树单独作为一个子问题,回到步骤1递归处理。
    • 这个算法的复杂度是多少呢?
      • 先介绍一个定理:以树的重心为根的有根树,最大子树大小不超过(frac{n}{2})。假设超过了,大小为(k>frac{n}{2}),那么其他子树大小之和等于(n−k−1)
      • 那么把重心往这个子树方向移动,最大子树大小一定减小,那么进一步地,就证明了经过这个算法,递归的次数是(O(logn)) 级别。

    1.6 算法实现

    • 按照上述步骤实现代码:

      1. 计算重心位置:使用一次简单的DFS来实现。
      2. 计算答案:直接用另一个DFS计算。
      3. 分治子问题:重新调用寻找重心的DFS函数,再递归求解即可。
    • 计算重心

      void GetRoot(int u,int f){
      	siz[u]=1;wt[u]=0;//siz[u]:u为根的子树节点数;wt[u]:u的节点最大的子树节点数
      	for(int i=head[u];i;i=e[i].next){
      		int v=e[i].to;
      		if(v!=f && !vis[v]){//vis[v]==1说明v是当前子树的父亲节点,如下图
      			GetRoot(v,u);//递归求子树v的重心
                  siz[u]+=siz[v];//累计u的子树大小
                  wt[u]=std::max(wt[u],siz[v]);//求u的最大子树
      		}
      	}//Tsiz[u]-siz[u]表示u的父亲节点除u以外其它子树之和,如下图,如果u=2,则把1子树也当做2的一个子树
      	wt[u]=std::max(wt[u],Tsiz-siz[u]);//利用的是无根树的特点
      	if(wt[Root]>wt[u])Root=u;//w[root]初始化为Inf,相当于求最大子树最小的节点u,即为重心
      }
      

      • 节点2为整个子树的重心,节点3为节点21为根的子树的重心。我们以重心3来求点分治的时候不能访问到22的其他子树。
    • 计算满足条件的答案

      void dfs(int u,int f,int d){//求以u为根的子树中其他点到u的距离+初始值d
      	a[++cnt]=d;//u到距离为d的祖先节点也是一条路径
      	for(int i=head[u];i;i=e[i].next){
      		int v=e[i].to;
      		if(v!=f && !vis[v])//vis同上图,截断子树的范围
      			dfs(v,u,d+e[i].w);
      	}
      }
      int Calc(int u,int d){
      	cnt=0;//记录u为根的子树中经过u路径条数
          dfs(u,0,d);//把经过u的路径的长度存储到a[1]~a[cnt]
      	std::sort(a+1,a+cnt+1);//从小到大排序
      	int sum=0;//计算满足条件的路径条数
      	for(int i=1,j=cnt;;++i){//双指针技巧求满足条件的组合数,比二分快
      		while(j && a[i]+a[j]>k)--j;//找到当前和a[i]组合的最大的a[j]
      		if(i>j)break;//说明找不到一个满足和a[i]组合的另一条链
      		sum+=j-i+1;//a[i]~a[j]的链都能和a[i]组合,包括a[i]单链
      	}//计算的组合包含共用同一段链的情况,如下图
      	return sum;
      }
      

      • 1为根计算路径的时候a[4]记录的1-2-4路径长度,a[5]记录的1-2-4路径的长度,他们共用了1-2这条边,点分治的核心思想,即路径要经过根节点,4~5的路径并不经过此时的根几点1,这需要我们在后面的计算中去掉。
    • 点分治核心代码

      void DFS(int u){
      	ans+=Calc(u,0);//计算u为根的子树满足条件两条路径之和小于等于k的条数(包括共边路径组合)
          vis[u]=1;//标记以u为重心的子树已计算
      	for(int i=head[u];i;i=e[i].next){
      		int v=e[i].to;
      		if(!vis[v]){//避免越界
      			ans-=Calc(v,e[i].w);//减去共边为u-v且满足条件的条数
      			Root=0;Tsiz=siz[v];//求以v为根子树的重心,Root记录子树的重心
                  GetRoot(v,0);
      			DFS(Root);//子树v从重心求解满足条件的组合,是一个递归的子问题
      		}
      	}
      }
      
    • 完整代码POJ 1741

    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    const int maxn = 1e4 + 5,Inf=0x3f3f3f3f;
    struct Edge{int to,w,next;}e[2*maxn];
    int n,k,ans,Root,Tsiz,cnt;
    int head[maxn],siz[maxn],wt[maxn],a[maxn];
    bool vis[maxn];
    void Insert(int u,int v,int w){
    	e[++head[0]].to=v;e[head[0]].w=w;e[head[0]].next=head[u];head[u]=head[0];
    }
    void GetRoot(int u,int f){
    	siz[u]=1;wt[u]=0;//siz[u]:u为根的子树节点数;wt[u]:u的节点最大的子树节点数
    	for(int i=head[u];i;i=e[i].next){
    		int v=e[i].to;
    		if(v!=f && !vis[v]){//vis[v]==1说明v是当前子树的父亲节点,如下图
    			GetRoot(v,u);//递归求子树v的重心
                siz[u]+=siz[v];//累计u的子树大小
                wt[u]=std::max(wt[u],siz[v]);//求u的最大子树
    		}
    	}//Tsiz[u]-siz[u]表示u的父亲节点除u以外其它子树之和,如下图,如果u=2,则把1子树也当做2的一个子树
    	wt[u]=std::max(wt[u],Tsiz-siz[u]);//利用的是无根树的特点
    	if(wt[Root]>wt[u])Root=u;//w[root]初始化为Inf,相当于求最大子树最小的节点u,即为重心
    }
    void dfs(int u,int f,int d){//求以u为根的子树中其他点到u的距离+初始值d
    	a[++cnt]=d;//u到距离为d的祖先节点也是一条路径
    	for(int i=head[u];i;i=e[i].next){
    		int v=e[i].to;
    		if(v!=f && !vis[v])//vis同上图,截断子树的范围
    			dfs(v,u,d+e[i].w);
    	}
    }
    int Calc(int u,int d){
    	cnt=0;//记录u为根的子树中经过u路径条数
        dfs(u,0,d);//把经过u的路径的长度存储到a[1]~a[cnt]
    	std::sort(a+1,a+cnt+1);//从小到大排序
    	int sum=0;//计算满足条件的路径条数
    	for(int i=1,j=cnt;;++i){//双指针技巧求满足条件的组合数,比二分快
    		while(j && a[i]+a[j]>k)--j;//找到当前和a[i]组合的最大的a[j]
    		if(i>j)break;//说明找不到一个满足和a[i]组合的另一条链
    		sum+=j-i+1;//a[i]~a[j]的链都能和a[i]组合,包括a[i]单链
    	}//计算的组合包含共用同一段链的情况,如下图
    	return sum;
    }
    void DFS(int u){
    	ans+=Calc(u,0);//计算u为根的子树满足条件两条路径之和小于等于k的条数(包括共边路径组合)
        vis[u]=1;//标记以u为重心的子树已计算
    	for(int i=head[u];i;i=e[i].next){
    		int v=e[i].to;
    		if(!vis[v]){//避免越界
    			ans-=Calc(v,e[i].w);//减去共边为u-v且满足条件的条数
    			Root=0;Tsiz=siz[v];//求以v为根子树的重心,Root记录子树的重心
                GetRoot(v,0);
    			DFS(Root);//子树v从重心求解满足条件的组合,是一个递归的子问题
    		}
    	}
    }
    void Solve(){
    	while(~scanf("%d%d",&n,&k) && n && k){
    		ans=0;memset(vis,0,sizeof(vis));
    		memset(head,0,sizeof(head));
    		for(int i=1;i<n;++i){
    			int u,v,w;scanf("%d%d%d",&u,&v,&w);
    			Insert(u,v,w);Insert(v,u,w);
    		}
    		wt[0]=Inf;//初始化重心所在子树节点数位无穷大,方便求重心,所以每次求子树重心前必须把root=0
            Root=0;Tsiz=n;GetRoot(1,0);//可以随便从一个节点开始求重心,这里我们从节点1开始
    		DFS(Root);//从重心Root开始求满足条件的组合
    		printf("%d
    ",ans-n);//每个单点我们计算是都当做了一条路径,需要减去
    	}
    }
    int main(){
    	Solve();
     	return 0;
    }
    
  • 相关阅读:
    exit()和_exit()函数(转)
    C语言struct内存占用问题 (转)
    C语言中memset函数(转)
    STDIN_FILENO与stdin区别(转)
    stderr,strerror,errno,perror,论坛大神的回答!
    C++ 函数模版
    C++ 内置函数
    offsetof
    LockSupportDemo
    读写锁,await和signa
  • 原文地址:https://www.cnblogs.com/hbhszxyb/p/12988533.html
Copyright © 2020-2023  润新知