[JOI 2021 Final] 地牢 3 / Dungeon 3 题解

[JOI 2021 Final] 地牢 3 / Dungeon 3

题意

现在的题目情境不方便表述,我们换一种等价的题目情境:
一条公路上有 \(n+1\) 个加油站,第 \(i (1\le i \le n)\) 个加油站和第 \(i+1\) 个加油站之间的距离为 \(a_i\),且第 $i (1\le i \le n) $ 个加油站的油价为 \(b_i\) 每升,一升油可以让汽车行驶一单位距离。\(m\) 次询问,每次询问给出旅程的起点 \(S\),终点 \(T\),以及汽车的邮箱容量 \(U\),问在初始邮箱为空,且中途任何时刻邮箱储存的油都不能超过 \(U\) 的前提下,从 \(S\) 开到 \(T\) 所需要的最少花费是多少。

题解

把公路看成数轴,\(1\) 号加油站为原点,记 \(s_i=\sum_{j<i} a_j\) 表示加油站 \(i\) 和原点的距离。

首先对于单次询问有一个 \(O(n)\) 的贪心做法:
我们的过程实际上是顺次考虑每个加油站,同时维护一个 \(R\) 表示目前能开到的最远的点(注意 \(R\) 这个点可能是两个加油站中间的路上的某个点),然后可以在每个加油站花费 \(b_i\) 的代价把 \(R\) 拓展一格,然后要求是在考虑完 \(i\) 这个加油站之后,\(R\) 至少要拓展到 \(s_{i+1}\),但又不能和 \(s_i\) 相差超过 \(U\)。于是每一格都会对应到某一个加油站,表示走过这一格所用的油是在哪个加油站买的,如果把第 \(u\) 格对应到的加油站记为 \(id_u\)\(id\) 数组显然是递增的。
那么假设现在在加油站 \(x\),下一个油价比 \(x\) 小的加油站为 \(y\),那么在考虑到 \(y\) 之前,我肯定是能在 \(x\) 这边买就在 \(x\) 这边买,于是我在当前 \(R\) 的基础上不断拓展,如果 \(R\) 到达 \(s_y\) 或者 \(s_x+U\) 了就停止,继续考虑 \(x+1\)

然后考虑所有 \(T\) 全都 \(=n+1\) 怎么做。
不妨先思考对于确定的 \(U\) 的处理方式。
无解的判定是简单的,如果区间内存在 \(a_i>U\) 显然无解。为了避免讨论无解的情况,在下面的讨论中,我们认为:出现这种情况时,你只需要花费 \(U\) 的代价就可以直接跳到 \(i+1\),或者说直接令 \(a_i\gets \min(a_i,U)\),显然这不会让除了 \((i,i+1)\) 之外的任何两个加油站从原先 \(>U\) 的距离变成 \(\le U\) 的距离。
从大到小对 \(S\) 扫描线,同时维护加油站的关于价格单调递减的单调栈,假设现在扫到 \(i\),我们已经知道了 \(i+1\) 的答案以及 \(id\) 数组(当然我们并不需要显示的维护这个数组)。在用 \(i\) 更新完单调栈之后,现在的栈顶 \(j\) 就是下一个价格比 \(i\) 小的加油站,那么根据上面的贪心我们需要把点 \(s_i\) 之后的 \(\min(U,s_j-s_i)\) 个格子的 \(id\) 全都覆盖成 \(i\),同时修改对应的代价,最后再加上开头新增的 \(a_i\) 个格子的代价。
具体的,在更新单调栈的过程中,假设当前栈顶是 \(x\)(他即将被弹掉),下一个栈顶是 \(y\),我们已经成功更新了 \(s_x\) 及以前的格子的代价,现在要更新 \([s_x,s_y]\) 中的格子:

  • \(U\le s_x-s_i\)(即使中间某些 \(a_k\) 变成了 \(\min(a_k,U)\),仍然改变不了 \(U\le s_x-s_i\) 的事实):\(i\) 最多让 \(R\) 扩展到 \(s_x\),不会影响后面的格子,不需要更新。
  • \(U> s_x-s_i\)(此时 \(a[i,x-1]\) 一定都 \(<U\),没有改变过):\(i\) 会把 \(s_x\) 后面 \(len=\min(s_i+U,s_y)-s_x\) 个格子的 \(id\) 覆盖成 \(i\),并且由于 \(len<U\)\(x\) 原本在单调栈上,所以这些格子的 \(id\) 原先一定都是 \(x\),因此代价的总变化量为 \(len\times (b_i-b_x)\)

最后不要忘了把开头的 \(\min(a_i,U)\) 个格子的贡献 \(\min(a_i,U)\times b_i\) 算上。

然后考虑 \(U\) 不一定全都相同的情况:经过上面的分析这其实是简单的,我们用线段树维护每个 \(U\) 的答案(以 \(U\) 为下标),上面提到的代价函数 \((b_i-b_x)\times (\min(s_i+U,s_y)-s_x)\)\(b_i\times \min(a_i,U)\) 全都是分段一次函数,于是相当于在线段树上面区间加一次函数,直接维护即可。
\(ans(S,U)\) 表示询问 \((S,n+1,U)\) 的答案。

