完美子图(这道题太难了,得写下来要不回头又忘了)

Posted liu-yi-tong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了完美子图(这道题太难了,得写下来要不回头又忘了)相关的知识,希望对你有一定的参考价值。

题目大意:

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

 

以上是关于完美子图(这道题太难了,得写下来要不回头又忘了)的主要内容,如果未能解决你的问题,请参考以下文章

常见的链表翻转,字节跳动加了个条件,面试者高呼:我太难了!

15 设计电影租借系统

一个python作业第二题我不会做?

写这篇博客之前,我又忘了“==”和equals的区别。

36个谜题!深入理解Scala的有趣途径

百度空间模板怎么做?太难了~我又那么笨怎么办啊?