题意:有一个二维平面,以及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,可能因为线段树的剪枝吧。