CF671E Organizing a Race

前置知识:洛谷P4198 楼房重建     

在本题中,需统计 $\mathrm{s[i]}>\max_{j=1}^{i-1}s[j]$ 的 $\mathrm{i}$ 的个数.   

1.$\mathrm{mx[i]}$ 为区间最大值

2.$\mathrm{cnt[i]}$ 为在考虑区间 $[\mathrm{l}, \mathrm{r}]$ 的情况下,右区间的答案.    

信息一非常好合并,关键是如何合并信息二.  

再定义函数 $\mathrm{calc(v, i)}$ 为在区间 $\mathrm{i}$ 前面放一个 $\mathrm{v}$ 的情况下 $\mathrm{i}$ 整体的答案.  

int calc(int v,int now) {
    if(l==r)  
        return v > mx[now];       
    if(mx[ls] > v) 
        return calc(v, ls) + cnt[now];  
    else 
        return 0 + calc(v, rs); 
}

这是 calc 函数的伪代码, 关键点在于每次将 $\mathrm{v}$ 与左区间最大值作比较, 且只会递归一个子树.  

这样每次 $\mathrm{calc}$ 的复杂度就是 $O(\log n)$ 的,总复杂度就是 $O(n \log^2 n)$ 的.  

#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm> 
#define ll long long 
#define pb push_back 
#define N  100008 
#define ls (now<<1) 
#define rs (now<<1|1)
#define setIO(s) freopen(s".in","r",stdin) 
using namespace std;   
int n,m,cnt[N<<2],H[N],id[N<<2];    
bool cmp(int i,int j) {
    if(!j) return H[i];   
    return (ll)H[i]*j>(ll)H[j]*i;  
}
void build(int l,int r,int now) {
    cnt[now]=0,id[now]=l;             
    if(l == r) return ; 
    int mid=(l+r)>>1; 
    build(l,mid,ls),build(mid+1,r,rs); 
}      
int calc(int l,int r,int now,int o) {
    if(l==r) {                
        return cmp(l, o);     
    }
    int mid=(l+r)>>1;  
    // left > o       
    if(cmp(id[ls], o)) {                               
        return calc(l,mid,ls,o)+cnt[now];  
    }
    else {           
        return 0+calc(mid+1,r,rs,o);  
    }
}
void update(int l,int r,int now,int p) {
    if(l==r)  return ;   
    int mid=(l+r)>>1;  
    if(p<=mid) update(l,mid,ls,p); 
    else update(mid+1,r,rs,p);  
    id[now]=cmp(id[ls], id[rs]) ? id[ls] : id[rs];    
    cnt[now]=calc(mid+1,r,rs,id[ls]);         
}
int main() { 
    // setIO("input");   
    scanf("%d%d",&n,&m);   
    build(1, n, 1);  
    for(int i=1;i<=m;++i) {
        int x, y;  
        scanf("%d%d",&x,&y);   
        H[x]=y, update(1,n,1,x);                         
        printf("%d\n",calc(1,n,1,0));     
    }
    return 0; 
}

  

在本题中,需要动态维护 $\mathrm{c[i]}=\mathrm{a[i] - \min_{j=1}^{i}}$ {$\mathrm{b[j]}$}.  

初始情况,给定 $\mathrm{a,b}$ 序列,并对 $\mathrm{b}$ 序列进行区间加法.    

最后,要查询满足 $\mathrm{c[i]} \leqslant K$ 的最大的 $\mathrm{i}$.   

先不考虑查询,先考虑如何查询 $\mathrm{c[i]}$ 的最小值.   

还是按照上面的套路,令 $\mathrm{ans[x]}$ 表示考虑 $[l,r]$ 的影响后线段树右子树的极小值.    

其中 $\mathrm{a,b}$ 的最小值都是非常容易维护的,而 $\mathrm{ans}$ 还是需要一个函数来更新.     

ll calc(int l,int r,int now,ll o) {
    if(l==r) return amin[now] - o;   
    pushdown(now); 
    int mid=(l+r)>>1;   
    if(o<=bmin[ls]) {
        return min(amin[ls]-o, calc(mid+1,r,rs,o));    
    }
    else {
        return min(calc(l,mid,ls,o), ans[now]);   
    } 
}

o 和前面的题一样,都是这个区间之前的最小值,并且需要考虑 $\mathrm{o}$ 对答案的干扰.   

这两道题对于 $\mathrm{ans}$ 数组的定义方式都使得查询的时候无需考虑答案是否满足可减性. 

维护完 $\mathrm{c[i]}$ 的最小值后就需要在线段树找这个最大的 $\mathrm{i}$.   

还是定义函数 $\mathrm{solve(x, p)}$ 表示序列最前面有一个 $\mathrm{p}$ 的影响下的答案.   

需要特判一下 $\mathrm{p}$ 小于等于左子树的最小值.  

在该情况下,左子树的 $\mathrm{c[i]=a[i]+b[i]}$ 中 $\mathrm{b[i]}$ 就是定值.   

所以直接在左子树换成普通的线段树二分递归即可.    

