传送门
总体思路
首先那个乘以 \(l_i\) 显然不好处理,我们可以简单转化一下问题把它规避掉:
给定一棵树,根节点为 1。
每个点有一个点权 \(c_i\)。
对于每个点,求以该点为根的子树内最多能取多少个点,使得这些点的点权和不超过一个给定常数 \(m\)。
(最后的答案就是每个点的答案乘上该点的 \(l_i\) 的最大值。)
是不是清真多了。
如果不是要求每个点的答案,只是要求根节点的答案,是不是很简单?一个贪心就可以了。
不会这个贪心的可以先别看这个题了,不适合。
那如果要求多次,最朴素的思路无疑是每个节点都来一遍贪心,这样的话是 \(O(\sum_{i=1}^{n}\textit{size}_i\log \textit{size}_i)\) 的复杂度,一条链就吃不消了。
那个 \(\log\) 是排序带来的复杂度。
优化 1:
把排序改为归并排序去掉这个 \(\log\)。
然而还是会被链卡爆 = = 。
优化 2:
多维护一下每个点达到最优时的方案。
容易发现,一个点的方案里的所有点,要么是这个点本身,要么也在某个儿子的方案里。
证明?调整法。
太简单了,这里略去。
这样的话可以直接归并子树方案然后从大到小扫直到满足,实际效果应该会好一点。
为啥要倒着扫?为了保证每个点在这一部分总共(指 \(n\) 个点每个点计算方案的时候总共)至多被扫描到一次,保证复杂度。
但是仍然会被链+巨大的预算+巨便宜的薪水卡爆 = = 。
优化 3(正解):
如果理解了优化 2,这一步就很简单了。
优化 2 被卡的瓶颈在于什么?合并子树信息的效率还不够。
倒着扫的操作已经很好地处理了合并之后每个点总共至多被扫描到 1 次,但是合并的时候还是会被反复扫描。
所以现在我们要维护什么?
其实就是,一个数据结构支持:
-
以一个元素初始化(因为根节点自己不会在子树的方案里出现,要特判一下。)
-
删除最大元素(上文的从大到小扫的时候要删除。)
-
与另一个同样的数据结构合并(合并子树信息。)
原来的数据结构相当于一个已排序的数组,初始化和删除最大都是 \(O(1)\),但第三个操作达到了 \(O(n)\)(\(n\) 是数组长度) 。
好,现在我们就要找一个能同时满足三个复杂度都比较可接受的数据结构,
第一个操作比较迷惑,先不理它。
只看后两个,应该很容易想到平衡树或者可并堆吧。
复杂度 \(O(n\log n)\),就,能过了。
代码&其它
我的代码是可并堆,偷懒用 pb_ds 的。
注意到保证了 \(b_i\le i\),所以连 dfs 都省了,倒着处理就行了(见代码)。
#include
#include
#define int unsigned long long
using namespace std;
const int N=1e5+9;
int b[N],c[N],l[N],sum[N];
__gnu_pbds::priority_queueq[N];
int n,m,ans;
signed main(){
cin>>n>>m;
for(int i=1;i<=n;++i)
cin>>b[i]>>c[i]>>l[i],
q[i].push(sum[i]=c[i]);
for(int i=n;i>=1;--i){
ans=max(ans,q[i].size()*l[i]),
sum[b[i]]+=sum[i],
q[b[i]].join(q[i]);
while(sum[b[i]]>m)
sum[b[i]]-=q[b[i]].top(),
q[b[i]].pop();
}
cout<
Over.
不理解的欢迎留言 AWA。