首先發出題目鏈接:
鏈接:https://ac.nowcoder.com/acm/contest/882/J
來源:牛客網
涉及:分治
點擊這裏回到2019牛客暑期多校訓練營解題—目錄貼
題目如下:
數組的長度已經被定下來了,爲,此時告訴我們肯定不能直接使用前綴和。
但是還有一個條件非常有用:那就是1的個數不多,而且我們知道每一段1的位置及長度。
那麼對於其他所有的-1,有一些-1是有用的(與其他的1或-1成爲一個區間使得區間和仍然大於0),有一些-1則完全沒有用,那些有用的-1和1組成的連續區間我們叫它有用區間。如圖:
於是可以完全不考慮那些沒用的-1,直接考慮所有的1和所有有用的-1,這樣我們需要考慮的區間長度最多隻有的長度。
還有一件事,原數組默認從0開始,然後我在輸入的時候就全部加1,打算從1開始
r[0]=l[0]=0,r[n+1]=l[n+1]=1e9+1;
for(i=1;i<=n;i++){
int a,b;
scanf("%d%d",&a,&b);
l[i]=a+1,r[i]=b+1;//全部加1,讓數組從下標爲1開始
}
對於每一段連續的1,都可以找出一段有用區間,有些有用區間可能重合或者相鄰,那麼這兩段有用區間可以合成爲一段有用區間。如圖:
上圖中對於兩段全1區間,它們所對應的有用區間重合了,於是這整個一段合併成爲一段有用區間。
如下圖仍然是兩段有用區間:
先將所有連續全1區間的左邊界位置和右邊界位置分別放在兩個數組和中
先創建兩個數組和,其中
表示以第i段全1區間的右邊界爲右邊界的最大區間和。
表示以第i段全1區間的左邊界爲左邊界的最大區間和。
那麼front和back數組滿足兩個遞推式:
解釋一下:假設以第i段全1區間的右邊界爲右邊界的區間最大和爲n,第i段全1區間和第i+1段中間的-1數量有m個。
如果,那麼以第i+1段全1區間的右邊界爲右邊界的區間最大和只能爲(表示第i+1段全1區間的1的數量);
如果,那麼
back[n+1]=front[0]=0;
for(i=1,j=n;i<=n && j>=1;i++,j--){
front[i]=r[i]-l[i]+1+max(front[i-1]-(l[i]-r[i-1]-1),0);//從頭到尾更新front的值
back[j]=r[j]-l[j]+1+max(back[j+1]-(l[j+1]-r[j]-1),0);//從尾到頭更新back的值
}
如果第i段有用區間和第i+1段有用區間能合併,那麼滿足
可以參照下圖理解(圖中沒有標出3個全1區間的有用區間):
可以判斷第i個有用區間往後可以合併多少有用區間,用一個數來表示,故
int pos=i;//此時遍歷到第i個有用區間
while(pos<=n && front[pos]+back[pos+1]>=l[pos+1]-r[pos]-1) pos++;//判斷往後能不能繼續合併後面的有用區間
合併完畢,我們就得到了真正意義上的每一段有用區間,對每一段有用區間進行求解,然後將每一段求解的答案加起來就是答案
合併之後的有用區間我們不知道他的左邊界和右邊界的位置,它其實是類似於下面這樣的區間:
這一段合併之後的區間我們還要看它往左和往右能延伸到什麼位置,分別存到變量和中,假設這一段合併區間跨過了第i段到第pos段的全1區間,那麼:
解釋一下:由於這一段合併區間跨過了第i段到第pos段的全1區間,於是l[i]就是合併區間中第一段全1區間的左邊界,r[pos]就是合併區間中最後一段全1區間的右邊界;
如果以l[i]爲左邊界的最大區間和爲back[i],那麼l[i]往左最多可以容納back[i]個-1;
如果以r[pos]爲右邊界的最大區間和爲front[pos],那麼r[i]往右最多可以容納front[pos]個-1;
於是這一段合併區間真正的邊界就被找出來了:
第一步對於這一段合併區間肯定是求前綴和,然後需要判斷有多少對正序對即可(參考逆序對),可以像求解逆序對一樣用樹狀數組,但是會超時。
求合併區間前綴和代碼:
int t=i;//此合併區間第i個全1區間開始,t表示下一個未遍歷到的全1區間的序號
for(int k=first;k<=last;k++){//k從區間開始遍歷到區間結尾,下面那個sum數組是合併區間的前綴和數組
if(k>=l[t] && k<=r[t]) sum[k-first+1]=sum[k-first]+1;//遍歷到某一段全1區間內,前綴和就加1
else sum[k-first+1]=sum[k-first]-1;//遍歷到-1區間內,前綴和就加減1
if(k==r[t]) t++;//此全1區間全部遍歷完畢,t++表示該遍歷第t+1個全1區間了
cnt[sum[k-first+1]+base]=0;//這個先不管
}
但是我們知道由於原合併區間中只有1或者-1,所以在前綴和數組中任意兩個相鄰的數相差1
觀察上方那個合併區間的前綴數組
代碼如下:
#include <iostream>
#include <cstring>
using namespace std;
typedef long long ll;
const int maxn=1e6+5;
const int maxm=1e7+5;
int n;
int l[maxn],r[maxn],back[maxn],front[maxn];
int cnt[3*maxm],num1=0,num2=0;
int sum[3*maxm]={0};
ll ans=0;
int main(){
scanf("%d",&n);
int i,j;
back[n+1]=front[0]=r[0]=l[0]=0,r[n+1]=l[n+1]=1e9+1;
for(i=1;i<=n;i++){
int a,b;
scanf("%d%d",&a,&b);
l[i]=a+1,r[i]=b+1;
}
for(i=1,j=n;i<=n && j>=1;i++,j--){
front[i]=r[i]-l[i]+1+max(front[i-1]-(l[i]-r[i-1]-1),0);
back[j]=r[j]-l[j]+1+max(back[j+1]-(l[j+1]-r[j]-1),0);
}
i=1;
int base=1e7+1;
while(i<=n){
int pos=i,t=i;
while(pos<=n && front[pos]+back[pos+1]>=l[pos+1]-r[pos]-1) pos++;
ll first=(ll)max(l[i]-back[i],1),last=(ll)min((int)1e9,r[pos]+front[pos]);
for(int k=first;k<=last;k++){
if(k>=l[t] && k<=r[t]) sum[k-first+1]=sum[k-first]+1;
else sum[k-first+1]=sum[k-first]-1;
if(k==r[t]) t++;
cnt[sum[k-first+1]+base]=0;
}
cnt[0+base]=1;
for(int k=1;k<=last-first+1;k++){
if(sum[k]>sum[k-1]) num2=num1+cnt[sum[k-1]+base];
else num2=num1-cnt[sum[k]+base];
cnt[sum[k]+base]++;
ans+=num2;
swap(num1,num2);
}
i=pos+1;
}
printf("%lld",ans);
}