2021.8.12携程笔试第三题:建树游戏DFS

3 篇文章 0 订阅

2021.8.12携程笔试

在做最后一题的时候把题意看错了,悔之莫及,故记录此文引以为戒!


建树游戏
问题描述

有n个节点和n-1条边,形成一棵树,每个节点有一个权值。把其中一条边删除就形成了两棵树,在两棵树之间重新接一条新的边就可以形成一颗新树。新树的权值等于新增边的两点权值相乘。
每条边都可以删除,且可新加的边有很多,故可以形成很多新树,请计算这些新树的数量;同时对于每一条边,删除后可以产生的若干新树的权值之和也不一定相同,请计算这些权值之和中的最大值。

输入描述

第一行整数n,表示点的数量,3⩽ n ⩽100000
第二行n-1个整数,空格隔开,第i个整数ai表示点ai与i之间有一条边
第三行n个整数,空格隔开,表示各个点的权值。0<权值<10000。

输出描述

一行,两个整数,用空格隔开,表示新树的总数量,以及各点删除后可以产生的新树的权值之和的最大值。

样例输入
3
2 3
1 2 3
样例输出
2 3
问题分析

首先简化问题,如果断开一条边,能产生多少种新树呢:答案是这条边两端节点数之积减去原来的边,即两两组合。
同时,能产生的所有新树的权值之和呢:答案这条边两端所有权值之积j减去原先连接的边即可。
那么我们只需DFS一遍,遍历所有的边即可。每个子节点对应一颗子树,将父节点与子节点之间的边断开就可以形成两棵树,然后更新答案。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10;
int n;
ll a[N],tot;
vector<int>g[N];
ll ans1,ans2;
void init()
{
    tot=ans1=ans2=0;
    a[0]=0;
    for(int i=1; i<=n; i++)
        g[i].clear();
}
struct node
{
    ll cnt,tmpMax,tmptot;
};
node dfs(int pre,int u) // 求所有新树权值之和的最大值
{
    long long cnt=1;
    long long tc=a[u];
    int len=g[u].size();
    node tmp;
    for(int i=0;i<len;i++)
    {
        int v=g[u][i];
        if(v==pre)continue;
        tmp = dfs(u,v);
        cnt+=tmp.cnt;
        tc+=tmp.tmptot;
        ans1+=(n-tmp.cnt)*tmp.cnt-1;
        ans2=max(ans2,(tot-tmp.tmptot)*tmp.tmptot-a[u]*a[v]);
//        cout<<v<<" "<<tmp.tmptot<<endl;
    }
    tmp.cnt=cnt,tmp.tmptot=tc;
    return tmp;
}
int main()
{
    int x;
    while(~scanf("%d",&n))
    {
        init();
        for(int i=1; i<n; i++)
        {
            scanf("%d",&x);
            g[i].push_back(x);
            g[x].push_back(i);
        }
        for(int i=1; i<=n; i++)
        {
            scanf("%lld",&a[i]);
            tot+=a[i];
        }
        dfs(0,1);
        cout<<ans1<<" "<<ans2<<endl;;
    }
    return 0;
}

/*
3
2 3
1 2 3
4
4 1 2
1 1 2 3

*/
问题变形