现在考虑原问题,\(T\) 不一定相同。
对于一个询问 \((S,T,U)\) 在判定不合法之后,我们用 ST 表找到 \([s_T-U,s_T]\) 中价格最小的加油站 \(x\),注意到此时 \(x\) 可以直接拓展到 \(T\),并且由于 \(s_T-U\) 之前的加油站无论如何都拓展不到 \(T\),而 \(x\) 又是能拓展到 \(T\) 的加油站中价格最小的,所以最后拓展到 \(T\) 的任务一定是 \(x\) 完成的。
我们断言此时的答案是 \(ans(S,U)-ans(x,U)+b_x\times (s_T-s_x)\),证明如下:
\(R'\) 表示在考虑完 \(s_T-U\) 之前的所有加油站之后,\(R\) 拓展到的值。

  • 如果 \(R'<s_x\)\(x\) 之前的加油站一定会把 \(R'\) 刚好拓展到 \(s_x\) 为止,此时 \(x\) 处的邮箱是空的,于是 \(ans(S,U)\)\(x\) 开始的后半部分的决策和 \(ans(x,U)\) 完全一致,差分一下即可得到 \(S\to x\) 的答案,再加上 \(x\to T\) 的贡献即可。
  • 否则 \(R'\ge s_x\):那么 \([s_T-U,s_x)\) 之间的加油站一定不会再拓展 \(R'\) 了,所以 \(ans(S,U)-ans(x,U)\) 实际上多减了 \([s_x,R']\) 这一部分的贡献,而 \(b_x\times (s_T-s_x)\) 实际上多加了 \([s_x,R']\) 这一部分的贡献,于是他们正好抵消了!

复杂度是 \(O((n+m)\log m)\)

code

#include<bits/stdc++.h>
#define Debug puts("-------------------------")
#define LL long long 
#define PII pair<int,int>
#define fi first
#define se second 
#define pb push_back
using namespace std;
const int N=2e5+5;
int n,m,d[N],w[N],maxn[20][N],dis[N],tot,stk[N],top; 
LL s[N],ans[N];
PII st[20][N];
vector<PII> que[N]; 
int Dis(LL x){ return lower_bound(dis+1,dis+tot+1,x)-dis; }
int RMQ_mx(int l,int r){ int k=__lg(r-l+1); return max(maxn[k][l],maxn[k][r-(1<<k)+1]); }
int RMQ_pos(int l,int r){ int k=__lg(r-l+1); return min(st[k][l],st[k][r-(1<<k)+1]).se; }
struct SegmentTree{
	struct node{
		int l,r;
		LL k,b,addk,addb;
		void tag(LL dk,LL db){ 		
			k+=dk,b+=db,addk+=dk,addb+=db;
		}
	}t[N<<2];
	void spread(int p){
		t[p<<1].tag(t[p].addk,t[p].addb),t[p<<1|1].tag(t[p].addk,t[p].addb),t[p].addk=t[p].addb=0;
	}
	void build(int p,int l,int r){
		t[p].l=l,t[p].r=r;
		if(l==r) return;
		int mid=(l+r)>>1;
		build(p<<1,l,mid),build(p<<1|1,mid+1,r);
	}
	void change(int p,int l,int r,LL dk,LL db){
		if(l>r) return;
		if(l<=t[p].l&&t[p].r<=r) return t[p].tag(dk,db),void();
		spread(p);
		int mid=(t[p].l+t[p].r)>>1;
		if(l<=mid) change(p<<1,l,r,dk,db);
		if(r>mid) change(p<<1|1,l,r,dk,db);
	}
	LL ask(int p,int x){
		if(t[p].l==t[p].r) return t[p].k*dis[x]+t[p].b;
		spread(p);
		int mid=(t[p].l+t[p].r)>>1;
		return (x<=mid)?ask(p<<1,x):ask(p<<1|1,x);
	}
}Seg;
void Init(){
	for(int i=1;i<=n;i++) maxn[0][i]=d[i],st[0][i]={w[i],i};
	for(int t=1;t<=__lg(n);t++){
		for(int i=1;i+(1<<t)-1<=n;i++){
			maxn[t][i]=max(maxn[t-1][i],maxn[t-1][i+(1<<t-1)]);
			st[t][i]=min(st[t-1][i],st[t-1][i+(1<<t-1)]);
		}
	}
}
signed main(){
	freopen("dungeon.in","r",stdin);
	freopen("dungeon.out","w",stdout);
	double beg=clock();
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&d[i]),s[i+1]=s[i]+d[i];
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);
	Init();
	for(int i=1;i<=m;i++){
		int l,r,U; scanf("%d%d%d",&l,&r,&U);
		if(RMQ_mx(l,r-1)>U){
			ans[i]=-1;
			continue;
		}
		dis[++tot]=U;
		int x=lower_bound(s+1,s+(n+1)+1,s[r]-U)-s; 
		x=max(x,l),x=RMQ_pos(x,r-1);
		ans[i]=(s[r]-s[x])*w[x];
		que[l].pb({U,i}),que[x].pb({U,-i});
	}
	sort(dis+1,dis+tot+1),tot=unique(dis+1,dis+tot+1)-dis-1;
	Seg.build(1,1,tot);
	stk[++top]=n+1;
	for(int i=n;i>=1;i--){
		while(w[i]<=w[stk[top]]){
			int x=stk[top],y=stk[top-1]; top--;
			int p1=Dis(s[x]-s[i]),p2=Dis(s[y]-s[i]);
			Seg.change(1,p1,p2-1,w[i]-w[x],1ll*(w[i]-w[x])*(s[i]-s[x]));
			Seg.change(1,p2,tot,0,1ll*(w[i]-w[x])*(s[y]-s[x]));
		}
		stk[++top]=i;
		int p=Dis(d[i]);
		Seg.change(1,1,p-1,w[i],0),Seg.change(1,p,tot,0,1ll*d[i]*w[i]);
		for(PII u:que[i]){
			int id=abs(u.se),x=Dis(u.fi);
			if(u.se>0) ans[id]+=Seg.ask(1,x);
			else ans[id]-=Seg.ask(1,x);
		}
	}
	for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
	cerr << "Time: " << (clock()-beg) << endl;
	return 0;
}
posted @ 2026-01-06 20:13  Green&White  阅读(5)  评论(0)    收藏  举报