(前缀和,二分)2024HNU小学期测试3_2

分享一道自认为很好的题 ...

题目

【问题描述】
数据图为一个 n × m 的01矩阵,定义稠密01正方形为一个包含的0的个数不大于 f 的正方形。请找出该数据图中最大的01稠密正方形,并输出其边长。
【输入形式】
第一行包含两个正整数 n、m、f

接下来 n 行,每行 m 个用空格间隔的‘0’或‘1’,表示给出的数据图
【输出形式】
输出为一行,包含一个正整数表示最大的01稠密正方形的边长
【样例输入】

3 3 1
1 1 1
1 0 0
0 1 1
【样例输出】

2
【样例说明】


【评分标准】

对于 30%的数据,保证1≤ n ≤50,1≤ m ≤50

对于 70%的数据,保证1≤ n ≤1500,1≤ m ≤1500

对于100%的数据,保证1≤ n ≤2000,1≤ m ≤2000,1≤f≤n*m

分析

最先想到的就是暴搜,遍历每个位置,再从大到小遍历每个边长,统计0个数,满足<=f就输出枚举的边长。

但是显而易见复杂度已经上天了... 

上千的数据量肯定TLE.....

降阶

二维的问题一般可以考虑下一维情况有没有较简单的算法?再推广到二维情况。

一维情况:

假设:有一个01字符串,要找出0个数不大于f的最长子串。怎么做呢?🤔

暴力:ans从大到小遍历,起始位置i,子串长度j,同时遍历,O(n^3)

导致复杂度过高的原因是:遍历过程中其实有“重复信息”被我们忽略了。如果用一次遍历,保存下尽可能多的信息,就可以避免再次遍历了。

前缀和算法:一遍遍历,用一个memo数组记录下这之前0的个数。当我们需要知道某一个子串中含0的个数,只需要算出memo[j] - memo[i]即可。

这样,一维的情况我们就优化成了O(n^2)

推广

到二维的情况呢?

我们可以用类似一维中前缀和的思想,用一个O(N^2)的遍历统计不同位置与坐标为(1,1)位置围成的矩形中的0个数。

根据小学的面积公式,如图:

(S_{ACIG}=S_{ADFC}+S_{AGHB}-S_{ADEB}+S_{EFIH})

可以得出下面的关系:

memo[i][j] = memo[i - 1][j] + memo[i][j - 1] - memo[i - 1][j - 1];

if (mat[i][j] == 0) memo[i][j]++;

这样就可以统计出0的个数了。

此时的复杂度为O(min(m,n)*m*n)

敲下代码...

#include <bits/stdc++.h>
using namespace std;
int mat[3000][3000];
int memo[3000][3000];

int main() {
	int n, m, f;
	cin >> n >> m >> f;

	for (int i = 0; i < n; i++) {//初始化
		for (int j = 0; j < m; j++) {
			memo[i][j] = 0;
			mat[i][j] = 0;
		}
	}

	for (int i = 1; i < n + 1; i++) {//注意这里从i=1,j=1开始存储!留一行一列便于memo数组初始化
		for (int j = 1; j < m + 1; j++) {
			cin >> mat[i][j];
		}
	}

	for (int i = 1; i < n + 1; i++) {
		for (int j = 1; j < m + 1; j++) {
			memo[i][j] = memo[i - 1][j] + memo[i][j - 1] - memo[i - 1][j - 1];

			if (mat[i][j] == 0)
				memo[i][j]++;
		}
	}

	for (int ans = min(m, n); ans >= 0; ans--) {
		bool flag = 0;

		for (int i = 1; i < n + 1; i++) {
			if (flag == 1)
				break;

			for (int j = 1; j < m + 1; j++) {
				int x = i - ans + 1;
				int y = j - ans + 1;

				if (x < 1 || y < 1)//越界,不判断
					continue;

				if (memo[i][j] - memo[x][y] <= f) {
					flag = 1;
					break;
				}
			}
		}

		if (flag == 1) {//能找到这样的正方形,输出结果
			cout << ans;
			break;
		}


	}
}

过了7个...还是不行,继续优化

二分优化

for (int ans = min(m, n); ans >= 0; ans--)  这次遍历可以用二分啊!

要遍历正方形的边长是单调的,也满足二分的条件(若较大的边长符合,那比它小的肯定也符合)

改进代码....

#include <bits/stdc++.h>
using namespace std;
vector<vector<int>> mat(3000, vector<int>(3000, 0));
vector<vector<int>> memo(3000, vector<int>(3000, 0));
//用vector吧,不然二维数组传参太麻烦了
bool func(vector <vector<int>> &memo, int ans, int n, int m, int f) {//判断ans是否满足,传引用&memo,更快
	for (int i = 1; i < n + 1; i++) {
		for (int j = 1; j < m + 1; j++) {
			int x = i - ans + 1;
			int y = j - ans + 1;

			if (x < 1 || y < 1)
				continue;

			 if (memo[i][j] - memo[x - 1][j] - memo[i][y - 1] + memo[x - 1][y - 1] <= f)//注意条件
  {
                return true;
			}
		}
	}

	return false;

}

int main() {
	int n, m, f;
	cin >> n >> m >> f;


	for (int i = 1; i < n + 1; i++) {
		for (int j = 1; j < m + 1; j++) {
			cin >> mat[i][j];
		}
	}

	for (int i = 1; i < n + 1; i++) {
		for (int j = 1; j < m + 1; j++) {
			memo[i][j] = memo[i - 1][j] + memo[i][j - 1] - memo[i - 1][j - 1];

			if (mat[i][j] == 0)
				memo[i][j]++;
		}
	}

	int left = 0, right = min(n, m);//二分,左右界
	int max_side = 0;

	while (left <= right) {
		int mid = (left + right) / 2;

		if (func(memo, mid, n, m, f) == 1) {
			max_side = mid;
			left = mid + 1;
		} else {
			right = mid - 1;
		}
	}

	cout << max_side;


}

时间复杂度O(log(min(m*n))*m*n))

空间复杂度O(m*n)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值