最小斯坦纳树学习笔记 & 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\) 不用是关键点,原因是可以帮助我们辅助转移),转移有两种:

  1. \(j\) 划分为两个子集并合并,有:

\[dp_{i,j}\gets\min_{k\subseteq j}dp_{i,k}+dp_{i,j/k} \]

  1. 找一个与 \(i\) 相邻的点 \(u\),将 \(i\to u\) 加入原本 \(u\) 所在的树,并把 \(i\) 换成新的根,这时贡献要加上 \(\operatorname{dis}(i,u)\)。为了方便转移,假设加入后的联通集合不变,因为即使没有把 \(i\)\(j\) 的影响算进去,后面也都会转移到新的正确的集合里面去。这时有:

\[dp_{i,j}\gets\min_{u与j相连}dp_{u,j}+\operatorname{dis}(i,u) \]

观察到第二种转移很像最短路的三角关系,这启示我们可以用第一种转移更新完后的 \(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;
}
posted @   Z3k7223  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示