【CF671E】Organizing a Race 单调栈+线段树

【CF671E】Organizing a Race

题意:n个城市排成一排,每个城市内都有一个加油站,赛车每次经过第i个城市时都会获得$g_i$升油。相邻两个城市之间由道路连接,第i个城市和第i+1个城市之间的道路长度为$w_i$,走一单位的路要花1升油。你想在某两个城市之间举办一场锦标赛。如果你选择的两个城市分别是a和b(a<b),则具体过程如下:

1. 赛车从a开始往右走一直走到b,走过城市时会在加油站加油,走过道路时会消耗油,且一开始时就已经在a处加完油了。你需要满足赛车能有足够的油能从a走到b,即不能出现在走到道路的中途时出现没有油的情况。

2. 赛车从b开始往左走一直走到a,过程同上。

你可以认为赛车的油箱是无限大的。

一场锦标赛所经过的城市越多,则这场锦标赛就越成功,即你希望最大化b-a+1。

现在你有k个机会,每个机会是:你可以使任意一个城市的$g_i$增加1。现在你需要合理利用这k次机会,从而最大化b-a+1。

$n\le 100000,k,w_i,g_i\le 10^9$

题解:先考虑从a走到b的这段。我们先维护个前缀和:pre[i]=pre[i-1]+g[i]-w[i]。则在不加油的情况下,一辆车i最远能走到的j 就是 i右面第一个满足pre[j-1]<pre[i-1]的j,我们可以用单调栈来搞一搞,并设i右面第一个走不到的j为next[i]。从i走到next[i]需要的花费就是pre[i-1]-pre[next[i]-1]。根据贪心的想法,如果我们最终选择的城市是a和b,那么在从a走到b的途中,我们应尽可能给右边的城市增加权值。即我们每次可以直接走到next,然后给next的权值增加pre[i-1]-pre[next-1]即可。

下面是一步非常神的操作,我们将所有的i和next[i]连边。然后DFS一遍这棵树,假如当前走到了i。我们令cost[j]表示从i沿着next一直走到j需要的花费,那么如何维护cost[j]呢?我们在进入i这棵子树的时候,将next[i]..n的所有cost都增加,在退出i的子树时再将cost都减回去,则用线段树维护即可。现在我们已经知道了往右走的花费,那如何计算往左走的花费呢?我们再维护个前缀和:suf[i]=suf[i-1]+g[i]-w[i-1](修改时用线段树维护)。根据贪心,如果我们在返回来时需要花费k次机会,则一定是在一开始就直接用完所有的机会。那么从j返回i的花费就是$max\{suf[k],i\le k< j\}-suf[j]$,总花费就是$max\{suf[k],i\le k< j\}-suf[j]+cost[j]$。

现在我们要做的就是找出右面最后一个满足$max\{suf[k],i\le k< j\}-suf[j]+cost[j]\le K$的j。但是左边这坨东西如何搞呢?

我们在线段树上维护这3个东西:

max_p[x]:令p[i]表示cost[i]-suf[i]。max_p[x]维护区间内p的最大值。

max_suf[x]:区间x内suf的最大值。

max_s[x]:如果当前区间是[l,r],则$max_s[x]=min\{max\{suf[j],l\le j<i\}+p[i],mid<i\le r\}$。

具体维护过程过于复杂,请见代码。

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define lson x<<1
#define rson x<<1|1
using namespace std;

