zoukankan      html  css  js  c++  java
  • T3118 01完美矩阵【计数,前缀和,差分,好题】

    Online Judge:未知

    Label:好题,计数,前缀和

    题目描述

    一个01矩形被称为是完美01矩形,如果满足下面3个条件:

    (1)它的四条边上都是1

    (2)内部(除了4条边)的0和1的个数之差不超过1

    (3)大小至少是2*2

    给定一个01矩阵,求可以在其中圈出多少完美01矩形。

    输入

    第一行两个整数n和m

    接下来n行,每行m个数,0或者1.

    输出

    输出完美01矩形的个数。

    样例

    Input

    4 4
    1 1 1 1
    1 0 1 1
    1 1 0 1
    1 1 1 1
    
    5 5
    1 0 1 1 1
    1 0 1 0 1
    1 1 0 1 1
    1 0 0 1 1
    1 1 1 1 1
    

    Output

    3
    
    3
    

    Hint

    对于30%的数据,n和m的数据范围([1,20]);
    对于60%的数据,n和m的数据范围([1,100]);
    对于100%的数据,n和m的数据范围([1,300]).

    题解

    60pts

    (O(N^4))随便怎么敲都可以。直接枚举矩形上的两角。对于随机数据,下面的代码跑的飞快。但是如果有大量1就挂掉了。

    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int N=302;
    int a[N][N],n,m;
    int sum[N][N];
    int le[N][N],ri[N][N],up[N][N],down[N][N];
    inline int read(){
        int x=0;char c=getchar();
        while(c<'0'||c>'9')c=getchar();
        while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
        return x;
    }
    void pre(){
        register int i,j;
        for(i=1;i<=n;++i){
            int lst=0;
            for(j=1;j<=m;++j){
                if(a[i][j])le[i][j]=++lst;
                else lst=0;
            }
            lst=0;
            for(j=m;j>=1;j--){
                if(a[i][j])ri[i][j]=++lst;
                else lst=0;
            }
        }
        for(j=1;j<=m;++j){
            int lst=0;
            for(i=1;i<=n;++i){
                if(a[i][j])up[i][j]=++lst;
                else lst=0;
            }
            lst=0;
            for(i=n;i>=1;i--){
                if(a[i][j])down[i][j]=++lst;
                else lst=0;
            }
        }
         
        for(i=1;i<=n;++i)for(j=1;j<=m;++j){
            sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+a[i][j];
        }
    }
    inline int calc(int x,int y,int x2,int y2){
        int res=sum[x2][y2]-sum[x-1][y2]-sum[x2][y-1]+sum[x-1][y-1];
        return res;
    }
    namespace p60{
        void solve(){
            register int i,j,k,l;
            ll ans=0;
            for(i=1;i<=n;++i)for(j=1;j<=m;++j)if(a[i][j]){
                for(k=i+1;k<=i+down[i][j]-1;k++)for(l=j+1;l<=j+ri[i][j]-1;l++)if(a[k][l]){
                    if((k-up[k][l]+1<=i)&&(l-le[k][l]+1<=j)){ 
                        int tmp=calc(i+1,j+1,k-1,l-1),o=(k-i-1)*(l-j-1);
                        if(abs(o-2*tmp)<=1)++ans;    
                    }
                }
            }
            printf("%lld
    ",ans);
        }
    }
    int main(){
    //  freopen("matrix.in","r",stdin);
    //  freopen("matrix.out","w",stdout);
        n=read(),m=read();
        for(register int i=1;i<=n;++i)for(register int j=1;j<=m;++j)a[i][j]=read();
        pre();
        p60::solve();
    }
    

    100pts

    看数据范围,正解复杂度应该是(O(N^3))的。

    对于这类矩阵的计数题,有一个常见套路做法:

    (O(N^2))时间枚举上下行边界,对于列,在(O(N))时间内扫一遍,用前缀和等维护计数。


    step0.预处理

    先预处理一个东西s[i][j],表示(i,j)的二维前缀和,但与普通的前缀和不同,在这里将0计为-1,将1计为+1。这样处理有什么好处呢?下面会提到。

    for(i=1;i<=n;i++)for(j=1;j<=m;j++){
    		scanf("%d",&a[i][j]);
    		s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+(a[i][j]==1?1:-1);
    	}
    

    这样可以求得以(x,y)为左上角,(x2,y2)为右下角的矩形"面积"(为了方便描述,下面都用“面积”表示矩形内的01个数差)。比如这个矩形内0个数比1个数多5,则面积为-5,如果0个数等于1个数,则面积为0。

    inline int Area(int x,int y,int x2,int y2){
    	return s[x2][y2]-s[x-1][y2]-s[x2][y-1]+s[x-1][y-1];
    }
    

    step1.统计

    先枚举上下边。

    for(i=1;i<n;i++)for(j=i+1;j<=n;j++){//上下边 
    

    接下里要考虑在(O(N))时间内求得以(i,j)为上下边的01完美矩阵个数。


    先思考一种比较low的做法。

    下面代码中,直接暴力统计左右边在([k,l])范围内的符合条件的矩形个数。虽然看起来是(O(N^3))的,但实际是(O(N^2))的(因为后面(k=l+1)跳了一下)。

    for(k=1;k<=m;k++)if(a[i][k]&&a[j][k]){
    	int l=k;
    	while(l+1<=m&&a[i][l+1]&&a[j][l+1])l++;//找到
    	if(k==l)continue;	
    	
        //暴力统计
        int o1,o2;
        for(o1=k;o1<=l;o1++)for(o2=o1;o2<=l;o2++){
            if(是01完美矩形(i,j,o1,o2))计数;
        }
        
    	k=l+1;	
    }
    

    瓶颈在于这个(O(N^2))的暴力统计。

    解决方法就是用到之前预处理的前缀和。见下面代码。

    利用差分思想。每次找到合法的列,查询前面的前缀和是否有和它相差1以内的,计入答案。然后把自己的前缀和加入统计。

    for(p=k;p<=l;p++)if(Area(i,p,j,p)==j-i+1){	
    	int pre=Area(i+1,k+1,j-1,p-1)+base,now=Area(i+1,k+1,j-1,p)+base;	
    	ans+=cnt[pre-1]+cnt[pre]+cnt[pre+1];
    	cnt[now]++;
    }
    

    由于矩形面积可能为负,所以要加上一个基量(base)

    而且计算完([k,l])内的矩形后,还要清空(cnt[])数组,清空代码如下,和上面反一下:

    for(p=k;p<=l;p++)if(Area(i,p,j,p)==j-i+1){
    	int now=Area(i+1,k+1,j-1,p)+base;
    	cnt[now]--;
    }	
    

    综上时间复杂度为(O(N^3)),完整代码如下:

    //枚举上下边,前缀和统计 
    #include<bits/stdc++.h>
    typedef long long ll;
    using namespace std;
    const int N=305,base=N*N;
    int n,m,a[N][N],s[N][N],cnt[2*N*N];
    ll ans=0;
    inline int Area(int x,int y,int x2,int y2){
    	return s[x2][y2]-s[x-1][y2]-s[x2][y-1]+s[x-1][y-1];
    }
    int main(){
    	scanf("%d%d",&n,&m);
    	register int i,j,k,p; 
    	for(i=1;i<=n;i++)for(j=1;j<=m;j++){
    		scanf("%d",&a[i][j]);
    		s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+(a[i][j]==1?1:-1);
    	}
    	for(i=1;i<n;i++)for(j=i+1;j<=n;j++){//上下边 
    		for(k=1;k<=m;k++)if(a[i][k]&&a[j][k]){
    			int l=k;
    			while(l+1<=m&&a[i][l+1]&&a[j][l+1])l++;
    			if(k==l)continue;	
    			for(p=k;p<=l;p++)if(Area(i,p,j,p)==j-i+1){	
    				int pre=Area(i+1,k+1,j-1,p-1)+base,now=Area(i+1,k+1,j-1,p)+base;	
    				ans+=cnt[pre-1]+cnt[pre]+cnt[pre+1];
    				cnt[now]++;
    			}
    			for(p=k;p<=l;p++)if(Area(i,p,j,p)==j-i+1){
    				int now=Area(i+1,k+1,j-1,p)+base;
    				cnt[now]--;
    			}	
    			k=l+1;	
    		}
    	}
    	printf("%lld
    ",ans);
    }
    
  • 相关阅读:
    PAT B1027 打印沙漏 (20 分)
    PAT B1025 反转链表 (25 分)
    PAT B1022 D进制的A+B (20 分)
    PAT B1018 锤子剪刀布 (20 分)
    PAT B1017 A除以B (20 分)
    PAT B1015 德才论 (25 分)
    PAT B1013 数素数 (20 分)
    PAT B1010 一元多项式求导 (25 分)
    HDU 1405 The Last Practice
    HDU 1165 Eddy's research II
  • 原文地址:https://www.cnblogs.com/Tieechal/p/11626714.html
Copyright © 2011-2022 走看看