斯坦纳树学习笔记

斯坦纳树

前置

  • 百度一下
  • 会用到的知识:状压DP,spfa(或者一些最短路算法),生成树基础知识。

问题引入:

  • 假如有nn个城市,计划修一些道路,每条路有一些花费(花费均为正),现在请你求出使得nn个城市连通的最小花费。

我们可以知道使这nn个城市连通所选的边尽量越少越好,那么显然我们至少需要n1n-1条边,那么则就是一个树,于是我们可以使用最小生成树算法(Prim或者Kruskal)轻松解决这个问题。

那么如果现在有一些中转点,可以让一些道路在这里中转,也就是这些点不一定用,但是用了有可能使得修路的花费更少,如下图:

eg

我们假如点号为1,3,4,51,3,4,5的点为城市,22号点为中转站。
如果我们仍然只用1,3,4,51,3,4,5号点,那么道路的花费则为9090
但是我们使用中转站22号点,那么代价大大减小,为1616
但是如果所有的点都选,如下图,也就不一定优秀了。
eg2
此时66号点也是中转站,1,3,4,51,3,4,5还是城市,22号点还是中转站,选择点1,3,4,5,61,3,4,5,6是最优的,为1111
所以这时,最小生成树就不能解决我们的问题了。


当一般所需要连通点集比较小(我们把必须要连通的点称作必须点),那么我们可以用一个动态规划(DP)来求的最优解。

点集比较小,大多数情况下可以状压,用二进制位的0/10/1来表示当前这个必须点是否已经连通。

暴力的想就是状压所有的点,就令f[S]f[S]表示当前连通集合状态为SS,然后2n2^n枚举集合与和n2n^2的枚举新加的点和连边转移,这样总的复杂度为n22nn^22^nn2n^2是不会满的),且空间为2n2^n,似乎复杂度不是很优秀。

我们继续观察,发现有很多不需要的状态(也就是没有必须点的状态),所以我们可以这样转化一下状态的描述,f[i][S]f[i][S]表示当前的连通块的根为ii,必须点的连通状态为SS,所以我们要先对必须点重新编一个号,假如kk个必须点,然后就从0k0sim k编号,这时,SS的状态只包含了必须点,那么就会去掉很多不必要的点,但是有些不必要的点可能还是会选,所以我们再加上一维ii,表示当前的根,这样就可以描述所有有效状态了。

下面我们来看转移,分为两种:

  1. 按照点为媒介进行连通块的合并,也就是如下图这样,假如1,2,3,41,2,3,4为必须点:
    eg
    f[1][1110]f[1][1110]状态
    eg2
    f[1][1001]f[1][1001]状态

合并的图如下图:
merge

这两个可以合并为f[1][1111]f[1][1111]状态,转移如下:
f[1][(1110)(1001)]=min(f[1][(1110)(1001)],f[1][1110]+f[1][1001])f[1][1111]=9+13=21 f[1][(1110)|(1001)]=min(f[1][(1110)|(1001)],f[1][1110]+f[1][1001]) \ f[1][1111]=9+13=21
如果有点权的话,转移会把根节点多算一次,所以减去,下面val[i]val[i]表示ii号点的点权,就为:
f[1][(1110)(1001)]=min(f[1][(1110)(1001)],f[1][1110]+f[1][1001]val[1]) f[1][(1110)|(1001)]=min(f[1][(1110)|(1001)],f[1][1110]+f[1][1001]-val[1])

  1. 按照边为媒介转移,也就是如下图这样,假如1,2,3,41,2,3,4为必须点:

eg

其实这是两个集合,分别为f[1][1001]f[1][1001]f[2][0110]f[2][0110],但是我们可以通过这个边121 ightarrow 2将它们连接起来,那么转移如下,我们将它转移为11为根的:

f[1][(1001)(0110)]=min(f[1][(1001)(0110)],f[1][1001]+f[2][0110]+side[1][2])side[1][2]=3 f[1][(1001)|(0110)]=min(f[1][(1001)|(0110)],f[1][1001]+f[2][0110]+side[1][2]) \ side[1][2]=3

所以这样就可以转移所有的状态了。


代码实现

对于第一种转移,我们枚举集合和子集还有根节点进行转移,复杂度为n3kn3^k,其中kk为必须点的个数(当k=nk=n时,我们就可以使用最小生成树算法)。

然后边的怎么办呢?总不能O(m)O(m)的枚举边(假设边有mm条),然后(2k)2(2^k)^2枚举边两边的情况吧,这样的复杂度为O(m(2k)2+n3k)O(m(2^k)^2+n3^k)mm最大会达到n×(n1)2frac{n imes (n-1)}{2}条,所以不能这样暴力转移。

那么我们可以想,对于图上的边权和最小,我们可以使用最短路之类的算法啊,这里介绍SPFA m SPFA

我们可以通过像跑分层最短路一样,确定一个状态xx,然后将所有的可以去更新答案的f[i][x]f[i][x]加入队列,然后开始进行SPFA m SPFA,每次枚举一条边和一个点,假如当前点为aa,枚举的边对面的点为vv,则可以更新的话就f[v][x(id[v])]=f[a][x]+side[a][v]f[v][x|(id[v])]=f[a][x]+side[a][v]id[v]id[v]vv的二进制编号,如果为不必须点,则为0,如果有点权的话就为f[v][x(id[v])]=f[a][x]+side[a][v]+val[v]f[v][x|(id[v])]=f[a][x]+side[a][v]+val[v],这样用一个集合加一条边和一个点的更新(松弛操作)方式,也就相当于完成了我们的第二种转移。

