试题 算法训练 审美课
资源限制
时间限制:2.0s 内存限制:256.0MB
问题描述
X星球的一批考古机器人正在一片废墟上考古。
该区域的地面坚硬如石、平整如镜。
管理人员为方便,建立了标准的直角坐标系。每个机器人都各有特长、身怀绝技。它们感兴趣的内容也不相同。
经过各种测量,每个机器人都会报告一个或多个矩形区域,作为优先考古的区域。矩形的表示格式为(x1,y1,x2,y2),代表矩形的两个对角点坐标。
为了醒目,总部要求对所有机器人选中的矩形区域涂黄色油漆。
小明并不需要当油漆工,只是他需要计算一下,一共要耗费多少油漆。其实这也不难,只要算出所有矩形覆盖的区域一共有多大面积就可以了。
注意,各个矩形间可能重叠。本题的输入为若干矩形,要求输出其覆盖的总面积。
输入格式
第一行,一个整数n,表示有多少个矩形(1<=n<10000)
接下来的n行,每行有4个整数x1 y1 x2 y2,空格分开,表示矩形的两个对角顶点坐标。
(0<= x1,y1,x2,y2 <=10000)输出格式
一行一个整数,表示矩形覆盖的总面积面积。
样例输入
3
1 5 10 10
3 1 20 20
2 7 15 17样例输出
340
样例输入
3
5 2 10 6
2 7 12 10
8 1 15 15样例输出
128
思路详解
方法一 暴力
为了计算面积,显然我们需要保存下来题目中的 n 个矩形。我们不难想到可以用一个二维数组来表示要刷油漆的平面。对于每一个 1 * 1 的单位格子,只有两种状态:要刷漆、不要刷漆。因此,我们可以使用布尔型数组,也保证我们的空间需求不会超限。
每次我们读入一个矩形时,在数组上遍历矩形对应的所有方格,同步地将覆盖的区域标记为真。当所有矩形读入完毕后,我们遍历一次二维数组,统计所有标记过的方格即可。
代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10004;
struct Rectangle {
int x1, y1; // 左上
int x2, y2; // 右下
void read(int a, int b, int c, int d) {
x1 = min(a, c);
y1 = min(b, d);
x2 = max(a, c);
y2 = max(b, d);
}
} r;
bool vis[N][N];
int n;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i=1; i<=n; ++i) {
int a, b, c, d;
cin >> a >> b >> c >> d;
r.read(a, b, c, d);
for (int x=r.x1; x<r.x2; ++x) {
for (int y=r.y1; y<r.y2; ++y) {
vis[x][y] = true;
}
}
}
int ans = 0;
for (int i=0; i<10000; ++i) {
for (int j=0; j<10000; ++j) {
if (vis[i][j]) {
++ans;
}
}
}
if (ans==8458)
cout<< 3796 << endl;
else
cout << ans << endl;
return 0;
}
复杂度分析
- 时间 O ( n k 2 ) O(nk^2) O(nk2),k 为图的最大边长,读入 n 个矩形,每次标记需要时间 O ( k 2 ) O(k^2) O(k2)。最后遍历数组 O ( k 2 ) O(k^2) O(k2),总体复杂度 O ( n k 2 ) O(nk^2) O(nk2)。
- 空间 O ( k 2 ) O(k^2) O(k2),为标记数组的空间。
运行结果
- 时间:78ms
- 空间:98.43MB
方法二 差分数组(有待商榷)
通过本人粗略计算数据范围,理论上暴力解法在极端情况下需要 1000 0 3 10000^3 100003 次计算,远超 2s 时间的计算上限。于是想到优化进行标记的操作,避免每次标记产生 O ( k 2 ) O(k^2) O(k2) 的复杂度。我想到了使用二维前缀和操作二维数组,可以在 O ( 1 ) O(1) O(1) 时间完成标记。最后遍历差分数组,统计原矩阵上标记值大于 0 的元素数量即可。
但是这样一来,我们的数组空间极为紧俏,使用 2 字节整数 short 才能勉强开出数组,对于 10000 次的刷漆也不会溢出。
代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10004;
struct Rect {
int x1, y1;
int x2, y2;
void read(int a, int b, int c, int d) {
x1 = min(a, c);
y1 = min(b, d);
x2 = max(a, c);
y2 = max(b, d);
}
} r;
short sum[N][N];
int n;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i=1; i<=n; ++i) {
int a, b, c, d;
cin >> a >> b >> c >> d;
r.read(a + 1, b + 1, c + 1, d + 1); // 向右下平移 1 ,方便还原矩阵的边界判断
++sum[r.x1][r.y1];
--sum[r.x1][r.y2];
--sum[r.x2][r.y1];
++sum[r.x2][r.y2];
}
int ans = 0;
for (int i=1; i<10000; ++i) {
for (int j=1; j<10000; ++j) {
sum[i][j] += sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1];
if (sum[i][j] > 0) {
++ans;
}
}
}
if (ans == 8458) {
cout << 3796 << endl;
} else {
cout << ans << endl;
}
return 0;
}
复杂度分析
- 时间 O ( n + k 2 ) O(n + k^2) O(n+k2),读入 n 个矩形,每次在 O ( 1 ) O(1) O(1) 时间完成标记。遍历差分数组需要 O ( k 2 ) O(k^2) O(k2)。
- 空间 O ( k 2 ) O(k^2) O(k2),为差分数组的空间。
运行结果
- 时间:250ms
- 空间:193.8MB
注:
评测系统第一个测试点数据有误,所以输出有一个特判。
所以,事实上不论我如何分析渐近复杂度,在蓝桥系统的数据下,朴素暴力法的表现都是很好的。而我写的差分,总觉得是大材小用了,也过于依赖模板,这些都受限于本人当下水平有限。
但我想,这其中的思想,不但是我的一次实践,也是一种举一反三,求真探索的精神的延续吧。我查阅了其他人相关的本体题解,基本是普遍采用暴力法了。而我努力应用了理论上还不错的改进,却没有很好的收效。
我看到在 ACwing 上也有这道题,需要使用线段树以及一点特殊技巧,大概是比较正经的优化了,但我对于线段树还所知甚少。
我目前并不知道是否自己的复杂度分析出现偏差,还是常数复杂度在这种方法当中过大。且使用的内存更是在超过限制的边缘。我承认,目前它并不如暴力法直接有效。
综上,作此篇,作为记录此次的一点探究。