完美子图(这道题太难了,得写下来要不回头又忘了)
题目大意:
给你一个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; }