深入浅出程序设计竞赛 第1章 常见优化技巧
习题1 唯一的雪花
这题好像没有地方评测,可以跳过
题意:给定 \(n\) 个数字,求最长的连续区间,区间内的数字各不相同。
题解:用 map 维护数字出现次数,deque 维护区间内的数字。也可以不用 deque 存储数组后双指针。
view code
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
map<int, int> mp;
deque<int> dq;
int ans = 0;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
dq.push_back(x);
if (++mp[x] == 1) {
ans = max(ans, int(dq.size()));
}
while (mp[x] > 1) {
int q = dq.front();
dq.pop_front();
if (--mp[q] == 1 && q == x) {
ans = max(ans, int(dq.size()));
}
}
}
cout << ans << '\n';
}
}
习题2 Sure Bet
题意:给定 A, B 两组数字,选择一个数字的代价是1。选择若干数字后,要求最大化 A, B 两组被选中数字之和的最小值(并减去选择的数字数量)。
题解:维护 A, B 两组选中数字之和,哪一方小时贪心地从该组中未选择的数字中选择最大的。
有一些边界情况考虑之后发现可以不处理。例如两边数字之和一样时其实是 A, B 两组各取一个数字,代码的逻辑可以处理这种情况。
代码这里 priority_queue<double> 其实不是一个好的写法,因为 double 类型的大小比较不具有传递性。题目没有给出输入浮点数的精度,如果有的话(例如是小数点后4位),就可以乘以 10000 转成整型来做更严谨。
view code
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
priority_queue<double> pq1, pq2;
for (int i = 0; i < n; i++) {
double a, b;
cin >> a >> b;
pq1.push(a);
pq2.push(b);
}
double ans = 0, s1 = 0, s2 = 0;
while (true) {
if (s1 <= s2 && pq1.size()) {
s1 += pq1.top() - 1;
s2 -= 1;
pq1.pop();
} else if (s1 >= s2 && pq2.size()) {
s2 += pq2.top() - 1;
s1 -= 1;
pq2.pop();
} else {
break;
}
ans = max(ans, min(s1, s2));
}
cout << fixed << setprecision(4) << ans << '\n';
}
习题3 Diamond Collector S
题意:给定 \(n\) 个数字,选择其中若干个数字到2个集合,每个集合的最大值和最小值之差不超过 \(k\)。问两个集合的大小之和的最大值。
题解:对于任意2个集合,总是可以让其中小的一批数字在集合A,其余的在集合B。如果这题只要求1个集合,就是经典的双指针(尺取法)。我们对数字排序后枚举集合 A 的最大值 \(x\),即集合A只从小于等于 \(x\) 的数字取,用 pre 数组维护,并且这个数组是单调不减的。集合B则只从大于 \(x\) 的数字取,用 suff 数组维护,并且这个数组是单调不增的。求 pre 数组也是双指针,枚举 \(j\) 时更新数组信息,表示取 \(a[j]\) 时集合的大小的最大值。
view code
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
sort(a.begin(), a.end());
vector<int> pre(n), suff(n);
for (int i = 0, j = 0; i < n; i++) {
while (j < n && a[j] - a[i] <= k) {
pre[j] = max(pre[j], j - i + 1);
j++;
}
}
for (int i = 1; i < n; i++) pre[i] = max(pre[i], pre[i - 1]);
for (int i = n - 1, j = n - 1; i >= 0; i--) {
while (j >= 0 && a[i] - a[j] <= k) {
suff[j] = max(suff[j], i - j + 1);
j--;
}
}
for (int i = n - 2; i >= 0; i--) suff[i] = max(suff[i], suff[i + 1]);
int ans = min(2, n);
for (int i = 0; i < n - 1; i++) {
ans = max(ans, pre[i] + suff[i + 1]);
}
cout << ans << '\n';
}
习题4 插入排序
题意:给定 \(n\) 个数字,两种操作:单点修改、查询稳定排序后的新位置。\(n\leq 8000\), \(q_1\leq5000\)(\(q_1\) 指操作1)。
题解:这道题的数据范围是一个很重要的提示。对于每一次单点修改,我们模拟插入排序,并 \(O(n)\) 维护每一个数字的新位置。代码中使用 pair 可以实现同时比较数字大小和原位置(因为是稳定排序)。
view code
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
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> b(n);
for (int i = 0; i < n; i++) b[a[i].second] = i;
while (q--) {
int op;
cin >> op;
if (op == 1) {
int x, v;
cin >> x >> v;
int p = b[x - 1];
a[p].first = v;
while (p < n - 1 && a[p] > a[p + 1]) {
swap(a[p], a[p + 1]);
p++;
}
while (p > 0 && a[p] < a[p - 1]) {
swap(a[p], a[p - 1]);
p--;
}
for (int i = 0; i < n; i++) b[a[i].second] = i;
} else {
int x;
cin >> x;
cout << b[x - 1] + 1 << '\n';
}
}
}
习题5 奶牛浴场
题意:二维平面上有若干障碍点,要求最大的矩形并且不覆盖障碍点。障碍点数 \(n\leq5\times10^3\),平面 \(L,W\leq3\times10^4\)
题解:用悬线法的话时间复杂度是 \(O(LW)\),常数优化一下也只有 79 分。
view code
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int X, Y;
cin >> X >> Y;
int n;
cin >> n;
vector<pair<int, int>> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i].first >> a[i].second;
}
sort(a.begin(), a.end());
int cur = 0;
while (cur < n && a[cur].first == 0) cur++;
vector<int> h(Y + 1);
int ans = 0;
for (int i = 1; i <= X; i++) {
vector<int> vis(Y + 1);
while (cur < n && a[cur].first == i) {
vis[a[cur].second] = 1;
cur++;
}
vector<int> l(Y + 1), r(Y + 1);
stack<pair<int, int>> stk;
for (int j = 0; j <= Y; j++) {
h[j]++;
while (stk.size() && stk.top().first >= h[j]) stk.pop();
l[j] = stk.size() ? j - stk.top().second : j;
stk.push({h[j], j});
}
stack<pair<int, int>>().swap(stk);
for (int j = Y; j >= 0; j--) {
while (stk.size() && stk.top().first >= h[j]) stk.pop();
r[j] = stk.size() ? stk.top().second - j : Y - j;
stk.push({h[j], j});
ans = max(ans, h[j] * (l[j] + r[j]));
if (vis[j]) h[j] = 0;
}
}
cout << ans << '\n';
}
最后是参考了点赞最高的题解,他是这么做的:定义四周都有障碍的矩形为极大矩形。按照横坐标排序障碍点后,从左往右枚举点,表示极大矩形最左边的点,然后扫描后面的点,过程中维护纵坐标的最大值和最小值。此外考虑到极大矩形的左边可能没有点,还要从右往左也做一遍。还有一种情况是左右两边都没有障碍点,此时按照纵坐标排序,枚举相邻的点作为极大矩形的上边界和下边界。
view code
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int X, Y;
cin >> X >> Y;
int n;
cin >> n;
vector<pair<int, int>> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i].first >> a[i].second;
}
a.emplace_back(0, 0);
a.emplace_back(0, Y);
a.emplace_back(X, 0);
n += 3;
int ans = 0;
// 从左往右搜
sort(a.begin(), a.end(),
[](const auto& p1, const auto& p2) { return p1.second < p2.second; });
for (int i = 0; i < n; i++) {
int lo = 0, hi = X, same = 0;
for (int j = i + 1; j < n; j++) {
if (!same) {
ans = max(ans, (hi - lo) * (a[j].second - a[i].second));
} else {
ans = max(ans, (hi - a[i].first) * (a[j].second - a[i].second));
ans = max(ans, (a[i].first - lo) * (a[j].second - a[i].second));
}
if (a[j].first < a[i].first) {
lo = max(lo, a[j].first);
} else if (a[j].first == a[i].first) {
same = 1;
} else {
hi = min(hi, a[j].first);
}
}
}
// 从右往左搜
reverse(a.begin(), a.end());
for (int i = 0; i < n; i++) {
int lo = 0, hi = X, same = 0;
for (int j = i + 1; j < n; j++) {
if (!same) {
ans = max(ans, (hi - lo) * (a[i].second - a[j].second));
} else {
ans = max(ans, (hi - a[i].first) * (a[i].second - a[j].second));
ans = max(ans, (a[i].first - lo) * (a[i].second - a[j].second));
}
if (a[j].first < a[i].first) {
lo = max(lo, a[j].first);
} else if (a[j].first == a[i].first) {
same = 1;
} else {
hi = min(hi, a[j].first);
}
}
}
// 纵坐标
sort(a.begin(), a.end());
for (int i = 0; i < n - 1; i++) {
ans = max(ans, (a[i + 1].first - a[i].first) * Y);
}
cout << ans << '\n';
}
习题6 PLA-Postering
题意:有若干紧挨着、下对齐的矩形,问最少可以用几个矩形不重不漏地覆盖所有矩形。
题解:考虑“凸”字型的结构,即最左边和最右边的矩形高度一样,中间的矩形比两侧都高。此时可以横着在最下面摆一个矩形,它对答案的优化贡献就是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;
stack<int> stk;
int ans = n;
for (int i = 0; i < n; i++) {
int _, h;
cin >> _ >> h;
while (stk.size() && stk.top() > h) stk.pop();
if (stk.size() && stk.top() == h) {
ans--;
} else {
stk.push(h);
}
}
cout << ans << '\n';
}
习题7 【模板】单调队列 / 滑动窗口
题意:给定 \(n\) 个数和大小为 \(k\) 的窗口,要求滑动窗口的过程中每一时刻窗口内元素的最小值和最大值。
题解:要求窗口的最小值,deque 维护一个单调不减的队列,最小值就是队首元素;最大值则是维护单调不增的队列。
view code
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
deque<pair<int, int>> q1, q2; // {a[i], i}
vector<int> ans1(n), ans2(n);
for (int i = 0; i < n; i++) {
if (q1.size() && q1.front().second + k <= i) q1.pop_front();
while (q1.size() && q1.back().first > a[i]) q1.pop_back();
q1.push_back({a[i], i});
ans1[i] = q1.front().first;
if (q2.size() && q2.front().second + k <= i) q2.pop_front();
while (q2.size() && q2.back().first < a[i]) q2.pop_back();
q2.push_back({a[i], i});
ans2[i] = q2.front().first;
}
for (int i = k - 1; i < n; i++) cout << ans1[i] << " \n"[i + 1 == n];
for (int i = k - 1; i < n; i++) cout << ans2[i] << " \n"[i + 1 == n];
}
习题8 Balanced Lineup G
题意:静态查询区间极差。
题解:线段树模板。奇怪,这种题怎么会出现在这个题单里。
view code
#include <bits/stdc++.h>
using namespace std;
template <class Info, class Merge = plus<Info>>
struct SegmentTree {
const int n;
const Merge merge;
vector<Info> info;
SegmentTree(int n) : n(n), merge(Merge()), info(4 << __lg(n)) {}
SegmentTree(vector<Info> init) : SegmentTree(init.size()) {
auto build = [&](auto& self, int p, int l, int r) -> void {
if (r - l == 1) {
info[p] = init[l];
return;
}
int m = (l + r) / 2;
self(self, 2 * p, l, m);
self(self, 2 * p + 1, m, r);
pull(p);
};
build(build, 1, 0, n);
}
void pull(int p) { info[p] = merge(info[2 * p], info[2 * p + 1]); }
void modify(int p, int l, int r, int x, const Info& v) {
if (r - l == 1) {
info[p] = v;
return;
}
int m = (l + r) / 2;
if (x < m) {
modify(2 * p, l, m, x, v);
} else {
modify(2 * p + 1, m, r, x, v);
}
pull(p);
}
void modify(int p, const Info& v) { modify(1, 0, n, p, v); }
Info rangeQuery(int p, int l, int r, int x, int y) {
if (l >= y || r <= x) {
return Info();
}
if (l >= x && r <= y) {
return info[p];
}
int m = (l + r) / 2;
return merge(rangeQuery(2 * p, l, m, x, y),
rangeQuery(2 * p + 1, m, r, x, y));
}
Info rangeQuery(int l, int r) { return rangeQuery(1, 0, n, l, r); }
};
struct node {
int mi, mx;
node() : mi(1e9), mx(-1e9) {}
node(int _x) : node(_x, _x) {}
node(int _mi, int _mx) : mi(_mi), mx(_mx) {}
};
node operator+(const node& a, const node& b) {
return node(min(a.mi, b.mi), max(a.mx, b.mx));
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
vector<node> a;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
a.push_back(node(x));
}
SegmentTree<node> tree(a);
while (q--) {
int l, r;
cin >> l >> r;
node ans = tree.rangeQuery(l - 1, r);
cout << ans.mx - ans.mi << '\n';
}
}
习题9 切蛋糕
题意:求长度小于等于 \(k\) 的区间之和的最大值。
题解:综合单调队列和前缀和的好题目。区间之和可以表示为两个前缀和之差,枚举区间右端点,此时要求的就是长度为 \(k\) 的前缀和数组的滑动窗口的最小值(因为减去最小值得到的就是答案要求的最大值)。要注意区间不能为空,所以每次枚举右端点时,减去的滑动窗口最小值都是上一次的信息,这样来确保区间内至少有一个元素。
view code
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
int sum = 0;
deque<pair<int, int>> q; // {sum_a, i}
q.push_back({0, -1});
int ans = *max_element(a.begin(), a.end());
for (int i = 0; i < n; i++) {
sum += a[i];
ans = max(ans, sum - q.front().first);
if (q.front().second + k <= i) q.pop_front();
while (q.size() && q.back().first > sum) q.pop_back();
q.push_back({sum, i});
}
cout << ans << '\n';
}
习题10 琪露诺
题意:给定长度为 \(n\) 的序列。人物在序列上每次走 \([l, r]\) 步,要求经过的数字之和的最大值。
题解:状态转移很明确,\(dp[i]=a[i]+\max{dp[i-j]}(l\leq j\leq r)\),随着 \(i\) 增大,注意到区间 \(dp[i-j](l\leq j\leq r)\) 构成一个滑动窗口,可以用单调队列维护得到滑动窗口的最大值。
view code
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, l, r;
cin >> n >> l >> r;
vector<int> a(n + 1 + r);
for (int i = 0; i <= n; i++) cin >> a[i];
vector<int> dp(n + 1 + r), mx(n + 1 + r);
deque<pair<int, int>> q;
q.push_back({0, 0});
for (int i = 1; i <= n + r; i++) {
if (i < l)
dp[i] = INT_MIN;
else
dp[i] = a[i] + mx[i - l];
if (q.front().second + r - l < i) q.pop_front();
while (q.size() && q.back().first < dp[i]) q.pop_back();
q.push_back({dp[i], i});
mx[i] = q.front().first;
}
cout << *max_element(dp.begin() + n + 1, dp.end()) << '\n';
}

浙公网安备 33010602011771号