在读题的时候没看到之和,而误以为是求所有新树的权值的最大值。即每棵新树对应一个权值,求所有可能的新树权值的最大值。
如果是这种题意该如何解答呢,问题相当于任意两个原本不连接的点的权值之积最大。
这个问题也可以通过DFS来解决。
我们只需找出除父节点之外其余节点的最大值即可,与当前点进行连接。
如何理解呢:

  • 首先要明白父节点和当前子节点代表两颗新树
  • 因为答案中肯定存在两点相连,并且在DFS的时候任意两点肯定有遍历的先后,并且我们需要使得这两点边权尽量大,这样使得乘积尽量大。
  • 我们无需同时考虑两点,我们只需更新遍历过的所有点除父节点外的最大权值,那么在遍历当前点的时候,当前点就是被连接的点,当前子节点代表的子树还未被遍历。
  • 即固定当前点,我们只需找到除父节点外,父节点所代表的子树的所有权值最大值。
  • 父节点所代表的子树会先被遍历,如果父节点那边存在未遍历的点怎么办呢,还是原来的问题,两个点会存在先后遍历的顺序,当我们遍历到被连接的点的时候,其他可能的点会先被遍历到
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10;
int n;
ll a[N],tot;
vector<int>g[N];
ll ans1,ans2;
void init()
{
    tot=ans1=ans2=0;
    a[0]=0;
    for(int i=1; i<=n; i++)
        g[i].clear();
}
struct node
{
    ll cnt,tmpMax,tmptot;
};
node dfs1(int pre,int u,ll preMax)  // 求所有新树权值的最大值
{
    int len=g[u].size();
    long long cnt=1;
    node tmp;
    for(int i=0; i<len; i++)
    {
        int v=g[u][i];
        if(v==pre)
            continue;
        ans2=max(ans2,preMax*a[v]);
        tmp=dfs1(u,v,max(preMax,a[u]));
        ans1+=(tmp.cnt*(n-tmp.cnt))-1;
        cnt+=tmp.cnt;
        preMax=max(preMax,tmp.tmpMax); // 其他子树下的最值
    }
    tmp.cnt=cnt;
    tmp.tmpMax=max(preMax,a[u]);
   // cout<<u<<" "<<cnt<<endl;
    return tmp;
}
int main()
{
    int x;
    while(~scanf("%d",&n))
    {
        init();
        for(int i=1; i<n; i++)
        {
            scanf("%d",&x);
            g[i].push_back(x);
            g[x].push_back(i);
        }
        for(int i=1; i<=n; i++)
        {
            scanf("%lld",&a[i]);
            tot+=a[i];
        }
        dfs(0,1);
        cout<<ans1<<" "<<ans2<<endl;;
    }
    return 0;
}

/*
3
2 3
1 2 3
4
4 1 2
1 1 2 3
*/

这种类型的题目与POJ3140有些类似

POI 3140 Contestants Division

问题描述

给你一棵树,要求你选择一条边进行断开,使得断开后两棵子树权值和之差最小。

问题分析

这个题解法很简单,DFS统计子树权值之和,然后计算差值更新答案。
但是这题坑点很多,首先数据范围,在long long的数据范围,而且差值绝对值不能用abs()函数对 long long 取绝对值!应该用llabs(long long )函数原型,或者取负号,求正负最值。

整型类型变量(整数)取绝对值:
int abs( int x );
long int labs( long x );
long long int llabs( long long x );

浮点类型变量(小数)取绝对值:
double( double x );  
float fabsf(float x);
long double fabsl( long double x) ;
相关头文件:
#include <stdlib.h> // #include <cstdlib>
#include <math.h> // #include <cmath> 

另外一个坑点在于m虽然很大,但是实际一棵树只与n有关,m是误导信息,不过内存稍微开大几倍即可。

const int N=1e5+10;
struct Edge
{
    int to,next;
}e[N*20];
int n,m,head[N],tot;
ll sum,ans,a[N];
void init()
{
    ans=1e15;
    sum=tot=0;
    memset(head,-1,sizeof(head));
}
void add(int u,int v)
{
    e[tot].to=v,e[tot].next=head[u];
    head[u]=tot++;
}
ll dfs(int pre,int u)
{
    ll tmp=a[u];
    for(int i=head[u];i+1;i=e[i].next)
    {
        int v=e[i].to;
        if(v==pre) continue;
        ll cnt=dfs(u,v);
        ans=min(ans,llabs(cnt-(sum-cnt))); // ans=min(ans,max(cnt-(sum-cnt),(sum-cnt)-cnt));
        tmp+=cnt;
    }
    return tmp;
}
int main()
{
    int u,v;
    int cnt=0;
    while(~scanf("%d%d",&n,&m)&&(n||m))
    {
        init();
        for(int i=1;i<=n;i++)
        {
            scanf("%lld",&a[i]);
            sum+=a[i];
        }

        for(int i=0;i<m;i++)
        {
            scanf("%d%d",&u,&v);
            add(u,v);
            add(v,u);
        }
        dfs(0,1);
//        cout<<ans<<endl;
        printf("Case %d: %lld\n",++cnt,ans);
    }

    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值