加虚拟源点把「建电站」变成一条边,答案成为 n+1 个点的最小生成树;稠密完全图上选 O(n²) 的 Prim 现场扫描,不落地保存两百万条边。
OJ: codeforces
题目 ID: 1245D
难度:普及+/提高-
标签:图论最小生成树建模贪心
创建: 2026-10-21 20:00
更新: 2026-10-28 17:33
形式化题目
平面上有
- 在城市
建电站,花费 ; - 与另一个已经有电的城市
之间拉一条电线,花费
即两端系数之和乘以曼哈顿距离(不是
电力可以沿电线传递。求让所有城市通电的最小总花费,并给出方案:建电站的城市列表和拉线的城市对列表。
一个合法方案的结构是:选出若干城市建电站,其余每个城市通过拉线链最终连到某个电站城市——即若干棵以电站城市为根的树互不重叠地覆盖所有
暴力解法
思路
最直接的想法分两层:先枚举「哪些城市建电站」,再在固定电站集合后决定「其余城市怎么连」。
第一层用子集枚举:设
求这个森林只需一次朴素 Prim:把
总复杂度
代码
/**
* 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-21 20:00
* update_at: 2026-10-21 20:00
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
// 真正的暴力:枚举「哪些城市建电站」的所有子集 S(01 枚举),
// 固定 S 后,最优拉线方案 = 以 S 中城市为根、覆盖所有城市的最小生成森林,
// 也就是「T 中城市 = 全部城市」时把 S 看作已免费连通再对其余城市做 Prim。
// 复杂度 O(2^n * n^2),只适合 n <= 14 左右的小数据,与 main.cpp 不共享实现。
// 注意:输出只有最小花费,用于与主解对拍数值。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 16;
ll n;
ll x[maxn], y[maxn]; // 城市坐标
ll c[maxn]; // c[i]:在城市 i 建电站的花费
ll k[maxn]; // k[i]:拉线时每公里付出的系数
// 城市 i 与 j 之间拉线的代价:(k_i + k_j) * 曼哈顿距离
ll wire_cost(ll i, ll j) {
return (k[i] + k[j]) * (abs(x[i] - x[j]) + abs(y[i] - y[j]));
}
// 固定建电站集合 mask(第 i-1 位为 1 表示城市 i 建电站)后的最优总花费:
// 电站城市视为已通电,其余城市用最小生成森林的方式连进来。
// 朴素 Prim:dist[i] 是未入树城市 i 到「已通电集合」的最小拉线代价。
ll forest_cost(ll mask) {
ll station_cost = 0;
for (ll i = 1; i <= n; i++)
if (mask >> (i - 1) & 1) station_cost += c[i];
ll dist[maxn]; // dist[i]:城市 i 到已通电集合的最小拉线代价
bool in_tree[maxn]; // in_tree[i]:城市 i 是否已通电(含建电站的城市)
for (ll i = 1; i <= n; i++) {
in_tree[i] = mask >> (i - 1) & 1;
dist[i] = 0;
}
// 非电站城市初始只能直接连到某个电站城市:取到最近电站的拉线代价
for (ll i = 1; i <= n; i++)
if (!in_tree[i]) {
dist[i] = -1; // -1 作为无穷哨兵
for (ll s = 1; s <= n; s++)
if (in_tree[s]) {
ll cost = wire_cost(s, i);
if (dist[i] == -1 || cost < dist[i]) dist[i] = cost;
}
}
ll wire_total = 0;
ll left = n;
for (ll i = 1; i <= n; i++)
if (in_tree[i]) left--;
while (left > 0) { // 每轮把离已通电集合最近的城市连进来
ll best_i = 0;
for (ll i = 1; i <= n; i++)
if (!in_tree[i] && (best_i == 0 || dist[i] < dist[best_i]))
best_i = i;
in_tree[best_i] = true;
wire_total += dist[best_i];
left--;
for (ll i = 1; i <= n; i++)
if (!in_tree[i]) {
ll cost = wire_cost(best_i, i); // 新通电城市刷新其余城市的最近距离
if (cost < dist[i]) dist[i] = cost;
}
}
return station_cost + wire_total;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 1; i <= n; i++) cin >> x[i] >> y[i];
for (ll i = 1; i <= n; i++) cin >> c[i];
for (ll i = 1; i <= n; i++) cin >> k[i];
// 枚举所有建电站子集:城市 1..n 的建/不建构成 01 序列,mask 的第 i-1 位即城市 i
ll best = -1; // 最优总花费,-1 表示还没有答案
for (ll mask = 1; mask < (1LL << n); mask++) { // 至少建一个电站
ll total = forest_cost(mask);
if (best == -1 || total < best) best = total;
}
if (n == 0) best = 0;
cout << best << "\n";
return 0;
}复杂度
时间
瓶颈
暴力的两层结构把「谁建电站」和「谁连谁」当成互相独立的枚举:
正解
思路
虚拟源点建模:新增一个点
| 操作 | 对应的边 | 边权 |
|---|---|---|
| 在城市 |
||
| 城市 |
这张表是全部建模:左列是题目里的操作,右列是
「所有城市通电」等价于「
与合法方案的双向等价(这是正确性的核心,两个方向都要成立):
- 方案 → 生成树:把方案里每个「城市
建电站」替换成边 ,每条拉线替换成对应边。方案合法 ⟺ 每个城市建站或沿拉线链到达电站 ⟺ 替换后的 条边让 个点全部连通,而 条边连通 个点必无环,故是生成树。总花费逐条相等。 - 生成树 → 方案:读出
的邻边得到电站集合,其余树边作为拉线。每个非 的点沿树中唯一路径走向 ,路径第一条边必是某条 电站边,因此每个城市要么自己建站、要么连着一个最终有电的城市。总花费同样逐条相等。
两个方向的花费都严格相同,所以「最小花费的合法方案」与「最小生成树」一一对应,MST 算法输出的树直接翻译成题目要求的输出格式。
为什么能在建电站和拉线之间自由选择:MST 每一步只看当前最小边,不区分它是电站边还是拉线边。dist 数组会自然被更便宜的电站边覆盖。两种操作在同一棵树上竞争,不需要任何分类讨论——这正是把「点决策」也变成「边」的回报。
实现选择:dist 最小的未入树点,再用它刷新其余点的 dist,总共约
Prim 还免费送了输出方案:from[i] 记录 dist[i] 这条边的另一端,from[i] = 0(虚拟源点)说明是电站边,否则是拉线边,逐点分类即可。注意数值上 long long 范围内。
Python 版是同一算法的实现,读入用 next() 顺序消费,内层循环写紧凑即可。
代码
/**
* 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-21 20:00
* update_at: 2026-10-21 20:00
*/
// CF1245D Shichikuji and Power Grid
// 建模:加虚拟源点 0,0->i 边权 c_i(建电站),i->j 边权 (k_i+k_j)*曼哈顿距离(拉线),
// 答案 = n+1 个点的最小生成树。
// n <= 2000 时完全图约有 2*10^6 条边,对它们排序的 Kruskal 在时限边缘;
// 这里用 O(n^2) 的 Prim:每轮 O(n) 现场扫描选最近点,不落地保存任何边。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 2005;
ll n;
ll x[maxn], y[maxn]; // 城市坐标
ll c[maxn]; // c[i]:在城市 i 建电站的花费
ll k[maxn]; // k[i]:拉线时每公里付出的系数
ll dist[maxn]; // dist[i]:城市 i 到"当前已通电集合"的最小边权(初始为建电站代价 c_i)
int from[maxn]; // from[i]:dist[i] 对应的边的另一端,0 表示"建电站"这条虚拟边
bool used[maxn]; // used[i]:城市 i 是否已并入生成树
// 城市 i 与 j 之间拉线的代价:(k_i + k_j) * 曼哈顿距离
ll wire_cost(ll i, ll j) {
return (k[i] + k[j]) * (abs(x[i] - x[j]) + abs(y[i] - y[j]));
}
vector<ll> stations; // 建电站的城市编号(对应树里 0->i 的边)
vector<pair<ll, ll>> connections; // 拉线的城市对(对应树里 i->j 的边)
void prim() {
// 初始所有点都未加入,到集合的最近距离就是"建电站"这条虚拟边的权
for (ll i = 1; i <= n; i++) {
dist[i] = c[i];
from[i] = 0; // 0 是虚拟源点:先默认每个城市自己建电站
}
for (ll round = 1; round <= n; round++) { // 一共要把 n 个城市全部并入
// 找到未加入城市中 dist 最小的一个
ll best = 0;
for (ll i = 1; i <= n; i++)
if (!used[i] && (best == 0 || dist[i] < dist[best]))
best = i;
used[best] = true;
if (from[best] == 0)
stations.push_back(best); // 与虚拟源点相连:建电站
else
connections.push_back({from[best], best}); // 与已有城市相连:拉线
// 新点并入后,用它的出边更新其余城市的最近距离
for (ll i = 1; i <= n; i++)
if (!used[i] && wire_cost(best, i) < dist[i]) {
dist[i] = wire_cost(best, i);
from[i] = best;
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 1; i <= n; i++) cin >> x[i] >> y[i];
for (ll i = 1; i <= n; i++) cin >> c[i];
for (ll i = 1; i <= n; i++) cin >> k[i];
prim();
ll total = 0;
for (ll s : stations) total += c[s];
for (auto e : connections)
total += wire_cost(e.first, e.second);
cout << total << "\n";
cout << stations.size() << "\n";
for (ll i = 0; i < (ll)stations.size(); i++)
cout << stations[i] << " \n"[i + 1 == (ll)stations.size()];
cout << connections.size() << "\n";
for (ll i = 0; i < (ll)connections.size(); i++)
cout << connections[i].first << " " << connections[i].second << "\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-21 20:00
# update_at: 2026-10-21 20:00
import sys
INF = float("inf") # 已加入城市的 dist 哨兵,让 min 扫描自动跳过它们
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
pairs = [(next(data), next(data)) for _ in range(n)] # (x_i, y_i) 成对读入
X = [px for px, _ in pairs] # 题面的 x_i,下标 0..n-1 对应城市 1..n
Y = [py for _, py in pairs]
C = [next(data) for _ in range(n)] # 建电站代价
K = [next(data) for _ in range(n)] # 拉线系数
def wire_cost(i: int, j: int) -> int:
"""城市 i 与 j 之间拉线的代价:(k_i + k_j) * 曼哈顿距离。"""
return (K[i] + K[j]) * (abs(X[i] - X[j]) + abs(Y[i] - Y[j]))
# Prim:dist[i] 是城市 i 到已并入集合的最近边权,初始为“建电站”虚拟边 c_i。
dist = C[:]
source = [None] * n # source[i]:dist[i] 这条边的另一端城市,None 表示建电站
joined = [False] * n
stations: list[int] = []
connections: list[tuple[int, int]] = []
for _ in range(n): # 每轮并入一个城市,共 n 轮
best = min(range(n), key=lambda i: INF if joined[i] else dist[i])
joined[best] = True
if source[best] is None:
stations.append(best + 1) # 树里挂在虚拟源点下:这个城市建电站
else:
connections.append((source[best] + 1, best + 1))
for i in range(n):
cost = wire_cost(best, i) # 新点并入后用它刷新其余城市的最近距离
if not joined[i] and cost < dist[i]:
dist[i] = cost
source[i] = best
total = sum(C[s - 1] for s in stations) + sum(wire_cost(a - 1, b - 1) for a, b in connections)
out = [str(total), str(len(stations)), " ".join(map(str, stations)), str(len(connections))]
out += [f"{a} {b}" for a, b in connections]
print("\n".join(out))
if __name__ == "__main__":
solve()复杂度
时间
总结
- 本题全部内容浓缩在一个建模:加虚拟源点
,「建电站」变成边 (权 ),「拉线」变成边 (权 曼哈顿距离),答案就是 个点的最小生成树。 - 读题时最危险的一处是拉线代价乘了曼哈顿距离;建模时最关键的一步是意识到「点上的建站决策」也能表达成一条边,从而消除暴力里
层的电站枚举。 - 稠密完全图上,
的 Prim 现场扫描优于「生成两百万条边再排序」的 Kruskal,时间和内存都更稳。 - 验证记录:官方样例
in1/in2两实现花费与官方一致(、 )且方案通过校验器; gen.py以tiny()、 small_c(电站都便宜)、big_k(拉线都贵)、dup_pos(坐标压到强制重复)、 normal五种模式生成 320 组小数据,main.cpp与main.py的输出全部通过自洽校验器(花费 = 电站代价之和 + 拉线代价之和、索引合法、无重复无自环、所有城市连通),且两者的最小花费均与brute.cpp的最优值一致,320/320 通过;另有、值域全上限随机的满规模数据一组,两实现花费一致且校验通过。因答案不唯一(电站顺序、并列最优解均可不同),正式判定使用校验器而非逐字节比对。