2022.03.10牛客 小琛和他的学校(dfs)

传送门

  • 题目描述
    小琛是一所学校的校长。
    他的学校有n个校区(编号1~n),被n-1条双向道路连接,呈树形结构。
    第i个校区共有Ai个学生。
    第i天早上,所有的学生会沿最短路走到第i个校区参加活动,晚上再原路返回。
    一个人通过第j条通道一次(即一人次),需要小琛支付wj的维护费用。
    小琛想知道第n天结束之后,对于每一条通道,他总共需要支付多少费用。
    对于100%的数据,1≤ n ≤ 200,000,1≤ A[i]≤ 10,000,1≤ w[i] ≤ 10,000。
  • 输入描述:
    第一行一个整数n,表示校区的数量。
    接下来一行,n个整数,表示A1~An。
    第3到第n+1行,每行包含3个整数。第i行包含三个整数ui-2,vi-2,wi-2,表示第i-2条通道所连接的两个校区的编号,以及一人次通过这条通道的费用。
  • 输出描述:
    共n-1行,每行一个整数。
    第i行的整数表示小琛对于第i条通道所需支付的费用。
  • 示例1
    输入
    4
    2 1 2 3
    1 3 1
    1 2 3
    4 1 2
    输出
    24
    60
    56
    错误写法
#include <bits/stdc++.h>

using namespace std;
const int  N=2e5+5;
int n,sums;//sums所有学生数
int a[N];//每个校区的人数
struct node{
    int u,v,cost;
}edge[N];//每条路的端点和花费
int main()
{
    cin>>n;
    sums=0;
    for(int i=1;i<=n;++i){
        cin>>a[i];
        sums+=a[i];
    }
    for(int i=1;i<=n-1;++i){
        int u,v,cost;
        cin>>u>>v>>cost;
        if(u>v) swap(u,v);//把每条边的两个点按小大来排,大的负责这条边
        //比如1 2 那就把边看做是校区2的
        edge[i].u=u;
        edge[i].v=v;
        edge[i].cost=cost;
    }
    for(int i=1;i<=n-1;++i){
        int ans=0;
        //每条边就视为
        //1.它这条边所在校区的学生到外面校区,外面有多少校区就要经过该边多少次
        //2.外面校区学生到这个校区
        ans+=(a[edge[i].v]*(n-1)+(sums-a[edge[i].v]))*edge[i].cost*2;
        cout<<ans<<endl;
    }
    return 0;
}

这种情况是符合样例的,但是如果碰上下图中这种,多一层甚至几层的就有问题
比如对a-b这条边来说
1.它这条边所在校区的学生到外面校区,外面有多少校区就要经过该边多少次。外面有4个校区,但是经过该边的只有3个校区
2.外面校区学生到这个校区。同理,e校区的学生进到b校区也不用经过该边
在这里插入图片描述

  • 分析
    对于该题,我们可以把它想象成树(题目说了)
    那必有一个根节点,从根节点进入每一条分支,然后维护每一条边
    如何维护边呢?
    一条边连接了两个点,也就是两个校区a(假设为根节点),b,那就有a(以及和a相连的其他点)要通过边到b,也有b(以及与b相连的其他点)要通过边到a。
    a(以及和a直接相连或间接相连的其他点)要通过边到b:
    求出a下面几个结点的总学生人数(包括a),求出b下面有几个结点(包括b),a下面的所有学生都要通过a-b去到b下面的所有点
    b(下面的点)要通过边到a:求b下面一共有多少学生(包括b),求出a下面有几个结点(包括a),b及下面的点的所有学生都必须通过a-b去到a以及其他点
    在这里插入图片描述- 实现
    如何得到一个点下所有的学生数,以及所有的结点个数呢?
    应该想到用dfs,从上层到下层,下层在得到结论后,再层层返回给上层使得上层有一个总体的认知
    可以看到,叶子结点的总学生数就是它自己的学生数,总结点数就是1,即它自己,所以,在dfs到达叶子结点后,就能开始回传,上层结点通过对子节点的汇总最终得到数据
    注意记得开long long,不开过不了,因为结点个数有2e5个,每个节点的学生也有1e4,边的权重有1e4,乘一下会很大
