题意简述
给定一棵树,每个节点有一个权值k,表示该节点有多少个海狸,从根节点出发,每吃一个海狸便能够且必须跳到与当前节点有直接边相连的节点上,要求最终跳回根节点,求最多能吃多少个海狸。
算法概述
考虑每个节点产生的贡献。
首先明确一点:每个节点产生的贡献与且只与其儿子节点有关。
先dfs递归计算出每个儿子的贡献。然后考虑当前节点,若当前节点u不是树根,则先将k[u]减去1(因为还要跳回父亲,需要吃掉1个海狸)。
设sons[u]为节点u的儿子数,计算当前节点的贡献:
(i) 若k[u]<sons[u],即无法吃遍所有儿子,则将儿子的贡献值排序,从大到小吃,每吃一个儿子将k[u]减1,直到k[u]减为0结束。
(ii) 若k[u]>=sons[u],说明可以吃遍所有儿子,那么进行完(i)中的操作(即吃遍所有儿子)之后k[u]还有剩余,记为last,则考虑在u与其儿子节点之间来回跳跃,sum统计所有儿子的剩余权值之和,那么还可产生的贡献即为2*min(last,sum)。
时间复杂度分析:
不难发现,时间复杂度的瓶颈主要在对所有儿子的贡献值进行排序的操作上。
设每个节点的儿子数量为s,则总时间=s1logs1+s2logs2+……+snlogsn<=s1logn+s2logn+……+snlogn=(s1+s2+……+sn)logn=(n-1)logn。
故时间复杂度为O(nlogn)。
参考代码
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <vector> #define x first #define y second using namespace std; typedef long long ll; typedef pair<ll,ll> pll; //从该点出发往下跳的贡献值,剩余权值 const int N=1e5+10; struct Edge{ int to,next; }edge[N<<1];int idx; int h[N]; int k[N]; int n,root; void add_edge(int u,int v){edge[++idx]={v,h[u]};h[u]=idx;} pll dfs(int p,int fa) { vector<ll> v; //所有儿子的贡献放入vector中 ll sum=0; int flag=1; //是否为叶子节点 for(int i=h[p];~i;i=edge[i].next) { int to=edge[i].to; if(to==fa)continue; flag=0; pll s=dfs(to,p); sum+=s.y; v.push_back(s.x); } if(flag)return make_pair(0,k[p]-1); /* 由于第一维是统计从该点出发往下跳,最后跳回来的总贡献值, 而该点为叶子节点,故为0。 第二维即从该点跳回父亲之后,剩余的权值, 由于跳回父亲需要吃掉一个海狸 故为k[p]-1。 而其跳回父亲这一步的贡献会在其父亲节点处计算。 */ sort(v.begin(),v.end()); ll last=k[p]-(p==root?0:1),eat=0; //若为根,则不必跳回父亲,否则需要跳回父亲,故减1。 for(int i=v.size()-1;i>=0&&last;i--,last--)eat+=v[i]+2; //v[i]为以该儿子为根的子树的总贡献,而后需要加上从该点跳到该儿子的贡献1以及从该儿子跳回该点的贡献1,故加2。 eat+=2*min(last,sum); //来回跳跃,一来一回即产生了2的贡献,故而是两倍的跳跃步数。 last-=min(last,sum); //维护剩余权值,来回跳跃之后需要减去跳跃步数。 //此处上面两行不必考虑last是否已经减为0,因为若last为0的话,则即使执行上面两行,也不会对答案产生影响。 return make_pair(eat,last); } int main() { memset(h,-1,sizeof h); scanf("%d",&n); for(int i=1;i<=n;i++)scanf("%d",&k[i]); for(int i=1;i<=n-1;i++) { int u,v;scanf("%d%d",&u,&v); add_edge(u,v),add_edge(v,u); } scanf("%d",&root); printf("%lld ",dfs(root,0).x); return 0; }