深入浅出程序设计竞赛 第2章 前缀和、差分和离散化
习题1 领地选择
题意:在二维平面上选择一块 \(C\times C\) 的区域,要最大化区域内元素之和。
题解:非常模板的二维前缀和题目。
view code
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, c;
cin >> n >> m >> c;
vector<vector<int>> a(n, vector<int>(m));
vector<vector<i64>> pre(n + 1, vector<i64>(m + 1));
i64 ans = LONG_LONG_MIN;
int x, y;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> a[i][j];
pre[i + 1][j + 1] =
a[i][j] + pre[i][j + 1] + pre[i + 1][j] - pre[i][j];
if (i >= c - 1 && j >= c - 1) {
i64 res = pre[i + 1][j + 1] - pre[i + 1 - c][j + 1] -
pre[i + 1][j + 1 - c] + pre[i + 1 - c][j + 1 - c];
if (res > ans) {
ans = res;
x = i - c + 2;
y = j - c + 2;
}
}
}
}
cout << x << ' ' << y << '\n';
}
习题2 Brownie Slicing G
题意:给定二维数组,要在行上分出 \(A\) 块,每一块在列上分出 \(B\) 块,要最大化元素之和最小的块。
题解:二分答案,判断答案时,利用前缀和快速判断每一块的元素之和,只要大于等于当前二分的答案,就切出一块来。
view code
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, a, b;
cin >> n >> m >> a >> b;
vector<vector<int>> g(n, vector<int>(m));
vector<vector<int>> pre(n + 1, vector<int>(m + 1));
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> g[i][j];
pre[i + 1][j + 1] =
g[i][j] + pre[i][j + 1] + pre[i + 1][j] - pre[i][j];
}
}
auto solve = [&](int ans) {
int last_x = 0, cut_x = 0;
for (int i = 1; i <= n; i++) {
int last_y = 0, cut_y = 0;
for (int j = 1; j <= m; j++) {
if (pre[i][j] - pre[last_x][j] - pre[i][last_y] +
pre[last_x][last_y] >=
ans) {
last_y = j;
cut_y++;
}
}
if (cut_y >= b) {
last_x = i;
cut_x++;
}
}
if (cut_x >= a) return true;
return false;
};
int l = 0, r = pre[n][m] / (a * b) + 1;
while (l + 1 < r) {
int ans = (l + r) / 2;
if (solve(ans))
l = ans;
else
r = ans;
}
cout << l << '\n';
}
习题3 海底高铁
题意:\(n\) 个城市之间有两种支付地铁费用的方式:每次付 \(A_i\) 或花费 \(C_i\) 购买卡之后每次付 \(B_i\)。给定一段出行行程,问最小花费。
题解:每次移动都拆分成若干次相邻城市之间的移动,用差分数组维护,就可以知道每两个相邻城市之间总共要经过多少次,从而知道是否要购买 IC 卡。
view code
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<int> a(m);
for (int i = 0; i < m; i++) {
cin >> a[i];
a[i]--;
}
vector<int> d(n);
for (int i = 0; i < m - 1; i++) {
int x = a[i], y = a[i + 1];
if (x > y) swap(x, y);
d[x]++, d[y]--;
}
int sum = 0;
i64 ans = 0;
for (int i = 0; i < n - 1; i++) {
int A, B, C;
cin >> A >> B >> C;
sum += d[i];
ans += min((i64)sum * A, (i64)sum * B + C);
}
cout << ans << '\n';
}
习题4 借教室
题意:给定 \(n\) 个数,\(m\) 次区间减法,问是否在某一次减法中把数字减成负数。
题解:线段树模板题,不过既然在差分专题就用差分做。考虑到每一次做区间减法时,数字越来越小,如果出现负数后面也不会变正数,即整个过程是具有单调性的:前若干次操作没有负数,从某一次开始出现负数。我们就用二分答案来查找变成负数的那一次,每一次 check 采用差分来 \(O(1)\) 处理区间减法,最后 \(O(n)\) 做一次前缀和就得到是否每个数都是非负的。
view code
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
vector<array<int, 3>> b(m);
for (int i = 0; i < m; i++) {
cin >> b[i][0] >> b[i][1] >> b[i][2];
b[i][1]--;
b[i][2]--;
}
vector<i64> diff(n + 1);
diff[0] = a[0];
for (int i = 0; i < n - 1; i++) diff[i + 1] = a[i + 1] - a[i];
auto check = [&](int d) {
auto df = diff;
for (int i = 0; i <= d; i++) {
df[b[i][1]] -= b[i][0];
df[b[i][2] + 1] += b[i][0];
}
i64 pre = 0;
for (int i = 0; i < n; i++) {
pre += df[i];
if (pre < 0) return false;
}
return true;
};
int l = -1, r = m;
while (l + 1 < r) {
int mid = (l + r) / 2;
if (check(mid))
l = mid;
else
r = mid;
}
if (l == m - 1) {
cout << "0\n";
} else {
cout << "-1\n" << r + 1 << '\n';
}
}
习题5 Face The Right Way G
题意:有 \(n\) 头牛方向朝前或朝后,选择数字 \(k\),每次可以让连续的 \(k\) 头牛转向,问最小的操作数和对应的 \(k\)。
题解:枚举 \(k\),贪心扫描序列,遇到一个朝后的牛就从这个位置开始让连续的 \(k\) 头牛转向。朝后的牛可以记作 1,朝前的是 0,一段区间内转向就相当于区间加一,此时偶数是朝前,奇数是朝后。然后用差分数组来 \(O(1)\) 的执行区间加一操作。也可以用异或1操作代替加1。
view code
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 0; i < n; i++) {
char c;
cin >> c;
a[i] = (int)(c == 'B');
}
vector<int> D(n + 1);
D[0] = a[0];
for (int i = 0; i < n; i++) D[i + 1] = a[i + 1] - a[i];
int ans = n + 1, min_k;
for (int k = 1; k <= n; k++) {
auto d = D;
int pre = 0, cnt = 0;
for (int i = 0; i < n; i++) {
if ((pre + d[i]) & 1) {
if (i + k - 1 >= n) {
cnt = n + 1;
break;
}
cnt++;
d[i] += 1;
d[i + k] -= 1;
}
pre += d[i];
}
if (cnt < ans) {
ans = cnt;
min_k = k;
}
}
cout << min_k << ' ' << ans << '\n';
}
习题6 IncDec Sequence
题意:有 \(n\) 个数,每次可以选择一个连续区间加一或减一,问最少需要多少次操作可以让所有数一样大,并且此时有多少种情况。
题解:考虑原数组的差分数组 \(d_i\),让所有数一样大其实就是除了 \(d_0\) 以外其他数均为0,多少种情况即 \(d_0\) 有多少种取值。区间加一或减一在差分数组上就是对两个数分别加一、减一。统计差分数组所有正数之和和负数的相反数之和,分别记为 \(p, q\),不妨让 \(p<q\)(两者是对称的),那么最小操作数就是 \(q\),而差分数组第一个数有多少种取值呢?我们前 \(p\) 次操作是平衡正数和负数到0,和 \(d_0\) 无关;后 \(q-p\) 次操作则有两种选择,选择 \(d_0\) 或 \(d_n\) 来平衡,所以 \(d_0\) 有 \(q-p+1\) 种取值。
view code
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 0; i < n; i++) cin >> a[i];
vector<int> d(n + 1);
d[0] = a[0];
for (int i = 1; i <= n; i++) d[i] = a[i] - a[i - 1];
long long p = 0, q = 0;
for (int i = 1; i < n; i++) {
if (d[i] > 0)
p += d[i];
else
q += -d[i];
}
if (p > q) swap(p, q);
cout << q << '\n' << q - p + 1 << '\n';
}
习题7 Cow Lineup S
题意:有 \(n\) 个牛,每头牛具有坐标和 ID,要求一个坐标范围最小的区间,满足区间内牛的 ID 种类数最多。
题解:双指针/尺取法,关键是学会用 set 和 map 维护牛的种类数和每个种类的个数。
view code
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<pair<int, int>> a(n);
set<int> s;
for (int i = 0; i < n; i++) {
cin >> a[i].first >> a[i].second;
s.insert(a[i].second);
}
sort(a.begin(), a.end());
int ans = INT_MAX;
set<int> t;
map<int, int> mp;
for (int i = 0, j = 0; i < n; i++) {
while (j < n && t.size() < s.size()) {
t.insert(a[j].second);
mp[a[j].second]++;
j++;
}
if (t.size() == s.size()) {
ans = min(ans, a[j - 1].first - a[i].first);
}
if (--mp[a[i].second] == 0) {
t.erase(a[i].second);
}
}
cout << ans << '\n';
}
习题8 天际线
题意:有 \(n\leq 5\times10^3\) 个长方形,横坐标不超过 \(10^4\),底边排列在同一水平线上。现在问轮廓线的坐标,奇数点输出横坐标,偶数点输出纵坐标。
题解:偶数点的纵坐标其实和奇数点的纵坐标一致,本质是输出奇数点的横纵坐标。注意到横坐标范围很小,可以直接暴力算出每一个位置长方形的最大高度,观察到轮廓线的奇数点就是最大高度和左边不一致的点。暴力计算长方形最大高度时要注意是左闭右开的。
view code
#include <bits/stdc++.h>
using namespace std;
int main() {
// freopen("P1904.in", "r", stdin);
vector<int> max_h(10001);
int l, h, r;
while (cin >> l >> h >> r) {
for (int i = l; i < r; i++) {
max_h[i] = max(max_h[i], h);
}
}
int last = 0;
for (int i = 1; i <= 10000; i++) {
if (max_h[i] != last) {
cout << i << ' ' << max_h[i] << ' ';
last = max_h[i];
}
}
}
习题9 Out of Sorts G
题意:给定一个序列 \(a_i\),如果不是有序的就做一次正序的冒泡排序和倒序的冒泡排序,问要经过多少次操作序列会变成有序的。
题解:对任意前缀 \(i\),如果出现 \(x\) 个最终排名不应该存在于前缀 \(i\) 中的数(定义为 \(bad(i)\)),那么它恰好要经过 \(x\) 次操作才会让 \(bad(i)\) 变为 0。最终答案就是 \(max(1,bad(i))\),用树状数组维护当前枚举到位置 \(i\) 时,排名比当前小于等于的数字出现的位置。
证明 \(bad(i)\) 在每一次操作中恰好减少1:研究前缀 \(i\) 时,把序列二值化,排名大于 \(i\) 的数重定义为 1,其余为 0。正向冒泡排序到边界时左侧一定是1,例如 \(1 | x\),此时如果 \(x\) 是 0 那么 \(bad(i)\) 减一并且倒序冒泡排序不会改变 \(0 | 1\)。如果 \(x\) 是 1,那么倒序冒泡时会把边界变成 \(1 | 0\)(一定会把一个0挤到边界右侧),然后发生交换,\(bad(i)\) 仍然减少一。
view code
#include <bits/stdc++.h>
using namespace std;
template <typename T>
struct Fenwick {
const int n;
vector<T> a;
Fenwick(int n) : n(n), a(n) {}
void add(int x, T v) {
for (int i = x + 1; i <= n; i += i & -i) {
a[i - 1] += v;
}
}
T sum(int x) {
T ans = 0;
for (int i = x; i > 0; i -= i & -i) {
ans += a[i - 1];
}
return ans;
}
T rangeSum(int l, int r) { return sum(r) - sum(l); }
};
int main() {
int n;
cin >> n;
vector<pair<int, int>> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i].first;
a[i].second = i;
}
sort(a.begin(), a.end());
vector<int> rank(n);
for (int i = 0; i < n; i++) rank[a[i].second] = i;
int ans = 1;
Fenwick<int> fen(n);
for (int i = 0; i < n; i++) {
fen.add(rank[i], 1);
ans = max(ans, i + 1 - fen.sum(i + 1));
}
cout << ans << '\n';
}
习题10 Parity Game
题意:有一个 01 序列,有若干组询问,每组询问告诉你一个区间的异或和的奇偶性,当你可以判断出矛盾时打印出来。
题解:除了考察区间端点的离散化处理外,这道题主要是一道异或并查集的模板题:区间的异或和等价于前缀异或和 \(S_{l-1}\) 异或 \(S_r\)。异或并查集的模板具体就看代码自行理解,核心是记住 val[x] 维护 \(x\) 异或 \(p[x]\) 的结果。
view code
#include <bits/stdc++.h>
using namespace std;
struct DSU {
vector<int> f, val, siz;
DSU(int n) : f(n), val(n), siz(n, 1) { iota(f.begin(), f.end(), 0); }
int find(int x) {
if (f[x] == x) {
return x;
}
int p = f[x];
f[x] = find(p);
val[x] ^= val[p];
return f[x];
}
bool unite(int x, int y, int w) {
int fx = find(x);
int fy = find(y);
if (fx == fy) {
return (val[x] ^ val[y]) == w;
}
if (siz[fx] < siz[fy]) {
f[fx] = fy;
val[fx] = val[x] ^ val[y] ^ w;
siz[fy] += siz[fx];
} else {
f[fy] = fx;
val[fy] = val[x] ^ val[y] ^ w;
siz[fx] += siz[fy];
}
return true;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
DSU dsu(2 * m);
map<int, int> mp;
int id = 1, ans = m, fail = 0;
auto get_id = [&](int x) {
if (mp[x])
return mp[x];
else
return mp[x] = id++;
};
for (int i = 0; i < m; i++) {
int l, r;
string s;
cin >> l >> r >> s;
if (fail) {
continue;
}
int x = get_id(l - 1), y = get_id(r), w;
if (s == "odd") {
w = 1;
} else {
w = 0;
}
if (!dsu.unite(x, y, w)) {
ans = i;
fail = 1;
}
}
cout << ans << '\n';
}

浙公网安备 33010602011771号