CF2023E Tree of Life

· · 题解

不会 1C,破防去看 1E,大战 1.5h 无果,破防*2,下大分。

神秘贪心题,赛时只想对了一半不到。

写这题题解前先安利一道思路比较类似的树上覆盖问题(P7246 手势密码),做完这两道题应该会对这类问题有一定启发?

如何设计一个合理的贪心策略?首先第一个思路肯定是从下往上解决问题,肯定设 h_u 表示使子树 u 中的所有边二元组(注意,需要包括 uu 的父亲这条边)全部被覆盖的最小路径条数。

不如设现在考虑到了点 u,已经求出了其儿子的 h_{v_1},h_{v_2},...,h_{v_k}。如何求出 h_u?从最简单的情况开始想,首先 (u,v_1),(u,v_2),...,(u,v_k),(u,fa_u) 这些边之间组成的边二元组(称这些边之间组成的边二元组为新的边二元组)是一定要被满足的。满足一个边二元组 (u,v_i),(u,v_j) 有两种方式:

自然的,一个新的定义出现了:设 f_u 表示在让 h_u 最小的情况下,最多能拿出多少条向上的路径去帮助其父亲 fa_u

继续考虑如何计算 h_u,可以知道的是,首先对于其任意一个儿子 v,至少要拿出 k 条路径出来以满足新的边二元组。如果 f_v\le k,那就新开 k-f_v 条路径,然后进行合并满足新的边二元组。在这个过程中,f_u 至少会获得 k 条向上的路径,因为要满足 (u,v_1),(u,fa_u),...,(u,v_k),(u,fa_u)k 个新的边二元组,然后对于一个儿子 v,如果 f_v>k,那么 f_u 又可以多获得 f_v-k 条路径,显然从 v 向上的路径可以往上再延伸一步去帮助 fa_u

这个算法看上去很对,实际上,这是错误的!!!

举个例子,对于一个 u 的两个儿子 v_i,v_j,完全可以分别拿出一条向上的路径在 u 处合并,让使用的路径数量减少 1。这样为什么可以让答案更优呢?因为可能向上帮助的路径不需要那么多。

如何修正这个想法呢?不如考虑先让所有向上的路径在 u 处合并,需要用的时候再拆开,这显然是合法的。可以认为这里使用了一个反悔贪心的思想。

可能看到这里你已经晕了。重新书写一下所有的定义:

h_u 表示使子树 u 中的所有边二元组(注意,需要包括 uu 的父亲这条边)全部被覆盖的最小路径条数。设 f_u 表示在让 h_u 最小的情况下,路径合并后拿出多少条向上的路径(也就是合并不了剩下的路径)去帮助其父亲 fa_u。设 g_u 表示在让 h_u 最小的情况下,子树 u 中最多合并出了多少条路径。

考虑如何进行转移,同样地,先考虑解决新的边二元组的问题。

如果 f_v\le k,应考虑从 g_v 中把一条路径拆成 2 条路径,如果这样子拆完都不够(f_v+2g_v\le k),那就只有新开路径了。(这里有一个需要注意的点,如果 f_vk 奇偶性不同,会出现拆完路径后多出一条空余的路径,而这条空余的路径是可以继续向上延伸的!)

在满足新的边二元组后,同样的,有初值 f_u=k,g_u=0,设 f^{'}_vg^{'}_v 分别表示在 v 的子树中还剩下多少条向上延伸的路径,v 的子树中还剩下多少条未拆的路径。

显然的,我们的目的是要对这些路径进行拆分重组,使尽量多的路径结合在一起,即让 g_u 最大。

