Steiner树
Steiner树大概就是指在一张无向有权图中给定若干个关键点,求出一个边权和最小的边集,使得在仅保留这个边集中的边的情况下,任意两个关键点相互之间连通。
(当然如果是点权也没有问题,因为这个算法的扩展性很强?)
就以下面这题为例讲一下求Steiner树吧。
题目
这就是求Steiner树的模板题,不过写起来因为坐标二维和数字相互转化的问题比较麻烦。
然后求Steiner树大概是一个NP问题,我们只能设计一个状压来解决。
设(f_{i,S})表示以关键点(i)为根(或者说在包含关键点(i)的情况下),当前已经连通的关键点集合为(S)的最小代价。
转移分为两种:
(1.f_{i,S}=minlimits_{tsubset s}(f_{i,t}+f_{i,ssetminus t}-a_i))
转移的意义非常显然就不说了,(-a_i)是因为(i)这个点会被重复算两次。
如果是边权代价的话可以把(-a_i)去掉。
(2.f_{i,S}=minlimits_{exists (i,j)in E}(f_{j,S}+a_i))
这个类似于Bellman-Ford中的松弛操作。
如果是边权代价的话把(a_i)换成(w(e_{i,j}))。
然而转移顺序比较麻烦,我们需要制定一个符合拓扑序的转移顺序。
一种可行的方案是先枚举集合(S),再枚举根(i),然后枚举子集完成第一类转移,再用队列优化的Bellman-Ford完成第二类转移。
这样的复杂度应该是(O(|V|3^{|S|}+|V||E|2^{|S|}))。
#include<bits/stdc++.h>
#define pi pair<int,int>
#define fi first
#define se second
using namespace std;
const int inf=0x3f3f3f3f,dx[4]={0,0,1,-1},dy[4]={1,-1,0,0};
int n,m,k,root,f[101][1024],a[101],ans[101],vis[101];
pi pre[101][1024];
queue<int>q;
int read(){int x;scanf("%d",&x);return x;}
int trans(pi a){return a.fi*m+a.se;}
pi trans(int a){return pi(a/m,a%m);}
int in(pi a){return a.fi>=0&&a.se>=0&&a.fi<n&&a.se<m;}
pi operator+(pi a,pi b){return pi(a.fi+b.fi,a.se+b.se);}
void spfa(int s)
{
while(!q.empty())
{
int u=q.front();q.pop(),vis[u]=0;
for(int i=0;i<4;++i)
{
if(!in(trans(u)+pi(dx[i],dy[i]))) continue;
int v=trans(trans(u)+pi(dx[i],dy[i]));
if(f[v][s]<=f[u][s]+a[v]) continue;
f[v][s]=f[u][s]+a[v],pre[v][s]={u,s};
if(!vis[v]) vis[v]=1,q.push(v);
}
}
}
void dfs(int u,int s)
{
if(!pre[u][s].se) return ;
ans[u]=1;
if(pre[u][s].fi==u) dfs(u,s^pre[u][s].se);
dfs(pre[u][s].fi,pre[u][s].se);
}
int main()
{
memset(f,0x3f,sizeof f),n=read(),m=read(),k=0;
for(int i=0;i<n*m;++i) if(!(a[i]=read())) f[i][1<<(k++)]=0,root=i;
for(int s=1,t,i,x;s<1<<k;++s)
{
for(i=0;i<n*m;++i)
{
for(t=s&(s-1);t;t=(t-1)&s) if(f[i][s]>(x=f[i][t]+f[i][s^t]-a[i])) f[i][s]=x,pre[i][s]={i,t};
if(f[i][s]<inf) q.push(i),vis[i]=1;
}
spfa(s);
}
printf("%d
",f[root][(1<<k)-1]),dfs(root,(1<<k)-1);
for(int i=0,x=0,j;i<n;++i,puts("")) for(j=0;j<m;++j) if(!a[x]) ++x,putchar('x'); else putchar(ans[x]? 'o':'_'),++x;
}
如果不需要具体方案的话我们有一个简单很多的写法。
#include<bits/stdc++.h>
using namespace std;
int read(){int x;scanf("%d",&x);return x;}
int min(int &a,int b){return b<a? a=b,1:0;}
int amin(int a,int b){return a<b? a:b;}
const int N=101,inf=0x3f3f3f3f,dx[4]={0,0,1,-1},dy[4]={1,-1,0,0};
int n,m,k,a[N],f[N][1024],vis[N];queue<int>q;
int move(int p,int k){int x=p/m+dx[k],y=p%m+dy[k],z=x*m+y;return (x<0||y<0||x==n||y==m)? -1:z;}
void spfa(int s)
{
for(int i=0;i<n*m;++i) if(f[i][s]<inf) q.push(i),vis[i]=1;
for(int u,v,k;!q.empty();) for(u=q.front(),q.pop(),vis[u]=k=0;k<4;++k) if(~(v=move(u,k))&&min(f[v][s],f[u][s]+a[v])&&!vis[v]) vis[v]=1,q.push(v);
}
int main()
{
memset(f,0x3f,sizeof f),n=read(),m=read(),k=0;int root;
for(int i=0;i<n*m;++i) if(!(a[i]=read())) f[i][1<<(k++)]=0,root=i;
for(int s=1,i,t;s<1<<k;spfa(s++)) for(i=0;i<n*m;++i) for(t=(s-1)&s;t;--t&=s) min(f[i][s],f[i][t]+f[i][s^t]-a[i]);
printf("%d",f[root][(1<<k)-1]);
}