点分治

树的点分治
【感觉自己要搞的东西好多啊】
·重心
·重心的概念
与此点相连的结点数最多的连通块的结点数最小【感觉这个概念特别抽象】
即删去重心后,生成的多棵树尽可能平衡

· 重心的性质
(1)所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么他们的距离和一样。
证明:感性理解一下应该是正确的,感觉有点像山区建小学那道,网上说是用调整法。
(2)把两个树通过一条边相连得到一个新的树,那么新的树的重心在连接原来两个树的重心的路径上。
(3)把一个树添加或删除一个叶子,那么它的重心最多只移动一条边的距离。

【感觉这些就只是感性理解了一下】

·找重心的方法
1.先任选一个节点作为根,把无根树变为有根树(dfs)
2.求出每个节点的子树大小siz[i](一个简单的树上dp)
3.不断更新,使得max(siz[i],n-siz[i])最小的i即是这个点的重心

感觉重心大概就这样比较基本的,至于动态维护什么的就之后再说吧
接下来刚重点
·树的分治
·除去树中的某些对象,使原树被分为若干个互不相交的部分
分为:
1.点分治 如果每次都选取重心,则每次至多递归logN次
2.边分治 最坏情况下递归N次
二者的空间复杂度均为N
3.链分治(我只知道有这么个东西,是树的路径剖分思想的更高级的体现,一般链分治的题目都可以用路径剖分解决)
·点分治

·对于点分治的一些理解
对于一棵有根树,树中的每一条路径对于每一个节点都分为两种情况:
1.经过该节点
2.不经过该节点(可递归求解)

·基本流程:
1.找到重心作为根 把无根树变成有根树
2.以重心的儿子节点为根,遍历整棵子树得到这个根到每个节点的路径信息 所以可以得到包含这个重心的所有路径的信息
3.标记根节点(可看作删除根节点)
4.对剩下的路径进行递归处理,跟之前一样的操作 直到只剩一个点为止(注意这个点所构成的路径有时也需要处理)(所以我还是每次都处理好了)

关于一道入门题:POJ 1741
【其实之前求重心有问题,我T了好多遍,气死,还写了读优】
【后来的方法比较高效】

//我觉得POJ就是看不起我 就是不让我过 
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ms(x,y) memset(x,y,sizeof(x))
using namespace std;

const int N = 10010;
const int INF = 1<<30;

int n,k;
int tot=0,head[N<<1];

inline int Max(int a,int b) {return a>b?a:b;}
inline int Min(int a,int b) {return a<b?a:b;}

inline int read()
{
    int data=0,w=1; char ch=0;
    while(ch!='-' && (ch<'0' || ch>'9')) ch=getchar();
    if(ch=='-') w=-1,ch=getchar();
    while(ch>='0' && ch<='9') data=data*10+ch-'0',ch=getchar();
    return data*w;
}

struct node{
    int pre,v,l;
}edge[N<<1];

void adde(int from,int to,int w){
    tot++;
    edge[tot].pre=head[from];
    edge[tot].v=to;
    edge[tot].l=w;
    head[from]=tot;
}

int root;
int size;
bool vis[N],delet[N];
int s[N],f[N];

void getroot(int u,int fa){
    s[u]=1,f[u]=0;
    for(int i=head[u];i;i=edge[i].pre){
        int v=edge[i].v;
        if((v!=fa)&&(!delet[v])){
          getroot(v,u);
          s[u]+=s[v];
          f[u]=Max(f[u],s[v]);
        }
    }
    f[u]=Max(f[u],size-s[u]);
    if(f[u]<f[root]) root=u;//感觉这个方法真的是高效很多啊 
}

/*bool vis[N],vvis[N],delet[N];
int siz[N];
int size=INF,root=1;

void getroot(int u,int f){
    vvis[u]=true;siz[u]=0;
    int tmp=-1;
    for(int i=head[u];i;i=edge[i].pre){
        int v=edge[i].v;
        if(v==f||delet[v]) continue;
        if(!vvis[v]){
            vvis[v]=true;
            getroot(v,u);
            siz[u]+=siz[v]+1;
            tmp=Max(tmp,siz[v]+1);
        }
    }
    tmp=Max(tmp,n-siz[u]-1);
    if((tmp<size||(tmp==size&&root<u))&&!delet[u]){
        size=tmp;
        root=u;
    }
}*/

int dis[N];
int num=0;

void getdis(int u,int d,int f){
    dis[num++]=d;
    for(int i=head[u];i;i=edge[i].pre){
        int v=edge[i].v;
        if(v!=f&&!delet[v]){
           getdis(v,d+edge[i].l,u);
        }
    }
}//每个点到重心的距离 

