把小人与房子的最短步数(曼哈顿距离)作为权值,转化为二分图最小权完美匹配,用匈牙利算法 O(n³) 求出最少花费。
OJ: roj
题目 ID: 3220
难度:提高+/省选-
标签:图论二分图带权匹配
创建: 2026-10-02 02:46
更新: 2026-10-10 11:30
形式化题目
给一个 m)和 H),其余格子是空地(.)。小人每单位时间可以上、下、左、右移动一格,每走一步花费 1 美金,直到进入某间房子为止;每间房子只能容纳一个小人。求让所有小人都进屋的最少总花费。
网格中没有障碍物,小人也可以踩在房门格上再选择进入,所以一个小人从
以样例 2 为例,3 个小人
| 小人 \ 房子 | |||
|---|---|---|---|
| 4 | 3 | 4 | |
| 4 | 5 | 4 | |
| 5 | 4 | 3 |
表格每格是对应小人走到对应房子的最少步数。从表中挑
正解
思路
朴素做法是枚举小人与房子的全部对应方式:把
观察每次配对的代价只由「哪个小人、哪间房子」决定,与其他人如何配对无关——这正是二分图最小权完美匹配的结构:左部
为每个小人
取等号的边构成「相等子图」。把所有顶标加起来:
算法逐轮把小人挂入交错树:从当前小人
用上文样例 2 的权值矩阵示意匹配过程(边上的数字为权值,加粗为最终匹配):
graph LR
subgraph L["左部:小人"]
P1["P1"]
P2["P2"]
P3["P3"]
end
subgraph R["右部:房子"]
H1["H1"]
H2["H2"]
H3["H3"]
end
P1 ---|"4"| H1
P1 ==>|"3"| H2
P1 ---|"4"| H3
P2 ==>|"4"| H1
P2 ---|"5"| H2
P2 ---|"4"| H3
P3 ---|"5"| H1
P3 ---|"4"| H2
P3 ==>|"3"| H3
KM 每轮在交错树上做
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll FAR = 1000000000000000000LL;
int cost[105][105]; // 小人 -> 房子的曼哈顿距离
ll pot_l[105]; // 左部顶标
ll pot_r[105]; // 右部顶标
int match_r[105]; // match_r[j] = 配到房子 j 的小人,0 表示空房
int prev_[105]; // 交错树上房子 j 的前驱房子
ll slack[105];
bool used[105];
// 匈牙利(KM)求 n×n 最小权完美匹配
ll min_cost_perfect_matching(int n) {
for (int i = 0; i <= n; i++) { pot_l[i] = 0; pot_r[i] = 0; }
for (int i = 0; i <= n; i++) match_r[i] = 0;
for (int i = 1; i <= n; i++) {
match_r[0] = i;
for (int j = 0; j <= n; j++) slack[j] = FAR;
for (int j = 0; j <= n; j++) used[j] = false;
int j0 = 0;
while (true) {
used[j0] = true;
int u = match_r[j0];
ll delta = FAR;
int j1 = 0;
for (int j = 1; j <= n; j++) {
if (!used[j]) {
ll gap = (ll)cost[u - 1][j - 1] - pot_l[u] - pot_r[j];
if (gap < slack[j]) {
slack[j] = gap;
prev_[j] = j0;
}
if (slack[j] < delta) {
delta = slack[j];
j1 = j;
}
}
}
for (int j = 0; j <= n; j++) {
if (used[j]) {
pot_l[match_r[j]] += delta;
pot_r[j] -= delta;
} else {
slack[j] -= delta;
}
}
j0 = j1;
if (match_r[j0] == 0) break;
}
while (j0) {
match_r[j0] = match_r[prev_[j0]];
j0 = prev_[j0];
}
}
ll total = 0;
for (int j = 1; j <= n; j++)
total += (ll)cost[match_r[j] - 1][j - 1];
return total;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
while (true) {
cin >> n >> m;
if (n == 0 && m == 0) break;
vector<string> grid(n);
for (int i = 0; i < n; i++) cin >> grid[i];
vector<pair<int, int> > people, houses;
for (int r = 0; r < n; r++)
for (int c = 0; c < m; c++) {
if (grid[r][c] == 'm') people.push_back(make_pair(r, c));
else if (grid[r][c] == 'H') houses.push_back(make_pair(r, c));
}
int cnt = (int)people.size();
for (int i = 0; i < cnt; i++)
for (int j = 0; j < cnt; j++)
cost[i][j] = abs(people[i].first - houses[j].first)
+ abs(people[i].second - houses[j].second);
cout << min_cost_perfect_matching(cnt) << "\n";
}
return 0;
}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 02:47
# update_at: 2026-10-02 03:30
import sys
FAR = 10 ** 18 # 松弛量初值:比任何曼哈顿距离都大,表示「还没连到交错树上」
def min_cost_perfect_matching(cost: list[list[int]]) -> int:
"""匈牙利算法(KM):求 n×n 最小权完美匹配,返回最小总权值。
顶标维持可行性 pot_l[i] + pot_r[j] <= cost[i][j],取等号的边构成「相等子图」。
只要相等子图里有完美匹配,每条匹配边都顶着等式取到下界,和必为全局最小。
每轮把一个人 i 挂入交错树找增广路;找不到时给树上点整体改顶标:
左部点 +delta、右部点 -delta(delta 是树到树外房子的最小松弛量),
相等边一加一减保持不动,树外松弛量同步减 delta,至少冒出一条新等式边且可行性不破。
"""
n = len(cost)
pot_l = [0] * (n + 1) # 左部(小人)顶标
pot_r = [0] * (n + 1) # 右部(房子)顶标
match_r = [0] * (n + 1) # match_r[j] = 配到房子 j 的小人,0 表示空房
prev = [0] * (n + 1) # 交错树上房子 j 的前驱房子
for i in range(1, n + 1):
match_r[0] = i # 虚拟房子 0 指向本轮待增广的小人 i,当树根
slack = [FAR] * (n + 1) # 树外房子 j 到树上小人的最小松弛量
used = [False] * (n + 1)
j0 = 0
while True:
used[j0] = True
row = cost[match_r[j0] - 1] # 沿匹配边回退到的树上小人
delta, j1 = FAR, 0
for j in range(1, n + 1): # 用新入树的小人刷新松弛量
if not used[j]:
gap = row[j - 1] - pot_l[match_r[j0]] - pot_r[j]
if gap < slack[j]:
slack[j], prev[j] = gap, j0
if slack[j] < delta:
delta, j1 = slack[j], j
for j in range(n + 1): # 树上左 +delta 右 -delta,树外松弛量减 delta
if used[j]:
pot_l[match_r[j]] += delta
pot_r[j] -= delta
else:
slack[j] -= delta
j0 = j1
if match_r[j0] == 0: # 撞上空房 = 找到增广路
break
while j0: # 沿 prev 翻转整条增广路
match_r[j0] = match_r[prev[j0]]
j0 = prev[j0]
return sum(cost[match_r[j] - 1][j - 1] for j in range(1, n + 1))
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
out: list[str] = []
while True:
n, m = int(next(data)), int(next(data))
if n == 0 and m == 0: # 题面约定的结束标记
break
grid = [next(data) for _ in range(n)]
people = [(r, c) for r in range(n) for c in range(m) if grid[r][c] == ord('m')]
houses = [(r, c) for r in range(n) for c in range(m) if grid[r][c] == ord('H')]
# 权值取曼哈顿距离:空地、房门格都能踩,最短步数就是 |dr| + |dc|
cost = [[abs(pr - hr) + abs(pc - hc) for hr, hc in houses] for pr, pc in people]
out.append(str(min_cost_perfect_matching(cost)))
print('\n'.join(out))
if __name__ == "__main__":
solve()复杂度
时间复杂度
总结
把「最少步数」算成曼哈顿距离后,题目就是标准的二分图最小权完美匹配:朴素的全排列枚举是 0 0 结束,权值矩阵由小人、房子坐标列表用推导式一次算出。