CF480E - Parking Lot

发布时间 2023-08-10 15:57:53作者: ereoth

Problem

给出一个 \(n \times m\) 的矩阵,有一些点不能选。

现在按顺序给出 \(k\) 次操作,每次都让一个点变成不可选(每次操作都有后效性,将一个点变为不可选后就会一直不可选),每次都问当前可选的最大正方形

Input

一行三个整数 \(n,m,k\),表示矩阵大小和操作数。

\(n\) 行,每行一个长度为 \(m\) 的字符串,.为可选的点,X为不可选的点。

\(k\) 行,每行两个整数 \(x_i,y_i\),表示把点 \((x_i,y_i)\) 变为不可选。

\(n, m, k \le 2000, 1 \le x_i \le n, 1 \le y_i \le m\)

Output

\(k\) 行,每行输出这次操作后最大可选的子正方形的边长。

Sample

Input 1

7 8 4
........
X.....X.
........
........
.X......
........
........
1 5
6 4
3 5
4 6

Output 1

5
4
4
3

Solution

这种有后效性的题目有一个固定的套路,可以叫操作反过来,即将一个不可选的点变成一个可以选择的点。

不难看出这样的答案单调不降,且如果答案增加一定是由一个点的状态改变导致的。同时答案的取值较小,于是问题转化为每当一个点被更改状态后,不断判断答案能否增加 \(1\),即是否存在一个长度为答案 \(+1\)、包含 \((x,y)\) 的正方形。

那该如何判断?我们将 \((x,y)\) 所在的列看作一条纵轴,正方形看作一个在纵轴上滑动的块。块的宽会受到左右不可选点的限制。我们预处理出纵轴上每一个点分别往左右两边拓展最多几个格子,计算每一次矩阵滑动后左右能拓展到的位置就相当于行区间左右分别能拓展的最小值,这是满足单调性的。此外,纵轴两边相互独立,可以开两个单调队列分别对纵轴左右分别处理,最后左右能拓展的最小值的和 \(+1\)(纵轴)即为当前正方形最大边长。

最后的结果倒叙记录,正序输出。

代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <deque>

using namespace std;
using Pii = pair<int, int>;

const int kmax = 2005;

int n, m, k;
Pii q[kmax];
int a[kmax][kmax], s[kmax][kmax];
int l[kmax][kmax], r[kmax][kmax];
int res, ress[kmax];
char ch;

bool Check(int mid) {
	for(int i = 1, _i = mid; _i <= n; i++, _i++) {
		for(int j = 1, _j = mid; _j <= m; j++, _j++) {
			if(s[_i][_j] - s[i - 1][_j] - s[_i][j - 1] + s[i - 1][j - 1] == 0) return 1;
		}
	}
	return 0;
}

int Calc() { // 二分求初始答案
	int l = 1, r = min(n, m);
	for(int mid; l <= r;) {
		mid = (l + r) >> 1;
		if(Check(mid)) {
			l = mid + 1;
		} else {
			r = mid - 1;
		}
	}
	return l - 1;
}

bool Solve(int k, int j) {
	deque<int> q[2];
	for(int i = 1; i <= n; i++) { // 枚举正方形的下底
		for(; !q[0].empty() && q[0].front() < i - k + 1; q[0].pop_front()) { // 不在矩阵内
		} 
		for(; !q[1].empty() && q[1].front() < i - k + 1; q[1].pop_front()) {
		} 
		for(; !q[0].empty() && l[q[0].back()][j] >= l[i][j]; q[0].pop_back()) { // 拓展的比当前行远的要弹出
		}
		for(; !q[1].empty() && r[q[1].back()][j] >= r[i][j]; q[1].pop_back()) {
		}
		q[0].push_back(i), q[1].push_back(i);
        if(i < k) continue; // 正方形长度不够
		if(l[q[0].front()][j] + r[q[1].front()][j] - 1 >= k) return 1;
	}
	return 0;
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m >> k;
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= m; j++) {
			cin >> ch;
			a[i][j] = ch == 'X';
		}
	}	
	for(int i = 1; i <= k; i++) {
		cin >> q[i].first >> q[i].second;
		a[q[i].first][q[i].second] = 1; // 先标记
	}
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= m; j++) {
			s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
		}
	}
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= m; j++) {
			l[i][j] = (a[i][j] ? 0 : l[i][j - 1] + 1); // 这里的计算包括原点
		}
		for(int j = m; j; j--) {
			r[i][j] = (a[i][j] ? 0 : r[i][j + 1] + 1);
		}
	}
	res = Calc(); // 计算初始答案
	for(int i = k; i; i--) { // 倒叙计算
		a[q[i].first][q[i].second] = 0; // 还原标记
		ress[i] = res;
		for(int j = 1; j <= m; j++) {
			l[q[i].first][j] = (a[q[i].first][j] ? 0 : l[q[i].first][j - 1] + 1);
		}
		for(int j = m; j; j--) {
			r[q[i].first][j] = (a[q[i].first][j] ? 0 : r[q[i].first][j + 1] + 1);
		}
		for(; Solve(res + 1, q[i].second); res++) { // 判断答案能否增加
		}
	}
	for(int i = 1; i <= k; i++) { // 正序输出
		cout << ress[i] << '\n';
	}
	return 0;
}