极大子矩阵——悬线法总结
定义
顾名思义,就是从每一个点(或者边界)开始,以此为边界,开始像用一根竖线一样不断移动,在遇到障碍点或边界时确定出极大子矩阵。
核心内容就是确定一条边界,不断扩展并修改其他边界
可能有点抽象,做题理解一下就好。
在 wzk 大佬的国家队论文浅谈用极大化思想解决最大子矩阵问题中有详细的讲解
从题目入手讲解
例题
本人在洛谷建了一个有关悬线法的题单,可以拿这些题入门-->悬线法合集
棋盘制作
题目链接
思路分析
- 基本上是悬线法板子了。
- 依据悬线法基本思路,我们对于每个点 \((i,j)\) 开三个数组:\(left[i][j]\),\(right[i][j]\),\(up[i][j]\)。分别表示 \((i,j)\) 这一点在矩阵内向左能扩展到的最远位置,向右能扩展到的最远位置,向上扩展的长度。至于如何进行扩展,看下面的代码就能理解了
- 而这道题扩展的时候判断一下点值相不相同就好了,这样就保证了01交错。
- 正方形一定包含在矩形里面,每次取矩形的较短边就好了
\(Code\)
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#define R register
#define N 2020
using namespace std;
inline int read(){
int x = 0,f = 1;
char ch = getchar();
while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
int n,m,a[N][N],l[N][N],r[N][N],up[N][N];//l,r代表每个点向左(右)扩展到的最远位置,up表示向上扩展的高度
int ans1,ans2;
int main(){
n = read(),m = read();
for(R int i = 1;i <= n;i++){
for(R int j = 1;j <= m;j++)a[i][j] = read(),l[i][j] = r[i][j] = j,up[i][j] = 1;
}
for(R int i = 1;i <= n;i++){
for(R int j = 2;j <= m;j++){
if(a[i][j]!=a[i][j-1])l[i][j] = l[i][j-1];//在当前行内扩展左界
}
}
for(R int i = 1;i <= n;i++){
for(R int j = m-1;j;j--){
if(a[i][j]!=a[i][j+1])r[i][j] = r[i][j+1];//在当前行内扩展右界
}
}
for(R int i = 1;i <= n;i++){
for(R int j = 1;j <= m;j++){
if(i>1&&a[i][j]!=a[i-1][j]){//向上扩展
l[i][j] = max(l[i][j],l[i-1][j]);//左边界和右边界需要缩小范围就缩小,保证矩阵是极大的同时也是合法的,应该比较好理解
r[i][j] = min(r[i][j],r[i-1][j]);
up[i][j] = up[i-1][j]+1;
}
int length = r[i][j]-l[i][j]+1;
int width = min(length,up[i][j]) ;
ans1 = max(ans1,width*width);
ans2 = max(ans2,length*up[i][j]);
}
}
printf("%d\n%d\n",ans1,ans2);
return 0;
}
奶牛浴场
题目链接
思路分析
- 这题的特殊之处在于,正常的悬线法是没法做的,因为你数组开不下这么大,所以需要从点来入手
- 而从点入手的话,在不断寻找极大子矩阵的时候可能会出现许多不同的情况,而这些情况都需要进行特殊处理
- 首先依据悬线法的思路,我们将所有点按横坐标排序(注意这题是坐标系,而不是矩阵的几行几列),然后以每个点为左(右)边界,向右(左)不断寻找极大子矩阵,在此过程中依据经过的点的纵坐标修改矩阵的上界和下界。
- 这时很有可能出现矩阵的另一个左右边界到头了的情况,所以最后再更新一下答案
- 然后就是如果遇到纵坐标相同的点怎么办,其实这时候直接跳过即可,因为如果纵坐标相同,我们修改上界还是下界都是可行的,而无论是修改下界还是上界,都是会在纵坐标相同的点的扫描过程中再次被扫描到的。
- 上面的过程都是枚举每个点为左(右)边界,而还有一种情况就是矩阵的左右边界都到头了(比如样例的答案),这时候我们在扫描的时候是无法扫描到的,所以最后再将所有点按纵坐标排序,排完序后相邻的纵坐标不同的两点之间是没有其它点的,这时候就会出现以这两点为上下界,整个牛场的左右两端为左右边界的极大矩阵。
- 还有就是要将矩阵的四个顶点人为添加进去,因为边缘显然也有极大矩阵
\(Code\)
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#define N 5010
#define R register
using namespace std;
inline int read(){
int x = 0,f = 1;
char ch = getchar();
while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
int l,w,n,ans;
struct data{
int x,y,id;
}a[N];
inline bool cmp1(data a,data b){
return a.x==b.x ? a.y < b.y : a.x < b.x;
}
inline bool cmp2(data a,data b){
return a.y==b.y ? a.x < b.x : a.y < b.y;
}
int main(){
l = read(),w = read();
n = read();
for(R int i = 1;i <= n;i++)a[i].x = read(),a[i].y = read();
a[++n] = data{0,0},a[++n] = data{0,w},a[++n] = data{l,0},a[++n] = data{l,w};//人为加点
sort(a+1,a+1+n,cmp1);
for(R int i = 1;i <= n;i++){
int up = w,down = 0,len = l-a[i].x,flag = 0;//先向右扫,up、down表示上下界,len表示这个点到牛场右边界的长度,flag表示遇到纵坐标相同的点
for(R int j = i+1;j <= n;j++){
if(a[j].y>=down&&a[j].y<=up){
if(len*(up-down)<=ans)break;
ans = max(ans,(up-down)*(a[j].x-a[i].x));//因为全是坐标,所以千万别加1……
if(a[i].y==a[j].y){
flag = 1;break;//遇到纵坐标相同的点直接退出
}
if(a[j].y>a[i].y)up = min(up,a[j].y);//更新上下界
else down = max(down,a[j].y);
}
}
if(!flag)ans = max(ans,len*(up-down));//如果中途退出就不能再更新了,因为上下界没有更新完
up = w,down = 0,len = a[i].x,flag = 0;//向左扫,同理
for(R int j = i-1;j;j--){
if(a[j].y>=down&&a[j].y<=up){
if(len*(up-down)<=ans)break;
ans = max(ans,(up-down)*(a[i].x-a[j].x));
if(a[i].y==a[j].y){
flag = 1;break;
}
if(a[j].y>a[i].y)up = min(up,a[j].y);
else down = max(down,a[j].y);
}
}
if(!flag)ans = max(ans,len*(up-down));
}
sort(a+1,a+1+n,cmp2);//上面说的左右边界均在牛场边界的情况
for(R int i = 1;i < n;i++)ans = max(ans,(a[i+1].y-a[i].y)*l);
printf("%d\n",ans);
return 0;
}