树的点分治 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;
}
源码来自:https://pan.quark.cn/s/a4b39357ea24 ### 操作指南:洗衣机使用方法详解#### 1. 启动与水量设定- **使用方法**:使用者必须首先按下洗衣设备上的“启动”按键,同时依据衣物数量设定相应的“水量选择”旋钮(高、中或低水量)。这一步骤是洗衣机运行程序的开端。- **运作机制**:一旦“启动”按键被触发,洗衣设备内部的控制系统便会启动,通过感应器识别水量选择旋钮的位置,进而确定所需的水量高度。- **技术执行**:在当代洗衣设备中,这一流程一般由微处理器掌管,借助电磁阀调控进水量,直至达到指定的高度。#### 2. 进水过程- **使用说明**:启动后,洗衣设备开始进水,直至达到所选的水位(高、中或低)。- **技术参数**:水量的监测通常采用浮子式水量控制器或压力感应器来实现。当水位达到预定值时,进水阀会自动关闭,停止进水。- **使用提醒**:务必确保水龙头已开启,并检查水管连接是否牢固,以防止漏水。#### 3. 清洗过程- **使用步骤**:2秒后,洗衣设备进入清洗环节。在此期间,滚筒会执行一系列正转和反转的动作: - 正转25秒 - 暂停3秒 - 反转25秒 - 再次暂停3秒- **重复次数**:这一系列动作将重复执行5次,总耗时为280秒。- **技术关键**:清洗环节通过电机驱动滚筒旋转,利用水流冲击力和洗衣液的化学效果,清除衣物上的污垢。#### 4. 排水与甩干- **使用步骤**:清洗结束后,洗衣设备会自动进行排水,将污水排出,然后进入甩干阶段,甩干时间为30秒。- **技术应用**:排水是通过泵将水抽出洗衣设备;甩干则是通过高速旋转滚筒,利用离心力去除衣物上的水分。- **使用提醒**:...
代码下载地址: https://pan.quark.cn/s/c289368a8f5c 在安卓应用开发领域,构建一个高效且用户友好的聊天系统是一项核心任务。 为了协助开发者们迅速达成这一目标,本文将分析几种常见的安卓聊天框架,并深入说明它们的功能特性、应用方法及主要优势。 1. **环信(Easemob)** 环信是一个专为移动应用打造的即时通讯软件开发套件,涵盖了文本、图片、语音、视频等多种消息形式。 通过整合环信SDK,开发者能够迅速构建自身的聊天平台。 环信支持消息内容的个性化定制,能够应对各种复杂的应用场景,并提供多样的API接口供开发者使用。 2. **融云(RongCloud)** 融云作为国内领先的IM云服务企业,提供了全面的聊天解决方案,包括一对一交流、多人群聊、聊天空间等。 融云的突出之处在于其稳定运行和高并发处理性能,以及功能完备的后台管理工具,便于开发者执行用户管理、消息发布等操作。 再者,融云支持多种消息格式,如位置信息、文件传输、表情符号等,显著增强了用户聊天体验。 3. **Firebase Cloud Messaging(FCM)** FCM由Google提供的云端消息传递服务,可达成安卓设备与服务器之间的即时数据交换。 虽然FCM主要应用于消息推送,但配合Firebase Realtime Database或Firestore数据库,开发者可以开发基础的聊天软件。 FCM的显著优势在于其全球性的推送网络,保障了消息能够及时且精确地传输至用户。 4. **JMessage(极光推送)** 极光推送是一款提供消息发布服务的软件开发工具包,同时具备基础的即时通讯能力。 除了常规的文字、图片信息外,极光推送还支持个性化消息,使得开发者能够实现更为复杂的聊天功能。 此...
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值