悬线法
学习资料
- 题解 P1169 【[ZJOI2007]棋盘制作By 顾z
- 最大子矩阵问题&悬线法 学习笔记By Clove_unique
- [DP专题]悬线法 ByLevenKoko
- 浅谈用极大化思想解决最大子矩形问题By 王知昆
一些说明:这个算法比较套路
悬线:上端点覆盖了一个障碍点或达到整个矩形上端的有效竖线。
如果将一个悬线向左右两个方向尽可能移动所得到的有效子矩形称为这个悬线所对应的子矩形,那么所有悬线所对应的有效子矩形的集合一定包含了所有极大子矩形的集合。
题目
[ZJOI2007]棋盘制作
【题目描述】
国际象棋是世界上最古老的博弈游戏之一,和中国的围棋、象棋以及日本的将棋同享盛名。据说国际象棋起源于易经的思想,棋盘是一个
8
∗
8
8 * 8
8∗8大小的黑白相间的方阵,对应八八六十四卦,黑白对应阴阳。而我们的主人公小Q,正是国际象棋的狂热爱好者。作为一个顶尖高手,他已不满足于普通的棋盘与规则,于是他跟他的好朋友小W决定将棋盘扩大以适应他们的新规则。小Q找到了一张由
N
∗
M
N * M
N∗M个正方形的格子组成的矩形纸片,每个格子被涂有黑白两种颜色之一。小Q想在这种纸中裁减一部分作为新棋盘,当然,他希望这个棋盘尽可能的大。不过小Q还没有决定是找一个正方形的棋盘还是一个矩形的棋盘(当然,不管哪种,棋盘必须都黑白相间,即相邻的格子不同色),所以他希望可以找到最大的正方形棋盘面积和最大的矩形棋盘面积,从而决定哪个更好一些。于是小Q找到了即将参加全国信息学竞赛的你,你能帮助他么?
【输入文件】
第一行包含两个整数N和M,分别表示矩形纸片的长和宽。接下来的N行包含一个N * M的01矩阵,表示这张矩形纸片的颜色(0表示白色,1表示黑色)。
【输出文件】
包含两行,每行包含一个整数。第一行为可以找到的最大正方形棋盘的面积,第二行为可以找到的最大矩形棋盘的面积(注意正方形和矩形是可以相交或者包含的)。
【输入样例】
3 3
1 0 1
0 1 0
1 0 0
【输出样例】
4
6
【数据规模】
对于20%的数据,N, M ≤ 80
对于40%的数据,N, M ≤ 400
对于100%的数据,N, M ≤ 2000
分析:
悬线法:
l
e
f
t
[
i
]
[
j
]
left[i][j]
left[i][j]表示从
(
i
,
j
)
(i,j)
(i,j)所能走到的最左边的位置
r
i
g
h
t
[
i
]
[
j
]
right[i][j]
right[i][j]表示从
(
i
,
j
)
(i,j)
(i,j)所能走到的最右边的位置
o
v
e
r
[
i
]
[
j
]
over[i][j]
over[i][j]表示从
(
i
,
j
)
(i,j)
(i,j)所能走到的最上边的长度
这个方法挺套路的。这样求出的面积中一定包含最优解
注意事项:
(1)调试过后或者修改之后必要时记得还原
(2)注意处理边界,谨慎
(3)看清有些变量的值不一定相等
代码
/***********************
User:Mandy.H.Y
Language:c++
Problem:luogu1169
Algorithm:悬线法
Date:2019.8.13
***********************/
#include<bits/stdc++.h>
#define Max(x,y) ((x) > (y) ? (x) : (y))
#define Min(x,y) ((x) < (y) ? (x) : (y))
using namespace std;
const int maxn = 2005;
int n,m,ans1,ans2;
int a[maxn][maxn];
int lef[maxn][maxn],rit[maxn][maxn],ovr[maxn][maxn];
template<class T>inline void read(T &x){
x = 0;bool flag = 0;char ch = getchar();
while(!isdigit(ch)) flag |= ch == '-',ch = getchar();
while(isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48),ch = getchar();
if(flag) x = -x;
}
template<class T>void putch(const T x){
if(x > 9) putch(x / 10);
putchar(x % 10 | 48);
}
template<class T>void put(const T x){
if(x < 0) putchar('-'),putch(-x);
else putch(x);
}
void file(){
freopen("chess.in","r",stdin);
freopen("chess.out","w",stdout);
}
void readdata(){
read(n);read(m);
for(int i = 1;i <= n; ++ i)
for(int j = 1;j <= m; ++ j){
read(a[i][j]);
lef[i][j] = rit[i][j] = j;
ovr[i][j] = 1;
}
}
void work(){
for(int i = 1;i <= n; ++ i)
for(int j = 2;j <= m; ++ j)
if(a[i][j] != a[i][j - 1])
lef[i][j] = lef[i][j - 1];
for(int i = 1;i <= n; ++ i)
for(int j = m - 1;j >= 1; -- j)
if(a[i][j] != a[i][j + 1])
rit[i][j] = rit[i][j + 1];
for(int i = 1;i <= n; ++ i)//调试过后或者修改之后必要时记得还原
for(int j = 1;j <= m; ++ j){
if(i > 1 && a[i][j] != a[i - 1][j]){//大于1时就更新,等于1(上边界)直接算
lef[i][j] = max(lef[i][j],lef[i - 1][j]);
rit[i][j] = min(rit[i][j],rit[i - 1][j]);
ovr[i][j] = ovr[i - 1][j] + 1;
}
int len1 = rit[i][j] - lef[i][j] + 1;
int len2 = min(ovr[i][j],len1);
ans1 = max(ans1,len2 * len2);
ans2 = max(ans2,len1 * ovr[i][j]);//ovr[i][j]不一定等于len2
}
put(ans1);puts("");
put(ans2);
}
int main(){
// file();
readdata();
work();
return 0;
}
luogu4147 玉蟾宫
题目传送门:luogu4147
题目背景
有一天,小猫rainbow和freda来到了湘西张家界的天门山玉蟾宫,玉蟾宫宫主蓝兔盛情地款待了它们,并赐予它们一片土地。
题目描述
这片土地被分成N*M个格子,每个格子里写着’R’或者’F’,R代表这块土地被赐予了rainbow,F代表这块土地被赐予了freda。
现在freda要在这里卖萌。。。它要找一块矩形土地,要求这片土地都标着’F’并且面积最大。
但是rainbow和freda的OI水平都弱爆了,找不出这块土地,而蓝兔也想看freda卖萌(她显然是不会编程的……),所以它们决定,如果你找到的土地面积为S,它们每人给你S两银子。
输入格式
第一行两个整数N,M,表示矩形土地有N行M列。
接下来N行,每行M个用空格隔开的字符’F’或’R’,描述了矩形土地。
输出格式
输出一个整数,表示你能得到多少银子,即(3*最大’F’矩形土地面积)的值。
输入输出样例
输入 #1 复制
5 6
R F F F F F
F F F F F F
R R R F F F
F F F F F F
F F F F F F
输出 #1 复制
45
说明/提示
对于50%的数据,1<=N,M<=200
对于100%的数据,1<=N,M<=1000
分析:
模板题。
注意事项:
(1)注意看清题目要求
(2)right右边最远位置要倒序,+1递推
(3)注意边界
代码:
/**********************
User:Mandy.H.Y
Language:
Problem:
Algorithm:
**********************/
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1005;
int n,m,ans = 0;
char a[maxn][maxn];
int _left[maxn][maxn];
int _right[maxn][maxn];
int _over[maxn][maxn];
template<class T>inline void read(T &x){
x = 0;bool flag = 0;char ch = getchar();
while(!isdigit(ch)) flag |= ch == '-',ch = getchar();
while(isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48),ch = getchar();
if(flag) x = -x;
}
template<class T>void putch(const T x){
if(x > 9) putch(x / 10);
putchar(x % 10 | 48);
}
template<class T>void put(const T x){
if(x < 0) putchar('-'),putch(-x);
else putch(x);
}
void file(){
freopen("testdata(2).in","r",stdin);
// freopen("palace.out","w",stdout);
}
void readdata(){
read(n);read(m);
for(int i = 1;i <= n; ++ i){
for(int j = 1;j <= m; ++ j){
char c = getchar();
while(c != 'R' && c != 'F') c = getchar();
a[i][j] = c;
if(c == 'R') continue;//题目要求必须等于'F'
_left[i][j] = _right[i][j] = j;
_over[i][j] = 1;
}
}
}
void work(){
for(int i = 1;i <= n; ++ i)
for(int j = 2;j <= m; ++ j)
if(a[i][j] == a[i][j - 1] && a[i][j] == 'F')
_left[i][j] = _left[i][j - 1];
for(int i = 1;i <= n; ++ i)
for(int j = m - 1;j >= 1; -- j)
if(a[i][j] == a[i][j + 1] && a[i][j] == 'F')//注意倒序 + 1
_right[i][j] = _right[i][j + 1];
for(int i = 1;i <= n; ++ i){
for(int j = 1;j <= m; ++ j){
if(i > 1 && a[i][j] == a[i - 1][j] && a[i][j] == 'F'){//注意边界
_left[i][j] = max(_left[i][j],_left[i - 1][j]);
_right[i][j] = min(_right[i][j],_right[i - 1][j]);
_over[i][j] = _over[i - 1][j] + 1;
}
int len1 = _right[i][j] - _left[i][j] + 1;
ans = max(ans,_over[i][j] * len1);
}
}
put(ans * 3);
}
int main(){
// file();
readdata();
work();
return 0;
}
枚举所有极大子矩形
学习资料
- 浅谈用极大化思想解决最大子矩形问题By 王知昆
一些说明:(引自王知昆论文:浅谈用极大化思想解决最大子矩形问题)
- 定义有效子矩形为内部不包含任何障碍点且边界与坐标轴平行的子矩形
- 极大有效子矩形:一个有效子矩形,如果不存在包含它且比它大的有效子矩形,就称这个有效子矩形为极大有效子矩形。(为了叙述方便,以下称为极大子矩形)
- 定义最大有效子矩形为所有有效子矩形中最大的一个(或多个)。以下简称为最大子矩形。
规定障碍数为
S
S
S,算法的复杂度为
O
(
S
2
)
O(S^2)
O(S2)
则这种思想方法的核心:
算法的思路是通过枚举所有的极大子矩形找出最大子矩形
为了处理方便,首先在障碍点的集合中加上整个矩形四角上的点。
算法的思路是这样的:先枚举极大子矩形的左边界,然后从左到右依次扫描每一个障碍点,并不断修改可行的上下边界,从而枚举出所有以这个定点为左边界的极大子矩形。
还有遗漏:
可以发现,这样做只考虑到了左边界覆盖一个点的矩形,因此我们还需要枚举左边界与整个矩形的左边界重合的情况。这还可以分为两类情况。
一种是左边界与整个举行的左边界重合,而右边界覆盖了一个障碍点的情况,对于这种情况,可以用类似的方法从右到左扫描每一个点作为右边界的情况。
另一种是左右边界均与整个矩形的左右边界重合的情况,对于这类情况我们可以在预处理中完成:先将所有点按纵坐标排序,然后可以得到以相邻两个点的纵坐标为上下边界,左右边界与整个矩形的左右边界重合的矩形,显然这样的矩形也是极大子矩形,因此也需要被枚举到。
适用性:
虽然以上的算法(算法1)看起来是比较高效的,但也有使用的局限性。可以发现,这个算法的复杂度只与障碍点的个数s有关。但对于某些问题,s最大有可能达到n×m,当s较大时,这个算法就未必能满足时间上的要求了。
与悬线法比较:
以上说了两种具有一定通用性的处理算法,时间复杂度分别为 O ( S 2 ) O(S^2) O(S2)和 O ( N M ) O(NM) O(NM)。两种算法分别适用于不同的情况。从时间复杂度上来看,第一种算法对于障碍点稀疏的情况比较有效,第二种算法则与障碍点个数的多少没有直接的关系(当然,障碍点较少时可以通过对障碍点坐标的离散化来减小处理矩形的面积,不过这样比较麻烦,不如第一种算法好),适用于障碍点密集的情况。
题目
奶牛浴场
传送门:luogu1578
题目描述
由于John建造了牛场围栏,激起了奶牛的愤怒,奶牛的产奶量急剧减少。为了讨好奶牛,John决定在牛场中建造一个大型浴场。但是John的奶牛有一个奇怪的习惯,每头奶牛都必须在牛场中的一个固定的位置产奶,而奶牛显然不能在浴场中产奶,于是,John希望所建造的浴场不覆盖这些产奶点。这回,他又要求助于Clevow了。你还能帮助Clevow吗?
John的牛场和规划的浴场都是矩形。浴场要完全位于牛场之内,并且浴场的轮廓要与牛场的轮廓平行或者重合。浴场不能覆盖任何产奶点,但是产奶点可以位于浴场的轮廓上。
Clevow当然希望浴场的面积尽可能大了,所以你的任务就是帮她计算浴场的最大面积。
输入格式
输入文件的第一行包含两个整数L和W,分别表示牛场的长和宽。文件的第二行包含一个整数n,表示产奶点的数量。以下n行每行包含两个整数x和y,表示一个产奶点的坐标。所有产奶点都位于牛场内,即:0<=x<=L,0<=y<=W。
输出格式
输出文件仅一行,包含一个整数S,表示浴场的最大面积。
输入输出样例
输入 #1 复制
10 10
4
1 1
9 1
1 9
9 9
输出 #1 复制
80
说明/提示
0<=n<=5000
1<=L,W<=30000
Winter Camp 2002
感谢 @凯瑟琳98 提供了4组hack数据
分析:
你看这个边界:
30000
30000
30000,
N
∗
M
N*M
N∗M的悬线法靠不住了
(谁说的?!明明可以离散化)
离散化也行,但复杂了
看障碍点数:
5000
5000
5000,优秀
O
(
S
2
)
O(S^2)
O(S2)的算法不正好?
不过听说这个算法容易出锅,看看dalao的题解:题解 P1578 【奶牛浴场】
注意:
(1)枚举极大子矩阵方法
(2)也可以用悬线法 + 离散化
(3)输出优化写错了,竟然还有10分 ( ̄▽ ̄)
(4)down - up不是up - down
(5)还是,注意边界情况
代码:
/*************************
User:Mandy.H.Y
Language:c++
Problem:luogu1578
Algorithm:
Date: 2019.8.14
*************************/
#include<bits/stdc++.h>
#define Max(x,y) ((x) > (y) ? (x) : (y))
#define Min(x,y) ((x) < (y) ? (x) : (y))
using namespace std;
const int maxn = 3e4 + 5;
const int maxp = 5e3 + 10;
typedef long long ll;
int n,m,p;
long long ans = 0;
struct Node{
int x,y;
}node[maxp];
template<class T>inline void read(T &x){
x = 0;bool flag = 0;char ch = getchar();
while(!isdigit(ch)) flag |= ch == '-',ch = getchar();
while(isdigit(ch)) x = (x << 1) + (x << 3) + (ch ^ 48),ch = getchar();
if(flag) x = -x;
}
template<class T>void putch(const T x){
if(x > 9) putch(x / 10);//输出优化写错了,竟然还有10分
putchar(x % 10 | 48);
}
template<class T>void put(const T x){
if(x < 0) putchar('-'),putch(-x);
else putch(x);
}
void file(){
freopen("testdata(1).in","r",stdin);
// freopen("bath.out","w",stdout);
}
void readdata(){
read(n);read(m);read(p);
for(int i = 1;i <= p;++ i) read(node[i].x),read(node[i].y);
node[++p].x = 0;node[p].y = 0;
node[++p].x = 0;node[p].y = m;
node[++p].x = n;node[p].y = 0;
node[++p].x = n;node[p].y = m;//防止超出矩形边界
}
bool cmp1(const Node &a,const Node &b){
return a.y < b.y;
}
bool cmp2(const Node &a,const Node &b){
return a.x < b.x;
}
void work(){
sort(node + 1,node + p + 1,cmp1);
for(int i = 1;i <= p; ++ i){
int up = 0,down = n,l = m - node[i].y;
for(int j = i + 1;j <= p; ++ j){//右边在矩形边界与右边是障碍物的情况
//down - up不是up - down
if(node[j].x <= up || node[j].x >= down) continue;//在上下界之外不考虑
if((down - up) * l <= ans) break;//剪枝
ans = max(ans,(down - up) * ((long long)node[j].y - node[i].y));//作为右边界
if(node[j].x == node[i].x) break;//如果横坐标相等,后来就都是0
if(node[j].x < node[i].x) up = node[j].x;
else down = node[j].x;
}
up = 0,down = n,l = node[i].y;
for(int j = i - 1;j >= 1; -- j){//左边在矩形边界上
if(node[j].x <= up || node[j].x >= down) continue;
if((down - up) * l <= ans) break;
ans = max(ans,(down - up) * ((long long)node[i].y - node[j].y));
if(node[j].x == node[i].x) break;
if(node[j].x < node[i].x) up = node[j].x;
else down = node[j].x;
}
}
sort(node + 1,node + p + 1,cmp2);
for(int i = 2;i <= p; ++ i) ans = max(ans,(long long)m * (node[i].x - node[i - 1].x));
//考虑左右都在矩形边界上的情况
put(ans);
}
int main(){
// file();
readdata();
work();
return 0;
}
单调栈
学习资料
- 浅谈单调栈的实现方式和简单应用 By COLIN·GAO