此时在一般的图上,SPFA m SPFA一次的均摊复杂度为O(n×t)O(n imes t),近似看作O(n)O(n),(tt一般小于33左右,但是特殊构造的图,如稠密图就可能比较大,但是不会超过mm(边数)),所以用边更新的复杂度为n2kn2^k,所以总复杂度就为O(n2k+n3k)O(n2^k+n3^k),大概能在1s1s跑过k10,n300,m10000kleq 10,nleq 300,mleq 10000的数据吧。

在所有的点权边权均为正数的情况下,则可以使用Dijistra+ m Dijistra+堆优化,可以将复杂度保证为O(nlogn2k)O(nlogn2^k),而不是SPFA m SPFAO(nt2k)O(ncdot t2^k)

下面给出我的模板题目的代码,【模板题in洛谷

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define RG register
using namespace std;
const int M=3e5+10;
const int S=1<<10|1,N=510;
const int inf=0x3f3f3f3f;
inline char nc(){
    static char buf[100000],*p1=buf,*p2=buf;
    return p1==p2&&(p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++;
}
void readInt(int &x){
	 x=0;RG char c=0;
	 while(c<'0'||c>'9')c=nc();
     while(c>='0'&&c<='9'){x=x*10+(c&15);c=nc();}
}//快读fread,读入请用文件读入
int f[N][S],id[N],sze,ans;
int que[M],p,q;bool vis[N],isimp[N];
struct ss{
	int to,last,w;
	ss(){}
	ss(int a,int b,int c):to(a),last(b),w(c){}
}g[M<<1];
int head[N],cnt;
void add(int a,int b,int c){
	g[++cnt]=ss(b,head[a],c);head[a]=cnt;
	g[++cnt]=ss(a,head[b],c);head[b]=cnt;
}
void init(){
	sze=0;ans=inf;
	memset(f,-1,sizeof(f));
}
int n,m,useful,val[N],ned[N];
void spfa(int x){
	for(;p<=q;p++){
		int a=que[p];
		vis[a]=0;
		for(RG int i=head[a];i;i=g[i].last){
			int v=g[i].to,y=(id[v]|x);
			if(f[v][y]==-1||f[v][y]>f[a][x]+g[i].w+val[v]){
				f[v][y]=f[a][x]+g[i].w+val[v];//媒介为边的更新
				if(y==x&&!vis[v]){
					vis[v]=1;que[++q]=v;
				}
			}
		}
	}
}
int staner(){
	init();
//	for(int i=1;i<=n;i++)if(ned[i])f[i][id[i]=(1<<sze)]=0,++sze;
	for(int i=1;i<=useful;i++)f[ned[i]][id[ned[i]]=(1<<(i-1))]=val[ned[i]],isimp[ned[i]]=1;//重新编号
	for(int i=1;i<=n;i++)if(!isimp[i])f[i][0]=val[i];//初始值为点权
	sze=useful;
	int up=(1<<sze);
	for(RG int x=1;x<up;++x){
		p=1;q=0;
		for(RG int i=1;i<=n;++i){
			if(id[i]&&(!(id[i]&x))) continue;
			for(RG int y=(x-1)&x;y;y=(y-1)&x){
				int xx=id[i]|y,yy=id[i]|(x-y);
				if(f[i][xx]!=-1&&f[i][yy]!=-1){
					if(f[i][x]==-1||f[i][xx]+f[i][yy]-val[i]<f[i][x]){
						f[i][x]=f[i][xx]+f[i][yy]-val[i];//媒介为点,更新
					}
				}
			}
			if(f[i][x]!=-1)que[++q]=i,vis[i]=1;//加入队列
		}
		spfa(x);//用spfa,边去松弛更新
	}
	--up;
	for(int i=1;i<=n;i++)if(f[i][up]!=-1&&f[i][up]<ans)ans=f[i][up];
	return ans;
}
int a,b,c;
int fa[N];
int find(int a){return fa[a]==a?a:fa[a]=find(fa[a]);}
struct edge{
	int u,v,w;
	edge(){}
	edge(int a,int b,int c):u(a),v(b),w(c){}
	void in(){readInt(u);readInt(v);readInt(w);}
	bool operator <(const edge &a)const{return w<a.w;}
}e[M];
int mst(){
	int tot=0,ans=0;
	sort(e+1,e+m+1);
	for(RG int i=1;i<=n;++i)fa[i]=i;
	for(RG int i=1;i<=m;++i){
		int a=find(e[i].u),b=find(e[i].v);
		if(a==b) continue;
		fa[a]=b;
		ans+=e[i].w;
		if(++tot==n-1) break;
	}
	return ans;
}
int main(){
	readInt(n);readInt(m);readInt(useful);
	for(int i=1;i<=n;i++)readInt(val[i]);
	for(int i=1;i<=useful;i++)readInt(ned[i]);
	if(useful>10){
		//最小生成树的部分分
		int sum=0;
		for(RG int i=1;i<=n;++i)sum+=val[i];
		for(RG int i=1;i<=m;++i)e[i].in();
		cout<<mst()+sum<<'
';
		return 0;
	}
	for(RG int i=1;i<=m;++i){
		readInt(a);readInt(b);readInt(c);
		add(a,b,c);
	}
	cout<<staner()<<'
';
	return 0;
}
原文地址:https://www.cnblogs.com/VictoryCzt/p/10053416.html