这个问题比较麻烦,因为可以将 g^{'}_v 的路径进行拆分与其他儿子子树中的路径进行配对。

先考虑一个弱化的问题,现在有 n 个数,a_1,a_2,a_3,...a_n,每次可以选择两个非零的数同时减去 1,问最大操作次数。

结论是:

先假设 a_1\le a_2\le ...\le a_n

证明其实非常简单,首先第一种情况是显然的,主要证第二种情况,设 t=\lfloor \frac{\sum a_i}{2}\rfloor,考虑一个 t\times 2 的矩阵,每一行描述了操作的两个位置,于是只需要按列竖着填就行了,显然这样子填数不会造成一行操作的是两个同样的位置。

这个问题跟本题有什么关系呢?如果所有的 g^{'}_v 都是 0,上述问题的操作就相当于路径配对。但是有一个非常烦人的 g^{'}_v,怎么处理呢?

不如先形式化一下问题,现在有 k 个二元组 (f^{'}_i,g^{'}_i),对于每一个二元组,可以让 g^{'}_i\leftarrow g^{'}_i-1,f^{'}_i\leftarrow f^{'}_i+2,这样的操作可以选择任意次(在保证 g^{'}_i>0 的情况下才能操作)。然后要让最后所有 f^{'}_i 作为上面问题的输入生成的答案加上 \sum g^{'}_i 的和最大。

这看似是一个非常复杂的问题,因为不清楚每个二元组中的 g^{'}_i 到底拆多少出来才能更优秀。

不如先对二元组进行一个排序,下面假设 f^{'}_1\le f^{'}_2\le ... \le f^{'}_k

f^{'}_k< \sum_{i=1}^{k-1} f^{'}_i + 2g^{'}_i,这个时候情况比较复杂。如果要说的清楚需要分类讨论。

那么至此就可以以 \mathcal{O}{(n)} 复杂度算出 f,g,在实现时,并不需要去维护这个 h,实时计算答案就可以了。

放个代码,由于作者写这题处于红温状态,图方便用了排序,复杂度带了 log,仅供参考。

#include <cstdio>
#include <map>
#include <iostream>
#include <algorithm>
#include <bitset>
#include <queue>
#include <stack>
#include <vector>
#include <random>
#include <cstring>
#include <ctime>
#include <cmath>
#include <assert.h> 
#include <unordered_map>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/hash_policy.hpp>
using namespace __gnu_pbds;
using namespace std;
#define LL long long
#define pp pair<LL,LL>
#define mp make_pair 
#define ull unsigned long long
namespace IO{
    const int sz=1<<22;
    char a[sz+5],b[sz+5],*p1=a,*p2=a,*t=b,p[105];
    inline char gc(){
    //  return p1==p2?(p2=(p1=a)+fread(a,1,sz,stdin),p1==p2?EOF:*p1++):*p1++;
        return getchar();
    }
    template<class T> void gi(T& x){
        x=0; int f=1;char c=gc();
        if(c=='-')f=-1;
        for(;c<'0'||c>'9';c=gc())if(c=='-')f=-1;
        for(;c>='0'&&c<='9';c=gc())
            x=x*10+(c-'0');
        x=x*f;
    }
    inline void flush(){fwrite(b,1,t-b,stdout),t=b; }
    inline void pc(char x){*t++=x; if(t-b==sz) flush(); }
    template<class T> void pi(T x,char c='\n'){
        if(x<0)pc('-'),x=-x;
        if(x==0) pc('0'); int t=0;
        for(;x;x/=10) p[++t]=x%10+'0';
        for(;t;--t) pc(p[t]); pc(c);
    }
    struct F{~F(){flush();}}f; 
}
using IO::gi;
using IO::pi;
using IO::pc;
const int mod=998244353;
inline int add(int x,int y){
    return x+y>=mod?x+y-mod:x+y;
}
inline int dec(int x,int y){
    return x-y<0?x-y+mod:x-y;
}
inline int mul(int x,int y){
    return 1ll*x*y%mod;
}
inline int qkpow(int a,int b){
    if(b<0)return 0;
    int ans=1,base=a%mod;
    while(b){
        if(b&1)ans=1ll*ans*base%mod;
        base=1ll*base*base%mod;
        b>>=1;
    }
    return ans;
}
int fac[2000005],inv[2000005],Invn[600005];
inline int binom(int n,int m){
    if(n<m||m<0)return 0;
    return 1ll*fac[n]*inv[m]%mod*inv[n-m]%mod;
}
void init_C(int n){
    fac[0]=1;
    for(int i=1;i<=n;i++)fac[i]=1ll*fac[i-1]*i%mod; 
    inv[0]=1;
    inv[n]=qkpow(fac[n],mod-2);
    for(int i=n-1;i>=1;i--)inv[i]=1ll*inv[i+1]*(i+1)%mod;
//  Invn[0]=Invn[1]=1;
//  for(int i=1;i<=200000;i++)Invn[i]=(LL)(mod-mod/i)*Invn[mod%i]%mod;
}
int t,n,rt;
LL res,f[500005],g[500005];//f:up g:bouns 
vector<int>G[500005];
vector<pp>buc[500005],Q[500005];
inline bool cmp(pp A,pp B){
    return A.first<B.first; 
}
inline void dfs(int u,int ff){
    int son=0;
    for(auto v:G[u]){
        if(v==ff)continue;
        son++;
        dfs(v,u);
        buc[u].push_back(pp(f[v],g[v]));
    }
    if(!son)return ;
    res-=1ll*son*(son-1)/2;
    if(u==rt)son--;
    for(auto x:buc[u]){
        LL v1=x.first,v2=x.second;
        if(v1<=son){
            LL need=son-v1;
            if(2*v2>=need){
                if(need&1){
                    v2-=(need+1)/2;
                    res+=(need+1)/2;
                    //up-->1 bouns--> v2
                    Q[u].push_back(pp(1,v2));
                }else{
                    v2-=need/2;
                    res+=need/2;
                    //up-->0 bouns--> v2
                    Q[u].push_back(pp(0,v2));
                } 
            }else{
                //0 0 
                res+=v2; 
                res+=son-v1-v2*2;
            }
        }else{
            v1-=son;
            Q[u].push_back(pp(v1,v2));
        }
    }
    f[u]=son;g[u]=0;
    sort(Q[u].begin(),Q[u].end(),cmp);
    if(Q[u].size()){
        LL mx=0,sum=0;
        for(auto x:Q[u])mx=max(mx,x.first);
        LL sum2=0;
        for(int i=0;i<Q[u].size()-1;i++){
            pp x=Q[u][i];
            res+=x.second;
            sum2+=x.first+2*x.second;
        }
        if(sum2>=mx){
            f[u]+=(sum2+mx)%2;
            res-=(sum2+mx)/2;
                g[u]+=(sum2+mx)/2+Q[u][Q[u].size()-1].second;
        }else{
            f[u]+=mx-sum2;
            res-=sum2;
            g[u]+=sum2+Q[u][Q[u].size()-1].second;
        }
    }
}
inline void solve(){
    gi(n);
    for(int i=1;i<=n;i++)G[i].clear(),buc[i].clear(),Q[i].clear(),f[i]=g[i]=0;
    for(int i=1;i<n;i++){
        int u,v;
        gi(u),gi(v);
        G[u].push_back(v);
        G[v].push_back(u);
    }
    rt=0;
    for(int i=1;i<=n;i++)if(G[i].size()==1)rt=i;
    res=0;dfs(rt,0);
    pi(res);
}
signed main(){
    srand(time(0));
    gi(t);
    while(t--)solve();
    return 0;
}