深入浅出程序设计竞赛 第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 种类数最多。

题解:双指针/尺取法,关键是学会用 setmap 维护牛的种类数和每个种类的个数。

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';
}
posted @ 2026-07-20 19:01  Linqi05  阅读(2)  评论(0)    收藏  举报