OpenJudge1043 树上游戏(换根dp+细节处理)

树上游戏

给定一棵 n n n 个节点的树,点从 1 1 1 n n n 编号,点有点权,边有边权, Alice \text{Alice} Alice Bob \text{Bob} Bob 两人在做游戏。

棋子以某一个点 s s s 为起点,玩家移动该棋子,有以下两条规则:

  1. 移动时不能经过已经走过的边
  2. 能移动则必须移动,不能在可移动时停留在原地

Alice \text{Alice} Alice 开始,轮流移动棋子,最终的得分为经过的点权之和减去经过的边权之和。

Alice \text{Alice} Alice 想要最大化得分, Bob \text{Bob} Bob 想要最小化得分,假设两人都采取最优策略,那么最终得分是多少呢?

请你对于每个点为起点都输出一个答案。
1 ≤ n ≤ 1 0 5 , ∣ v i ∣ ,   ∣ w i ∣ ≤ 1 0 9 1\le n\le 10^5,\vert v_i\vert,\ \vert w_i\vert \le 10^9 1n105,vi, wi109


不难发现当起点确定时可以通过如下树形dp确定结果:

状态表示: f u , 0 / 1 f_{u,0/1} fu,0/1 u u u为起点向子树中移动,当前该 Alice / Bob \text{Alice}/\text{Bob} Alice/Bob最终的值
状态转移:
下面 val u \text{val}_u valu表示 u u u的点权, w i w_i wi表示 u → v u\to v uv的边权

  • Alice \text{Alice} Alice需要让结果尽可能大: f u , 0 = max ⁡ v ∈ son u ( f v , 1 + val u − w i ) f_{u,0}=\max_{v\in \text{son}_u}(f_{v,1}+\text{val}_u-w_i) fu,0=maxvsonu(fv,1+valuwi)
  • Bob \text{Bob} Bob需要让结果尽可能小: f u , 1 = min ⁡ v ∈ son u ( f v , 0 + val u − w i ) f_{u,1}=\min_{v\in \text{son}_u}(f_{v,0}+\text{val}_u-w_i) fu,1=minvsonu(fv,0+valuwi)

注意:在叶子节点进行初始化

由于存在换根操作,不难发现只要记录 f u , 0 f_{u,0} fu,0的最大次大值以及 f u , 1 f_{u,1} fu,1的最小次小值即可从父节点更新子节点实现换根操作。


注意这种情况:
当前根是 u u u,准备换根到 v v v,我们需要让 u u u的信息去更新 v v v的信息,如果 v v v在树中是叶子节点,需要特殊地 u u u的信息直接赋给 v v v而不是更新,因为 v v v是叶子节点,在以 u u u为根时走到 v v v就会停止,而在以 v v v为根时,并不会停止并且一定会向 u u u移动!!!

还有在第一遍dfs时不能以入度为1的点为根进行dfs,因为这个点在其他点为根的时是叶子节点,某些信息会错误更新比如下面数据

5
2147483647 2147483647 2147483647 2147483647 -2147483647 
1 2 2147483647
2 3 2147483647
3 4 2147483647
4 5 2147483647

总的来说需要注意叶子节点的信息的更新

#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
using ll=long long;
constexpr int N=100010;
constexpr ll INF=0x3f3f3f3f3f3f3f3f;
int h[N],e[2*N],ne[2*N],w[2*N],idx;
void add(int a,int b,int c){e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;}
int val[N],n;
ll f[N][2],g[N][2],ans[N];// f[u][0/1]表示u子树 该A/B移动
int fr[N][2];
int d[N];
bool leaf[N];
void update(int u,int k,int v,ll x)// 更新 最大次大/最小次小
{
    if(k==0)
    {
        if(x>f[u][k]) 
        {
            g[u][k]=f[u][k];
            f[u][k]=x;
            fr[u][k]=v;
        }
        else if(x>g[u][k]) g[u][k]=x;
    }
    else
    {
        if(x<f[u][k])
        {
            g[u][k]=f[u][k];
            f[u][k]=x;
            fr[u][k]=v;
        }
        else if(x<g[u][k]) g[u][k]=x;
    }
}
void dfs1(int u,int fa)
{
    leaf[u]=1;
    for(int i=h[u];i!=-1;i=ne[i])
    {
        int v=e[i];
        if(v==fa) continue;
        leaf[u]=0;
        dfs1(v,u);
        for(int k=0;k<=1;k++)
            update(u,k,v,f[v][k^1]+val[u]-w[i]);
    }
    if(leaf[u]) f[u][0]=f[u][1]=val[u];//叶子节点初值
}
void dfs2(int u,int fa)
{
    ans[u]=f[u][0];
    for(int i=h[u];i!=-1;i=ne[i])
    {
        int v=e[i];
        if(v==fa) continue;
        for(int k=0;k<=1;k++)
        {
            if(leaf[v])//叶子节点特殊处理
            {
                if(fr[u][k]==v) 
                    f[v][k^1]=g[u][k]+val[v]-w[i];
                else 
                    f[v][k^1]=f[u][k]+val[v]-w[i];
            }
            else//换根
            {
                if(fr[u][k]==v) 
                    update(v,k^1,u,g[u][k]+val[v]-w[i]);
                else
                    update(v,k^1,u,f[u][k]+val[v]-w[i]);
            }
        }
        dfs2(v,u);
    }
}
int main()
{
    ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
    cin>>n;
    memset(h,-1,sizeof h);
    for(int i=1;i<=n;i++) cin>>val[i];
    for(int i=1;i<=n;i++) f[i][0]=g[i][0]=-INF,f[i][1]=g[i][1]=INF;
    for(int i=1;i<n;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c),add(b,a,c);
        d[a]++,d[b]++;
    }
    int rt=1;
    while(d[rt]==1) rt++;//找一个度数不为1的为根
    dfs1(rt,0);
    dfs2(rt,0);
    for(int i=1;i<=n;i++) cout<<ans[i]<<'\n';
}

这题我一共写了4次
第一次:比赛口胡
第二次:赛后写代码没过懒得调了
第三次:2021/01/04刚放寒假重新写,没注意叶子的细节一直没过
第四次:2021/02/25快开学了准备吧收藏夹的题补一补,有看见这个题,造了造特数数据发现了代码问题,注意到叶子细节成功AC(经历2.5h)

终于把这题干掉了!!!
要加油哦~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值