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

浙公网安备 33010602011771号