一、题意
输入一个n*m的矩阵,'.'表示空地,'#'表示障碍物,要求你对于每一个空地,求出以它为右下角的空矩形的最大周长。然后统计每个周长出现了多少次。
二、解析
看到题意,应该就能回想到一些之前做过的题目:找最大空矩形面积。
而这道题要找的是最大周长。其实是异曲同工的,也就是先将原图转化为n个直方图,然后对每个直方图通过单调栈就最大面积的矩形。这样的方法总用时O(n*m),不会超时。
而在本题中就是要用单调栈求最大周长,并且是要对每一个空地都求一个最大周长的空矩形。
在直方图中从左向右遍历时,设当前列为i,对应的矩形右下角即为(i, 0),我们要找的就是矩形的左上角(c, h)使得周长 2*(h + i - c + 1) 最大。其实就是要找到一个左侧矩形中 h-c 最大的那一个位置作为左上角。由于对于栈中,若更靠近栈底的一个矩形的 h-c 要大于 更靠近栈顶的一个矩形的 h-c,则此时其实更靠近栈顶的那个矩形的存在是没有意义的,因为它活在了另一个矩形的阴影下,也就是它根本就不需要入栈。因此,也就是说,我们可以维护一个 h-c 单调递增的单调栈(当然还得保证h递增),这样每次需要找周长最大的矩形时,只需要比对当前矩形和栈顶矩形即可。
ps: 本题比较复杂,最好细细揣摩代码。
三、代码
#include <iostream> #include <stack> #include <string> #include <map> using namespace std; const int maxn = 1000 + 5, INF = 1 << 30; struct rect { int c, h; rect(int c, int h) : c(c), h(h) {} }; int kase, n, m, a[maxn]; map<int, int> cnt; void solve() { stack<rect> stk; // 栈中存放矩形左上角坐标(c, h) for(int i = 0; i < m; i ++) { int p = i; // 以a[i]为高的矩形左上角(p, a[i]) while(!stk.empty() && stk.top().h >= a[i]) { p = stk.top().c; // 最后一个被弹出的矩形即为我们要求的左上角位置 stk.pop(); } if(!a[i]) continue; if(stk.empty() || a[i] - p > stk.top().h - stk.top().c) { // 最优左上角要么在已弹出的最后位置(以a[i]为高),要么就是栈顶 cnt[2 * (a[i] + i - p + 1)] ++; stk.push(rect(p, a[i])); // 保持 h-c 在栈中单调递增 } else cnt[2 * (stk.top().h + i - stk.top().c + 1)] ++; } } int main() { cin >> kase; while(kase --) { cin >> n >> m; fill(a, a + m, 0), cnt.clear(); for(int i = 0; i < n; i ++) { string row; cin >> row; for(int j = 0; j < m; j ++) { char ch = row[j]; if(ch == '.') a[j] ++; else a[j] = 0; } solve(); } for(const auto& p : cnt) cout << p.second << " x " << p.first << endl; } }
四、归纳
- 单调栈:在复杂一些的题目可以存放结构体,如本题中存放了rect(h, c),即矩形的左上角,而不只是存放下标c。
- 单调栈出栈时一般都是按照矩形的高度将 [大于等于] 的都出栈,这是矩形题目固有的特点所决定的;入栈时可能会根据实际需要进行入栈,如本体中,h-c 小于原栈顶的就不需要入栈了(因为没有意义),通过这样来保证栈顶元素的 h-c 在栈中最大。
- 有时我们在单调栈中并不只是要获取弹出后的栈顶元素,而是需要弹出的最后一个元素,则可以通过一个外部临时变量p来存储。