int solve(int l,int r,int now,ll &p) {
    if(l==r) {
        int ret;       
        if(amin[now] - p <= K)  ret = l;  
        else ret = 0; 
        p = min(p, bmin[now]); 
        return ret;  
    }  
    pushdown(now); 
    int mid=(l+r) >> 1;  
    // p : 前缀的最小值.      
    if(p > bmin[ls]) {
        // p 比左区间的最小值要大, 对右区间无影响.    
        if(ans[now] <= K) {
            p = bmin[ls]; 
            return solve(mid+1,r,rs,p);  
        }   
        else {
            // 不能向右区间走, 只能走左边.            
            int re=solve(l,mid,ls,p);                            
            return re; 
        }
    }
    else {    
        int re=amin[ls]<=K+p?solve2(l,mid,ls, K+p):0;              
        return max(re, solve(mid+1,r,rs,p));         
    }
}

问题转化:
$\mathrm{pre[i]}$ 表示 $1$ 开到 $\mathrm{i}$ 的最小代价.  

$\mathrm{suf[i]}$ 表示 $\mathrm{i}$ 开到 $1$ 的最小代价.   

$\mathrm{cost(l,r)}$ 表示从 $\mathrm{l}$ 开到 $\mathrm{r}$ 的代价. 

  

由于我们要用最少的油,所以最佳情况是每次都恰好开到 $\mathrm{l}$ 位置.    

假如说 $\mathrm{i}$ 到 $\mathrm{j}$ 的过程中都有 $\mathrm{pre[j]} \leqslant \mathrm{pre[i]}$, 则无需代价.   

令 $\mathrm{next[i]}$ 表示 $\mathrm{i}$ 右边第一个 $\mathrm{j}$,满足 $\mathrm{pre[j]}>\mathrm{pre[i]}$.   

则 $\mathrm{i}$ 开到 $\mathrm{j}$ 的代价就是 $\mathrm{cost(i,j)}=\mathrm{pre[j]-pre[i]}$.    

这个代价可以在 i ~ j 路径上任一点进行加油,但是为了返回方便,不妨在 $\mathrm{j-1}$ 位置加油.   

 

