出生点

GitHub跳转原题关系图返回列表

先算完整 n×m 网格所有出生点对的曼哈顿距离和,再减去所有涉及障碍点的贡献,最后补回障碍之间被多减的一次。

OJ: luogu

题目 ID: P6692

难度:提高+/省选-

标签:数学推导曼哈顿距离组合计数思维

日期: 2026-06-20 07:09

题意

在一个 n x m 的网格里,有 k 个障碍点不能出生。

小 W 和小 H 会各自随机出生在一个非障碍点上。
如果两人交换出生点,视为同一种情况;两人也可以出生在同一个点上。

要求输出:

  • 所有合法出生点安排中,两人曼哈顿距离的总和

思路

先看一个可以直接验证的小数据暴力:

cpp
#include <bits/stdc++.h>
using namespace std;

using i64 = long long;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m, k;
    cin >> n >> m >> k;

    set<pair<int, int>> blocked;
    for (int i = 0; i < k; i++) {
        int x, y;
        cin >> x >> y;
        blocked.insert({x, y});
    }

    vector<pair<int, int>> pts;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (!blocked.count({i, j})) {
                pts.push_back({i, j});
            }
        }
    }

    i64 ans = 0;
    for (int i = 0; i < (int) pts.size(); i++) {
        for (int j = i; j < (int) pts.size(); j++) {
            ans += llabs(pts[i].first - pts[j].first)
                 + llabs(pts[i].second - pts[j].second);
        }
    }

    cout << ans << '\n';
    return 0;
}

暴力版先把所有非障碍点列出来,再枚举所有 i <= j 的点对,直接累加曼哈顿距离。

正解的关键是:

  • 先算完整网格的答案
  • 再把障碍点带来的影响修正掉

1. 完整网格的答案

如果没有障碍,那么曼哈顿距离可以拆成横纵两个方向分别统计。

先看行坐标部分。
假设两点的行分别是 i < j,那么它们在行方向贡献:

  • j - i

而列可以随便选,有 m^2 种搭配。
所以行方向总贡献是:

  • m^2 * sum_{1<=i<j<=n} (j-i)

同理,列方向总贡献是:

  • n^2 * sum_{1<=i<j<=m} (j-i)

并且有经典公式:

  • sum_{1<=i<j<=L} (j-i) = L(L-1)(L+1)/6

于是完整网格总答案可以直接算出来。

2. 删除障碍点

设完整网格点集是 U,障碍点集合是 B,真正可出生点集合是 R = U - B

我们想要的是:

  • sum_{p,q in R, p<=q} dist(p,q)

可以从完整答案里扣掉所有“涉及障碍点”的点对。

对一个障碍点 (x, y) 来说,它到整张网格所有点的距离和是:

  • m * sum_{i=1}^n |x-i| + n * sum_{j=1}^m |y-j|

其中:

  • sum_{i=1}^n |x-i|

可以拆成左边一段和右边一段,直接公式算。

把所有障碍点都这样减掉后,会有一个重复:

  • 障碍点之间的点对被减了两次

所以最后还要把“障碍点之间的曼哈顿距离和”补回一次。

3. 最终公式

于是最终答案就是:

  • 完整网格总和
  • 减去 所有障碍点到整张网格的距离和
  • 加上 障碍点之间的无序距离和

障碍点之间的距离和也能拆成 xy 两部分,排序后用前缀和在线性里算完。

4. 为什么要手写大整数

n, m 都能到 1e9,答案量级远远超过 long long
这题没有取模,所以必须输出完整整数。

本地编译环境没有 boost::multiprecision,所以代码里手写了一个只支持:

  • 加法
  • 减法
  • 乘小整数
  • 除小整数

的非负大整数,已经够用。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

using i64 = long long;

struct BigInt {
    static const int BASE = 1000000000;
    vector<int> d; // 小端存储

    BigInt(i64 x = 0) {
        if (x == 0) {
            d.push_back(0);
            return;
        }
        while (x > 0) {
            d.push_back((int) (x % BASE));
            x /= BASE;
        }
    }

    void trim() {
        while (d.size() > 1 && d.back() == 0) {
            d.pop_back();
        }
    }

    BigInt &operator += (const BigInt &other) {
        int n = max(d.size(), other.d.size());
        d.resize(n, 0);

        i64 carry = 0;
        for (int i = 0; i < n; i++) {
            i64 cur = carry + d[i];
            if (i < (int) other.d.size()) {
                cur += other.d[i];
            }
            d[i] = (int) (cur % BASE);
            carry = cur / BASE;
        }

        if (carry) {
            d.push_back((int) carry);
        }
        return *this;
    }

