[CEOI2010 day2] mp3player(线段树维护min-max复合函数,转化)
yuanzhiteng
·
·
题解
题目链接
一. 错误思路
拿到题后一头雾水......
容易想到的是二分 t,然后倒推模拟。
这种做法有两个错误。
但是本题中的 $t$ 具有单调性吗?
换句话说,若对于 $t_1$ **不**满足题意,$\forall t_2>t_1$ 都**不**满足题意吗?
稍微手玩一下会发现 $t$ 并没有单调性。
比如看下面这个例子:
```cpp
3 3 1
- 0
+ 3
+ 6
```
若 $t=3$,它是不满足题意的。
但若 $t=4$,它却是满足题意的。
所以 **不能二分 $t$** 。(注意不是不能二分,而是不能二分 $t$,下面会说)
$(2)$ 倒推真的可以吗?
我们知道,当给定一个序列和 $V_1$ 后,正推是很好推的。
但是倒推呢?
如果没有 $触底/触顶$ 机制还好说,可以倒推,但是如果有了这个机制呢?
由于状态 $i$ 是由状态 $i-1$ 得到,所以我们无法知道状态 $i-1$ 时是否触底/顶。
比如:倒推此时 $nowv=0,op=-$,无法确定上一个状态是 $1$ 还是 $0$。
所以倒推也是不行的。
~~当然如果你对于触底时随机选择,多跑几遍取 max 当我没说,脸好可以骗点分。~~
****
### 二. 题目分析
那怎么办?
~~俗话说:“OI 暴力始”~~ 先考虑暴力如何做。
既然 $t$ 不能二分,那干脆直接枚举 $[0,maxt]$ 中的所有 $t$。
有了 $t$ 后还不够。
上面已经说了不能倒推。
所以只能正着推。
问题又来了,正着推缺条件—— $V_1$。
管他的,暴力枚举。
那么有了 $V_1$ 和 $t$ 后就能 $\mathcal{O}(n)$ 模拟得到相应的 $V_2$ 了。
如果对于 $t=maxt$ 仍然能满足题意,那么输出 infinity。
时间复杂度 $\mathcal{O}(nV_{max}\max(C_i))$。
好恐怖的复杂度。(*  ̄▽ ̄* )
考虑优化。
首先,$t$ 是否需要枚举所有的 $t$?
发现不需要,只需要枚举 **时间段** 即可。
什么意思呢?
呐,比如说样例一。
对于 $t=2$ 和 $t=3$ 效果是一样的。
枚举的关键应该在于 **操作序列状态的改变**。
所以在样例一中只需要枚举 $t=1,4,5,6,8$。
这样便将 $t$ 的范围缩小到了 $\mathcal{O}(n)$。
其次,真的都不能二分吗?
发现 **$V_1$ 可以二分**。
感性地理解,当 $V_1$ 增加了,相当于“起点”上升了,但后面的过程一样,由于起点更高,终点自然也就不会更低。
具体地,设 $f(V_1)$ 表示 $V_1$ 经过操作序列后能得到的 $V_2$。
那么 $f(V_1)$ 是**非严格单调递增**的。
所以可以 $\mathcal{O}(\log n)$ 枚举 $V_1$。
所以优化后的时间复杂度为 $\mathcal{O}(n^2\log n)$。
有分,但不多。
再考虑去优化它。
时间复杂度的瓶颈在哪里呢?
二分 $V_1$ 是没有办法省去的,毕竟要正着推。
那......$t$ 呢?
好像也没法直接地省去。
没办法了吗?
难道......真的要在这里倒下了吗哦(* >﹏<* )?
换一种思考角度,既然没办法直接去掉时间复杂度,那能不能将它“拆”开呢?
**换句话说,能不能先判断 $t$ 有无对应的 $V_1$ 符合题意,求出最大的 $t$ 后再去二分 $V_1$ 呢?**
假设判断是至多 $\mathcal{O}(\log n)$ 的,那么时间复杂度就降为了 $\mathcal{O}(n\log n+logn)=\mathcal{O}(n\log n)$ 。
这不就可以通过了吗?
所以关键在于**如何快速判断对于一个给定的 $t$ 是否存在合法的 $V_1$ 使之满足题意**。
如何判断?
再次仔细观察题面:
如果当前操作成功,则
对于 $+$ 操作,音量 $x$ 会变为 $\min(V_{max},x+1)$;
对于 $-$ 操作,音量 $x$ 会变为 $\max(0,x-1)$;
欸,感觉来了。 (≧∇≦)
**这不就是 $\min(c,\max(b,x+a))$ 复合函数的形式吗?**
对于 $+$ 操作,即 $c=V_{max},b=0,a=1$;
对于 $-$ 操作,即 $c=V_{max},b=0,a=-1$;
对于不成功的操作,即 $c=V_{max},b=0,a=0$;
而且经验告诉我们,**这个函数是可以复合的!!!**
证明如下:
> 由于在网上翻遍了也没有对复合函数的详细证明,要么是“手玩一下”,要么是“易知”,要么是提都不提,但真的有这么容易吗?反正我是不会。
捣敲了老半天,终于搞出来了。
自己推的复合函数 $g(f(x))$ 推法:
> 令 $f(x)=\min(c_1,\max(b_1,x+a_1)),g(x)=\min(c_2,\max(b_2,x+a_2))$,则
> $$\begin{aligned}
g(f(x))&=\min(c_2,\max(b_2,\min(c_1,\max(b_1,x+a_1))+a_2)) \\
&=\min(c_2,\max(b_2,\min(a_2+c_1,\max(a_2+b_1,x+a_1+a_2))))
\end{aligned}$$
然后分类进行讨论:
> (1) 若 $a_2+c_1<\max(a_2+b_1,x+a_1+a_2)$,则:
> $$g(f(x))=\min(c_2,\max(b_2,a_2+c_1))$$
> 然后注意到有一个**重要结论**:
> $$\min(a,\max(b,c))=\max(b,\min(a,c))$$
> 这个结论可以对 $a,b,c$ 大小关系分类去证明。
> 所以就可以继续化简:
> $$g(f(x))=\max(b_2,\min(c_2,a_2+c_1))$$
> (其实情况 $1$ 并不需要继续化简,但情况 $2$ 必须这样同样化简,所以先给出)
> (2) 若 $a_2+c_1\ge \max(a_2+b_1,x+a_1+a_2)$,则:
> $$\begin{aligned}
g(f(x))&=\min(c_2,\max(b_2,\max(a_2+b_1,x+a_1+a_2)))\\
&=\min(c_2,\max(x+a_1+a_2,\max(b_2,a_2+b_1))) \\
&=\max(x+a_1+a_2,\min(c_2,\max(b_2,a_2+b_1)))
\end{aligned}$$
> 因此才有:
> $$f(g(x))=\min(\max(b_2,\min(c_2,a_2+c_1))\ ,\ \max(\min(c_2,\max(b_2,a_2+b_1)),x+(a_1+a_2)))$$
> 这个形式与 $\min(c,\max(b,x+a))$ 是吻合的。
告诉我,对于一段序列的复合函数如何维护?
**线段树!**
噫吁嚱,豁然开朗呼!
做法便明朗了:
令每一个操作的 $t_i=Time_{i}-Time_{i-1}$。
先将所有操作打入线段树,用线段树维护贡献。
然后判断 $V_2$ 是否落在 $f(V_1)$ 的值域中。
(因为 $V_1$ 的定义域不变,始终是 $[0,V_{max}]$)
如果是,那么说明所有操作均有效时可以,$t$ 可以无穷大,输出 infinity。
否则,将 $t_i$ 从大到小排序。
一个一个枚举 $nowtime=t_i$,将所有 $t=nowtime$ 的操作的贡献从线段树中单点修改去掉。
然后判断 $V_2$ 是否落在 $f(V_1)$ 的定义域中。
即判断 $V_2\ge tree[1].calc(0)\text{且}V_2\le tree[1].calc(V_{max})$ 是否成立。
如果成立,那么 $nowtime-1$ 即为 $t$ 的答案。
为什么要 $-1$ 呢?
因为此时的 $nowtime$ 表示所有 $\le nowtime$ 的操作均不合法,要使得 $t_i=nowtime$ 的操作不合法,就需要 $t<nowtime$,故应为 $nowtime-1$。
最后确定 $t$ 即确定操作序列的状态后,直接二分 $V_1$,判断 $tree[1].calc(V_1)$ 与 $V_2$ 的大小关系即可。
时间复杂度 $\mathcal{O}(n\log n)$。
嘛,还有一点就是,那个,**操作 $1$ 一定是无效操作!!!**
所以要忽略掉操作 $1$。
别踩坑了。
具体细节见代码。
****
### 三. 代码实现
```cpp
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <string.h>
using namespace std;
const int maxn = 1e5 + 5;
template <typename T>
inline void read(T& x){
x = 0; int f = 1; char c = getchar(); while (c < '0' || c > '9') { if (c == '-') f = -f; c = getchar(); }
while (c >= '0' && c <= '9') { x = (x << 3) + (x << 1) + (c ^ 48), c = getchar(); } x *= f;
}
template <typename T, typename... Args>
inline void read (T &x, Args&... Arg) { read (x), read (Arg...); }
int n,Vmax,V2;
int Time[maxn],val[maxn];
struct Operation{
int time,val,id;
}a[maxn]; //存放操作序列
struct Segment{
int a,b,c; //线段树维护函数min(c,max(b,x + a))
int calc(int x){
return min(c,max(b,x + a));
}
}tree[maxn << 2];
inline int left(int x){
return x << 1;
}
inline int right(int x){
return x << 1 | 1;
}
inline void push_up(int p){
int lson = left(p),rson = right(p);
int a1 = tree[lson].a,b1 = tree[lson].b,c1 = tree[lson].c;
int a2 = tree[rson].a,b2 = tree[rson].b,c2 = tree[rson].c;
tree[p].c = max(b2,min(c2,c1 + a2));
tree[p].b = min(c2,max(b2,b1 + a2));
tree[p].a = a1 + a2; //这里的复合要手推一下,博客有详细证明
}
inline void build(int p,int l,int r){
if(l == r){
tree[p] = (Segment){val[l],0,Vmax};
return;
}
int mid = (l + r) >> 1;
build(left(p),l,mid);
build(right(p),mid + 1,r);
push_up(p);
}
inline void update(int p,int l,int r,int x){ //将操作x的贡献抹除掉
if(l == r){
tree[p] = (Segment){0,0,Vmax};
return;
}
int mid = (l + r) >> 1;
if(x <= mid) update(left(p),l,mid,x);
else update(right(p),mid + 1,r,x);
push_up(p);
}
inline bool cmp(Operation x,Operation y){
return x.time > y.time;
}
inline int get_V1(){ //由于复合函数对于f(V1)具有单调性,故在确定操作序列(即已经确定了t)后可以二分找V1
int V1 = 0;
int l = 0,r = Vmax,mid;
while(l <= r){
mid = (l + r) >> 1;
if(tree[1].calc(mid) <= V2) V1 = mid,l = mid + 1;
else r = mid - 1;
}
return V1;
}
int main(){
read(n,Vmax,V2);
for(int i=1;i<=n;i++){
char c;
cin >> c;
if(c == '+') val[i] = 1;
else val[i] = -1;
read(Time[i]);
}
n--; //将数组整体向左移了一格,至于为什么这么做嘛,第一步操作肯定是无效的
for(int i=1;i<=n;i++){
val[i] = val[i+1]; //左移操作
Time[i] = Time[i+1] - Time[i]; //由于t与间隔有关,所以只存t的阶段
a[i] = (Operation){Time[i],val[i],i};
}
build(1,1,n);
if(V2 >= tree[1].calc(0) && V2 <= tree[1].calc(Vmax)){ //所有操作都存在时满足(对于V1的值域[0,Vmax]经过复合函数后得到的值域包含了V2)那么t就是无穷解
printf("infinity\n");
return 0;
}
sort(a + 1,a + 1 + n,cmp);
int i,j;
for(i=1;i<=n;i=j){ //从大到小枚举时间段t,并将操作一个一个从序列中减去,当V2恰好落在复合函数的值域中时,t最大,为nowtime-1(-1是因为要想让这些操作失效,必须<nowtime)
int nowtime = a[i].time;
for(j=i;a[j].time == a[i].time;j++) //将所有这个时间段内的贡献抹去
update(1,1,n,a[j].id);
if(V2 >= tree[1].calc(0) && V2 <= tree[1].calc(Vmax)){ //第一次满足时即为答案
printf("%d %d\n",nowtime-1,get_V1());
return 0;
}
}
return 0;
}
```