返程时所需的油量就是 $\mathrm{suf2[r]}-\mathrm{suf'(l,r)}$,其中 $suf'(l,r)$ 表示 $[l,r)$ 中 $\mathrm{suf}$ 的最小值.     

而由于在向右走的过程中给一些加油站加油了,所以这个 $\mathrm{suf}$ 数组是会变的.   

总结一下:$\mathrm{w}=\mathrm{cost(l,r)}+\mathrm{suf2[r]}-\mathrm{suf'(l,r)}$.    

 

固定一个左端点 $\mathrm{l}$, 不妨从右向左枚举并更新.    

我们发现若想对 $\mathrm{cost(l,r)}$ 有影响,则一定满足 $\mathrm{pre[x]} > \mathrm{pre[l]}$.     

这个 $\mathrm{x}$ 就是沿着 $\mathrm{next[l]}$ 向前的一条链.   

加入对 $\mathrm{x}$ 对 $\mathrm{cost(l,r)}$ 的影响后对应的是一个后缀加(从 $\mathrm{x}$ 开始向后) 

加入对 $\mathrm{x}$ 对 $\mathrm{suf2}$  的影响对应是一个后缀减.  

加和减的位置和数值都相同,故可以抵消掉影响.  

上面那个 $\mathrm{w}$ 可以进一步被化简成 $\mathrm{w}=\mathrm{suf[r]}-\mathrm{suf'(l,r)}$  

即 cost 与 suf2 永远等于 $\mathrm{suf}$ 本身,一直不变.   

这个东西就用上文介绍的前缀线段树维护即可,然后将 $\mathrm{l}$ 与 $\mathrm{next[l]}$ 连边形成森林.  

求解时遍历这个森林.

还有很多细节没有讲,看代码吧.   

#include <cstdio>
#include <vector>  
#include <cstring>
#include <algorithm>
#define ll long long 
#define pb push_back   
#define ls now<<1  
#define rs now<<1|1  
#define N  100009 
#define setIO(s) freopen(s".in","r",stdin)    
using namespace std;       
const ll inf=(ll)1e16;    
int n ; 
// ans: 考虑 [l,r] 的情况下右区间的极小值.   
ll K; 
vector<int>G[N]; 
ll suf[N],pre[N],amin[N<<2],bmin[N<<2],ans[N<<2],tag[N<<2];  
// 对 b 进行加法.        
void mark(int now,ll v) {
    bmin[now]+=v;   
    ans[now]-=v, tag[now]+=v; 
}
void pushdown(int now) {    
    if(tag[now]) {
        mark(ls, tag[now]); 
        mark(rs, tag[now]);  
    }
    tag[now]=0; 
}   

ll calc(int l,int r,int now,ll o) {
    if(l==r) return amin[now] - o;   
    pushdown(now); 
    int mid=(l+r)>>1;   
    if(o<=bmin[ls]) {
        return min(amin[ls]-o, calc(mid+1,r,rs,o));    
    }
    else {
        return min(calc(l,mid,ls,o), ans[now]);   
    } 
}
void build(int l,int r,int now) {
    if(l==r) { 
        amin[now]=suf[l]; 
        bmin[now]=suf[l];      
        return ; 
    }   
    int mid=(l+r)>>1;   
    build(l,mid,ls),build(mid+1,r,rs);  
    amin[now]=min(amin[ls], amin[rs]);   
    bmin[now]=min(bmin[ls], bmin[rs]);   
    ans[now]=calc(mid+1,r,rs,bmin[ls]);   
}     
void modify(int l,int r,int now,int L,int R,ll v) {
    if(l>r||L>R) return ; 
    if(l>=L&&r<=R) {
        mark(now, v);  
        return ; 
    }
    pushdown(now); 
    int mid=(l+r)>>1;  
    if(L<=mid)  modify(l,mid,ls,L,R,v); 
    if(R>mid)   modify(mid+1,r,rs,L,R,v);  
    bmin[now]=min(bmin[ls], bmin[rs]); 
    ans[now]=calc(mid+1,r,rs,bmin[ls]);    
}
int solve2(int l,int r,int now,ll v) {
    if(l==r) return l;              
    int mid=(l+r)>>1;             
    if(amin[rs]<=v) return solve2(mid+1,r,rs,v);  
    else return solve2(l,mid,ls,v);  
}                        
int solve(int l,int r,int now,ll &p) {
    if(l==r) {
        int ret;       
        if(amin[now] - p <= K)  ret = l;  
        else ret = 0; 
        p = min(p, bmin[now]); 
        return ret;  
    }  
    pushdown(now); 
    int mid=(l+r) >> 1;  
    // p : 前缀的最小值.      
    if(p > bmin[ls]) {
        // p 比左区间的最小值要大, 对右区间无影响.    
        if(ans[now] <= K) {
            p = bmin[ls]; 
            return solve(mid+1,r,rs,p);  
        }   
        else {
            // 不能向右区间走, 只能走左边.            
            int re=solve(l,mid,ls,p);                            
            return re; 
        }
    }
    else {    
        int re=amin[ls]<=K+p?solve2(l,mid,ls, K+p):0;              
        return max(re, solve(mid+1,r,rs,p));         
    }
}
int w[N], g[N], sta[N], nex[N], top, fink;   
void dfs(int x) {
    // 先处理当前节点.     
    sta[++top] = x;  
    if(nex[x] <= n) {
        // 下一个点小等于 n.  
        // 后缀减 b.     
        modify(1, n, 1, nex[x]-1, n, pre[x]-pre[nex[x]]);    
    }
    if(x <= n) {
        // 处理当前节点.     
        int l=2,r=top-1,re=1; 
        while(l<=r) {
            int mid=(l+r)>>1;    
            if(pre[sta[mid]] - pre[x] > K) re = mid, l = mid + 1;  
            else r = mid - 1; 
        }   
        int rmax = sta[re] - 1;  
        //     printf("%d %d\n",x,rmax); 
        ll oo = inf; 
        if(x > 1) modify(1, n, 1, 1, x-1, inf);  
        modify(1, n, 1, rmax, n, -inf);   
        // 左开右闭.  
        int pos = solve(1, n, 1, oo);  
        modify(1, n, 1, rmax, n, +inf); 
        if(x > 1) modify(1, n, 1, 1, x-1, -inf);  
        fink = max(fink, pos - x + 1);          
    }
    for(int i=0;i<G[x].size();++i) {
        dfs(G[x][i]); 
    }
    if(nex[x] <= n) {
        modify(1, n, 1, nex[x]-1, n, pre[nex[x]] - pre[x]);  
    }
    --top;   
}
int main() {
    // setIO("input");  
    // freopen("de.out","w",stdout);  
    scanf("%d%lld",&n,&K); 
    for(int i=1;i<n;++i) scanf("%d",&w[i]); 
    for(int i=1;i<=n;++i) scanf("%d",&g[i]); 
    for(int i=2;i<=n;++i) {
        pre[i]=pre[i-1]+w[i-1]-g[i-1];       
        suf[i]=suf[i-1]+w[i-1]-g[i];    
    }
    build(1, n, 1);     // 搭建完毕.    
    // 求 nex[i]: j > i 且 pre[j] > pre[i]       
    pre[n + 1] = inf, sta[++top]=n+1, nex[n+1]=n+1; 
    for(int i=n;i>=1;--i) {
        while(pre[i] >= pre[sta[top]]) --top;   
        nex[i] = sta[top];  
        G[nex[i]].pb(i); 
        sta[++top] = i;       
    }          
    for(int i=1;i<=top;++i) sta[i]=0; 
    top=0; 
    // 求完 next 了.    
    dfs(n + 1);         
    printf("%d\n",fink);  
    return 0; 
}

  

 

posted @ 2021-09-14 08:59  guangheli  阅读(27)  评论(0)    收藏  举报