D. Paths on the Tree(树形dp)

https://codeforces.com/contest/1746/problem/D

题意
给定一个 n 个节点的有根树,根节点编号为 1。
对于每个节点有价值 s [ i ] s[i] s[i]

现在要选出 k 条简单路径,满足:

  • 每条路径都从根节点 1 开始;
  • 定义节点 i i i 一共被经过了 c i c_i ci 次,要满足对于任意的兄弟节点 ( u , v ) (u, v) (u,v) ∣ c u − c v ∣ ≤ 1 |c_u−c_v|≤1 cucv1

这 k 条简单路径的总价值定义为 ∑ i = 1 n c i ∗ s i \sum\limits_{i=1}^n c_i *s_i i=1ncisi

问,如何挑选能够使得总价值最大?输出最大价值。

2 ≤ n ≤ 2 ⋅ 1 0 5 ,   1 ≤ k ≤ 1 0 9 2 \le n \le 2 \cdot 10^5,\ 1 \le k \le 10^9 2n2105, 1k109
0 ≤ s i ≤ 1 0 4 0 \le s_i \le 10^4 0si104

思路
一共 k 条路径,那么根节点 1 就被经过了 k 次,为了让总价值最大,并且满足兄弟节点被经过次数之差不超过 1,假设根节点有 cnt 个儿子,那么每个儿子初始要被经过 k/cnt 次,还有 k%cnt 个儿子多经过一次。(场上连这个都没想到,只是看着样例画画,啥也没画出来,没有一点想法。。

根节点经过 k 次,那么所有节点至少被经过的次数就被确定了,假设为 k i k_i ki 次。
假设一个节点 x 被经过了 k i k_i ki 次,有 c n t cnt cnt 个儿子,那么所有儿子都至少经过了 k i / c n t k_i/cnt ki/cnt 次,然后 k i % c n t k_i\%cnt ki%cnt 个儿子多经过一次。

如何判断哪些儿子被多经过一次呢?
如果一个儿子多经过一次之后,相比于原来得到的增量最大,那么这个儿子要被多经过一次。
按照增量从大到小排序,取前 k i % c n t k_i\%cnt ki%cnt 个,多经过一次。

所以要维护出来每个节点经过 k i k_i ki 次 和 再多经过一次 得到的最大价值。

定义 f[i][0] 表示,节点 i 经过 k i k_i ki 次之后,以该节点为根的子树得到的最大价值;
f[i, 1] 表示,节点 i 经过 k i + 1 k_i + 1 ki+1 次之后,以该节点为根的子树得到的最大价值。

节点 x 经过 k i k_i ki 次: f[x, 0],从增量最大的 k i % c n t k_i\%cnt ki%cnt 个儿子的 f[tx, 1] 转移,剩下的从 f[tx, 0] 来转移;

for(int i=1;i<=idx;i++) //所有的儿子节点a[i]
{
	int tx = a[i];
	if(i <= left) ans[x][0] += ans[tx][1];
	else ans[x][0] += ans[tx][0];
}

节点 x 经过 k i + 1 k_i + 1 ki+1 次: f[x, 1],注意要从增量最大的 k i % c n t + 1 k_i\%cnt + 1 ki%cnt+1 个儿子的 f[tx, 1] 转移,而不是从最大的 ( k i + 1 ) % c n t (k_i+1)\%cnt (ki+1)%cnt 个儿子,因为儿子至少经过的次数是 k i k_i ki,不是 k i + 1 k_i+1 ki+1,这里只是额外多经过一次,也就是取模之后剩余的多了一个。然后剩下的儿子从 f[tx, 0] 来转移。

for(int i=1;i<=idx;i++)
{
	int tx = a[i];
	if(i <= left + 1) ans[x][1] += ans[tx][1];
	else ans[x][1] += ans[tx][0];
}

最后的答案就是根节点经过 k 次的最大价值 f[1, 0]

Code

#include<bits/stdc++.h>
using namespace std;

#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define int long long
#define fi first
#define se second

const int N = 200010, mod = 1e9+7;
int T, n, m;
int a[N], c[N];
int ans[N][2];
vector<int> e[N];
int k;

void init(){
	for(int i=1;i<=n;i++) e[i].clear();
}

bool cmp(int x, int y){
	return ans[x][1] -ans[x][0] > ans[y][1] - ans[y][0];
}

void dfs(int x, int sum)
{
	ans[x][0] = sum * c[x];
	ans[x][1] = (sum + 1) * c[x];
	
	int cnt = e[x].size();
	for(int tx : e[x])
	{
		dfs(tx, sum / cnt);
	}
	int idx = 0;
	for(int tx : e[x]) a[++idx] = tx;
	sort(a+1, a+idx+1, cmp); //根据答案增量排序,选增量最大的sum%cnt个儿子多经过一次。 
	
	if(cnt)
	{
		int left = sum % cnt;
		for(int i=1;i<=idx;i++)
		{
			int tx = a[i];
			if(i <= left) ans[x][0] += ans[tx][1];
			else ans[x][0] += ans[tx][0];
			
			if(i <= left + 1) ans[x][1] += ans[tx][1];
			else ans[x][1] += ans[tx][0];
		}
	}
}

signed main(){
	Ios;
	cin >> T;
	while(T--)
	{
		cin >> n >> k;
		init();
		
		for(int i=2;i<=n;i++)
		{
			int x; cin >> x;
			e[x].push_back(i);
		}
		for(int i=1;i<=n;i++) cin >> c[i];
		
		dfs(1, k);
		
		cout << ans[1][0] << endl;
	}
	
	return 0;
}

还是太菜了。。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值