斯坦纳树小记
斯坦纳树问题
平面上有一些点,其中一些是关键点。给出一些点之间的线段,选择一些线段连通这些关键点并且线段总长度最小。
最小斯坦纳树——在图论中的运用
一张无向连通图,选择一个部分结点的生成树,结点包含点集 \(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 就行。
每个格子预处理出往上/下/左/右走到达的格子,这个可以记搜。
代码不想写。