Loading

斯坦纳树小记

斯坦纳树问题

平面上有一些点,其中一些是关键点。给出一些点之间的线段,选择一些线段连通这些关键点并且线段总长度最小。

最小斯坦纳树——在图论中的运用

一张无向连通图,选择一个部分结点的生成树,结点包含点集 \(S\),求生成树的最小边权和。

\(n\le 100,\space m\le 1000,\space |S|\le 10\)

解决

考虑状压 dp。

\(f[S,u]\) 表示选出了包含的关键点集合为 \(S\) 且当前树根为 \(u\) 的最小生成树边权和。

转移比较容易:

  • 合并两棵树:\(f[S,u]+f[T,u]\to f[S\cup T,u]\)

  • 走向新的根:\(f[S,u]+w(u,v) \to f[S,v]\)

初始化:对于第 \(i\) 个关键点 \(a_i\),有 \(f[\{i\},a_i]=0\)

注意第二种转移没有明确状态,需要用最短路。

使用 dijkstra,时间复杂度 \(O(n3^{|S|}+|S|m\log m)\)

点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned ll
#define mkp make_pair
#define fi first
#define se second
#define pir pair<ll,ll>
#define pb push_back
using namespace std;
const ll maxn=110;
ll n,m,k,u,v,w,head[maxn],tot,f[1<<10][maxn],ans=1e17;
struct edge{
	ll v,w,nxt;
}e[maxn*10];
void ins(ll u,ll v,ll w){
	e[++tot]=(edge){v,w,head[u]};
	head[u]=tot;
}
priority_queue<pir>q;
int main(){
	scanf("%lld%lld%lld",&n,&m,&k);
	for(ll i=1;i<=m;i++){
		scanf("%lld%lld%lld",&u,&v,&w);
		ins(u,v,w), ins(v,u,w);
	}
	memset(f,0x3f,sizeof f);
	for(ll i=1,x;i<=k;i++){
		scanf("%lld",&x);
		f[1<<i-1][x]=0;
	}
	for(ll S=1;S<(1<<k);S++){
		for(ll i=1;i<=n;i++){
			for(ll T=S;T;T=(T-1)&S)
				f[S][i]=min(f[S][i],f[T][i]+f[S^T][i]);
			q.push(mkp(-f[S][i],i));
		}
		while(!q.empty()){
			pir t=q.top(); ll d=-t.fi, u=t.se;
			q.pop();
			if(f[S][u]!=d) continue;
			for(ll i=head[u];i;i=e[i].nxt){
				ll v=e[i].v, w=e[i].w;
				if(f[S][v]>d+w){
					f[S][v]=d+w;
					q.push(mkp(-d-w,v));
				}
			}
		}
	}
	for(ll i=1;i<=n;i++) ans=min(ans,f[(1<<k)-1][i]);
	printf("%lld",ans);
	return 0;
}

例题

就是求包含景点的部分点生成树中的最小点权和。

考虑每次合并生成树的时候,减去当前根的权值,因为多算了一次。

