完美子圖(這道題太難了,得寫下來要不回頭又忘了)
題目大意:
給你一個n×n的圖,向其中放n個點,求其中有幾個“完美子圖”。
完美子圖的定義是:一個m×m的圖(1<=m<=n),其中含有m個點,這樣的子圖叫完美子圖。
已知:在原圖中每一行每一列都只有一個點。
分析:
1.對於此類“n×n的圖中有n個點且每一行每一列只有一個點”的問題,我們一般可以把二維的圖拍扁成一維的區間問題,這道題的一維化轉化為:m×m圖裡面有m個點————>l到r的連續區間內,最大列數減去最小列數等於最大行數減最小行數,同時由於區間是連續的,所以轉化為最大行數減去最小行數等於r-l+1。
得出的公式為Max[r,l]-Min[r,l]=r-l,在滿足這個條件的前提下,可以看作多增了一個完美子圖。
why?
我們可以這樣想,一個m×m圖內有m個點,且滿足上面那個同列同行只有一點的條件,那麼在l到l+m-1這個區間裡面,最大的行數一定是正方形的下邊界,最小行數一定是正方形的上邊界,l是左邊界,l+m-1是右邊界,顯然可得上面的推論。
處理方法:
我們可以在輸入的時候,定義一個a陣列,儲存每一列上第幾行有點,方便之後處理。
2.這道題已經被我們轉化到區間問題了,在區間dp沒有明顯的狀態階段時候,我們可以考慮到用線段樹來維護,啊不,線段樹的祖宗:分治思想。
(當然這道題肯定用線段樹是可以的啊)
分治思想就是把一個大問題分成幾個型別相同的遞迴子問題,在這裡我們就可以:
void Fenzhi(int l,int r){ if(l==r){ ans++; return; } int mid=l+r>>1; Fenzhi(l,mid);Fenzhi(mid+1,r); }
根據題目條件,我們可以知道,1×1的格子,只要有點就是完美子圖,而且正巧我們的圖中保證每一列必然存在一個點,所以終止條件如上。
3.這裡的分治時候把l,r分為了兩個區間,我們無需處理那兩個小區間以內的完美子圖數量了,因為它是遞迴子問題,我們需要處理的是區間橫跨兩個小區間的完美子圖們。
例如:l=1,r=5,mid=3。假設1到2,3到4都有一個完美子圖,我們在遞迴處理時候,1,2這個區間就包括在子問題裡了,我們需要處理的是3到4的圖。
那麼如何處理呢?
4.我們定義一些變數:
i:目前處理區間的左端點。
j:目前處理區間的右端點。
Min[x]:x點到mid的區間內的最小值。
Max[x]:x點到mid的區間內的最大值。
對於一段區間(i到j),我們會出現下面四種情況:
1.區間的最大值與最小值都在mid左側。
Max
↓
l————i————————mid————————j——————r
↑
Min
如果這個時候Max-Min=j-i;
根據上面的推論我們可以知道,i到j的區間是一個完美子圖。
我們該如何表示這種情況呢?
顯然為Max[i]-Max[j]==j-i。(因為Max,Min都在mid左側,即Max=Max[i]; Min=Min[i]);
if(Max[i]-Min[i]==j-i&&Max[i]>Max[j]&&Min[i]<Min[j])ans++;
當然,因為區間[i,j]的Min,Max都在i到mid一側,所以必須滿足上面的那幾個條件,可以自己推一下。
2.區間的最大值與最小值都在mid右側。
這種情況與上一種類似,就不多贅述了。
if(Max[j]-Min[j]==j-i&&Max[j]>Max[i]&&Min[j]<Min[i])ans++;
3.區間最小值在mid左側,區間最大值在mid右側
Min
↓
l————i————mid—————j———r
↑
Max
那麼根據類似上面的推法,這種狀態的滿足條件就是Max[j]-Max[i]=j-i;
if(Max[j]-Min[i]==j-i&&Max[j]>Max[i]&&Min[j]>Min[i])ans++;
當然Max在右側,那麼必須保證Max[j]>Max[i],Min也一樣。
4.區間最大值在mid左側,區間最小值在mid右側。
也是與上一種類似:
if(Max[i]-Min[j]==j-i&&Max[j]<Max[i]&&Min[j]<Min[i])ans++;
分析到這裡,程式碼也就呼之欲出了,附上程式碼:
#include<bits/stdc++.h> using namespace std; const int maxn=50010; int n,Max[maxn],Min[maxn],a[maxn]; int ans=0,Xiao,Da; void Fenzhi(int l,int r){ if(l==r){ ans++; return; } int mid=l+r>>1; Fenzhi(l,mid);Fenzhi(mid+1,r); Min[mid]=a[mid];Max[mid]=a[mid]; Max[mid+1]=a[mid+1];Min[mid+1]=a[mid+1]; Xiao=a[mid];Da=a[mid]; for(int i=mid-1;i>=l;i--){ Min[i]=min(Xiao,a[i]); Max[i]=max(Da,a[i]); Xiao=min(Xiao,Min[i]); Da=max(Da,Max[i]); } Xiao=a[mid+1];Da=a[mid+1]; for(int i=mid+2;i<=r;i++){ Min[i]=min(Xiao,a[i]); Max[i]=max(Da,a[i]); Da=max(Max[i],Da); Xiao=min(Min[i],Xiao); } for(int i=mid;i>=l;i--){ for(int j=mid+1;j<=r;j++){ if(Max[i]-Min[j]==j-i&&Max[j]<Max[i]&&Min[j]<Min[i])ans++; if(Max[j]-Min[i]==j-i&&Max[j]>Max[i]&&Min[j]>Min[i])ans++; if(Max[i]-Min[i]==j-i&&Max[i]>Max[j]&&Min[i]<Min[j])ans++; if(Max[j]-Min[j]==j-i&&Max[j]>Max[i]&&Min[j]<Min[i])ans++; } } } int main(){ scanf("%d",&n); for(int i=1;i<=n;i++){ int x,y; scanf("%d%d",&x,&y); a[x]=y; } Fenzhi(1,n); printf("%d",ans); return 0; }
這樣就完了嗎?當然沒有。
因為這道題的n是<50000的,所以這種n方效率的肯定是過不了的,至少要優化到nlogn。
想一想我們在哪裡可以優化呢。
我們注意到,主要的時間複雜度位於那個“4種情況”的位置,我們列舉每一個左端點,再列舉每一個右端點,導致了n方的效率,我們需要對此加以優化。
1、對於1、2兩種情況:
我們列舉每一個左端點i時,根據(Max[i]-Min[i]=j-i),我們可以直接推出j!這樣,處理這兩種情況的時候的效率就變成了n。
for(int i=mid;i>=l;i--){ int j=i+Max[i]-Min[i]; if(j>mid&&j<=r&&Max[j]<Max[i]&&Min[j]>Min[i])ans++; } //狀態1:列舉左端點 for(int j=mid+1;j<=r;j++){ int i=j-Max[j]+Min[j]; if(i>=l&&i<=mid&&Max[i]<Max[j]&&Min[i]>Min[j])ans++; } //狀態2,列舉右端點
2.對於3、4兩種情況:
好麻煩啊啊啊啊好難處理的對於這種情況我們可以(通過意念)找到一個單調性:
例如情況3:
公式變形:(j-i==Max[j]-Min[i]——>j-Max[j]=i-Min[i])
我們從mid到l列舉每一個左端點,我們知道在這個列舉順序下(假設右端點不變),區間的Min只會變小或不變(這是顯然吧!)
好,那麼我們假設列舉到了某個i,我們就先固定住它,由它去更新右區間,如果右區間的Min[j]>Min[i]時,j++,同時記錄cnt[j-Max[j]]++(當前的狀態,後面用到)。直到不滿足條件為止。
j向右列舉的時候跟i類似,也是隻會變小或不變,那麼只要有一個Min[j]<Min[i]這個j後面的點就一定也<Min[i]了。
同時,我們考慮到,左端點在左移時Min值只會變小,那麼上一個左端點遍歷的右端點們既然Min值都大於上一個左端點了,也一定大於這個新的左端點,這樣右端點就不用再從頭再遍歷了,只要接著之前的右端點繼續向後遍歷就ok了。(通過這樣把效率由n方改成了n)
但是!右端點滿足Min值的關係顯然還不夠,還需要滿足一對Max的關係,這樣我們再跑一個k,如果某個j值滿足Min值的關係但不滿足Max值的關係,我們把上面的cnt值再--。
最後每跑完一個i,我們把ans+=cnt[i-Min[i]]。
此時cnt[i-Min[i]]儲存的是所有與i-Min[i]相等的j-Max[j]所儲存的cnt值,而當這兩個相等時候,根據我們一開頭對等式的變形,i到j就是一個完美子圖了!
int j=mid+1,k=mid+1; //注意這裡i-Max[i]可能為負數,所以加上一個n,保證是正數 for(int i=mid;i>=l;i--){ while(Min[j]>Min[i]&&j<=r){ cnt[j-Max[j]+n]++;j++; } while(Max[k]<Max[i]&&k<j){ cnt[k-Max[k]+n]--;k++; } //注意當j跳出迴圈時,指向的是一個不滿足條件的點,這裡k更新的是滿足Min條件的點,所以k<j ans+=cnt[i-Min[i]+n]; } while(k<j){ cnt[k-Max[k]+n]--; k++; } //這裡需要把cnt清零,方便之後使用,但不能memset,否則超時