斯坦纳树小结

废话

做题时偶然遇到就学了一下,并不是什么很难的高科技。

正题

最小斯坦纳树和最小生成树类似,不过为了达到最小开销,最小斯坦纳树允许加一些额外点。

例题:有一张图,有 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=minSS{fi,S+fi,SS},其中 ⊕ \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[2k1] 就是答案。

代码如下:

#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]);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值