const int maxn=100010;
typedef long long ll;
int n,K,top,cnt,ans;
int st[maxn],to[maxn],nxt[maxn],head[maxn],nt[maxn];
ll g[maxn],w[maxn],pre[maxn],suf[maxn];
ll mp[maxn<<2],sp[maxn<<2];	//mp:max_p(p=cost-suf),sp:min_{max_suf{l..j-1}+p}
ll ms[maxn<<2],tag[maxn<<2];	//ms:max_suf,tag:区间+标记,cost+=tag,suf+=tag,所以p不变。
inline void add(int,int);
inline void upd(int,ll);
inline void pushdown(int);
ll calc(int,int,int,ll);
inline void pushup(int,int,int);
void build(int,int,int);
void updata(int,int,int,int,int,ll);
int solve(int,int,int,ll);
int query(int,int,int,ll);
void dfs(int);
inline int rd();
int main()
{
	memset(head,-1,sizeof(head));
	n=rd(),K=rd();
	int i;
	for(i=1;i<n;i++)	w[i]=rd();
	w[n]=1e17;
	for(i=1;i<=n;i++)	g[i]=rd(),pre[i]=pre[i-1]+g[i]-w[i],suf[i]=suf[i-1]+g[i]-w[i-1];	//预处里pre,suf
	for(st[top=0]=n,i=n-1;i>=0;i--)	//求next
	{
		while(top&&pre[st[top]]>=pre[i])	top--;
		nt[i+1]=st[top]+1,add(st[top]+1,i+1),st[++top]=i;
	}
	build(1,n,1);
	top=0,dfs(n+1);
	printf("%d",ans);
	return 0;
}
//------------------------------按照顺序从往下看------------------------------

inline void add(int a,int b)	//略
{
	to[cnt]=b,nxt[cnt]=head[a],head[a]=cnt++;
}
void dfs(int x)	//首先按照之前说的,我们先建出next树,然后遍历next树。
{
	st[++top]=x;
	if(x!=n+1)
	{
		updata(1,n,1,1,x-1,-1e17);	//排除掉i左面的点的干扰
		updata(1,n,1,nt[x]-1,n,pre[x-1]-pre[nt[x]-1]);	//维护cost和suf
		int l=1,r=top,mid;
		while(l<r)
		{
			mid=(l+r)>>1;
			if(pre[x-1]-pre[st[mid]-1]<=K)	r=mid;
			else	l=mid+1;
		}
		updata(1,n,1,st[r-1],n,1e17);	//二分,排除掉i右面过远的点的干扰(如果往右走过不去,则不考虑往左走的情况)。
		ans=max(ans,query(1,n,1,-1e17)-x+1);	//更新答案
		updata(1,n,1,st[r-1],n,-1e17);	//复原
		updata(1,n,1,1,x-1,1e17);
	}
	for(int i=head[x];i!=-1;i=nxt[i])	dfs(to[i]);
	if(x!=n+1)
	{
		updata(1,n,1,nt[x]-1,n,-(pre[x-1]-pre[nt[x]-1]));	//复原
	}
	top--;
}
void build(int l,int r,int x)	//预处理结束时,构建线段树。
{
	if(l==r)
	{
		ms[x]=suf[l];
		mp[x]=-suf[l];
		return ;
	}
	int mid=(l+r)>>1;
	build(l,mid,lson),build(mid+1,r,rson);
	pushup(l,r,x),mp[x]=min(mp[lson],mp[rson]);
}
void updata(int l,int r,int x,int a,int b,ll t)	//区间加操作也跟普通线段树没什么区别。
{
	if(a>b)	return ;
	if(a<=l&&r<=b)
	{
		upd(x,t);
		return ;
	}
	pushdown(x);
	int mid=(l+r)>>1;
	if(a<=mid)	updata(l,mid,lson,a,b,t);
	if(b>mid)	updata(mid+1,r,rson,a,b,t);
	pushup(l,r,x);
}
inline void pushup(int l,int r,int x)	//pushup和pushdown两个操作慢慢讲。
{
	ms[x]=max(ms[lson],ms[rson]);	//ms(max_suf):直接取最值即可,max_p:由于永远不会改变,所以不用维护。
	int mid=(l+r)>>1;
	sp[x]=calc(mid+1,r,rson,ms[lson]);	//sp数组维护起来比较复杂,我们引入calc函数,下面讲。
}
inline void pushdown(int x)	//pushdown比较简单
{
	if(tag[x])
	{
		upd(lson,tag[x]),upd(rson,tag[x]);
		tag[x]=0;
	}
}
inline void upd(int x,ll y)	//比较简单
{
	tag[x]+=y,ms[x]+=y,sp[x]+=y;
}
ll calc(int l,int r,int x,ll t)	//***关键函数*** calc(...,t)=min{max(max_suf{l..i-1},t)+p[i],l<=i<=r}	即我们已知了左边
								//的max_suf,现在要求这个区间中答案的最小值。如何计算呢?
{
	if(l==r)	return t+mp[x];
	pushdown(x);
	int mid=(l+r)>>1;
	if(ms[lson]>=t)	return min(calc(l,mid,lson,t),sp[x]);	//如果max_suf{l,mid}>=t,则t对[mid+1,r]的答案都没有影响,
															//所以直接调用之前的答案sp即可(注意sp维护的是什么!)。
															//然后我们只递归左边就行了。
	return min(t+mp[lson],calc(mid+1,r,rson,t));	//否则,左边的max_suf{l..i-1}都应该取t,则用t+max_p{l,mid}更新答案
													//然后只递归右面就行了。
}								//整个calc的复杂度是O(log)的。