    BigInt &operator -= (const BigInt &other) {
        i64 borrow = 0;
        for (int i = 0; i < (int) d.size(); i++) {
            i64 cur = (i64) d[i] - borrow;
            if (i < (int) other.d.size()) {
                cur -= other.d[i];
            }

            if (cur < 0) {
                cur += BASE;
                borrow = 1;
            }
            else {
                borrow = 0;
            }
            d[i] = (int) cur;
        }

        trim();
        return *this;
    }

    BigInt mul_small(i64 x) const {
        if (x == 0) {
            return BigInt(0);
        }

        BigInt res;
        res.d.assign(d.size(), 0);

        i64 carry = 0;
        for (int i = 0; i < (int) d.size(); i++) {
            i64 cur = (i64) d[i] * x + carry;
            res.d[i] = (int) (cur % BASE);
            carry = cur / BASE;
        }

        while (carry > 0) {
            res.d.push_back((int) (carry % BASE));
            carry /= BASE;
        }
        res.trim();
        return res;
    }

    BigInt div_small(int x) const {
        BigInt res;
        res.d.assign(d.size(), 0);

        i64 rem = 0;
        for (int i = (int) d.size() - 1; i >= 0; i--) {
            i64 cur = rem * BASE + d[i];
            res.d[i] = (int) (cur / x);
            rem = cur % x;
        }

        res.trim();
        return res;
    }

    string to_string() const {
        string s = std::to_string(d.back());
        char buf[16];
        for (int i = (int) d.size() - 2; i >= 0; i--) {
            snprintf(buf, sizeof(buf), "%09d", d[i]);
            s += buf;
        }
        return s;
    }
};

BigInt operator + (BigInt a, const BigInt &b) {
    a += b;
    return a;
}

BigInt operator - (BigInt a, const BigInt &b) {
    a -= b;
    return a;
}

int k;
i64 n, m;
vector<i64> xs, ys;

// 计算 pos 到 1..len 所有点的距离和。
BigInt sum_to_all(i64 len, i64 pos) {
    BigInt left = BigInt(pos - 1).mul_small(pos).div_small(2);
    i64 right_cnt = len - pos;
    BigInt right = BigInt(right_cnt).mul_small(right_cnt + 1).div_small(2);
    return left + right;
}

// 计算一个有序数组中,两两差值和。
BigInt pair_diff_sum(vector<i64> a) {
    sort(a.begin(), a.end());

    BigInt ans(0);
    BigInt prefix(0);
    for (int i = 0; i < (int) a.size(); i++) {
        BigInt cur((i64) a[i] * i);
        cur -= prefix;
        ans += cur;
        prefix += BigInt(a[i]);
    }
    return ans;
}

BigInt line_pair_sum(i64 len) {
    // sum_{1<=i<j<=len} (j-i) = len*(len-1)*(len+1)/6
    BigInt res(len);
    res = res.mul_small(len - 1);
    res = res.mul_small(len + 1);
    res = res.div_small(6);
    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> k;
    xs.resize(k);
    ys.resize(k);
    for (int i = 0; i < k; i++) {
        cin >> xs[i] >> ys[i];
    }

    // 完整 n*m 网格中,所有无序点对的曼哈顿距离总和。
    BigInt total_full = line_pair_sum(n).mul_small(m).mul_small(m)
                      + line_pair_sum(m).mul_small(n).mul_small(n);

    // 障碍点到整张网格所有点的距离和(每个障碍点只算一次)。
    BigInt remove_sum(0);
    for (int i = 0; i < k; i++) {
        remove_sum += sum_to_all(n, xs[i]).mul_small(m);
        remove_sum += sum_to_all(m, ys[i]).mul_small(n);
    }

    // 障碍点之间的无序距离和。前面 remove_sum 把这些对减了两次,这里补回一次。
    BigInt blocked_pair_sum = pair_diff_sum(xs) + pair_diff_sum(ys);

    BigInt answer = total_full + blocked_pair_sum;
    answer -= remove_sum;
    cout << answer.to_string() << '\n';
    return 0;
}

复杂度

设障碍数为 k

  • 读入并统计障碍对整图贡献:O(k)O(k)
  • 排序并计算障碍点之间距离和:O(klogk)O(k log k)

总时间复杂度:

  • O(klogk)O(k log k)

空间复杂度:

  • O(k)O(k)

总结

这题最核心的想法不是“直接数可出生点”,而是:

  1. 先算完整网格答案
  2. 再按障碍做删点修正

因为障碍只有 5e5 个,而整张图可能有 1e18 个点,所以必须把复杂度压到只和障碍数有关。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析