点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned ll
#define mkp make_pair
#define fi first
#define se second
#define pir pair<ll,ll>
#define pb push_back
using namespace std;
const ll maxn=110, mod=998244353;
ll n,m,a[maxn][maxn],f[1<<10][maxn],pre[1<<10][maxn][2],head[maxn],tot,val[maxn];
char ans[maxn][maxn];
struct edge{
	ll v,w,nxt;
}e[maxn*10];
void ins(ll u,ll v,ll w){
	e[++tot]=(edge){v,w,head[u]};
	head[u]=tot;
}
priority_queue<pir>q;
void wr(ll S,ll u,ll z){
	ll x=(u-1)/m+1, y=u-(x-1)*m;
	if(ans[x][y]!='x') ans[x][y]='o';
	if(pre[S][u][0]==1) wr(S,pre[S][u][1],1);
	else if(pre[S][u][0]==2) wr(pre[S][u][1],u,1), wr(S^pre[S][u][1],u,0);
}
int main(){
	scanf("%lld%lld",&n,&m);
	memset(f,0x3f,sizeof f); ll c=0;
	for(ll i=1;i<=n;i++){
		for(ll j=1;j<=m;j++){
			ll x; scanf("%lld",&x);
			ll u=(i-1)*m+j;
			if(x==0) f[1<<c][u]=x, ++c, ans[i][j]='x';
			else ans[i][j]='_';
			if(i>1) ins((i-2)*m+j,(i-1)*m+j,x);
			if(i<n) ins(i*m+j,(i-1)*m+j,x);
			if(j>1) ins((i-1)*m+j-1,(i-1)*m+j,x);
			if(j<m) ins((i-1)*m+j+1,(i-1)*m+j,x);
			a[i][j]=val[u]=x;
		}
	}
	if(!c){
		puts("0");
		for(ll i=1;i<=n;i++){
			for(ll j=1;j<=m;j++) putchar('_'); puts("");
		} return 0;
	}
	for(ll S=1;S<(1<<c);S++){
		for(ll i=1;i<=n*m;i++)
			for(ll T=S;T;T=(T-1)&S){
				if(f[T][i]+f[S^T][i]-val[i]<f[S][i]){
					f[S][i]=f[T][i]+f[S^T][i]-val[i];
					pre[S][i][0]=2, pre[S][i][1]=T;
				}
			}
		for(ll i=1;i<=n*m;i++)
			q.push(mkp(-f[S][i],i));
		while(!q.empty()){
			pir t=q.top(); q.pop();
			ll d=-t.fi, u=t.se;
			for(ll i=head[u];i;i=e[i].nxt){
				ll v=e[i].v, w=e[i].w;
				if(d+w<f[S][v]){
					f[S][v]=d+w, pre[S][v][0]=1, pre[S][v][1]=u;
					q.push(mkp(-d-w,v));
				}
			}
		}
	}
	ll res=1e17, p=0;
	for(ll i=1;i<=n*m;i++)
		if(f[(1<<c)-1][i]<res) res=f[(1<<c)-1][i], p=i;
	wr((1<<c)-1,p,1); printf("%lld\n",res);
	for(ll i=1;i<=n;i++){
		for(ll j=1;j<=m;j++)
			putchar(ans[i][j]); puts("");
	}
	return 0;
}

这里变成了多个关键点点集,要求每个点集连通。

考虑我们会选出若干个生成树,每个生成树包含几个关键点点集,只需求出对于一些关键点点集的生成树就好。其实就是先模板,最后再 dp 合并。

点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned ll
#define mkp make_pair
#define fi first
#define se second
#define pir pair<ll,ll>
#define pb push_back
using namespace std;
const ll maxn=1010;
ll n,m,k,u,v,w,head[maxn],tot,f[1<<10][maxn],res[1<<10],ans=1e17,msk[maxn],g[maxn],dp[1<<10];
struct edge{
	ll v,w,nxt;
}e[maxn*6];
void ins(ll u,ll v,ll w){
	e[++tot]=(edge){v,w,head[u]};
	head[u]=tot;
}
priority_queue<pir>q;
int main(){
	scanf("%lld%lld%lld",&n,&m,&k);
	for(ll i=1;i<=m;i++){
		scanf("%lld%lld%lld",&u,&v,&w);
		ins(u,v,w), ins(v,u,w);
	}
	memset(f,0x3f,sizeof f);
	for(ll i=1,x,y;i<=k;i++){
		scanf("%lld%lld",&x,&y);
		g[x]|=(1<<i-1);
		f[1<<i-1][y]=0;
	}
	for(ll S=1;S<(1<<k);S++){
		for(ll i=1;i<=n;i++){
			for(ll T=S;T;T=(T-1)&S)
				f[S][i]=min(f[S][i],f[T][i]+f[S^T][i]);
			q.push(mkp(-f[S][i],i));
		}
		while(!q.empty()){
			pir t=q.top(); ll d=-t.fi, u=t.se;
			q.pop();
			if(f[S][u]!=d) continue;
			for(ll i=head[u];i;i=e[i].nxt){
				ll v=e[i].v, w=e[i].w;
				if(f[S][v]>d+w){
					f[S][v]=d+w;
					q.push(mkp(-d-w,v));
				}
			}
		}
	}
	for(ll i=1;i<(1<<k);i++){
		res[i]=1e17;
		for(ll j=1;j<=n;j++)
			res[i]=min(res[i],f[i][j]);
	}
	memset(dp,0x3f,sizeof dp);
	dp[0]=0;
	for(ll i=1;i<(1<<10);i++){
		for(ll j=i;j;j=(j-1)&i){
			ll S=0;
			for(ll x=1;x<=10;x++)
				if(j&(1<<x-1)) S|=g[x];
			dp[i]=min(dp[i],dp[i^j]+res[S]);
		}
			
	}
	printf("%lld",dp[1023]);
	return 0;
}