//--------------------分割线-------------------- 上面主要是修改,下面主要是查询。

int query(int l,int r,int x,ll t)	//***关键函数*** 查询函数(即树上二分操作),我们想找到最右面那个答案<=m的点
									//t的定义和calc()里的一样,我们已知了左边的max_suf{l..i-1}=t。实现过程也和calc类似。
{
	if(l==r)	return t+mp[x]<=K?l:0;
	pushdown(x);
	int mid=(l+r)>>1;
	if(ms[lson]>=t)	//讨论:如果max_suf{l,mid}>=t,则t对[mid+1,r]没有影响,我们可以直接调用sp数组。
	{
		if(sp[x]<=K)	return query(mid+1,r,rson,ms[lson]);	//如果[mid+1,r]中的最小值<=K,显然我们应该进入右面查询。
		else	return query(l,mid,lson,t);	//否则呢,显然右面的都不合法,我们进入左边查询。
	}
	else	//如果max_suf{l,mid}<t,则左面的max_suf都应该取t,我们引入solve函数,表示的就是
			//当一个区间的max_suf{..i-1}都取t时的查询结果。而对于右面的,我们还需要递归查询。
	{
		return max(solve(l,mid,lson,t),query(mid+1,r,rson,t));
	}
}
int solve(int l,int r,int x,ll t)	//说白了就是已知区间的max_suf{..i-1}=t时的query函数,但是相对简单一些。
{
	if(l==r)	return t+mp[x]<=K?l:0;
	pushdown(x);
	int mid=(l+r)>>1;
	if(t+mp[rson]<=K)	return solve(mid+1,r,rson,t);	//如果右边的答案<=K,则去右面
	return solve(l,mid,lson,t);	//否则去左边
}									//一次solve的复杂度是O(log)的
inline int rd()
{
	int ret=0,f=1;	char gc=getchar();
	while(gc<‘0‘||gc>‘9‘)	{if(gc==‘-‘)	f=-f;	gc=getchar();}
	while(gc>=‘0‘&&gc<=‘9‘)	ret=ret*10+(gc^‘0‘),gc=getchar();
	return ret*f;
}
//所以呢,我们的总复杂度就是O(n\log^2n)的。

原文地址:https://www.cnblogs.com/CQzhangyu/p/8538525.html

时间: 2024-10-08 16:53:00

【CF671E】Organizing a Race 单调栈+线段树的相关文章

2019南昌网络赛-I(单调栈+线段树)

题目链接:https://nanti.jisuanke.com/t/38228 题意:定义一段区间的值为该区间的和×该区间的最小值,求给定数组的最大的区间值. 思路:比赛时还不会线段树,和队友在这题上弄了3小时,思路大体都是对的,但就是没法实现.这几天恶补线段树. 首先可以利用单调栈来查找满足a[i]为最小值的最大区间L[i]~R[i].然后利用线段树求一个段的和sum.最小前缀lsum和最小后缀rsum.然后遍历a[i]: a[i]>0:最优为sum(L[i],R[i])*a[i] a[i]<

单调栈+线段树

262144K Alice has a magic array. She suggests that the value of a interval is equal to the sum of the values in the interval, multiplied by the smallest value in the interval. Now she is planning to find the max value of the intervals in her array. C

