点分治:统计长度不大于K的路径条数

来源:互联网 发布:mac如何打罗马数字 编辑:程序博客网 时间:2024/05/01 22:14

树的路径统计
统计有多少条长度不大于k的路径
树的点分治:将路径分为经过重心和不经过重心的路径。
解法一:
1.统计经过重心的满足条件的路径条数。
1)统计经过重心,且路径可在重心周围有重合的满足条件的路径条数—O(n)
2)统计经过重心的子节点,且路径可在重心子节点周围有重合的满足路径条数O(n)
3)经过重心满足条件的路径条数=1)-2)中的路径条数(巧妙之处!)
2.不经过重心,将重心除去,分为x个子树,分别寻找每个子树的重心,重复上述步骤即可。由于重心的定义可知,递归层数不会超过O(logn) ,因此总体复杂度为O(n*logn)

#include<stdio.h>#include<iostream>#include<algorithm>#include<string.h>#define maxn 20010#define lowbit(x) x&(-x)#define mem(a,b) memset(a,b,sizeof(a))#define ll long long#define INF 0x3f3f3f3fusing namespace std;int n;int k;struct node{    int to,next,w;}edge[maxn*2];int head[maxn],tot;int sum,f[maxn],vis[maxn],son[maxn],core,deep[maxn],d[maxn];//getcoreint ans;void init(){    mem(head,-1);    mem(vis,0);    tot=0;    ans=0;}void add(int a,int b,int c){    edge[tot].to=b,edge[tot].next=head[a],edge[tot].w=c;    head[a]=tot++;}void getcore(int u,int fa){    son[u]=1,f[u]=0;//每次更新前必须初始化!!!---dfs可以在搜索子树前在内部对点初始化    for(int i=head[u];i!=-1;i=edge[i].next){        int v=edge[i].to;        if(v==fa||vis[v]) continue;//除去已找到的重心后求子树重心        getcore(v,u);        son[u]+=son[v],f[u]=max(f[u],son[v]);    }    f[u]=max(f[u],sum-son[u]);    if(f[u]<f[core]) core=u;//core记录该树重心}void getdeep(int u,int fa){//得到u子树每个节点的深度    deep[++deep[0]]=d[u];    for(int i=head[u];i!=-1;i=edge[i].next){        int v=edge[i].to,w=edge[i].w;        if(v==fa||vis[v]) continue;        d[v]=d[u]+w;        getdeep(v,u);    }}int cal(int u,int w){//1.该点是重心:求该点子树每点到该点深度2.该点父节点是重心:求该点子树到父节点的深度    deep[0]=0,d[u]=w,getdeep(u,0);    int tmp=0;    sort(deep+1,deep+deep[0]+1);//<strong><span style="color:#ff0000;">技巧</span></strong>    for(int l=1,r=deep[0];l<r;)        if(deep[l]+deep[r]<=k) tmp+=(r-l),l++;        else r--;    return tmp;}void work(int u){    ans+=cal(u,0);//经过原树重心的k路径条数    vis[u]=1;//原树重心已算,标记    for(int i=head[u];i!=-1;i=edge[i].next){        int v=edge[i].to,w=edge[i].w;        if(vis[v]) continue;//如果v已经作为重心枚举过,u不会遍历到v,因此不必除去        ans-=cal(v,w);//除去经过重心同一子树的路径        sum=son[v];//更改当前树节点总和        core=0;        getcore(v,core);        work(core);    }}int main(){    //freopen("a.txt","r",stdin);    while(scanf("%d%d",&n,&k)!=EOF){        if(!n&&!k) break;        init();        for(int i=0;i<n-1;i++){            int a,b,c;            scanf("%d%d%d",&a,&b,&c);            add(a,b,c),add(b,a,c);        }        core=0,sum=n,f[0]=INF;        getcore(1,0);        work(core);        printf("%d\n",ans);    }    return 0;}

易错点:1、sum、core、vis[u]的更新 2、边集开两倍数组大小
技巧:统计一列数和<=k的数对数量
扫描法。1)先将数从小到达排序2)每次统计左端点作为较小数时的数对对数。详见代码

