最小斯坦纳树
模拟赛遇到又爆零了。
正文
给定 \(G=(V,E)\) 和点集 \(S\),让你求一个连通块包含 \(S\) 内所有点且边权最小。
首先最后的连通块一定是一棵树,证明显然。
所以我们不妨钦定一个根,考虑dp。设 \(f[i,s]\) 代表以 \(i\) 为根,考虑 \(s\) 里的点形成一个连通块(虽然是连通块,但是因为保证了最小所以是棵树)。
通过两种方法转移:
-
假设根的度数为 \(1\),那么可以从别的根转移过来 \(f[i,s]=\min f[j,s]+w(j,i)]\)。
-
考虑合并 \(i\) 的两的部分,\(f[i,s]=\min f[i,s]+f[i,s-t]\)。
对于第二种操作我们枚举子集,所以要求我们从小到大枚举。
对于第一种操作很像最短路的松弛操作,所以直接跑最短路。
时间复杂度 \(O(3^{\mid S\mid}\mid V\mid^2+2^{\mid S\mid}\mid V\mid\mid E\mid)\)
卡常
一些卡常技巧可以让你跑 \(9e8\)。
首先让 dp 数组访问连续,也就是第一维是 \(s\),第二维是 \(i\),其次用 spfa 可以让平均复杂度降低。
Code
#include<bits/stdc++.h>
#define pii std::pair<int,int>
namespace _name{
const int maxn=200,mod=998244353,inf=0x3f3f3f3f;
template<typename T>
inline void read(T &x){
T flag=1;
char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')flag=-1;
for(x=0;isdigit(ch);ch=getchar())x=x*10+ch-'0';
x*=flag;
}
inline void cmax(int &a,int b){if(b>a)a=b;}
inline void cmin(int &a,int b){if(b<a)a=b;}
}using namespace _name;
int n,m,k,f[1<<14][200];
std::vector<pii>vec[maxn];
std::queue<int>q;
bool vis[maxn];
void spfa(int s){
for(int i=0;i<n;i++)if(f[s][i]!=inf)q.emplace(i),vis[i]=1;
while(!q.empty()){
int u=q.front();q.pop();vis[u]=0;
for(auto [v,w]:vec[u]){
if(f[s][u]+w<f[s][v]){
f[s][v]=f[s][u]+w;
if(!vis[v])q.emplace(v),vis[v]=1;
}
}
}
}
int main(){
read(n);read(m);read(k);
for(int i=1,u,v,w;i<=m;i++){
read(u);read(v);read(w);u--;v--;
vec[u].emplace_back(v,w);vec[v].emplace_back(u,w);
}
memset(f,0x3f,sizeof(f));
for(int i=0,x;i<k;i++){
read(x);x--;
f[1<<i][x]=0;
}
for(int s=1;s<(1<<k);s++){
for(int t=s&(s-1);t;t=(t-1)&s){
if(t>(s^t)){
for(int i=0;i<n;i++)cmin(f[s][i],f[t][i]+f[s^t][i]);
}
}
spfa(s);
}
int ans=inf;
for(int i=0;i<n;i++)ans=std::min(ans,f[(1<<k)-1][i]);
printf("%d\n",ans);
return 0;
}