zoukankan      html  css  js  c++  java
  • 完美子图(这道题太难了,得写下来要不回头又忘了)

    题目大意:

    给你一个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,否则超时   

     情况4与情况3一样:

    只不过枚举每个左端点时候,保存最大值,然后把两重循环条件换一下即可。

    附上代码:

    #include<bits/stdc++.h>
    using namespace std;
    const int maxn=100010;
    int n,Max[maxn],Min[maxn],a[maxn],cnt[maxn];
    int ans=0,Xiao,Da;
    void Fenzhi(int l,int r){
        //printf("%d",l);
        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--){
            int j=i+Max[i]-Min[i];
            if(j>mid&&j<=r&&Max[j]<Max[i]&&Min[j]>Min[i])ans++;
        }
        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++;
        }
        int j=mid+1,k=mid+1;
        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++;
            }
            ans+=cnt[i-Min[i]+n];
        }
        while(k<j){
            cnt[k-Max[k]+n]--;
            k++;
        }
        j=mid+1;k=mid+1;
        for(int i=mid;i>=l;i--){
            while(Max[j]<Max[i]&&j<=r){
                cnt[j+Min[j]]++;j++;
            }
            while(Min[k]>Min[i]&&k<j){
                cnt[k+Min[k]]--;k++;
            }
            ans+=cnt[i+Max[i]];
        }
        while(k<j){
            cnt[k+Min[k]]--;k++;
        }
    }
    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;
    }
  • 相关阅读:
    棋盘问题 简单搜索DFS
    Equivalent Strings
    素数环
    LeetCode Maximum Product Subarray
    UVA 725 – Division
    矩形嵌套问题
    ACM最大子串和问题
    ACM装箱问题
    ACM田胫赛马
    ACM雷达安放问题
  • 原文地址:https://www.cnblogs.com/liu-yi-tong/p/13285503.html
Copyright © 2011-2022 走看看