1. 程式人生 > 實用技巧 >斯坦納樹

斯坦納樹

轉載註明來源:https://www.cnblogs.com/syc233/p/13650130.html


姑且當作狀壓DP的複習了。


斯坦納樹問題是組合優化問題,與最小生成樹相似,是最短網路的一種。最小生成樹是在給定的點集和邊中尋求最短網路使所有點連通。而最小斯坦納樹允許在給定點外增加額外的點,使生成的最短網路開銷最小。

用一道模板題來引入問題:

P6192 【模板】最小斯坦納樹

給定一個包含 \(n\) 個結點和 \(m\) 條帶權邊的無向連通圖 \(G=(V,E)\)

再給定包含 \(k\) 個結點的點集 \(S\),選出 \(G\) 的子圖 \(G′=(V′,E′)\),使得:

  1. \(S\subseteq V'\)
  2. \(G'\) 為連通圖;
  3. \(E'\) 中所有邊的權值和最小。

\(E'\) 中所有邊的權值和。

為了使邊權和最小, \(G'\) 一定是一棵樹,證明類比最小生成樹。

考慮狀壓DP,令 \(f(i,s)\) 表示以 \(i\) 為根的包含點集 \(s \subseteq S\) 的樹的最小邊權和。則有轉移:

  • \(i\) 的度數為 \(1\) ,則列舉與它有邊相連的點轉移, \(f(i,s)=\min_{j}f(j,s)+w(i,j)\)
  • \(i\) 的度數大於 \(1\) ,則劃分成幾個子樹求解, \(f(i,s)=\min_{t \subseteq s}f(i,t)+f(i,s-t)\)

第一種轉移可以每次跑最短路實現。

\(\text{Code}:\)

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <queue>
#include <vector>
#define maxn 105
#define maxm 505
#define Rint register int
#define INF 0x3f3f3f3f
using namespace std;
typedef long long lxl;

template <typename T>
inline void read(T &x)
{
	x=0;T f=1;char ch=getchar();
	while(ch<'0'||ch>'9') {if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9') {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
	x*=f;
}

struct edge
{
	int u,v,w,next;
}e[maxm<<1];

int head[maxn],k;

inline void add(int u,int v,int w)
{
	e[k]=(edge){u,v,w,head[u]};
	head[u]=k++;
}

int n,m,K;
int f[maxn][1<<11];
bool vis[maxn];

typedef pair<int,int> pii;
priority_queue<pii,vector<pii>,greater<pii> > q;

inline void Dijkstra(int S)
{
	memset(vis,0,sizeof(vis));
	while(!q.empty())
	{
		int u=q.top().second;q.pop();
		if(vis[u]) continue;
		vis[u]=true;
		for(int i=head[u];~i;i=e[i].next)
		{
			int v=e[i].v;
			if(f[v][S]>f[u][S]+e[i].w)
			{
				f[v][S]=f[u][S]+e[i].w;
				q.push(make_pair(f[v][S],v));
			}
		}
	}
}

int p[11];

int main()
{
	// freopen("P6192.in","r",stdin);
	read(n),read(m),read(K);
	memset(head,-1,sizeof(head));
	for(int i=1,u,v,w;i<=m;++i)
	{
		read(u),read(v),read(w);
		add(u,v,w);add(v,u,w);
	}
	memset(f,0x3f,sizeof(f));
	for(int i=1;i<=K;++i)
	{
		read(p[i]);
		f[p[i]][1<<(i-1)]=0;
	}
	for(int S=1;S<(1<<K);++S)
	{
		for(int i=1;i<=n;++i)
		{
			for(int S0=S&(S-1);S0;S0=(S0-1)&S&(S-1))
				f[i][S]=min(f[i][S],f[i][S0]+f[i][S^S0]);
			if(f[i][S]!=INF) q.push(make_pair(f[i][S],i));
		}
		Dijkstra(S);
	}
	printf("%d\n",f[p[1]][(1<<K)-1]);
	return 0;
}

練習

P4294 [WC2008]遊覽計劃

棋盤圖上求解最小斯坦納樹。由於這道題是點權,所以轉移方程有所不同:

\[\begin{aligned} &f(i,s)=\min f(j,s)+a_i\\ &f(i,s)=\min_{t \subseteq s} f(i,t)+f(i,s-t)-a_i \end{aligned} \]

另外這題需要輸出方案,所以對每一個狀態都要記錄前驅,略噁心。

\(\text{Code}:\)

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <queue>
#include <vector>
#define maxn 105
#define maxm 2005
#define Rint register int
#define INF 0x3f3f3f3f
using namespace std;
typedef long long lxl;

template <typename T>
inline void read(T &x)
{
	x=0;T f=1;char ch=getchar();
	while(ch<'0'||ch>'9') {if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9') {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
	x*=f;
}

struct edge
{
	int u,v,next;
}e[maxm<<1];

int head[maxn],k;

inline void add(int u,int v)
{
	e[k]=(edge){u,v,head[u]};
	head[u]=k++;
}

typedef pair<int,int> pii;
pii pre[maxn][1<<11];
int n,m,K,a[maxn],p[maxn];
int f[maxn][1<<11];
bool vis[maxn];

priority_queue<pii,vector<pii>,greater<pii> > q;

inline void Dijkstra(int S)
{
	memset(vis,0,sizeof(vis));
	while(!q.empty())
	{
		int u=q.top().second;q.pop();
		if(vis[u]) continue;
		vis[u]=true;
		for(int i=head[u];~i;i=e[i].next)
		{
			int v=e[i].v;
			if(f[v][S]>f[u][S]+a[v])
			{
				f[v][S]=f[u][S]+a[v];
				pre[v][S]=make_pair(u,S);
				q.push(make_pair(f[v][S],v));
			}
		}
	}
}

inline int pos(int x,int y) {return (x-1)*m+y;}

int ans[15][15];

inline void dfs(int u,int S)
{
	if(!pre[u][S].second) return;
	ans[(u-1)/m+1][(u-1)%m+1]=1;
	if(pre[u][S].first==u) dfs(u,S^pre[u][S].second);
	dfs(pre[u][S].first,pre[u][S].second);
}

int main()
{
	// freopen("P4294.in","r",stdin);
	read(n),read(m);
	memset(head,-1,sizeof(head));
	memset(f,0x3f,sizeof(f));
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
		{
			int u=pos(i,j);
			read(a[u]);
			if(!a[u])
			{
				p[++K]=u;
				f[p[K]][1<<(K-1)]=0;
			}
			if(i-1>=1)
			{
				add(u,pos(i-1,j));
				add(pos(i-1,j),u);
			}
			if(j-1>=1)
			{
				add(u,pos(i,j-1));
				add(pos(i,j-1),u);
			}
		}
	for(int S=1;S<(1<<K);++S)
	{
		for(int i=1;i<=n*m;++i)
		{
			for(int S0=S&(S-1);S0;S0=(S0-1)&S&(S-1))
				if(f[i][S]>f[i][S0]+f[i][S^S0]-a[i])
				{
					f[i][S]=f[i][S0]+f[i][S^S0]-a[i];
					pre[i][S]=make_pair(i,S0);
				}
			if(f[i][S]!=INF) q.push(make_pair(f[i][S],i));
		}
		Dijkstra(S);
	}
	printf("%d\n",f[p[1]][(1<<K)-1]);
	dfs(p[1],(1<<K)-1);
	for(int i=1;i<=n;++i,puts(""))
		for(int j=1;j<=m;++j)
		{
			int u=pos(i,j);
			if(!a[u]) putchar('x');
			else if(ans[i][j]) putchar('o');
			else putchar('_');
		}
	return 0;
}

P3264 [JLOI2015]管道連線

不同的是這道題的關鍵點分為幾類,求的是使得任意同類關鍵點都連通的最小花費。

先求一遍最小斯坦納樹,得到 \(f\) 陣列。

\(g(s)\) 表示令點類 \(s\) 連通的最小花費, \(cnl_i\) 表示類別為 \(i\) 的點集。則有轉移:

  • 直接讓點類 \(s\) 中的每一類的所有點都連通,則 \(g(s)=\min_{i=1}^n f(i,\bigcup_{k \in s}cnl_k)\)
  • \(s\) 分裂成兩個集合,則 \(g(s)=\min_{t \subseteq s}g(t)+g(s-t)\)

這道題不知道為什麼用Dijkstra會比SPFA慢。

\(\text{Code}:\)

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <queue>
#define maxn 1005
#define maxm 3005
#define Rint register int
#define INF 0x3f3f3f3f
using namespace std;
typedef long long lxl;

template <typename T>
inline void read(T &x)
{
	x=0;T f=1;char ch=getchar();
	while(ch<'0'||ch>'9') {if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9') {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
	x*=f;
}

struct edge
{
	int u,v,w,next;
}e[maxm<<1];

int head[maxn],k;

inline void add(int u,int v,int w)
{
	e[k]=(edge){u,v,w,head[u]};
	head[u]=k++;
}

int n,m,K,p[11];
int f[maxn][1<<11],g[1<<11],cnl[11];
bool vis[maxn];

typedef pair<int,int> pii;
priority_queue<pii,vector<pii>,greater<pii> > q;

inline void Dijkstra(int S)
{
	memset(vis,0,sizeof(vis));
	while(!q.empty())
	{
		int u=q.top().second;q.pop();
		if(vis[u]) continue;
		vis[u]=true;
		for(int i=head[u];~i;i=e[i].next)
		{
			int v=e[i].v;
			if(f[v][S]>f[u][S]+e[i].w)
			{
				f[v][S]=f[u][S]+e[i].w;
				q.push(make_pair(f[v][S],v));
			}
		}
	}
}

int main()
{
	// freopen("P3264.in","r",stdin);
	read(n),read(m),read(K);
	memset(head,-1,sizeof(head));
	for(int i=1,u,v,w;i<=m;++i)
	{
		read(u),read(v),read(w);
		add(u,v,w);
		add(v,u,w);
	}
	memset(f,0x3f,sizeof(f));
	for(int i=1,c;i<=K;++i)
	{
		read(c);read(p[i]);
		f[p[i]][1<<(i-1)]=0;
		cnl[c]|=1<<(i-1);
	}
	for(int S=1;S<(1<<K);++S)
	{
		for(int i=1;i<=n;++i)
		{
			for(int S0=(S-1)&S;S0;S0=(S0-1)&(S-1)&S)
				f[i][S]=min(f[i][S],f[i][S0]+f[i][S^S0]);
			if(f[i][S]!=INF) q.push(make_pair(f[i][S],i));
		}
		Dijkstra(S);
	}
	memset(g,0x3f,sizeof(g));
	g[0]=0;
	for(int S=1;S<(1<<K);++S)
	{
		int S1=0;
		for(int i=0;i<K;++i)
			if((S>>i)&1) S1|=cnl[i+1];
		for(int i=1;i<=n;++i)
			g[S]=min(g[S],f[i][S1]);
		for(int S0=(S-1)&S;S0;S0=(S0-1)&(S-1)&S)
			g[S]=min(g[S],g[S0]+g[S^S0]);
	}
	printf("%d\n",g[(1<<K)-1]);
	return 0;
}