【CF】1998C-Construct a tree 题解

传送门:1998C
标签:图论

题目大意

米沙想要构建一个有n个顶点的有根树,顶点编号从1到n,其中根节点的索引为1。除了根节点外的每一个顶点都有一个父节点pi,而i则被称为顶点pi的一个子节点。顶点u属于顶点v的子树当且仅当通过遍历父节点(u,pu,ppu,…)可以从u到达v。显然,每个顶点都属于它自身的子树,并且子树中顶点的数量被称为该子树的大小。米沙只对那些每个顶点都属于顶点1的子树的树感兴趣。下面有一棵有6个顶点的树。顶点2的子树包含顶点2,3,4,5。因此它的子树大小为4。树的分支系数定义为任意顶点的最大子节点数。例如,对于上面的树,分支系数等于2。你的任务是构建一棵有n个顶点的树,使得所有顶点的子树大小之和等于s,并且分支系数尽可能小。

输入:唯一的输入行包含两个整数n和s —— 树中的顶点数量和期望的子树大小总和(2≤n≤105;1≤s≤1010)。

输出:如果所需的树不存在,则输出「No」。否则在第一行输出「Yes」,然后在下一行输出整数p2,p3,…,pn,其中pi表示顶点i的父节点。

算法分析

  • 很显然,每个顶点属于其到根节点路径上经过的所有顶点的子树。所以,所有子树的大小之和等于p + n,其中p是所有从根到各个顶点路径长度的总和。现在我们来考虑具有不大于k的分支系数的树可以达到的子树大小之和是多少。最小的子树大小之和可以在k叉树中实现(它由根节点开始,距离为1的有k个顶点,距离为2的有k²个顶点等等,最后一层可能不会完全填满)。
  • 最大的子树大小之和可以在竹竿树(bamboo)中实现——这种树只有一条长度为n的路径。如果s大于这种树中的总和,那么答案就是「No」。我们找到最小的k,使得s不小于k叉树中子树大小之和(可以使用二分查找)。现在s介于分支系数不大于k的树中子树大小之和的最小值和最大值之间。
  • 实现递归函数的具体方法如下。假设我们在一个子树中,并希望在这个子树中增加x的子树大小之和。我们可以将这个子树转换为相同顶点数目的竹竿树(如果子树大小之和不会太大)。否则,我们按照某种顺序从子树中运行这个函数。如果我们达到了所需的总和,我们将终止过程。否则,每个子节点的子树现在都是竹竿树,当前的子树大小之和小于所需的值,但如果合并这些竹竿树,它将会比所需的更大。让我们从其他竹竿树移动顶点到第一个竹竿树的末尾。随着时间推移,在下一次移动后,子树大小之和会变得过大。我们可以将顶点移动到第一个竹竿树中的另一个位置,以便使子树大小之和等于s,并终止过程。
  • 如果相信任何子树大小之和介于最大值和最小值之间的树都可以被构建,那么还有一个复杂度为O(nlogn)的更易于实现的解决方案。子树大小之和仅受每种距离上的顶点数的影响,而不是它们之间的相互排列。子树大小之和等于n + i * di,其中di是从根节点距离为i的顶点数。让我们构建这个计数数组。这些条件必须满足:如果第i个元素大于0(i > 0),那么第i-1个元素也必须大于0。如果第i个元素等于t,第i+1个元素不应该大于t * k。元素的总和必须等于n。假设我们已经恢复了这个数组的一些前缀(即我们知道我们可以填充剩余部分,并且子树大小之和等于s)。让我们尝试在下一个元素放置某个值。
  • 我们知道树可以重新构造,使得总和在最大值和最小值之间,所以有两个条件需要满足(我们希望在位置i放置值x)。x必须足够大,如果用x, xk, xk², …(最后一个非零数可能较小,这些数的总和等于n)填充后缀,子树大小之和不会超过s。x必须足够小,以至于如果用1填充后缀,子树大小之和不会小于s。这两个边界形成了一段可以放在这个位置的值范围。我们不需要寻找左边界,只需通过二分查找找到右边界,并放上等于它的数。就可以轻松地使用数组d恢复树。

代码实现

#include <iostream>
#include <algorithm>
#include <stack>
#include <queue>
#include <map>
#include <set>
#include <iomanip>

using namespace std;

typedef long long ll;

const int MAXN = 100005;

ll n,s,id;
ll c[MAXN];
ll st[MAXN];
ll ch[MAXN];

void solve(int bc)
{
	ll tmp = n,lev = 0;
	for (ll i = 1;tmp;i++)
	{
		lev = i;
		ll lim = min(tmp,(i == 1 ? 1 : c[i - 1] * bc));
		for (ll j = 1;j <= lim;j++)
		{
			ll remain = tmp - j - 1;
			if (j == lim || remain * (i + 1) + remain * (remain + 1) / 2 < s - i * (j + 1))
			{
				c[i] = j;
				tmp -= j;
				s -= j * i;
				break;
			}
		}
	}
	int ST = lev;
	for (int i = 2;i <= lev;i++)
		if (c[i] <= (c[i - 1] - 1) * bc)
		{
			ST = i - 1;
			break;
		}
	while (s)
	{
		int minus = min(s,lev - ST);
		s -= minus;
		c[ST]--;
		c[ST + minus]++;
		while (c[ST] <= (c[ST - 1] - 1) * bc)
			ST--;
	}
	st[1] = 1;
	id = 2;
	for (int i = 2;i <= lev;i++)
	{
		st[i] = id;
		for (int j = 1;j <= c[i];j++,id++)
		{
			cout << st[i - 1] << ' ';
			ch[i - 1]++;
			if (ch[i - 1] == bc)
			{
				st[i - 1]++;
				ch[i - 1] = 0;
			}
		}
	}
	cout << '\n';
}

int main()
{
	cin >> n >> s;
	if (n * (n + 1) / 2 < s || 2 * n - 1 > s)
	{
		cout << "No" << '\n';
		return 0;
	}
	cout << "Yes" << '\n';
	for (ll i = 1;i < n;i++)
	{
		ll sum = 0,cnt = 1,tmp = n;
		for (ll j = 1;tmp;j++)
		{
			sum += min(cnt,tmp) * j;
			tmp -= min(cnt,tmp);
			cnt *= i;
		}
		if (sum > s)
			continue;
		solve(i);
		return 0;
	}
	return 0;
}
  • 14
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值