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; }