深入浅出程序设计竞赛 第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';
}
posted @ 2026-07-06 18:00  Linqi05  阅读(2)  评论(0)    收藏  举报