【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)的。
【CF671E】Organizing a Race 單調棧+線段樹