【UOJ418】【集训队作业2018】三角形(贪心+启发式合并FHQ Treap)
- 一棵\(n\)个点的有根树,每个点上有一个数字\(a_i\)。
- 你可以执行两种操作:在\(i\)号点放\(a_i\)个石子(前提条件:它的每个子节点\(j\)上都已经放了\(a_j\)个石子);收回\(i\)号点上的石子。
- 对于每个\(i\),求出想在\(i\)号点放\(a_i\)个石子至少需要准备多少个石子。
- \(n\le2\times10^5\)
点开题解全是线段树合并却没怎么理解。
自己想了个贪心发现要启发式合并\(FHQ\ Treap\)来维护,结果没想到居然过了还跑到了目前最优解?
事件的定义
对于若干操作(称为一个事件),考虑它与另一个事件拼在一起,然后发现我们只需要维护两个信息:这个操作前后石子数的变化量\(d\)、石子数的历史最大值\(v\)。
在事件\((d_1,v_1)\)后接入事件\((d_2,v_2)\),得到的新事件就应该是\((d_1+d_2,\max\{v_1,d_1+v_2\})\)。
然后我们发现,关于两个事件的先后顺序,无论顺序如何变化量肯定相同,而历史最大值的局部最优策略就是全局最优策略。
所以我们可以定义事件\(g_1\)小于事件\(g_2\),当且仅当\(g_1+g_2\)的历史最大值\(\max\{v_1,d_1+v_2\}\)小于\(g_2+g_1\)的历史最大值\(\max\{v_2,d_2+v_1\}\)。
启发式合并
我们肯定是从下往上,每次先想办法让一个点的子节点满了,在这个点上放石子,然后立刻把这个点的子节点上的石子全部拿掉。
所以说,每个点都可以看做两个事件相加\((a_i,a_i)+(-\sum a_j,0)=(a_i-\sum a_j,a_i)\),接下来的问题就是要考虑不同点事件的合并。
这里我们唯一的限制就是,一个点的事件必须在子树内所有点的事件之后发生。
首先我们要合并不同子节点子树内的所有事件,只要启发式合并一下即可。(这里启发式合并的对象可以选择\(FHQ\ Treap\))
然后我们想要加入当前点的事件,如果比最后一个事件大就再好不过了,否则如果直接无脑加入会破坏原先的顺序。
这种树上贪心问题有一个经典的处理技巧,因为当前事件更优,所以做完原本的最后一个事件后我们必然会紧接着做当前点的事件。因此我们不断将当前事件与最后一个事件合并,直至比最后一个事件大为止。
由于每个事件最多被合并一次(合并后它就没掉了),所以复杂度正确。
这里要注意虽然我们只是维护最大值,却不能使用可并堆,因为我们每个点上都要查询当前所有事件拼起来总的历史最大值,需要保证事件从小到大有序,而堆是无法实现这一点的。
代码:\(O(nlog^2n)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 200000
#define LL long long
using namespace std;
int n,fa[N+5],a[N+5];LL s[N+5],ans[N+5];
struct Data
{
LL d,v;I Data(Con LL& a=0,Con LL& b=0):d(a),v(b){}
I Data operator + (Con Data& o) Con {return Data(d+o.d,max(v,d+o.v));}
I bool operator < (Con Data& o) Con {return max(v,d+o.v)<max(o.v,o.d+v);}
};
namespace FastIO
{
#define FS 100000
#define tc() (FA==FB&&(FB=(FA=FI)+fread(FI,1,FS,stdin),FA==FB)?EOF:*FA++)
#define pc(c) (FC==FE&&(clear(),0),*FC++=c)
int OT;char oc,FI[FS],FO[FS],OS[FS],*FA=FI,*FB=FI,*FC=FO,*FE=FO+FS;
I void clear() {fwrite(FO,1,FC-FO,stdout),FC=FO;}
Tp I void read(Ty& x) {x=0;W(!isdigit(oc=tc()));W(x=(x<<3)+(x<<1)+(oc&15),isdigit(oc=tc()));}
Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
Tp I void write(Ty x) {W(OS[++OT]=x%10+48,x/=10);W(OT) pc(OS[OT--]);pc(' ');}
}using namespace FastIO;
int Rt[N+5],sz[N+5];class FHQTreap
{
private:
#define SX O[k].S[1],O[x].S[1],y
#define SY O[k].S[0],x,O[y].S[0]
#define Rd() (seed=(302627441ull*seed+1174320758ull)%2147483647)
#define PU(x) (O[x].G=O[O[x].S[0]].G+O[x].V+O[O[x].S[1]].G)//注意按顺序合并事件
int rt,Nt,seed;struct node {Data V,G;int D,S[2];}O[2*N+5];
I void Merge(int& k,RI x,RI y)//合并
{
if(!x||!y) return (void)(k=x+y);
O[x].D>O[y].D?(k=x,Merge(SX)):(k=y,Merge(SY)),PU(k);
}
I void Split(RI k,int& x,int& y,Con Data& v)//裂开
{
if(!k) return (void)(x=y=0);
O[k].V<v?(x=k,Split(SX,v)):(y=k,Split(SY,v)),PU(k);
}
I void SplitT(RI k,int& x,int& y,RI fg=0)//分裂出最后一个元素
{
if(!k) return (void)(x=y=0);
(O[k].S[1]||fg)?(x=k,SplitT(SX,fg)):(y=k,SplitT(SY,1)),PU(k);
}
public:
I void A(int& rt,Con Data& v) {RI k=++Nt;O[k].G=O[k].V=v,O[k].D=Rd();Merge(rt,rt,k);}//插入v事件
I void A(int& rt,CI p) {RI x,y;Split(rt,x,y,O[p].V),Merge(x,x,p),Merge(rt,x,y);}//插入p节点
I int Pop(int& rt) {RI x;return SplitT(rt,rt,x),x;}//弹出最大值
I bool Try(int& rt,Data& v) {RI x=Pop(rt);return O[x].V<v?(Merge(rt,rt,x),false):(v=O[x].V+v,true);}//与最后一个元素比较,更大直接插入,更小则与其合并
I void Merge(int& x,RI y) {Merge(x,x,y);}I LL Q(CI rt) {return O[rt].G.v;}
}T;
I void Merge(int& x,RI y) {W(y) T.A(x,T.Pop(y));}//合并,不断取出y中每个元素插入x
int main()
{
RI ty,i;for(read(ty,n),i=2;i<=n;++i) read(fa[i]);for(i=1;i<=n;++i) read(a[i]),sz[i]=1;
Data o,t;for(i=n;i;--i)//从下往上
{
t=Data(a[i]-s[i],a[i]);W(Rt[i]&&T.Try(Rt[i],t));T.A(Rt[i],t),ans[i]=T.Q(Rt[i]);//把当前事件插入,询问所有事件拼起来总的历史最大值
i^1&&(s[fa[i]]+=a[i],sz[fa[i]]<sz[i]&&(swap(Rt[fa[i]],Rt[i]),0),Merge(Rt[fa[i]],Rt[i]),sz[fa[i]]+=sz[i]);//启发式合并给父节点
}
for(i=1;i<=n;++i) write(ans[i]);return clear(),0;
}