树的点分治 bzoj2152

聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃、两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏。他们的爸爸快被他们的争吵烦死了,所以他发明了一个新游戏:由爸爸在纸上画n个“点”,并用n-1条“边”把这n个“点”恰好连通(其实这就是一棵树)。并且每条“边”上都有一个数。接下来由聪聪和可可分别随即选一个点(当然他们选点时是看不到这棵树的),如果两个点之间所有边上数的和加起来恰好是3的倍数,则判聪聪赢,否则可可赢。聪聪非常爱思考问题,在每次游戏后都会仔细研究这棵树,希望知道对于这张图自己的获胜概率是多少。现请你帮忙求出这个值以验证聪聪的答案是否正确。

Input
输入的第1行包含1个正整数n。后面n-1行,每行3个整数x、y、w,表示x号点和y号点之间有一条边,上面的数是w。

Output
以即约分数形式输出这个概率(即“a/b”的形式,其中a和b必须互质。如果概率为1,输出“1/1”)。

Sample Input
5
1 2 1
1 3 2
1 4 1
2 5 3
Sample Output
13/25
【样例说明】
13组点对分别是(1,1) (2,2) (2,3) (2,5) (3,2) (3,3) (3,4) (3,5) (4,3) (4,4) (5,2) (5,3) (5,5)。

【数据规模】
对于100%的数据,n<=20000。

线性结构在分治时一般选择二分,因为这样可以使得分出来子结构中最大的那个最小,划分的次数少,递归的深度小
同样的,对树进行分治时,也要尽可能让分出来的最大的子树小,这样递归的深度才小
树的重心: 以一个点为根,若这个点的最大子树最小,则这个点是树的重心

树重心的求法:
先假设一个重心,让它的最大子树大小为inf
dfs整棵树,对每个点,记录它最大子树的大小,如果它的最大子树大小比重心的最大子树要小,更新重心
因为树本身是无向的,但dfs是随便找一个点 当根,有向地搜的,所以父亲结点那边也是一棵子树。

int root,f[maxn],vis[maxn],siz[maxn],sum;//f[u]表示以u为根的最大子树的大小
void getroot(int u,int fa)
{
    siz[u] = 1, f[u] = 0;
    for(int i=head[u];~i;i=e[i].next)
    {
        int v = e[i].to;
        if(v==fa || vis[v]) continue;
        getroot(v,u);
        siz[u] += siz[v];
        f[u] = max(f[u],siz[v]);//找子树大小的最大值
    }
    f[u] = max(f[u],sum - siz[u]);//把父亲看作是连在上方的子树,getroot前把sum初始化为整棵树的大小
    if(f[u] < f[root])
        root = u;
}

找完重心就以重心为根dfs处理子树信息

处理树中路径信息时,有两种情况: 经过根节点(跨子树) 和 不经过根节点(在一棵子树中)
处理的时候是无论经过与否一并统计的。可以发现,不经过根节点的路径在某个子树中一定是经过那个子树的根的
也就是说这部分的信息计了两次。把子树的信息算一遍减掉得到的就是正确答案了。

int t[3];
void getdis(int u,int dis,int fa)//把u为根的子树的信息全处理出来
{
    ++t[dis];
    for(int i=head[u];~i;i=e[i].next)
    {
        int v = e[i].to;
        if(v!=fa&&!vis[v])
            getdis(v,(dis + e[i].w)%3,u);
    }
}

int calc(int u,int dis)
{
    t[0] = t[1] = t[2] = 0;
    getdis(u,dis%3,0);
    return t[0]*t[0] + 2*t[1]*t[2];
    //t[0]记录的是子树中到u的距离为3的倍数的点,这些点两两间的路径显然距离也是3
    //t[1]的点到t[2]的点的路径距离是3的倍数,正反两个方向*2
}

int ans;
void solve(int u)//先加上u的贡献,再减去u的子树v的贡献
{
    ans += calc(u,0);
    vis[u] = 1;//相当于删除u这个点
    for(int i=head[u];~i;i=e[i].next)
    {
        int v = e[i].to;
        if(vis[v]) continue;
        ans -= calc(v,e[i].w);
        root = 0, sum = siz[v];
        f[root] = 0x7fffffff;
        getroot(v,0);
        solve(root);
    }
}

为什么减去的部分一开始是e[i].w呢, 因为getdis算出来的是子树中每个点到根的距离,而不经过根的路径每次计算必然都算多了根和根的儿子这一路径

完整代码

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;

const int maxn = 2e4 + 5;
int N;
struct Edge
{
    int to, next, w;
}e[maxn<<1];

int edgenum,head[maxn];
void add(int u,int v,int w)
{
    e[edgenum].to = v;
    e[edgenum].next = head[u];
    e[edgenum].w = w;
    head[u] = edgenum++;
}

int root,f[maxn],vis[maxn],siz[maxn],sum;//f[u]表示以u为根的最大子树的大小
void getroot(int u,int fa)
{
    siz[u] = 1, f[u] = 0;
    for(int i=head[u];~i;i=e[i].next)
    {
        int v = e[i].to;
        if(v==fa || vis[v]) continue;
        getroot(v,u);
        siz[u] += siz[v];
        f[u] = max(f[u],siz[v]);//找子树大小的最大值
    }
    f[u] = max(f[u],sum - siz[u]);//把父亲看作是连在上方的子树
    if(f[u] < f[root])
        root = u;
}

int t[3];
void getdis(int u,int dis,int fa)//把u为根的子树的信息全处理出来
{
    ++t[dis];
    for(int i=head[u];~i;i=e[i].next)
    {
        int v = e[i].to;
        if(v!=fa&&!vis[v])
            getdis(v,(dis + e[i].w)%3,u);
    }
}

int calc(int u,int dis)
{
    t[0] = t[1] = t[2] = 0;
    getdis(u,dis%3,0);
    return t[0]*t[0] + 2*t[1]*t[2];
    //t[0]记录的是子树中到u的距离为3的倍数的点,这些点两两间的路径显然距离也是3
    //t[1]的点到t[2]的点的路径距离是3的倍数,正反两个方向*2
}

int ans;
void solve(int u)//先加上u的贡献,再减去u的子树v的贡献
{
    ans += calc(u,0);
    vis[u] = 1;//相当于删除u这个点
    for(int i=head[u];~i;i=e[i].next)
    {
        int v = e[i].to;
        if(vis[v]) continue;
        ans -= calc(v,e[i].w);
        root = 0, sum = siz[v];
        f[root] = 0x7fffffff;
        getroot(v,0);
        solve(root);
    }
}

void init(int N)
{
    ans = root = 0;
    sum = N;
    f[0] = 0x7fffffff;
    ++N;
    memset(head,-1,4*N);
    memset(vis,0,4*N);
}

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

int main()
{
    int u,v,w,a;
    scanf("%d",&N);
    init(N);
    for(int i=0;i<N-1;++i)
    {
        scanf("%d%d%d",&u,&v,&w);
        add(u,v,w%3);
        add(v,u,w%3);
    }
    getroot(1,0);//随便找一个点把无根树拉成有根树,第一个参数填什么都行(只要<=N,填1最稳)
    //跑完getroot之后root就是树的重心了

    solve(root);
    a = gcd(N*N,ans);
    printf("%d/%d\n",ans/a,N*N/a);
    return 0;
}
posted @ 2018-09-07 15:23  Apale  阅读(78)  评论(0编辑  收藏  举报