Gym 102028J 扫描线/二维差分 + 解方程

题意:有一个二维平面,以及n个操作,每个操作会选择一个矩形,使得这个二维平面的一部分被覆盖。现在你可以取消其中的2个操作,问最少有多少块地方会被覆盖?

思路:官方题解简洁明了,就不细说了:https://codeforces.com/blog/entry/63729

此处重点记录一下两种做法的巧妙之处。

1:二维差分+解方程

二维差分:假设在矩形[(x1, y1), (x2, y2)]上加一个数,那么在(x1, y1), (x2 + 1, y2 + 1)加1, (x1, y2 + 1), (x2 +1, y1)减1。扫描的时候,cnt[i][j] += cnt[i - 1][j], cnt[i][j] += cnt[i][j - 1], cnt[i][j] -= cnt[i - 1][j - 1],这样线性扫描一遍就可以知道每个点被覆盖了多少次。

解方程:只知道一个点被覆盖多少次是没有用的,我们需要知道覆盖的具体方案。对于只被覆盖的一次的我们很容易知道。但是对于覆盖两次的呢?我们采用记录和 和 平方和的方式,这样通过解方程就知道这个位置的数是哪两个数了。

代码:

#include <bits/stdc++.h>
#define LL long long
#define INF 0x3f3f3f3f
#define db double
#define pii pair<int, int>
using namespace std;
const int maxn = 1510;
LL sqr[maxn][maxn], sum[maxn][maxn], cnt[maxn][maxn];
LL cnt0, num[300010];
void add(int x, int y, LL val, LL flag) {
	cnt[x][y] += flag;
	sum[x][y] += val * flag;
	sqr[x][y] += val * val * flag;
}
void update(int x1, int y1, int x2, int y2, LL flag) {
	cnt[x1][y1] += flag * cnt[x2][y2];
	sum[x1][y1] += flag * sum[x2][y2];
	sqr[x1][y1] += flag * sqr[x2][y2];
}
vector<pii> a;
int main() {
	int T, n, m, x1, y1, x2, y2;
	LL ans;
	scanf("%d", &T);
	while(T--) {
		scanf("%d%d", &n, &m);
		a.clear(), ans = 0, cnt0 = 0;
		for (int i = 1; i <= n; i++)
			num[i] = 0;
		for (int i = 1; i <= m; i++) {
			for (int j = 1; j <= m; j++)
				sqr[i][j] = sum[i][j] = cnt[i][j] = 0;
		}
		for (int i = 1; i <= n; i++) {
			scanf("%d%d%d%d", &x1, &x2, &y1, &y2);
		 	add(x1, y1, i, 1), add(x2 + 1, y2 + 1, i, 1);
		 	add(x1, y2 + 1, i, -1), add(x2 + 1, y1, i, -1);
		}
		for (int i = 1; i <= m; i++)
			for (int j = 1; j <= m; j++) {
				update(i, j, i - 1, j, 1);
				update(i, j, i, j - 1, 1);
				update(i, j, i - 1, j - 1, -1);
				if(cnt[i][j] == 0) cnt0++;
				else if(cnt[i][j] == 1) {
					num[sum[i][j]]++;
				} else if(cnt[i][j] == 2){
					LL tmp = sum[i][j] * sum[i][j] - sqr[i][j];
					LL x = (sum[i][j] + sqrt(sqr[i][j] - tmp)) / 2;
					LL y = sum[i][j] - x;
					if(x > y) swap(x, y);
					a.push_back(make_pair(x, y));
				}
			}
		LL res[3];
		res[0] = res[1] = res[2] = 0;
		for (int i = 1; i <= n; i++) {
			res[0] = num[i];
			sort(res, res + 3);
		}
		ans = max(ans, res[1] + res[2]);
		sort(a.begin(), a.end());
		for (int i = 0, j = 0; i < a.size(); i = j) {
			while(j < a.size() && a[i] == a[j])j++;
			ans = max(ans, num[a[i].first] + num[a[i].second] + (j - i));
		}
		printf("%lld\n", m * m - cnt0 - ans);
	}
}

2:扫描线,我们通过线段树来执行扫描线来知道覆盖一次和覆盖两次的个数以及具体方案,扫描线的实现比较巧妙,有种懒标记下放和标记永久化结合的味道。具体是这样的,对于每个矩形[(x1, y1), (x2, y2)], 我们在y1位置记录(x1, x2)加上一个数,在y2 + 1位置记录删除这个数。每扫描到一个新的位置,我们把在这个位置记录的标记打上,以及记录上要删除上。之后,我们遍历线段树,我们记录3个数,这3个数是从上层可以下放的标记,这样到最底层的时候,如果下放的标记小于3个,就可以在对应位置记录了。如果当前从上层可以下放的标记已经到达3个了,直接return就行。

代码:

