废话
做题时偶然遇到就学了一下,并不是什么很难的高科技。
正题
最小斯坦纳树和最小生成树类似,不过为了达到最小开销,最小斯坦纳树允许加一些额外点。
例题:有一张图,有 k k k 个关键点,选出一些边,使这 k k k 个点形成连通块,求边权和的最小值。
算法流程:
不难发现,这样的连通块一定是棵树,成环的话随便去掉环上一条边都可以使答案更小。
考虑状压dp,设 f i , S f_{i,S} fi,S 表示以 i i i 为根的子树,子树内包含 S S S 这些关键点,其中 S S S 在二进制下第 j j j 位为 1 1 1 表示包含第 j j j 个关键点。
转移分两类:
- 考虑将 i i i 的子树相互组合成新的子树,即 f i , S = min S ′ ∈ S { f i , S ′ + f i , S ⊕ S ′ } f_{i,S}=\min_{S'\in S}\{f_{i,S'}+f_{i,S\oplus S'}\} fi,S=minS′∈S{fi,S′+fi,S⊕S′},其中 ⊕ \oplus ⊕ 是异或,即取 S ′ S' S′ 的补集。
- 第二类是沿着边进行转移,即 f u , S = min ( u , v ) ∈ E { f v , S + w ( u , v ) } f_{u,S}=\min_{(u,v)\in E}\{f_{v,S}+w(u,v)\} fu,S=min(u,v)∈E{fv,S+w(u,v)}。
转移的顺序也很好确定,即从小到大枚举 S S S,每次先进行第一类转移,然后再进行第二类转移。
仔细观察第二类转移,本质上是if(f[v][S]+w[v][u]<f[u][S])f[u][S]=f[v][S]+w[v][u];
,跟最短路的转移是一样的,所以可以用 SPFA 或 Dijkstra 来优化。
然后就没了,十分简单。
代码如下:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 110
int n,m,k;
struct edge{int y,z,next;}e[2010];
int first[maxn],len=0;
void buildroad(int x,int y,int z){e[++len]=(edge){y,z,first[x]};first[x]=len;}
int f[maxn][(1<<10)+1];
int q[maxn],st,ed;bool v[maxn];
int main()
{
scanf("%d %d %d",&n,&m,&k);
for(int i=1,x,y,z;i<=m;i++){
scanf("%d %d %d",&x,&y,&z);
buildroad(x,y,z);buildroad(y,x,z);
}
memset(f,63,sizeof(f));
for(int i=1,x;i<=k;i++)
scanf("%d",&x),f[x][1<<(i-1)]=0;
for(int S=1;S<(1<<k);S++){
for(int i=1;i<=n;i++)
for(int S_=S;S_;S_=(S_-1)&S)
f[i][S]=min(f[i][S],f[i][S_]+f[i][S^S_]);
st=1,ed=1;
for(int i=1;i<=n;i++)
if(f[i][S]<2e8)q[ed++]=i,v[i]=true;
while(st!=ed){
int x=q[st++];st=st>n+1?1:st;v[x]=false;
for(int i=first[x];i;i=e[i].next){
int y=e[i].y;
if(f[y][S]>f[x][S]+e[i].z){
f[y][S]=f[x][S]+e[i].z;
if(!v[y])v[q[ed++]=y]=true,ed=ed>n+1?1:ed;
}
}
}
}
int ans=2e9;
for(int i=1;i<=n;i++)
ans=min(ans,f[i][(1<<k)-1]);
printf("%d",ans);
}
进阶题:有一张图,有一些关键点,每个关键点有一个频率,你要选出一些边,使相同频率的关键点成为连通块。
题解
问题不同在于,两两连通块之间可能共用一些边,从而使开销更小。
假如有两个连通块共用了一些边,那么他们的斯坦纳树就是联通的,并且依然不会出现环,也就是成为了一棵更大的斯坦纳树。
于是可以设 g [ S ] g[S] g[S] 表示 S S S 内的关键点都在一棵斯坦纳树内的最小开销,可以先将所有关键点跑一次斯坦纳树,这样就可以得到 g g g。
然后再dp一下, g [ S ] g[S] g[S] 的定义就变成了 S S S 内的关键点形成斯坦纳森林的最小开销。 g [ 2 k − 1 ] g[2^k-1] g[2k−1] 就是答案。
代码如下:
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
#define maxn 1010
int n,m,k;
struct edge{int y,z,next;}e[6010];
int first[maxn],len=0;
void buildroad(int x,int y,int z){e[++len]=(edge){y,z,first[x]};first[x]=len;}
vector<int> key[11];
int f[maxn][(1<<10)+1];
int q[maxn],st,ed;bool v[maxn];
int steiner(){
for(int S=1;S<(1<<k);S++){
for(int i=1;i<=n;i++)
for(int S_=S;S_;S_=(S_-1)&S)
f[i][S]=min(f[i][S],f[i][S_]+f[i][S^S_]);
st=ed=1;
for(int i=1;i<=n;i++)
if(f[i][S]<1e8)q[ed++]=i,v[i]=true;
while(st!=ed){
int x=q[st++];st=st>n+1?1:st;v[x]=false;
for(int i=first[x];i;i=e[i].next){
int y=e[i].y;
if(f[y][S]>f[x][S]+e[i].z){
f[y][S]=f[x][S]+e[i].z;
if(!v[y])v[q[ed++]=y]=true,ed=ed>n+1?1:ed;
}
}
}
}
}
int g[(1<<10)+1];
int main()
{
scanf("%d %d %d",&n,&m,&k);
for(int i=1,x,y,z;i<=m;i++){
scanf("%d %d %d",&x,&y,&z);
buildroad(x,y,z);buildroad(y,x,z);
}
memset(f,63,sizeof(f));
for(int i=1,x,y;i<=k;i++){
scanf("%d %d",&x,&y);
f[y][1<<i-1]=0;
key[x-1].push_back(1<<(i-1));
}
steiner();
for(int i=0;i<(1<<k);i++){
int S=0;
for(int j=0;j<k;j++)
if(i>>j&1)for(int p:key[j])S|=p;
g[i]=1e9;for(int j=1;j<=n;j++)
g[i]=min(g[i],f[j][S]);
}
for(int i=0;i<(1<<k);i++)
for(int j=i;j;j=(j-1)&i)
g[i]=min(g[i],g[j]+g[i^j]);
printf("%d",g[(1<<k)-1]);
}