计蒜客38228 Max answer 单调栈 + 线段树

Max answer 与POJ 2796 Feel Good类似,但是这个题有负数,需要特殊处理一下 #include <bits/stdc++.h> #define DBG(x) cerr << #x << " = " << x << endl using namespace std; typedef long long LL; #define iall 1, n, 1 #define lrrt int l, int r,

POJ 3162 Walking Race 树形DP+线段树

给出一棵树,编号为1~n,给出数m 漂亮mm连续n天锻炼身体,每天会以节点i为起点,走到离i最远距离的节点 走了n天之后,mm想到知道自己这n天的锻炼效果 于是mm把这n天每一天走的距离记录在一起,成为一段长度为n的数列 现在mm想要从这数列中选出一个连续的区间,要求这个区间的max-min<=m 输出最长的区间 做了一个下午 思路: 分成2个部分: 1.求出数列,即对于一棵树,求出每一个节点能到达的最远距离 2.对于这段数列,选出一个区间,使得区间的max-min<=m,并且使得区间长度尽量

BZOJ 1012: [JSOI2008]最大数maxnumber 单调队列/线段树/树状数组/乱搞

1012: [JSOI2008]最大数maxnumber Time Limit: 3 Sec  Memory Limit: 162 MBSubmit: 4750  Solved: 2145[Submit][Status][Discuss] Description 现 在请求你维护一个数列,要求提供以下两种操作: 1. 查询操作.语法:Q L 功能:查询当前数列中末尾L个数中的最大的数,并输出这个数的值.限制:L不超过当前数列的长度. 2. 插入操作.语法:A n 功能:将n加上t,其中t是最近一

bzoj-2286 消耗战【虚树+倍增lca+单调栈】

2286: [Sdoi2011消耗战 Time Limit: 20 Sec  Memory Limit: 512 MB Submit: 1815  Solved: 645 [Submit][Status][Discuss] Description 在一场战争中,战场由n个岛屿和n-1个桥梁组成,保证每两个岛屿间有且仅有一条路径可达.现在,我军已经侦查到敌军的总部在编号为1的岛屿,而且他们已经没有足够多的能源维系战斗,我军胜利在望.已知在其他k个岛屿上有丰富能源,为了防止敌军获取能源,我军的任务是

【bzoj3956】Count 单调栈+可持久化线段树

题目描述 输入 输出 样例输入 3 2 0 2 1 2 1 1 1 3 样例输出 0 3 题解 单调栈+可持久化线段树 本题是 bzoj4826 的弱化版(我为什么做题总喜欢先挑难的做QAQ) $k$对点对$(i,j)$有贡献,当且仅当$a_k=max(a_{i+1},a_{i+2},...,a_{r-1})$,且$a_k<a_i\&\&a_k<a_j$. 那么我们可以使用单调栈求出i左面第一个比它大的位置$lp[i]$,和右面第一个比它大的位置$rp[i]$,那么点对$(lp

[JXOI2017]颜色 线段树扫描线 + 单调栈

---题面--- 题解: 首先题目要求删除一些颜色,换个说法就是要求保留一些颜色,那么观察到,如果我们设ll[i]和rr[i]分别表示颜色i出现的最左边的那个点和最右边的那个点,那么题目就是在要求我们选出的区间要满足区间[l, r]内所有颜色的max(rr[i]) <= r,并且min(ll[i]) >= l. 因为是区间相关的问题,又涉及到左右端点,因此我们考虑扫描线,那么考虑如何维护它. 因为每个颜色的ll[i]和rr[i]可以看做构成了一个区间,那么现在已经进入线段树的节点就分2种情况.

Educational Codeforces Round 61 (Rated for Div. 2) G(线段树,单调栈)

#include<bits/stdc++.h>using namespace std;int st[1000007];int top;int s[1000007],t[1000007];int mx[4000007];int sum[4000007];int head[1000007],to[2000007],nex[2000007];int n,k;int a[10000077];int dfn;int tot;void pushup(int rt){    mx[rt]=max(mx[rt