#include <bits/stdc++.h>
#define ls (o << 1)
#define rs (o << 1 | 1)
#define ed tr[o].size() - 1
#define pii pair<int, int>
using namespace std;
const int maxn = 300010;
const int maxm = 1510;
vector<int> tr[maxm << 2], re_num[maxm], del_re[maxm];
vector<pii> a, re[maxm];
int del[maxn], num[maxn], cnt0;
void build(int o, int l, int r) {
	tr[o].clear();
	if(l == r) {
		return;
	}
	int mid = (l + r) >> 1;
	build(ls, l, mid);
	build(rs, mid + 1, r);
}
void add(int o, int l, int r, int ql, int qr, int val) {
	if(l >= ql && r <= qr) {
		tr[o].push_back(val);
		return;
	}
	int mid = (l + r) >> 1;
	if(ql <= mid) add(ls, l, mid, ql, qr, val);
	if(qr > mid) add(rs, mid + 1, r, ql, qr, val);
}
void pushdown(int o, int& a1, int& a2, int& a3) {
	int q[4];
	memset(q, 0, sizeof(q));
	while(tr[o].size() && q[0] < 3) {
		if(del[tr[o][ed]] == 1) tr[o].pop_back();
		else {
			q[++q[0]] = tr[o][ed];
			tr[o].pop_back();
		}
	}
	for (int i = 1; i <= q[0]; i++) {
		if(a1 == 0) a1 = q[i];
		else if(a2 == 0) a2 = q[i];
		else if(a3 == 0) a3 = q[i];
		tr[o].push_back(q[i]);
	}
}
void dfs(int o, int l, int r, int a1, int a2, int a3) {
	if(a3 != 0) return;
	pushdown(o, a1, a2, a3);
	if(l == r) {
		if(a1 == 0) cnt0++;
		else if(a3 == 0) {
			if(a2 == 0) num[a1]++;
			else a.push_back(make_pair(min(a1, a2), max(a1, a2)));
		}
		return;
	}
	int mid = (l + r) >> 1;
	dfs(ls, l, mid, a1, a2, a3);
	dfs(rs, mid + 1, r, a1, a2, a3);
}
int main() {
	int T, n, m, x1, x2, y1, y2;
	scanf("%d", &T);
	while(T--) {
		scanf("%d%d", &n, &m);
		build(1, 1, m);
		cnt0 = 0;
		a.clear();
		for (int i = 1; i <= m + 1; i++) {
			re_num[i].clear();
			del_re[i].clear();
			re[i].clear();
		}
		for (int i = 1; i <= n; i++) {
			scanf("%d%d%d%d", &x1, &x2, &y1, &y2);
			re[y1].push_back(make_pair(x1, x2));
			re_num[y1].push_back(i);
			del_re[y2 + 1].push_back(i);
			del[i] = 0;
			num[i] = 0;
		}
		for (int i = 1; i <= m; i++) {
			for (int j = 0; j < re[i].size(); j++) {
				pii x = re[i][j];
				add(1, 1, m, x.first, x.second, re_num[i][j]);
			}
			for (int j = 0; j < del_re[i].size(); j++) {
				del[del_re[i][j]] = 1;
			}
			dfs(1, 1, m, 0, 0, 0);
		}
		int tmp[3];
		memset(tmp, 0, sizeof(a));
		for (int i = 1; i <= n; i++) {
			tmp[0] = num[i];
			sort(tmp, tmp + 3);
		}
		int ans = tmp[1] + tmp[2];
		sort(a.begin(), a.end());
		for (int i = 0, j = 0; i < a.size(); i = j) {
			while(j < a.size() && a[i] == a[j]) j++;
			ans = max(ans, num[a[i].first] + num[a[i].second] + j - i);
		}
		printf("%d\n", m * m - cnt0 - ans);
	}
}

理论复杂度第一种比第二种略优秀,但是实际情况第二种比第一种快400ms,可能因为线段树的剪枝吧。

原文地址:https://www.cnblogs.com/pkgunboat/p/11025745.html

时间: 2024-08-29 20:02:15

Gym 102028J 扫描线/二维差分 + 解方程的相关文章

【bzoj5173】[Jsoi2014]矩形并 扫描线+二维树状数组区间修改区间查询

题目描述 JYY有N个平面坐标系中的矩形.每一个矩形的底边都平行于X轴,侧边平行于Y轴.第i个矩形的左下角坐标为(Xi,Yi),底边长为Ai,侧边长为Bi.现在JYY打算从这N个矩形中,随机选出两个不同的矩形,并计算它们的并的大小.JYY想知道,交的大小的期望是多少.换句话说即求在所有可能的选择中,两个矩形交的面积的平均大小是多大. 输入 输入一行包含一个正整数N. 接下来N行,每行4个整数,分别为Xi,Yi,Ai,Bi 2 < =  N < =  2*10^5, 0 < =  Xi,

二维差分

