题解 CF671E 【Organizing a Race】
皎月半洒花
2020-04-17 16:12:44
orz 兔,兔是我们的红太阳,我们不能没有他!
这边只是稍微详细地讲了讲转化后线段树该如何实现。前面的推导只是提供一个前情提要(
顺便增加了一点自己的个人理解。
_____
首先考虑,对于一段区间而言,需要多少操作多少次,才能保证正着走完并且反着走完。那么也就是需要算出正着走和反着走都需要额外的多少代价。
这个地方有个贪心。考虑定「向右走」为正方向。那么假设从 $i$ 开始走,如果遇到某个 $j>i$ 发现走不得,那么应该在何处加油?因为还要考虑反着走回来,所以必然是加在最靠右的地方最优,所以就会选择的在 $j-1$ 处加油。记 $i$ 向右走遇到的第一个这样的 $j$ 为 $stop_i$ 。
于是根据这个贪心就可以求出 $need(i,j)$ ,表示从 $i$ 走到 $j$ 需要多少代价。但这样也是只是保证了正着可以走。不妨令 $p_{i}$ 表示从 $1$ 走到 $i$ 花费的油量,$q_i$ 表示从 $i$ 走到 $1$ 花费的油量。 那么可知有递推:
$$ p_i=p_{i-1}+g_{i-1}-w_{i-1}$$
$$q_i=q_{i-1}+g_{i-1}-w_i $$
那么可以知道,在走 $i\to j$ 这条路线时,同时也在进行对
$$g_{stop_i},g_{stop_{stop_i}},g_{stop_{stop_{stop_i}}}\cdots$$
这些位置进行单点加,那么对 $q$ 的影响就是一个后缀加。记后缀加完之后的 $\{q_n\}$ 为 $\{\tau_n\}$ 。则如果不能从 $j$ 回到 $i$ ,就意味着着存在一个 $i\leq k<j$ ,使得 $q_{k}-q_{j}<0$ 。怎么量化这个东西呢?考虑还是贪心,如果从 $j$ 到 $i$ 走不了,那么一定会要把贡献累加到 $j$ 上,那需要累加的量就是 $q_{j}-\min_{k=i}^{j-1}\{q_{k}\}$ 。这也就是如果想要 $[i,j]$ 这个区间变得合法的最小贡献。
考虑如何计算这个东西。比较暴力的解法那必然是枚举一个左端点,然后向右走更新右端点。这样是 $n^2$ 的。发现如果想要优化,只能选择加速寻找右端点这个过程。但是有个问题在于,对于固定的 $i$ ,和想要二分出的 $j'$,要经过不同的 $stop$ 集合,同时有着不同的 $q_{j'}-\min_{k=i}^{j'-1}\{q_{k}\}$ ,求一次是 $O(n)$ 的,反而把复杂度搞成了 $n^2\log n$ 。
分开考虑这两点。对于经过不同 $stop$ 集合这个问题,可以继续深入挖掘性质。发现对于一个 $j$,可能存在一个连续段 $[k_1,k_2]$ 满足 $\forall z\in[k_1,k_2]\cap\mathbb{Z_+}$ ,$stop_z=j$ 。这种一对多的逻辑结构不难想到要用森林去表征。那么这个问题比较好解决了。建出一棵森林 $T$ ,连边 $i\leftrightarrow stop_i$ 。再建立一个虚根 $root$ ,与所有 $stop_i$ 未定义的结点相连。这样只要从 $root$ 开始 dfs,用退栈的方式辅助二分即可快速修改。
对于第二点,考虑对于一个固定的 $i$ ,本质上是在维护这么一个式子:
$$t_{j'}=q_{j'}-\min_{k=i}^{j'}\{\tau_k \}$$
首先变一下形:
$$t_{j'}=q_{j'}-\min_{k=1}^{j'}\{\tau_k \}$$
这样做的正确性在于,只要每次将 $<i$ 的那些 $k$ 的 $\tau_k$ 都置为 $+\infty$ 就可以了。
考虑到底要怎么维护这个东西。发现在查询的过程中,需要单点修改 $g_i$ ,那么也就是区间修改 $q_j$ 和 $\tau_k$ ,那么也就是说要支持:1、维护前缀最小值 2、区间加/减 3、查询出某个最小值的位置。那自然就是线段树了。
考虑后一半的前缀最小值,因为带上修改比较麻烦,所以维护起来依然要选择楼房重建的方式,拿左区间的最小值去二分右区间的前缀。于是可以比较简单地维护出一个区间的 $\min\{t_i\}$ 。
考虑修改。修改的话就是暴力打标记,注意到 $t_i$ 中的 $\tau_k$ 是负贡献,被 $\min$ 套着,但由于是区间修改所以可以直接减。
考虑如何查询。询问比较麻烦,因为相当于查询时要合并多个区间的单调栈,所以无法直接线段树二分。或者说,需要魔改一下二分,让这个二分可以「边二分,边合并」。
考虑一种比较神秘的方式去找。`query(root, l, r, val)` 查询的是区间 $[1,l-1]$ 内某个数 `val`,作为前缀最小值对区间 $[l,r]$ 有影响时,最靠右的 $\leq m$ 的位置。那么考虑分类:
1、如果当前左孩子区间的 $\min\{\tau_k\}$ 比 $val$ 要小,说明当前的前缀最小值不会对右区间产生影响(因为左右区间已经合并了),那么就可以直接判断在算上左区间对右区间贡献时,右区间 $t$ 的最小值是否 $\leq m$ —— 这就是比较朴素的线段树上二分 —— 如果是的话就应该去右儿子里面找,否则去左儿子里找。
2、如果当前左孩子区间的 $\min\{\tau_k\}$ 比 $val$ 要大,说明左区间对右区间产生的贡献会被 $val$ 产生的贡献代替,所以直接递归进右孩子。并且由于 $val$ 已经变成了当前左区间的前缀最小值,所以左区间的 $\min\{\tau_k\}$ 就变成了无用信息(因为肯定都比 $val$ 大),只需要查询在 $q_i+val\leq m$ 时的最大下标即可,这一部分可以直接线段树二分。
但…这似乎也不是最终的查询。考虑为了让最后的 $\min_{k=1}^{j'}\{\tau_k\}$ 真的变成从 $1$ 开始到 $i$ 结束的最小值, 所以需要动态修改 $val$ 的值。先序遍历线段树即可。当然也可以直接维护一个前缀最小 $\tau_k$ 。
这里就只给出线段树部分的代码吧,其他部分比较简单:
```cpp
ll s[N * 3] ;
ll sa[N * 3] ;
ll sb[N * 3] ;
ll tag[N * 3] ;
void _down(int rt, int l, int r){
if (tag[rt]){
s[ls] -= tag[rt] ;
s[rs] -= tag[rt] ;
sb[ls] += tag[rt] ;
sb[rs] += tag[rt] ;
tag[ls] += tag[rt] ;
tag[rs] += tag[rt] ;
tag[rt] = 0 ;
}
}
ll solve(int rt ,int l, int r, ll v){
if (l == r)
return sa[rt] - v ;
int mid = (l + r) >> 1 ; _down(rt, l, r) ;
if (sb[ls] < v)
return min(solve(ls, l, mid, v), s[rt]) ;
else return min(solve(rs, mid + 1, r, v), sa[ls] - v) ;
}
void build(int rt, int l, int r){
if (l == r)
return void(sa[rt] = sb[rt] = ss[l]) ;
int mid = (l + r) >> 1 ;
build(ls, l, mid) ;
build(rs, mid + 1, r) ;
sa[rt] = min(sa[ls], sa[rs]) ;
sb[rt] = min(sb[ls], sb[rs]) ;
s[rt] = solve(rs, mid + 1, r, sb[ls]) ;
}
void update(int rt, int l, int r, int ul, int ur, ll v){
if (ul <= l && r <= ur){
tag[rt] += v ;
sb[rt] += v ;
s[rt] -= v ; //1
return ;
}
_down(rt, l, r) ;
int mid = (l + r) >> 1 ;
if (ul <= mid)
update(ls, l, mid, ul, ur, v) ;
if (ur > mid)
update(rs, mid + 1, r, ul, ur, v) ;
sb[rt] = min(sb[ls], sb[rs]) ;
s[rt] = solve(rs, mid + 1, r, sb[ls]) ;
}
int ask(int rt, int l, int r, ll v){
if (l == r) return l ;
int mid = (l + r) >> 1 ;
if (sa[rs] <= v)
return ask(rs, mid + 1, r, v) ;
else return ask(ls, l, mid, v) ;
}
int query(int rt, int l, int r, ll &v){
if (l == r){
int ret ;
ret = sa[rt] - v <= m ? l : 0 ;
v = min(v, sb[rt]) ;
return ret ;
}
int mid = (l + r) >> 1 ; _down(rt, l, r) ;
if (sb[ls] <= v){
if (s[rt] <= m){
v = min(sb[ls], v) ;
return query(rs, mid + 1, r, v) ;
}
else {
int ret ;
ret = query(ls, l, mid, v) ;
v = min(v, sb[rt]) ; return ret ;
}
}
else {
int ret = 0 ;
if (sa[rt] - v <= m)
ret = ask(ls, l, mid, m + v) ;
return max(ret, query(rs, mid + 1, r, v)) ;
}
}
```