很经典的国家集训队论文:浅谈用极大化思想解决最大子矩形问题
最大子矩形问题:在一个给定的矩形网格中有一些障碍点,要找出网格内部不包含任何障碍点,且边界与坐标轴平行的最大子矩形。
悬线法 $O(nm)$ 个人理解:枚举在子矩形底边上的一个点,将它尽可能 地向上扩展成一条高线,然后将这条高左右尽可能 地平移得到一个矩形,用此矩形更新答案。 枚举的复杂度已经达到了 $O(nm)$,所以我们需要预处理扩展和平移操作。
我们可以dp(或者叫递推也行)预处理:将每个点尽可能向上、向左、向右扩展到的位置 存在数组 $u[][],l[][],r[][]$ 中(当然,存向上、向左、向右扩展的长度 也行,但存位置对求答案来说更方便一点)
1 2 3 4 5 6 For i = 1 to n For j = 1 to m u[i][j] = (能从(i-1,j)走到(i,j)) ? u[i-1][j] : i; l[i][j] = (能从(i,j-1)走到(i,j)) ? l[i][j-1] : j; For j = m to 1 r[i][j] = (能从(i,j+1)走到(i,j)) ? r[i][j+1] : j;
预处理后我们得到了点 $(i,j)$ 的高线。但是对于向左和向右,我们需要知道的不是每个点 向左向右扩展的位置,而是每条高线 向左向右扩展的位置,这个问题我们可以递推出来:
1 2 3 4 5 For i = 2 to n For j = 1 to m if 能从(i-1,j)走到(i,j) l[i][j] = max(l[i][j], l[i-1][j] r[i][j] = min(r[i][j], r[i-1][j]
矩形面积就是 $(r[i][j] - l[i][j] + 1) * (i - u[i][j] + 1)$
如此以来,我们就 $O(nm)$ 地解决了这个问题。
单调栈 $O(nm)$ 将每个点尽可能向上、向左、向右扩展到的长度 存在数组 $u[][],l[][],r[][]$ 中。具体做法如下:For i = 1 to n
枚举矩形底边,对每一次枚举维护一个单增栈,存储的数据包括高度与宽度,For j = 1 to m
,如果当前点高度大于栈顶元素高度,就直接入栈;否则,不断弹出栈顶元素,最后将所有弹出元素的宽度之和加上自身宽度作为新的宽度入栈。这样,我们就得到了一个点向左能扩展的最大长度(手动模拟一下就清楚了)。同理,For j = m to 1
就可以求出一个点向右扩展的最大长度。(当然,不用反过来for也能求出来,但是反过来for一遍挺方便的,何乐而不为?) 单调栈做法的思想其实和悬线法本质上是一样的,只不过省去了递推那一步。
注意:有些题并不是两种方法都可以的
$O(S^2)$算法 以奶牛浴场为经典的一类题,详见论文 以及洛谷题解 。
例题 底边已经定了,直接考察单调栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 # include <algorithm> # include <iostream> # include <cstdio> # include <stack> using namespace std;typedef long long LL;typedef pair<int , int > pii;# define mp(x, y) make_pair(x, y) const int N = 100005 ;int n, h[N], len[N];LL ans; stack<pii> sta; int main () { while (scanf ("%d" , &n) && n){ ans = 0 ; while (!sta.empty ()) sta.pop (); for (int i = 1 ; i <= n; i++){ scanf ("%d" , &h[i]); int sumL = 0 ; while (!sta.empty () && h[i] <= sta.top ().first){ sumL += sta.top ().second; sta.pop (); } len[i] = sumL; sta.push (mp (h[i], sumL + 1 )); } while (!sta.empty ()) sta.pop (); for (int i = n; i >= 1 ; i--){ int sumL = 0 ; while (!sta.empty () && h[i] <= sta.top ().first){ sumL += sta.top ().second; sta.pop (); } len[i] += sumL; sta.push (mp (h[i], sumL + 1 )); } for (int i = 1 ; i <= n; i++){ len[i]++; ans = max (ans, 1ll * len[i] * h[i]); } printf ("%lld\n" , ans); } return 0 ; }
枚举底边,按高度排个序,就变成了上一题。 有趣的是,由于排了序,我们不用写单调栈,运用它的思想即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 # include <algorithm> # include <cstdio> # include <stack> using namespace std;const int N = 1005 ;int n, m, g[N][N], H[N][N], h[N], len[N], ans;char c[N][N];int main () { while (scanf ("%d%d" , &n, &m) != EOF){ ans = 0 ; for (int i = 1 ; i <= n; i++){ scanf ("%s" , c[i]+1 ); for (int j = 1 ; j <= m; j++){ if (c[i][j] == '0' ) H[i][j] = 0 ; else H[i][j] = H[i-1 ][j] + 1 ; } } for (int i = 1 ; i <= n; i++){ for (int j = 1 ; j <= m; j++) h[j] = H[i][j]; sort (h+1 , h+m+1 ); for (int j = 1 ; j <= m; j++){ if (h[j] == h[j-1 ]) continue ; else ans = max (ans, h[j] * (m - j + 1 )); } } printf ("%d\n" , ans); } return 0 ; }
悬线法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 # include <cstdio> # include <algorithm> using namespace std;const int N = 1005 ;int T, n, m, ans, g[N][N], l[N][N], u[N][N], r[N][N];char ch[10 ];int main () { scanf ("%d" , &T); while (T--){ ans = 0 ; scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i++){ for (int j = 1 ; j <= m; j++){ scanf ("%s" , ch); g[i][j] = ch[0 ] == 'F' ? 1 : 0 ; u[i][j] = l[i][j] = r[i][j] = 0 ; if (g[i][j] == 1 ) u[i][j] = i, l[i][j] = r[i][j] = j; } } for (int i = 1 ; i <= n; i++){ for (int j = 1 ; j <= m; j++){ if (g[i][j] == 0 ) continue ; if (i != 1 ) u[i][j] = g[i-1 ][j] == 1 ? u[i-1 ][j] : i; if (j != 1 ) l[i][j] = g[i][j-1 ] == 1 ? l[i][j-1 ] : j; } for (int j = m; j >= 1 ; j--){ if (g[i][j] == 0 ) continue ; if (j != m) r[i][j] = g[i][j+1 ] == 1 ? r[i][j+1 ] : j; } } for (int i = 1 ; i <= n; i++){ for (int j = 1 ; j <= m; j++){ if (g[i][j] == 0 ) continue ; if (g[i][j] == 1 && g[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 ) * (i - u[i][j] + 1 )); } } printf ("%d\n" , ans * 3 ); } return 0 ; }
悬线法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 # include <cstdio> # include <algorithm> using namespace std;const int N = 2005 ;int n, m, l[N][N], r[N][N], u[N][N], g[N][N], ans;int main () { while (scanf ("%d%d" , &n, &m) != EOF){ for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= m; j++) scanf ("%d" , &g[i][j]); for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= m; j++) u[i][j] = l[i][j] = r[i][j] = 0 ; for (int i = 1 ; i <= n; i++){ for (int j = 1 ; j <= m; j++){ if (g[i][j] == 0 ) continue ; u[i][j] = g[i-1 ][j] == 1 ? u[i-1 ][j] : i; l[i][j] = g[i][j-1 ] == 1 ? l[i][j-1 ] : j; } for (int j = m; j >= 1 ; j--){ if (g[i][j] == 0 ) continue ; r[i][j] = g[i][j+1 ] == 1 ? r[i][j+1 ] : j; } } for (int i = 1 ; i <= n; i++){ for (int j = 1 ; j <= m; j++){ if (g[i][j] == 0 ) continue ; if (g[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 ) * (i - u[i][j] + 1 )); } } printf ("%d\n" , ans); ans = 0 ; } return 0 ; }
最大子矩形一定要么全是 $a$,要么全是 $b$,要么全是 $c$。 假设最大子矩形全是 $a$,那么把所有能变成 $a$ 的全变成 $a$ 一定比不变某些字母更好,以此求一次答案;同理,再全变成 $b$ 求一次答案,再全变成 $c$ 求一次答案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 # include <cstdio> # include <algorithm> using namespace std;const int N = 1005 ;int n, m, l[N][N], r[N][N], u[N][N], g[N][N], ans;char c[N][N];inline void work (char ch) { for (int i = 1 ; i <= n; i++){ for (int j = 1 ; j <= m; j++){ if (ch == 'a' && (c[i][j] == 'a' || c[i][j] == 'w' || c[i][j] == 'y' || c[i][j] == 'z' )) g[i][j] = 1 ; else if (ch == 'b' && (c[i][j] == 'b' || c[i][j] == 'w' || c[i][j] == 'x' || c[i][j] == 'z' )) g[i][j] = 1 ; else if (ch == 'c' && (c[i][j] == 'c' || c[i][j] == 'x' || c[i][j] == 'y' || c[i][j] == 'z' )) g[i][j] = 1 ; else g[i][j] = 0 ; u[i][j] = i, l[i][j] = r[i][j] = j; } } for (int i = 1 ; i <= n; i++){ for (int j = 1 ; j <= m; j++){ if (g[i][j] == 0 ) continue ; if (i > 1 ) u[i][j] = g[i-1 ][j] == 1 ? u[i-1 ][j] : i; if (j > 1 ) l[i][j] = g[i][j-1 ] == 1 ? l[i][j-1 ] : j; } for (int j = m; j >= 1 ; j--){ if (g[i][j] == 0 ) continue ; if (j < m) r[i][j] = g[i][j+1 ] == 1 ? r[i][j+1 ] : j; } } for (int i = 1 ; i <= n; i++){ for (int j = 1 ; j <= m; j++){ if (g[i][j] == 0 ) continue ; if (g[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 ) * (i - u[i][j] + 1 )); } } } int main () { while (scanf ("%d%d" , &n, &m) != EOF){ for (int i = 1 ; i <= n; i++) scanf ("%s" , c[i] + 1 ); work ('a' ); work ('b' ); work ('c' ); printf ("%d\n" , ans); ans = 0 ; } return 0 ; }
悬线法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 # include <cstdio> # include <cstring> # include <algorithm> using namespace std;const int N = 2005 ;int n, m, g[N][N], l[N][N], r[N][N], u[N][N], ans1, ans2;int main () { memset (g, -1 , sizeof g); scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i++){ for (int j = 1 ; j <= m; j++){ scanf ("%d" , &g[i][j]); u[i][j] = i, l[i][j] = r[i][j] = j; } } for (int i = 1 ; i <= n; i++){ for (int j = 1 ; j <= m; j++){ if (i != 1 ) u[i][j] = g[i][j] != g[i-1 ][j] ? u[i-1 ][j] : i; if (j != 1 ) l[i][j] = g[i][j] != g[i][j-1 ] ? l[i][j-1 ] : j; } for (int j = m; j >= 1 ; j--) if (j != m) r[i][j] = g[i][j] != g[i][j+1 ] ? r[i][j+1 ] : j; } for (int i = 1 ; i <= n; i++){ for (int j = 1 ; j <= m; j++){ if (i != 1 && g[i][j] != g[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]); } int t1 = r[i][j] - l[i][j] + 1 ; int t2 = i - u[i][j] + 1 ; ans1 = max (ans1, min (t1, t2) * min (t1, t2)); ans2 = max (ans2, t1 * t2); } } printf ("%d\n%d\n" , ans1, ans2); return 0 ; }
洛谷上的题解 很详尽
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 # include <bits/stdc++.h> using namespace std;const int N = 5010 ;int L, W, n, x, y, ans;struct Point { int x, y; }p[N]; bool cmp (Point a, Point b) { if (a.y == b.y) return a.x < b.x; return a.y < b.y; } bool cmp1 (Point a, Point b) { return a.x < b.x; } int main () { scanf ("%d%d%d" , &L, &W, &n); for (int i = 1 ; i <= n; i++) scanf ("%d%d" , &p[i].x, &p[i].y); p[++n] = (Point){0 , 0 }; p[++n] = (Point){0 , W}; p[++n] = (Point){L, 0 }; p[++n] = (Point){L, W}; sort (p+1 , p+n+1 , cmp1); for (int i = 2 ; i <= n; i++) ans = max (ans, (p[i].x - p[i-1 ].x) * W); sort (p+1 , p+n+1 , cmp); for (int i = 1 ; i <= n; i++){ int u = 0 , d = L; for (int j = i + 1 ; j <= n; j++){ if (p[j].y == p[i].y) continue ; ans = max (ans, (p[j].y - p[i].y) * (d - u)); if (p[j].x == p[i].x) u = d = p[j].x; else if (p[j].x > p[i].x) d = min (d, p[j].x); else if (p[j].x < p[i].x) u = max (u, p[j].x); } ans = max (ans, (W - p[i].y) * (d - u)); } for (int i = n; i >= 1 ; i--){ int u = 0 , d = L; for (int j = i - 1 ; j >= 1 ; j--){ if (p[j].y == p[i].y) continue ; ans = max (ans, (p[i].y - p[j].y) * (d - u)); if (p[j].x == p[i].x) u = d = p[j].x; else if (p[j].x > p[i].x) d = min (d, p[j].x); else if (p[j].x < p[i].x) u = max (u, p[j].x); } ans = max (ans, p[i].y * (d - u)); } printf ("%d\n" , ans); return 0 ; }