最小斯坦纳树
给定一张无相连通图,每条边有权值,有 \(k\) 个关键点,要求选择权值和最小的边使得关键点连通,求权值和。
类似最小生成树,但是限定了关键点就只能用指数级的复杂度解决,这里考虑类似状压 DP 的方法。
首先最终答案显然是一个树。
所以我们设 \(f_{i,S}\) 表示以 \(i\) 为根的树,包含了关键点中 \(S\) 的这些点,\(S\) 是一个 \(01\) 串。
考虑怎么转移,分 \(i\) 的度数为 \(1\),和不为 \(1\) 两种情况。
为 \(1\) 则:
\[f_{i,S}+w(i,j)\rightarrow f_{j,S} \]不为 \(1\) 则 \(i\) 有两个子树,把两个子树合并即可:
\[f_{i,S}+f_{i,T}\rightarrow f_{i,S+T} \]要注意在这里我们不用关心 \(i,j\) 是否是关键点而更新后面的 \(S\),我们只需在最开始把关键点 \(i\) 赋为 \(f_{i,\{i\}}=0\) 即可,然后后面的转移相当于把这些关键点合并。
由于 \(S\) 肯定是逐渐变大的,所以考虑在最外层枚举 \(S\),用之前的状态更新当前的状态,这里是 \(O(2^k)\)。
考虑怎么实现,对于上面一条转移,注意我们不是只做一条边,可能是很多条边连起来,那么就做最短路,用优先队列 \(Dijkstra\) 可以做到 \(O(n\log m)\),具体地就是一开始把所有点都推进队列里,总的就是 \(O(2^k n\log m)\)。
对于下面这条转移,就是一个枚举子集,而枚举子集可以做到 \(3^k\),具体来说是这样(\(j\) 是 \(i\) 的子集):
for(int i=0;i<(1<<k);i++)
for(int j=i;j;j=((j-1)&i))
do something
所以这一部分的总复杂度是 \(O(3^k n)\)。
注意一件事情,我们需要先做度数不为 \(1\) 的转移,再做为 \(1\) 的转移,相当于是先得到更大的 \(S\),然后才能把这个 \(S\) 扩展到更多的点;否则若更大的 \(S\) 还没合并,不是最优的,那么先扩展到更多的点也不是最优的。
最后的复杂度就是 \(O(3^kn+2^k n\log m)\)。
AC 代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,K;
const int N=105,M=505;
vector<pair<int,int>> g[N];
int f[N][1<<10],bz[N];
struct arr{
int val,num;
arr(int _val,int _num){
val=_val,num=_num;
}
};
int operator<(arr x,arr y){
return x.val>y.val;
}
int id[N];
int main(){
// freopen("P.in","r",stdin);
ios::sync_with_stdio(0);
cin>>n>>m>>K;
for(int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
g[u].push_back(make_pair(v,w));
g[v].push_back(make_pair(u,w));
}
int X;
memset(f,0x3f,sizeof f);
for(int i=1;i<=K;i++){
int x; cin>>x;
X=x; id[x]=i;
f[x][1<<i-1]=0;
}
for(int i=1;i<(1<<K);i++){
for(int j=i;j;j=((j-1)&i)){
for(int k=1;k<=n;k++)
f[k][i]=min(f[k][i],f[k][j]+f[k][i-j]);
}
memset(bz,0,sizeof bz);
priority_queue<arr> q;
for(int k=1;k<=n;k++)q.push(arr(f[k][i],k));
while(q.size()){
int u;
while(q.size()&&bz[u=q.top().num])q.pop();
if(q.empty())break;
bz[u]=1; q.pop();
for(auto _v:g[u]){
int v=_v.first,co=_v.second;
if(f[v][i]>f[u][i]+co){
f[v][i]=f[u][i]+co;
q.push(arr(f[v][i],v));
}
}
}
}
cout<<f[X][(1<<K)-1];
}