用极大化思想解决矩形问题学习笔记
-
问题引入:
Winter Camp2002,奶牛浴场
题意简述:
John要在矩形牛场中建造一个大型浴场,但是这个大型浴场不能包含任何一个奶牛的产奶点,但产奶点可以出在浴场的边界上。John的牛场和规划的浴场都是矩形,浴场要完全位于牛场之内,并且浴场的轮廓要与牛场的轮廓平行或者重合。要求所求浴场的面积尽可能大。
参数约定:产奶点的个数S不超过5000,牛场的范围N×M不超过30000×30000。
输入:
10 10 4 1 1 9 1 1 9 9 9
输出:
80
-
定义:
有效子矩形: 内部(不包含边界)没有障碍点的子矩形(四边均与坐标轴平行)
极大有效子矩形:不被任何一个有效子矩形包含(除本身)
最大有效子矩形:最大的有效子矩形
-
小性质 :
1. 极大有效子矩形的四条边无法向四边拓展, 也就是说四条边都存在障碍点或与大矩形边界重合
2.存在一个障碍点的矩形中最大有效子矩形一定是极大有效子矩形
-
【证明】:若最大有效子矩形不是极大有效子矩形,那么一定存在一个有效子矩形包括它,这与它的最大性相违背
-
解决问题:
1.思想一:枚举所有的极大有效子矩形
2.思想二:垂线法(后文介绍)
算法一 O():
由思想一我们可以得知,我们要枚举极大有效子矩形,尽量不枚举无效的,不是极大的子矩形
而由性质一得知,极大子矩形边界中必含障碍点
所以我们将所有障碍点按横坐标排序, 枚举障碍点所在纵线(与y轴平行的线)为极大有效子矩形的左边界(关于左边界是矩形边界下文讨论),再从左到右的扫描障碍点,更新答案后再更新上下边界
-
更新答案:ans = max ( (上边界 - 下边界)* 横坐标之差 , ans)
-
边界初始化:up = 0, down = 矩形纵长
-
更新边界: 如果新点在左边界点的上方,up = min(up, 其纵坐标);否则 down = max(down, 其纵坐标);
-
bug & 改bug :
bug在于枚举过程中忽略了两类情况,一种是左边界在矩形左边界或右边界在矩形右边界的极大有效子矩形,另一种是左右边界都在矩形边界上(王知昆大佬的分类)。解决方法:将大矩形的四个顶点加入点集,然后横坐标排序跑一遍,纵坐标排序再跑一遍。横坐标跑的时候可以解决右边界问题,纵坐标跑时可以解决剩下问题(就像跑横坐标时可以解决上边界与下边界问题一样)
最后贴一下代码:
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 6005;
struct node{
int x, y;
}a[N];
int L, W;
bool cmp1(node i,node j) {
return i.x < j.x;
}
bool cmp2(node i,node j) {
return i.y < j.y;
}
int n;
int main() {
cin >> L >> W >> n;
for (int i = 1;i <= n; i++)
scanf ("%d %d", &a[i].x, &a[i].y);
a[n+1].y = a[n+1].x = a[n+2].x = a[n+3].y = 0;
a[n+2].y = a[n+4].y = W;
a[n+3].x = a[n+4].x = L;
n += 4; //加入矩形的四个顶点
sort(a + 1, a + n + 1, cmp1);
int ans = 0, up, down, v;
for (int i = 1;i <= n; i++) {
up = 0, down = W, v = L - a[i].x;
for (int j = i + 1;j <= n; j++) {
if (v * (down - up) <= ans) break; //剪枝
ans = max(ans, (down - up) * (a[j].x - a[i].x));
if (a[j].y >= a[i].y) down = min(down, a[j].y);
else up = max(up, a[j].y);
}
}
sort(a + 1, a + n + 1, cmp2);
int l = 0, r = L;
for (int i = 1;i <= n; i++) {
l = 0, r = L, v = W - a[i].y;
for (int j = i + 1;j <= n; j++) {
if (v * (r - l) <= ans) break;
ans = max(ans, (r - l) * (a[j].y - a[i].y));
if (a[j].x >= a[i].x) r = min(r, a[j].x);
else l = max(l, a[j].x);
}
}
cout << ans << endl;
return 0;
}
缺陷:可拓展性不够(后文例题介绍),在点(最多N * M)密集的情况下表现较差
算法二 O(N*M):
前一种算法在许多点横坐标都相同时会做许多无用功,解决这个问题需要新的思路。通过极大有效矩形的定义得知,它的个数不会超过NM,所以我们决心寻找一种N*M级别的算法。
-
定义
有效竖线:除了两个端点,不包含任何障碍点的线段
悬线:上端点是障碍点或在矩形边界上的有效竖线
对于一个极大有效子矩形中,一定至少含有一条悬线,所以我们可以利用悬线来拓展形成极大有效子矩形
如果一条悬线尽可能的向两端扩张,直到碰到障碍点或矩形边界,则将此矩形面积更新ans,最终ans一定是最大有效子矩形面积
以每个格子为下端点的悬线有且仅有一条(除顶层格子),故悬线个数(n-1)*m。我们需要O(1)的查询每条悬线所对应的矩形;预处理出每个点向左到达的最远点,向右到达的最远点(向左和右扩张的宽度),向上到达的最远点(悬线的长度)
贴代码:
//洛谷P4147
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 2000;
int map[N][N], l[N][N], r[N][N], h[N][N];
int main() {
int n, m;
cin >> n >> m;
for (int i = 1;i <= n; i++)
for (int j = 1;j <= m; j++) {
char s[6];
scanf ("%s", s);
if (s[0] == 'R') map[i][j] = 1;
}
for (int i = 1;i <= n; i++) {
for (int j = 1;j <= n; j++)
h[i][j] = 1;
l[i][1] = 1; r[i][m] = m;
for (int j = 2;j <= m; j++)
if (map[i][j-1]) l[i][j] = j;
else l[i][j] = l[i][j-1];
for (int j = m - 1;j >= 1; j--)
if (map[i][j+1]) r[i][j] = j;
else r[i][j] = r[i][j+1];
}
int ans = 0;
for (int i = 1;i <= n; i++) {
for (int j = 1;j <= m; j++) {
if (map[i][j]) continue;
if (i == 1)
ans = max(ans, r[i][j] - l[i][j] + 1);
else {
if (!map[i-1][j]) {
h[i][j] = h[i-1][j] + 1;
l[i][j] = max(l[i][j], l[i-1][j]);
r[i][j] = min(r[i][j], r[i-1][j]);
ans = max(ans, (r[i][j] - l[i][j] + 1) * h[i][j]);
}
}
}
}
cout << 3 * ans << endl;
return 0;
}