【AGC023F】01 on Tree(树上一类全序问题)

本文探讨了一种在有树结构约束的优先级选择问题中,如何通过改进贪心算法来处理优先级相同节点的选择难题。提出了一种结合优先级和子节点最优性的解决方案,以及使用可删堆实现的算法。讨论了关键示例和代码实现,以确保序列构建的正确性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

显然如果没有树的限制,我们优先选 0 0 0,然后选 1 1 1

如果有了树的限制,我们考虑下面这么一种贪心方法:假设当前能够选的点的集合为 S S S(初始时 S S S 只包含根),然后选出 S S S 中优先级最大的点 u u u 0 0 0 的优先级大于 1 1 1 的优先级)放在序列末尾,然后把 u u u S S S 中删除,并且把 u u u 的儿子都塞进 S S S 里面,再重复上述过程直至 S S S 为空为止。

这个贪心方法看起来很对,但可能会出现下面这种情况:

在这里插入图片描述

如图,我们选完根节点 1 1 1 后, S S S 中包含的是节点 2 2 2 和节点 3 3 3,显然这两个优先级相同,那我们是不是随便选一个就好了呢?显然不是,因为如果选节点 3 3 3 之后能得到两个 0 0 0,但如果选节点 2 2 2 之后只能得到一个 0 0 0,所以显然选节点 3 3 3 更优。

这说明当 S S S 中两个点优先级相同时,应该还需要有第二关键字作比较。但你发现这个第二关键字不太好处理,下面两组数据应该能 hack 掉大部分:

  1. 在这里插入图片描述

    最优选择: { 1 , 3 , 5 , 6 , 2 , 4 , 7 , 8 , 9 } \{1,3,5,6,2,4,7,8,9\} {1,3,5,6,2,4,7,8,9},得到序列: { 0 , 1 , 0 , 0 , 1 , 0 , 1 , 1 , 1 } \{0,1,0,0,1,0,1,1,1\} {0,1,0,0,1,0,1,1,1}

  2. 在这里插入图片描述

    最优选择: { 1 , 2 , 4 , 7 , 8 , 9 , 10 , ⋯   , 114514 , 3 , 5 , 6 } \{1,2,4,7,8,9,10,\cdots,114514,3,5,6\} {1,2,4,7,8,9,10,,114514,3,5,6},得到序列: { 0 , 1 , 0 , 1 , 0 , 0 , 0 , ⋯   , 0 , 1 , 0 , 0 } \{0,1,0,1,0,0,0,\cdots,0,1,0,0\} {0,1,0,1,0,0,0,,0,1,0,0}

我们考虑换一种方法:我们先把所有点按优先级排序。

我们先取出最优的那个,设为 u u u。若 u u u 为根,那么直接选即可;若 u u u 不为根,那么设 u u u 的父亲为 f f f,那么当 f f f 被选之后是否一定马上选 u u u 呢?如果 f f f 的其他儿子的优先级都比 u u u 小那么肯定是的,但如果有多个儿子和 u u u 优先级相同呢?(就和我们一开始说的情况一样)答案也是肯定的,因为即使 f f f 有多个和 u u u 同样优先级的儿子,由于 u u u 是堆里面最优的,所以这些儿子的优先级肯定也都是最优的,于是要选肯定是把这些儿子一起全部选完,所以这些儿子之间没有先后顺序之分,所以你钦定让 f f f 被选之后马上选 u u u 是没有问题的。

注意这个结论只针对最优的那个点 u u u,对其他点并不适用。但既然我们已经知道了这个结论,不妨直接把最优点 u u u 的贡献给处理掉:若 u u u 为根则直接选它,若 u u u 不为根则 f f f 被选之后马上选 u u u ,不妨直接让 u u u f f f 合并。然后再找到此时的最优点重复上述步骤。这个过程需要用可删堆/set来维护。注意由于一个点可能是由多个点合并得到的,此时的优先级是以 0 的个数 1 的个数 \dfrac{0\text{的个数}}{1\text{的个数}} 1的个数0的个数 作为比较标准,这个可以用全序来证明。

代码如下:

#include<bits/stdc++.h>

#define N 200010
#define ll long long

using namespace std;

inline int read()
{
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=(x<<1)+(x<<3)+(ch^'0');
		ch=getchar();
	}
	return x*f;
}

int n,rt[N],fa[N];
int nn0[N],nn1[N];
bool del[N];
ll ans;

struct cmp//重载set中int比较的方法
{
	bool operator()(const int &a,const int &b) const //这里末尾一定要有const
	{
		if(1ll*nn1[a]*nn0[b]==1ll*nn0[a]*nn1[b]) return a<b;
		return 1ll*nn1[a]*nn0[b]<1ll*nn0[a]*nn1[b];
	}
};

set<int,cmp>s;

int find(int x)
{
	return x==rt[x]?x:(rt[x]=find(rt[x]));
}

int main()
{
	del[0]=1;
	n=read();
	for(int i=1;i<=n;i++) rt[i]=i;
	for(int i=2;i<=n;i++) fa[i]=read();
	for(int i=1;i<=n;i++) (read()?nn1[i]:nn0[i])++;
	for(int i=1;i<=n;i++) s.insert(i);
	int d0=0,d1=0;
	while(!s.empty())
	{
		int u=(*s.begin());
		s.erase(s.begin());
		int f=find(fa[u]);
		if(del[f])
		{
			del[u]=1;
			ans+=1ll*d1*nn0[u];
			d0+=nn0[u],d1+=nn1[u];
			continue;
		}
		s.erase(f);
		rt[u]=f;
		ans+=1ll*nn1[f]*nn0[u];
		nn0[f]+=nn0[u],nn1[f]+=nn1[u];
		s.insert(f);
	}
	printf("%lld\n",ans);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值