题意:有 \(n\) 个点,其中 \(1...k\) 中每个点有一个人,\(n-k+1...n\) 中每个点有一个房子。有 \(m\) 条坏掉的无向边,每个边有修复费用。现在需要修复一些边,使得每个人都能走到一个房子且每个房子最多容纳一个人,求最小修复费用,或者判断无解。

\(1\le n\le 50,\space 1\le m\le 1000,\space 1\le k\le 5\)

和上面的差不多,考虑我们选出的是若干个生成树。

点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned ll
#define mkp make_pair
#define fi first
#define se second
#define pir pair<ll,ll>
#define pb push_back
using namespace std;
const ll maxn=1010;
ll t,n,m,k,u,v,w,head[maxn],tot,f[1<<10][55],res[1<<10],ans=1e17,msk[maxn],g[maxn],dp[1<<10];
struct edge{
	ll v,w,nxt;
}e[maxn*6];
void ins(ll u,ll v,ll w){
	e[++tot]=(edge){v,w,head[u]};
	head[u]=tot;
}
priority_queue<pir>q;
int main(){
	scanf("%lld",&t);
	while(t--){
		scanf("%lld%lld%lld",&n,&m,&k);
		tot=0;
		for(ll i=1;i<=n;i++) head[i]=0;
		for(ll i=1;i<=m;i++){
			scanf("%lld%lld%lld",&u,&v,&w);
			ins(u,v,w), ins(v,u,w);
		}
		memset(f,0x3f,sizeof f);
		for(ll i=1;i<=k;i++){
			f[1<<i-1][i]=0;
			f[1<<i+k-1][n-i+1]=0;
		}
		for(ll S=1;S<(1<<2*k);S++){
			for(ll i=1;i<=n;i++){
				for(ll T=S;T;T=(T-1)&S)
					f[S][i]=min(f[S][i],f[T][i]+f[S^T][i]);
				q.push(mkp(-f[S][i],i));
			}
			while(!q.empty()){
				pir t=q.top(); ll d=-t.fi, u=t.se;
				q.pop();
				if(f[S][u]!=d) continue;
				for(ll i=head[u];i;i=e[i].nxt){
					ll v=e[i].v, w=e[i].w;
					if(f[S][v]>d+w){
						f[S][v]=d+w;
						q.push(mkp(-d-w,v));
					}
				}
			}
		}
		for(ll i=1;i<(1<<2*k);i++){
			res[i]=1e17;
			for(ll j=1;j<=n;j++)
				res[i]=min(res[i],f[i][j]);
		}
		memset(dp,0x3f,sizeof dp);
		dp[0]=0;
		for(ll i=1;i<(1<<2*k);i++){
			if(__builtin_popcount(i&((1<<k)-1))!=
			__builtin_popcount(i>>k)) continue;
			for(ll j=i;j;j=(j-1)&i)
				dp[i]=min(dp[i],dp[i^j]+res[j]);
		}
		if(dp[(1<<2*k)-1]<1e16) printf("%lld\n",dp[(1<<2*k)-1]);
		else puts("No solution");
	}
	return 0;
}

把状压改成区间 dp 就行。

每个格子预处理出往上/下/左/右走到达的格子,这个可以记搜。

代码不想写。

posted @ 2024-03-08 22:19  Lgx_Q  阅读(17)  评论(0编辑  收藏  举报