P1055 树的遍历 官方题解
Gioush OJ · P1055 树的遍历
【数据点 1∼31\sim 31∼3】
- 这一档保证 ,非叶结点最多只有 个,可以枚举被唤醒的守卫集合。
- 固定集合以后,从叶结点向上计算遍历序列。沉睡结点把字典序较小的子树序列放在前面,唤醒结点固定先左后右。
- 在花费不超过 的集合中取字典序最大的序列即可。
【数据点 4∼64\sim 64∼6】
- 这一档保证 且所有花费为正,因此没有守卫能够被唤醒。
- 每个非叶结点都由小 R 选择先访问哪棵子树。由于叶结点符文互不相同,只需比较两棵子树最终序列的第一个符文。
- 递归计算两棵子树,再把首项较小的序列放在前面。时间复杂度为 。
【数据点 7∼197\sim 197∼19】
令 表示结点 子树中的符文集合,定义
- 若 是写有符文 的叶结点,则 ,其余状态均不合法。
- 若 的左右儿子为 ,当 时,可以唤醒 ,也可以让右子树首项严格大于 。
- 当 时,不能唤醒 ,并且左子树首项必须严格大于 。
【数据点 7∼197\sim 197∼19:状态转移】
完整转移为
- 空集合的最小值视为正无穷。加法中只要有一项不合法,结果也不合法。
- 直接枚举 与另一棵子树的 ,可以做到 ,足以通过 。
【正解】
定义严格后缀最小值
于是转移可以写成
- 将两棵子树的状态按 合并,并从右向左维护后缀最小值,每个状态只需常数次计算。
- 整棵树共有 个状态,建表时间与空间复杂度均为 。
【答案恢复】
- 在根结点中选择最大的 ,使得 ,这就是最终序列的首项。
- 恢复答案时,先预留保证当前子树先被访问所需的最小花费,再把剩余预算交给先访问的子树,使这一段序列尽可能大。
- 第一段确定以后,再在仍满足首项比较关系的状态中选择第二棵子树的最大首项,继续递归。
- 每一步都先最大化当前尚未确定的第一个位置,因此得到的完整序列字典序最大。
【实现与复杂度】
- 当当前首项 来自左子树时,需要预留 。之后右子树首项若小于 ,就必须额外支付 唤醒当前守卫。
- 当 来自右子树时,需要预留 ,并且最后选择的左子树首项必须严格大于 。
- 实现中所有花费以及预算均使用
long long,不合法状态用足够大的正数表示。
【参考代码】
/*Author:EhundateghDate:2026/7/24Name:traverse.cppYou steal,I kill.*/#include <cstdio>#include <vector>#include <algorithm>#define MAXN 100010using namespace std;const long long INF=(1ll<<62); int c,n,Leaf,T,Line[MAXN<<1],Pos[MAXN],Right[MAXN<<1];long long k,Val[MAXN]; struct Data { int Mark; long long Val;}; vector <Data> Dp[MAXN<<1];vector <long long> Suff[MAXN<<1]; long long Add(long long x,long long y){return (x>=INF||y>=INF)?INF:x+y;} long long Query(int Now,int x) { int l=0,r=(int)Dp[Now].size(); while (l<r) { int Mid=(l+r)>>1; if (Dp[Now][Mid].Mark<=x) l=Mid+1; else r=Mid; } return l==(int)Dp[Now].size()?INF:Suff[Now][l];} void Build(int Now) { if (Now>=Leaf) { Right[Now]=Now-Leaf+1; Dp[Now].push_back({Line[Now],0}); Suff[Now].push_back(0); return; } int ls=Now<<1,rs=Now<<1|1; Build(ls);Build(rs); Right[Now]=Right[rs]; int i=0,j=0,p=0,q=0; Dp[Now].reserve(Dp[ls].size()+Dp[rs].size()); while (i<(int)Dp[ls].size()||j<(int)Dp[rs].size()) { bool Tag=j==(int)Dp[rs].size()|| (i<(int)Dp[ls].size()&&Dp[ls][i].Mark<Dp[rs][j].Mark); int Mark=Tag?Dp[ls][i].Mark:Dp[rs][j].Mark; while (p<(int)Dp[ls].size()&&Dp[ls][p].Mark<=Mark) p++; while (q<(int)Dp[rs].size()&&Dp[rs][q].Mark<=Mark) q++; if (Tag) { long long Ret=q==(int)Dp[rs].size()?INF:Suff[rs][q]; Dp[Now].push_back({Mark,Add(Dp[ls][i].Val,min(Val[Now],Ret))}); i++; } else { long long Ret=p==(int)Dp[ls].size()?INF:Suff[ls][p]; Dp[Now].push_back({Mark,Add(Dp[rs][j].Val,Ret)}); j++; } } Suff[Now].resize(Dp[Now].size()); for (int t=(int)Dp[Now].size()-1;t>=0;t--) { Suff[Now][t]=Dp[Now][t].Val; if (t+1<(int)Dp[Now].size()) Suff[Now][t]=min(Suff[Now][t],Suff[Now][t+1]); } return;} long long Print(int Now,int x,long long Limit) { if (Now>=Leaf) {printf("%d ",Line[Now]);return 0;} int ls=Now<<1,rs=Now<<1|1,Temp=0; if (Pos[x]<=Right[ls]) { long long Need=min(Val[Now],Query(rs,x)); long long Ret=Print(ls,x,Limit-Need); Limit-=Ret; for (int i=(int)Dp[rs].size()-1;i>=0;i--) { long long Extra=Dp[rs][i].Mark<x?Val[Now]:0; if (Add(Dp[rs][i].Val,Extra)<=Limit) { Temp=Dp[rs][i].Mark; break; } } long long Extra=Temp<x?Val[Now]:0; return Ret+Extra+Print(rs,Temp,Limit-Extra); } long long Need=Query(ls,x); long long Ret=Print(rs,x,Limit-Need); Limit-=Ret; for (int i=(int)Dp[ls].size()-1;i>=0;i--) { if (Dp[ls][i].Mark>x&&Dp[ls][i].Val<=Limit) { Temp=Dp[ls][i].Mark; break; } } return Ret+Print(ls,Temp,Limit);} void Solve() { scanf("%d%lld",&n,&k);Leaf=1<<n; for (int i=1;i<(Leaf<<1);i++) Dp[i].clear(),Suff[i].clear(); for (int i=1;i<Leaf;i++) scanf("%lld",&Val[i]); for (int i=Leaf;i<(Leaf<<1);i++) { scanf("%d",&Line[i]); Pos[Line[i]]=i-Leaf+1; } Build(1); int Temp=0; for (int i=(int)Dp[1].size()-1;i>=0;i--) { if (Dp[1][i].Val<=k) { Temp=Dp[1][i].Mark; break; } } Print(1,Temp,k); printf("\n"); return;} int main() { scanf("%d%d",&c,&T); while (T-->0) Solve(); return 0;}