题解 【P7011 [CERC2013]Escape】

command_block

2021-10-19 19:38:48

Solution

**题意** : 有一棵 $n$ 个顶点的树,点 $u$ 有权值 $w_u$ 。 初始时玩家在顶点 $1$ ,游戏目标是到达顶点 $t$ 。 记玩家血量为 $H$ ,初始为 $0$ 。第一次到达顶点 $u$ 时,令 $H\leftrightarrow H+w_u$ ,若此时 $H<0$ ,(无论 $u$ 是否是终点)则游戏失败。 判断游戏是否能成功。 多组数据,$n\leq 2\times 10^5,\sum n\leq 5\times10^5$ 。 ------------ 将 $1$ 视为根。 观察可能的策略:当你血少的时候,可能无法越过某些节点,要在一些分支赚到足够的血,然后越过之前无法越过的点,在其子树获取更多的血,不断重复……确实很像闯关变强的过程。 为了统一问题,为 $t$ 添加一个子节点 $t'$ ,令 $w_{t'}=\infty$ ,则问题变为判定最后 $H$ 是否能变为无穷大。 对于某个子树,我们**只关心**以 $a$ 的血量进入能赚多少血量。 记 $f_{u,a}$ 表示以 $a$ 的初始血量进入 $u$ 最终最多能获得的血量。显然 $f_{u,a}-a$ (赚的差值)随 $a$ 单调不减。 最后查看 $f_{1,0}$ 是否为无穷大即可。 考虑:若我们得到了各个子树的 $f$ ,如何求出本树的 $f$ 。 对于两种子树,若他们的 $f$ 相同,则是等效的。 我们可以根据 $f$ 构造等效双层菊花图,第 $i$ 个分支权值为 $\longrightarrow(-i-f_{u,i-1})\longrightarrow(+f_{u,i})$ 。 不难发现,若初始权值为 $i$ ,走且仅能前 $i$ 个分支,最终权值为 $f_{u,i}$ 。 注意到当 $f_{u,i-1}=f_{u,i}$ (即 $f_{u,i}$ 不为前缀最大值时),分支 $i$ 没有收益,可以删去。 我们可以进一步以二元组集合 $\{(a,f_{u,a})|f_{u,a}\text{为前缀最大值}\}$ 描述双层菊花图,代表初始有 $a$ 的血量就能达到 $f_{u,a}$ 的血量。 可以发现,此时剩余的区间 $[a,f_{u,a}]$ 互不相交。 考虑两个双层菊花图的合并,先令根节点的权值为 $0$。不难发现,相当于简单地将两个双层菊花图的根重合。 使用启发式合并,接下来只需分析单点插入。 插入一对 $(a,b)$ 后,区间可能相交。若区间 $[a,b],[a',b']$ 满足 $a'\in [a,b]$ ,则将两者删除并插入 $[a,b+b'-a']$ ,即合并。 这可以用 `std::map` 维护。 接下来考虑根节点 $w_u$ 的权值。 若 $w_u>0$ ,则相当于插入一个分支 $(0,+w_u)$ 。 若 $w_u<0$ ,记 $k=-w_u$ 。(即“门票”的存在) 按 $a$ 从小到大考虑分支 $(a,b)$ :若 $b-a\leq k$ ,则 $k\leftarrow k-(b-a)$ (走这个分支赚的钱不够付门票),并删除 $(a,b)$ ;若 $b-a>k$ ,则 $a\leftarrow a+k$ ,$k=0$ ,结束。 复杂度为 $O(n\log^2n)$。 ```cpp #include<algorithm> #include<cstdio> #include<vector> #include<map> #define Pr pair<ll,ll> #define mp make_pair #define fir first #define sec second #define Itor map<ll,ll>::iterator #define pb push_back #define ll long long #define MaxN 200500 using namespace std; const ll INF=1ll<<60; map<ll,ll> f[MaxN]; vector<int> g[MaxN]; ll w[MaxN]; void insert(int u,Pr now) { Itor it2;bool fl=1; while(1){ it2=f[u].upper_bound(now.fir); if (it2==f[u].begin())break; it2--; if (it2->fir>=it2->sec)f[u].erase(it2); else { if (now.fir<=it2->sec) {it2->sec+=now.sec-now.fir;fl=0;} break; } }if (fl)it2=f[u].insert(now).fir; while(1){ Itor it3=it2;it3++; if (it3==f[u].end()||it2->sec<it3->fir)break; it2->sec+=it3->sec-it3->fir; f[u].erase(it3); } } void dfs(int u,int fa) { int t=0; for (int i=0,v;i<g[u].size();i++)if ((v=g[u][i])!=fa){ dfs(v,u); if (!t){t=v;continue;} else { if (f[t].size()<f[v].size())swap(t,v); for (Itor it=f[v].begin();it!=f[v].end();it++) insert(t,*it); } } if (t)swap(f[u],f[t]); if (w[u]>0)insert(u,mp(0,w[u])); if (w[u]<0){ ll det=-w[u]; while(!f[u].empty()&&f[u].begin()->sec<=f[u].begin()->fir+det){ det-=f[u].begin()->sec-f[u].begin()->fir; f[u].erase(f[u].begin()); } if (det>0&&!f[u].empty()){ Pr now=*f[u].begin();now.fir+=det; f[u].erase(f[u].begin());f[u].insert(now); } } } void solve() { int n,t; scanf("%d%d",&n,&t); for (int i=1;i<=n;i++)scanf("%lld",&w[i]); w[n+1]=INF<<1;g[n+1].pb(t);g[t].pb(n+1); for (int i=1,u,v;i<n;i++){ scanf("%d%d",&u,&v); g[u].pb(v);g[v].pb(u); } dfs(1,0); puts( !f[1].empty()&&f[1].begin()->fir==0&&f[1].begin()->sec>INF ? "escaped" : "trapped" ); for (int i=1;i<=n+1;i++) {g[i].clear();f[i].clear();} } int main() { int T;scanf("%d",&T); while(T--)solve(); return 0; } ``` 进一步注意到,我们需要查询的只有“较小一端”的 $f$ ,于是可以改用小根堆维护合并(不考虑未访问的 $f$ 的合并),再用可并堆维护,复杂度可降为 $O(n\log n)$。