二维差分和一维差分思路上并没有什么区别,具体实现的区别就在于一维的直接对区间两端差分就好了,而二维的多了一维需要处理. 差分的思想是和前缀和有关的,一维的前缀和我们都懂求,那么二维的呢? 如图 因为是从左到右,从上到下的遍历,当要求红色部分,(0,0)到(i,j)处的前缀和时,我们黄色部分和蓝色部分已经是已知的了,而它们重叠的部分就是绿色部分,所以把黄色和蓝色部分的结果加起来,再减去绿色部分,最后加上(i,j)处的值就是(i,j)位置的前缀和了. 所以,二维前缀和就是sum[i][j]=a[i]

Codeforces Round #578 (Div. 2) 二维差分 可做模板

题意: 在n*n的矩阵中,你可以选择一个k*k的子矩阵,然后将这个子矩阵中的所有B全部变为W,问你怎么选择这个子矩阵使得最终的矩阵中某一行全是W或者某一列全是W的个数最多 题解:考虑每一行和每一列,对于特定的一行来说,要想让其全变为W,那么子矩阵的左上角端点是在一个范围中的,因此我们可以把范围中的每一个值加1 为了速度选择用二维差分来做,最终矩阵中的最大值就是答案 此题可以作为二维差分模板 #include<bits/stdc++.h> #define forn(i, n) for (int

二维差分前缀和——cf1202D(好题)

直接枚举每个点作为左上角是可以做的,但是写起来较麻烦 有一种较为简单的做法是对一列或一行统计贡献 比如某一行的B存在的区间是L,R那么就有三种情况 1.没有这样的区间,即一行都是W,此时这行对答案的贡献一直是1 2.R-L+1<=k,那么这一段必须要找一个点代表的矩形来覆盖,可以求出这样的点的存在区间是一个矩形,当且仅当点在这个矩形范围内时,这一行会有1的贡献. 3.R-L+1>k,永远不会有贡献 对于情况2,我们用二维的差分来统计一下,最后枚举每个点,看我们选择这个点代表的矩形时,贡献是否达

一维差分和二维差分

差分 一维: 原数组:\(c[i]\) 差分数组\(a[i]\):表示\(i{\sim}n\)的数,每一个数\(c[j](i<=j<=n)\)都加上一个\(a[i]\) 应用场景: ①把从第\(k-n\)位的数都加上一个\(w\) a[k]+=w; ②把从第\(i\)位到第\(j\)位的数都加上一个\(w\) a[i]+=w,a[j+1]-=w; 前提是需要对数组,进行多次①②这样的操作,使用差分才有意义,不然直接暴力就可以了 要注意的是①②操作只是把那些加减操作缓存了起来,而并不是完全分布给

cf1200 D White Lines(二维差分)

题目大意 有一个大小为n的矩阵,每个1*1的单位为黑或白,我们可以用一个(只有一个)大小为k*k的白色矩阵覆盖,问:最多的时候有几条白线(横的全为白 或竖的全为白 即为白线). 思路 要想把一条线(以横的为例)全变为白的,那么我们就需要从这一行最左边的黑色块覆盖到最右边的黑色块,如果两端距离超过k,则无法覆盖,否则就一定可以.那么就一定会产生一个矩阵,选取这个矩阵里面的任何一个点 都可以将这行变为白线:反之,矩阵外的一定不行.所以,可以用差分数组,因为只要选了矩阵里的点,答案就一定就加一.然后二

Problem 3 二维差分

$des$ 考虑一个 n ∗ n 的矩阵 A,初始所有元素均为 0.执行 q 次如下形式的操作: 给定 4 个整数 r,c,l,s, 对于每个满足 x ∈ [r,r+l), y ∈ [c,x−r+c]的元素 (x,y),将权值增加 s.也就是,给一个左上顶点为 (r,c).直角边长为 l 的下三角区域加上 s.输出最终矩阵的元素异或和. $sol$ 每次加减是一个等腰直角三角形 考虑对每行查分 即对垂直于 x 轴的腰上的每个点 +1 ,所有斜边的后一个点 -1 这样的话,每行形成了查分数组 简化

bzoj 3594 [Scoi2014]方伯伯的玉米田(DP+二维BIT)

[题目链接] http://www.lydsy.com/JudgeOnline/problem.php?id=3594 [题意] 给定一个n个数的序列,有K次将一个区间内的数加1的机会,问最长不下降子序列. [思路] 首先知道每次加1一个区间为[i,n]肯定不会差. 设f[i][j]为前i个数,还有j次机会的LIS,则有转移式: f[i][j] = max{ f[a][b],h[a]+b<=h[i]+j } 则可以用二维BIT加速方程转移. [代码] 1 #include<set> 2

一维 + 二维树状数组 + 单点更新 + 区间更新 详解

树状数组详解: 假设一维数组为A[i](i=1,2,...n),则与它对应的树状数组C[i](i=1,2,...n)是这样定义的: C1 = A1 C2 = A1 + A2 C3 = A3 C4 = A1 + A2 + A3 + A4 C5 = A5 C6 = A5 + A6 ................. C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 ................ 如图可知: 为奇数的时候他是代表他本身,而为偶数的时候则是代表着自