最大子矩阵( $ s^2 $ 枚举障碍与 $ n imes m $ 悬线法)
题目大意:
有 $ s $ 个障碍分布在一个 $ n imes m $ 的矩阵中。现在让你找出其中不包含任何一个障碍的最大子矩阵。
首先我们要引进一个新概念:(极大子矩阵:所有边都不能再向外扩张的合法矩阵 )(极大化思想:最大子矩阵一定是一个极大子矩阵 )
$ solution~1:~O(s^2) $
首先讲一个 $ O(s^5) $ 算法:枚举两个对角端点(就是四个边界),然后枚举障碍判断这个矩阵是否合法。这个算法很不优,因为它枚举了很多不是极大子矩阵的矩阵,就是个纯暴力。
所以我们优化一下 $ O(s^3) $ :枚举左右边界,然后将边界内的点按y轴排序,每两个相邻的点和左右边界组成一个合法矩阵。这个算法也枚举了左右边界,会产生非极大子矩阵的矩阵。
所以我们需要找到一种枚举方案,使得(矩阵合法)(极大子矩阵)两个条件尽量满足,于是:
为了不做多余运算,我们以点为边界 $ O(s^2) $ :将点按照横坐标排序,依次枚举每一个点作为矩阵的左边界(只要左边界再向左移就会碰到这个点),然后我们枚举它右边的点作为右边界,并在枚举同时更新上下边界。(因为一个合法的矩阵不能有障碍,所以枚举的这些点会限制上下边界(上下边界以外的矩阵不用考虑,因为他们的左边界不是当前的最外层枚举的那个点所限制的,也就是说它们的左边界不是当前枚举的左边界))。不过要注意,我们的 $ n imes m $ 的大边界也可作为极大子矩阵的边界,所以还需要倒着来一遍。
例题: $ POJ~3197~Corral~the~Cows $ (二分,最大子矩阵)
这道题并不是一道正宗的最大子矩阵,但是它同样用到了枚举点的思想,博主刷题不多,先用着凑活一下,大家只需要知道这种方法就行。
$ Code: $
#include<iostream>
#include<cstdio>
#include<iomanip>
#include<algorithm>
#include<cstring>
#include<cstdlib>
#include<ctime>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#include<set>
#define ll long long
#define db double
#define rg register int
using namespace std;
int n,c;
struct su{
int x,y;
inline bool operator <(const su &z)const{
return y<z.y;
}
}a[505];
inline bool cmp (const su &x,const su &y){
if(x.x==y.x)return x.y<y.y;
return x.x<y.x;
}
inline int qr(){
register char ch; register bool sign=0; rg res=0;
while(!isdigit(ch=getchar()))if(ch=='-')sign=1;
while(isdigit(ch))res=res*10+(ch^48),ch=getchar();
if(sign)return -res; else return res;
}
inline bool check(int x){
multiset<su> s; rg res=0;
for(rg i=1,j=1;i<=n;++i){
while(j<=n&&a[i].x+x-1>=a[j].x)
s.insert(a[j]), ++j;
multiset<su>::iterator l,r;
l=r=s.begin(); res=0;
while(l!=s.end()){
while(r!=s.end()&&(*r).y-(*l).y+1<=x) ++r,++res;
++l; if(res>=c)return 0; --res;
}
s.erase(s.find(a[i]));
}return 1;
}
int main(){
//freopen(".in","r",stdin);
//freopen(".out","w",stdout);
c=qr(); n=qr();
for(rg i=1;i<=n;++i)
a[i].x=qr(),a[i].y=qr();
sort(a+1,a+n+1,cmp);
rg l=1,r=10000,mid;
while(l<=r){
mid=(l+r)>>1;
if(check(mid))l=mid+1;
else r=mid-1;
}printf("%d
",l);
return 0;
}
$ solution~2:~O(n imes m) $ 悬线法
这是很早之前学的东西了,就当做个总结:
上文我们已经提到了极大子矩阵,还有一个 $ s^2 $ 做法,但是这个做法在障碍比较多的时候是不行的。所以我们考虑另一种与边界长度有关的最大子矩阵做法。上一种做法里,我们强调让枚举的矩阵尽量是极大子矩阵,这个思想在悬线法中也是核心地位。
我们知道每一个点向上向左向右的合法极大子矩阵有很多个,注意没有向下。所以我们不可能将每个点的向上向左向右的合法极大子矩阵求出来,这样会有大量重复。所以我们钦定从每一个点向上碰到的第一个障碍为上边界,然后向左右尽量延伸,所得到的矩阵为这个点用悬线法得到的矩阵,之所以不向下是为了方便从第一排开始递推寻找这个矩阵。我们可以从这个矩阵的定义里知道,所有极大子矩阵一定都可以用这种方式被找出来(证明:极大子矩阵的上边界的上一排一定有一个障碍,这个障碍一直向下到这个极大子矩阵的下边界所对应的那个点,它用悬线法求出的矩阵就是这个极大子矩阵)。而最大子矩阵被包含在极大子矩阵中,所以悬线法的正确性得到证明。接下来是复杂度和过程详解。
我们需要求出每一个点用悬线法求得的矩阵。为此,我们先求出每个点向左端延伸的最远距离,向右端延伸的最远距离,向上延伸的最远距离。然后开始枚举整个 $ n imes m $ 的矩阵,过程是从第一排向下递推。因为我们求的是一个矩阵,所以当前点在这一排向左向右延伸的最远距离不是所求矩阵的最远距离,它还受到上面几排的左右端的影响(所以我们从第一排向下推)。然后我们这个点用悬线法得到的矩阵的最左端就是它在当前排向左的最远距离和上面那些点到最左端的距离的最小值,这个递推记录一下就好,右端点同理,而矩阵的高从一开始就确定了,然后直接计算就好!
注意:每个点向左端延伸的最远距离,向右端延伸的最远距离,可以边悬线法边求。
例题:洛谷 $ P4147 $ 玉蟾宫
$ Code: $
#include<bits/stdc++.h>
using namespace std;
int n,m,i,j,l[1001][1001],r[1001][1001],h[1001][1001],tot;
int main(){
char c;
scanf("%d%d",&n,&m);
for(i=1;i<=n;i++)
for(j=1;j<=m;j++){
cin>>c;
if(c=='F')h[i][j]=h[i-1][j]+1;
}
for(i=1;i<=m;i++)r[0][i]=m+1;
for(i=1;i<=n;i++){
int f=0;
for(j=1;j<=m;j++){
if(h[i][j])l[i][j]=max(l[i-1][j],f);
else f=j;
}
f=m+1;
for(j=m;j>=1;j--){
if(h[i][j])r[i][j]=min(r[i-1][j],f);
else r[i][j]=m+1,f=j;
}
for(j=1;j<=m;j++)tot=max(tot,(r[i][j]-l[i][j]-1)*h[i][j]);
}
cout<<tot*3;
return 0;
}