解法二:
解法二在统计经过重心的路径条数时与法一略微不同。
1.统计经过重心的路径条数
1)记录以重心子节点为端点的路径,以子节点分类存储,则以重心为端点经过不同子树,且无重合的路径=该路径+dis< u,v >(重心到子节点的边长) O(n*logn)
2)统计每两个相邻的子节点路径和<=K的路径条数,再将其合并为一组,每一层复杂度为O(n),递归层数不超过logn,总复杂度为O(n*logn)
2.不经过重心,将重心除去,分为x个子树,分别寻找每个子树的重心,重复上述步骤即可。由于重心的定义可知,递归层数不会超过O(logn)
总复杂度为O(n*logn*logn)

#include<stdio.h>#include<iostream>#include<string.h>#include<algorithm>#include<limits.h>#include<queue>#include<vector>#define mem(a,b) memset(a,sizeof(a),b)#define ll long long#define MP make_pair#define MP(x,y) make_pair((x),(y))#define X first#define Y second#define oo 0x3f3f3f3fconst ll INF = 0x0fffffffffffffff;using namespace std;typedef long long lld;const int maxn = 500000+10;typedef pair<int,int> F;int n,m,K;//e存储边:X为长度,Y为目标点//a存储以重心为根时,每个子节点的边长:X为长度,Y为节点个数(不包括根节点)vector<F> e[maxn],a[maxn];int s[maxn];//子树节点个数bool vis[maxn];int ans;//计算a和b内边长和<=K的路径个数void calc(vector<F> &a,vector<F> &b){    int p=-1;    for(int i=a.size()-1;i>=0;i--){        while(p+1<b.size()&&b[p+1].X+a[i].X<=K) p++;        ans+=(p+1);    }}void build(){    for(int i=1;i<n;i++) e[i].clear();    for(int i=0;i<m;i++){        int a,b,c;        char ch[5];        scanf("%d%d%d%s",&a,&b,&c,ch);        e[a].push_back(MP(c,b));        e[b].push_back(MP(c,a));    }}//求剩余连通树的重心F Find(int u,int f,int sum){    s[u]=1;    int lar=0;    F ans=MP(oo,0);    for(int i=0;i<e[u].size();i++){        int v=e[u][i].Y;        if(v==f||vis[v]) continue;        F x=Find(v,u,sum);        s[u]+=s[v];        lar=max(lar,s[v]);//记录子树节点的最大值        ans=min(ans,x);//记录子树结果的最小值(权值最小,然后编号最小)    }    lar=max(lar,sum-s[u]);    ans=min(ans,MP(lar,u));    return ans;}void dfs2(int u,int f,int dis,int num,vector<F> &a){    a.push_back(MP(dis,num));    s[u]=1;//统计以u为根时子树大小    for(int i=0;i<e[u].size();i++){        int v=e[u][i].Y,c=e[u][i].X;        if(v==f||vis[v]) continue;        dfs2(v,u,dis+c,num+1,a);        s[u]+=s[v];    }}//树中分治void dfs(int u,int f,int sum){    u=Find(u,f,sum).Y;//u是该树重心    int m=0,left,x=-1;    for(int i=0;i<e[u].size();i++){//得到以重心u为根时,统计个子节点路径长度及相应的节点个数        int v=e[u][i].Y,c=e[u][i].X;        if(v==f||vis[v]) continue;        a[++m].clear();        dfs2(v,u,c,1,a[m]);        sort(a[m].begin(),a[m].end());    }    a[++m].clear();    a[left=m].push_back(MP(0,0));//建立虚拟节点    while(left>1){        int d=left/2;        for(int i=1;i<=d;i++){            calc(a[i],a[i+d]);            vector<F> b(a[i].size()+a[i+d].size());            //merge函数:将两个序列合并成一个序列,并使其有序化---按照最后一个数据结构的大小定义排序            merge(a[i].begin(),a[i].end(),a[i+d].begin(),a[i+d].end(),b.begin());            a[i]=b;        }        if(left&1) a[++d]=a[left];        left=d;    }    vis[u]=1;    for(int i=0;i<e[u].size();i++){        int v=e[u][i].Y;        if(v!=f&&!vis[v]&&s[v]>1) dfs(v,u,s[v]);    }}void init(){    mem(vis,0);    ans=0;}int  main(){    //freopen("a.txt","r",stdin);    scanf("%d%d",&n,&m);    init();    build();    scanf("%d",&K);    dfs(1,-1,n);    printf("%d\n",ans);}
0 0