题意
给定一张 n n n 个点 m m m 条边的无向图,边有正边权。指定 k k k 个点,求这 k k k 点两两可达的情况下选择的边边权和的最小值。
解法
边权非负,显然最终选择的边组成一棵树,否则去掉环上一条边后答案更优且不影响连通性。于是本题转化为最小斯坦纳树的模板题,纯粹的模板题在这里。特别地,MST 则可以认为是将所有 n n n 个点连通的最小斯坦纳树。
可以发现 k k k 很小,可以容得下带 2 k 2^k 2k 的时空复杂度。考虑使用状态压缩 dp,设 f i , j f_{i,j} fi,j 表示以 i i i 为根, k k k 个点的包含信息为 j j j( j j j 二进制下第 x x x 位为 1 1 1 则 k k k 个点中第 x x x 个点被包含)时的答案。初始皆为 + ∞ +\infty +∞,对于 k k k 中的每个点 u u u, f u , 2 u − 1 ← 0 f_{u,2^{u-1}}\gets0 fu,2u−1←0,即连通自己不需要连其他边。
假设当前 k k k 个点中包含的点集为 S S S,对于第 i i i 个点,分两种情况转移:
- 如果 i i i 的度数大于 1 1 1,考虑从当前状态的子集和补集进行转移,即对于一个被 S S S 包含的 T T T, f i , S ← min ( f i , S , f i , T + f i , S \ T ) f_{i,S}\gets\min(f_{i,S},f_{i,T}+f_{i,S\backslash T}) fi,S←min(fi,S,fi,T+fi,S\T)。
- 如果 i i i 的度数等于 1 1 1,考虑从与 i i i 相邻的点 j j j 转移, f i , S ← min ( f i , S , f j , S + w ( i , j ) ) f_{i,S}\gets\min(f_{i,S},f_{j,S}+w(i,j)) fi,S←min(fi,S,fj,S+w(i,j)),其中 w ( i , j ) w(i,j) w(i,j) 表示 ( i , j ) (i,j) (i,j) 这条边的边权。
前者直接循环计算,后者可以用类似于最短路的松弛操作计算。
实现
对于第一种情况,因为我们按照二进制思想压缩状态,所以对于状态 S S S,其最大的子集 T T T 为 S and ( S − 1 ) S\operatorname{and}(S-1) Sand(S−1),其中 and \operatorname{and} and 表示按位与;在 T T T 之后的下一个 S S S 的子集即为 ( T − 1 ) and S (T-1)\operatorname{and}S (T−1)andS; T T T 在 S S S 中的补集即为 S xor T S\operatorname{xor}T SxorT,其中 xor \operatorname{xor} xor 表示按位异或。
对于每一个枚举的状态 S S S,如果在第一种情况的转移完成后, f u , S f_{u,S} fu,S 有值,则 u u u 就可以作为第二种情况的一个起点。每一轮都以这些有值的点为可能的起点跑最短路即可。
最后在 k k k 个点中随意挑一个点 u u u,答案即为 f u , 2 k − 1 f_{u,2^k-1} fu,2k−1。时间复杂度 O ( 3 k n + m log n × 2 k ) O(3^kn+m\log n\times2^k) O(3kn+mlogn×2k)。
代码
#include<bits/stdc++.h>
#define mk make_pair
#define ll long long
using namespace std;
const int maxn = 1e5 + 5,maxk = 5;
int n,m,k,im[maxn];
ll f[maxn][(1 << maxk) + 5];
vector<pair<int,ll> > mp[maxn];
void addEdge(int u,int v,ll w) {
mp[u].push_back(mk(v,w));
}
priority_queue<pair<ll,int> > q;
bool vis[maxn];
void Dijkstra(int now) {
memset(vis,false,sizeof(vis));
while (!q.empty()) {
int u = q.top().second; q.pop();
if (vis[u]) continue;
vis[u] = true;
for (auto V : mp[u]) {
int v = V.first; ll w = V.second;
if (f[v][now] > f[u][now] + w) {
f[v][now] = f[u][now] + w;
if (!vis[v])
q.push(mk(-f[v][now],v));
}
}
}
}
int main() {
scanf("%d%d%d",&n,&k,&m); ll w;
for (int i = 1;i <= n;i ++)
for (int s = 0;s <= (1 << k) - 1;s ++)
f[i][s] = 1e18;
for (int i = 1;i <= k;i ++) {
scanf("%d",&im[i]);
f[im[i]][1 << (i - 1)] = 0;
}
for (int i = 1,u,v;i <= m;i ++) {
scanf("%d%d%lld",&u,&v,&w);
addEdge(u,v,w); addEdge(v,u,w);
}
for (int s = 0;s <= (1 << k) - 1;s ++) {
for (int i = 1;i <= n;i ++) {
for (int t = s & (s - 1);t;t = (t - 1) & s)
f[i][s] = min(f[i][s],f[i][t] + f[i][s ^ t]);
if (f[i][s] < 1e18) q.push(mk(-f[i][s],i));
}
Dijkstra(s);
}
printf("%lld",f[im[1]][(1 << k) - 1]);
return 0;
}