1. 程式人生 > 實用技巧 >完美子圖(這道題太難了,得寫下來要不回頭又忘了)

完美子圖(這道題太難了,得寫下來要不回頭又忘了)

題目大意:

給你一個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,否則超時