1. 程式人生 > >【CF671E】Organizing a Race 單調棧+線段樹

【CF671E】Organizing a Race 單調棧+線段樹

改變 sof AI 連接 長度 HA 什麽 順序 還需

【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)的。

【CF671E】Organizing a Race 單調棧+線段樹