#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
const int N=2e5+5;
struct node{
    int v,w,id;
};
vector<node>ve[N];
ll a[N],sum[N],sub[N],ans[N];//sum[i]存i这一点i以下的点的学生总数,sub[i]存i这一点i以下的点的个数
int n;
ll tot;
void dfs(int u,int pre){
    sum[u]=a[u];sub[u]=1;//sub[u]初始化为1,就它自己一个节点
    for(int i=0;i<ve[u].size();++i){
        int v=ve[u][i].v;
        int w=ve[u][i].w;
        int id=ve[u][i].id;
        if(v==pre) continue;//pre这个点就不要再深搜了,因为当初就是由pre到v的,再dfs会死循环
        dfs(v,u);
        sum[u]+=sum[v],sub[u]+=sub[v];
        ans[id]=(sum[v]*(n-sub[v])+sub[v]*(tot-sum[v]))*w;
        //加号前部分是v即它以下的点出去到其他点,后部分是其他点进到v及它以下的点
    }
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;++i){
        cin>>a[i];//每个校区的学生数
        tot+=a[i];
    } 
    for(int i=1;i<=n-1;++i){
        int u,v,w;
        cin>>u>>v>>w;
        ve[u].push_back({v,w,i});
        ve[v].push_back({u,w,i});
    }//n-1条边  把每个点相连边的另一个点放到它的数组中
    dfs(1,0);//从第一个点开始   0是一个树中没有的点,假设由这个点进入到1点  这种方法注意一下
    //为什么要有第二个参数?
    //为了知道当前结点是从哪个结点传进来的,即标记一下父结点
    //因为上面记录边的方式是看不出父结点,只知道相邻,
    //而汇总sum sub是要汇总下面结点的,即要排除父结点
    for(int i=1;i<n;++i) cout<<ans[i]*2<<endl;
    return 0;
}

补充:理解一下dfs()的结构
两个参数传进去,第一个参数表示即将进入的点,第二个参数表示从哪个点进入,可以理解为第二个点是第一个点的父结点
为什么要这样设计呢?
因为我们算ans数组的时候需要sum数组和sub数组,这两个数组可以通过不断地深搜再回溯得到,但是当初我们加边的时候,点和点之前是互相加到对方的ve数组里,没有区分谁是谁的父结点。我们传入两个参数就是为了能把父结点区分出来,这样得到的sum数组和sub数组才是我们想要的,才能计算出ans

也可以不用两个参数,在标记边的时候,就只记一侧,比如a-b,就记
a->b,到时候在b的相邻边里就可以不用判断有没有a了

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
const int N=2e5+5;
struct node{
    int v,w,id;
};
vector<node>ve[N];
ll a[N],sum[N],sub[N],ans[N];//sum[i]存i这一点i以下的点的学生总数,sub[i]存i这一点i以下的点的个数
int n;
ll tot;
void dfs(int u){
    sum[u]=a[u];sub[u]=1;//sub[u]初始化为1,就它自己一个节点
    for(int i=0;i<ve[u].size();++i){
        int v=ve[u][i].v;
        int w=ve[u][i].w;
        int id=ve[u][i].id;
        //if(v==pre) continue;//注释掉
        dfs(v);
        sum[u]+=sum[v],sub[u]+=sub[v];
        ans[id]=(sum[v]*(n-sub[v])+sub[v]*(tot-sum[v]))*w;
    }
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;++i){
        cin>>a[i];//每个校区的学生数
        tot+=a[i];
    } 
    for(int i=1;i<=n-1;++i){
        int u,v,w;
        cin>>u>>v>>w;
        if(u>v) swap(v,u);
        ve[u].push_back({v,w,i});
        //ve[v].push_back({u,w,i});注释掉
    }//n-1条边  把每个点相连边的另一个点放到它的数组中
    dfs(1);
    for(int i=1;i<n;++i) cout<<ans[i]*2<<endl;
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值