int calc(int u,int d){
    int cnt=0;num=0;
    getdis(u,d,0);
    sort(dis,dis+num);
    int i=0,j=num-1;
    while(i<j){
        while(dis[i]+dis[j]>k&&i<j) j--;
        cnt+=j-i;
        i++;
    }
    return cnt;
}//计算以root为根的子树中有多少点对距离小于等于k 

int ans=0;

void dfs(int u){
//  ms(vvis,0);ms(siz,0);
//  size=INF;getroot(u,0);//这两个操作应该是绑定的 
//原来这个求法大概memset的时间复杂度太大了不可忽略,所以会T掉 
    ans+=calc(root,0);
    delet[root]=1;
    for(int i=head[root];i;i=edge[i].pre){
        int v=edge[i].v;
        if(!delet[v]){
            ans-=calc(v,edge[i].l);
            f[0]=size=s[u];
            getroot(v,root=0);
            dfs(v);
        }
    }
}

void update(){
    ms(head,0);ms(delet,0);
    ans=0;tot=0;
}

int main(){
    while(1){
        n=read();k=read();
        if(!n&&!k) break;
        update();
        int u,v,l;
        for(int i=1;i<n;i++){
            u=read(),v=read(),l=read();
            adde(u,v,l);
            adde(v,u,l);
        }
        f[0]=size=n;
        getroot(1,root=0);
        //printf("%d\n",root);
        dfs(root);
        printf("%d\n",ans);
    }
    return 0;
}

BZOJ 2152
我当时为什么没有静态查错啊。。。一个小错误找了好久woc
关于找点的个数
一棵树内的合法点对可以表示为:(所有深度%3=0的点)^2+所有深度%3=1的点*所有深度%3=2的点*2。
证明(果然是我太弱了吗感觉网上都没有证明)
1.关于t[1]和t[2] 显然对于属于t[1]的每个点,都有t[2]个点可与之组合,根据乘法原理,易得到有t[1]*t[2]种组合结果,而对于两个点1和2,我们认为1到2和2到1是两种不同的组合,所以t[1]*t[2]*2得到一部分解。
2.关于t[3] 属于t[3]的所有点可看作一个集合,对于集合里的每个点,都可以与集合里的所有点组合(包括它本身) 所以总共有t[3]*t[3]种组合结果
所以再利用点分就可以A掉这道题了

//BZOj也看不起我 
#include<cstdio>
#include<cstring>
using namespace std;

const int N = 20010;
const int INF = 1<<30;

int n;
int tot=0;
int head[N<<1];
bool delet[N];

inline int Max(int a,int b){
    return a>b?a:b;
}

inline int gcd(int a,int b){
    return b==0?a:gcd(b,a%b);
}

struct node{
    int pre,v,w;
}edge[N<<1];

void adde(int from,int to,int w){
    tot++;
    edge[tot].pre=head[from];
    edge[tot].v=to;
    edge[tot].w=w;
    head[from]=tot;
}

int size,root;
int s[N],f[N];

void getroot(int u,int fa){
    s[u]=1,f[u]=0;
    for(int i=head[u];i;i=edge[i].pre){
        int v=edge[i].v;
        if(v!=fa&&!delet[v]){
            getroot(v,u);
            s[u]+=s[v];
            f[u]=Max(f[u],s[v]);
        }
    }
    f[u]=Max(f[u],size-s[u]);
    if(f[u]<f[root]) root=u;
}

int num;
int dis[N<<1];

void getdis(int u,int d,int fa){
    num++;
    dis[num]=d;
    for(int i=head[u];i;i=edge[i].pre){
        int v=edge[i].v;
        if(v!=fa&&!delet[v]){
           getdis(v,d+edge[i].w,u);
        }
    }
}
int t[3];

int calc(int u,int d){
    num=0;
    t[0]=t[1]=t[2]=0;
    getdis(u,d,root);
    for(int i=1;i<=num;i++)
        t[dis[i]%3]++;
    return t[0]*t[0]+t[1]*t[2]*2;
}

int ans=0;

void dfs(int u){
    ans+=calc(u,0);
    delet[u]=true;
    for(int i=head[u];i;i=edge[i].pre){
        int v=edge[i].v;
        if(!delet[v]){
           ans-=calc(v,edge[i].w);
           f[0]=size=s[v];
           getroot(v,root=0);
           dfs(root);
        }
    }
}

int main(){
    scanf("%d",&n);
    for(register int i=1;i<n;i++){
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        w%=3;
        adde(u,v,w);
        adde(v,u,w);
    }
    f[0]=size=n;
    getroot(1,root=0);
    dfs(root);
    if(!ans) printf("0/0\n");
    else{
       int aaa=gcd(n*n,ans);
       printf("%d/%d\n",ans/aaa,(n*n)/aaa);
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值