最小斯坦纳树学习笔记 & AT_abc364_g [ABC364G] Last Major City 题解
Problem
给定一个无向带权图和 \(k-1\) 个关键点(编号 \(1\) 至 \(k-1\)),求对于每个 \(i\in[k,n]\),将 \(i\) 加入关键点后使这 \(k\) 个关键点全部互相连通的最小边权和。
Solution
题目要我们求的即是最小斯坦纳树,模板题在这里,当然这篇题解也会讲解最小斯坦纳树。
最小斯坦纳树就是用来求解“使图上若干个关键点全部相互联通的最小边权和”一类问题的算法,虽然看起来很有用,但可惜是一个 \(\operatorname{NP-hard}\) 问题,一般只能求解 \(k\le10\) 规模的问题,所以实际应用范围不广。
下面给一些最小斯坦纳树的性质。
求出的最小斯坦纳树一定是一棵树。
证明:若该子图上有环,考虑删去环上任意一点,连通性不变且答案更优。
为了方便叙述,下面的指的全集对应的集合大小是本题中输入的 \(k\) 减去 \(1\) 的值。
回到原题,考虑状压 DP,记 \(dp_{i,j}\) 为在以 \(i\) 为根的树中,各关键点与 \(i\) 的联通状态为 \(j\) 的方案数(这里的 \(i\) 不用是关键点,原因是可以帮助我们辅助转移),转移有两种:
- 将 \(j\) 划分为两个子集并合并,有:
- 找一个与 \(i\) 相邻的点 \(u\),将 \(i\to u\) 加入原本 \(u\) 所在的树,并把 \(i\) 换成新的根,这时贡献要加上 \(\operatorname{dis}(i,u)\)。为了方便转移,假设加入后的联通集合不变,因为即使没有把 \(i\) 对 \(j\) 的影响算进去,后面也都会转移到新的正确的集合里面去。这时有:
观察到第二种转移很像最短路的三角关系,这启示我们可以用第一种转移更新完后的 \(dp\) 值跑最短路更新其他 \(dp\) 值。
关于答案,如果你在做模板题(求全部关键点联通的最小权值和),只需输出任意一个 \(dp_{i,j}\) 即可,其中 \(i\) 是任意关键点的编号,\(j\) 为全集。但是由于你正在做的是一场 ABC 的 G 题,所以我们还要结合 \(dp\) 的定义来看。对于 \(i\) 的答案,由于我们在计算 \(dp_{i,j}\) 时钦定了 \(i\) 一定在树中,且由于 \(i\) 不是确定的关键点,所以不用担心第二维的算漏(重),故直接输出 \(dp_{i,j}\) 即可,其中 \(j\) 也是全集。
关于最短路算法,一般来说在稀疏图或者 \(n,m\) 都较小的情况下 SPFA 的表现是优于 Dijkstra 的,但有的毒瘤题(如P4784 [BalticOI 2016 Day2] 城市)就会专门卡 SPFA 恶心人。但是本题无所谓,所以我的代码中还是使用了 SPFA。
然后是时间复杂度部分,枚举子集是 \(\Theta(3^n)\) 的,所以第一种转移是 \(\Theta(n\times3^k)\),而第二种转移如果用 SPFA 跑最短路是 \(\Theta(2^k\times nm)\) 的,使用 Dijkstra 可以降到 \(\Theta(2^k\times m\log m)\),总复杂度就是 \(\Theta(n\times3^k+2^k\times nm)\) 或 \(\Theta(n\times3^k+2^k\times m\log m)\)。
Code
这里面有两个优化,参考了模板题的这篇题解。
记得 long long
。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 4050;
int n, m, p;
int t[N];
int vis[N];
int f[1050][N];
struct node {
int v, w;
};
vector<node>e[N];
queue<int>q;
void spfa(int st) {
while (!q.empty()) {
int x = q.front();
q.pop();
vis[x] = 0;
for (auto i : e[x]) {
int v = i.v, w = i.w;
if (f[st][v] > f[st][x] + w) {
f[st][v] = f[st][x] + w;
if (!vis[v]) {
vis[v] = 1;
q.push(v);
}
}
}
}
}
signed main() {
memset(f, 0x3f, sizeof f);
cin >> n >> m >> p;
--p;
int n_ = (1 << p) - 1;
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
e[u].push_back({v, w});
e[v].push_back({u, w});
}
for (int i = 1; i <= p; i++) {
f[1 << (i - 1)][i] = 0;
}
for (int i = 1; i <= n_; i++) {
for (int j = 1; j <= n; j++) {
for (int k = i & (i - 1); k; k = i & (k - 1)) {
if ((i ^ k) > k) {
break;
}
f[i][j] = min(f[i][j], f[k][j] + f[i ^ k][j]);
}
if (f[i][j] < 0x3f3f3f3f3f3f3f3f) {
vis[j] = 1;
q.push(j);
}
}
spfa(i);
}
for (int i = p + 1; i <= n; i++) {
cout << f[n_][i] << '\n';
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!