这方面的资料不知道在哪里可以学习到,网上的资料也非常少。还是找几篇大佬博客跟着学吧:
https://www.cnblogs.com/ECJTUACM-873284962/p/7643445.html#autoid-0-0-0
https://www.cnblogs.com/zwfymqz/p/8977295.html
蒟蒻博主没能理解深刻,现在在我看来斯坦纳树其实就是一类问题:给出一个无向图,选择一些边使得几个特定点连接起来那就是斯坦纳树且边权和最小就是最小斯坦纳树。这样看来最小生成树其实是特殊的斯坦纳树,最少生成树要求连接所有的点。那么最小斯坦纳树怎么求?上边两位博主给出的解决办法是状压dp+最短路松弛。
上边的Angel_Kitty大佬说的比较清楚了:
总的来说就是不同state状态之间用状压dp来求解,而相同state状态之间用SPFA来松弛。有点每个连接状态就是一层的感觉,不同层用状压dp求解,然后SPFA只在同一层跑来松弛,这样时间也比较稳定优秀。(当然AngelKitty得博文还提到了其他解决办法,像是直接当成分层最短路跑或是先Floyd预处理然后让出纯dp跑,这两种办法前者速度慢后者只能适用于n十分小的情况)。
ok然后是跟着大佬做题环节:
洛谷P4294游览计划
基本上是斯坦纳树裸题,写了可以当模板用。基本上斯坦纳树的解题框架就状压dp+SPFA这样,然后具体状态转移方程和spfa具体题目具体分析。
#include<bits/stdc++.h> using namespace std; typedef pair<int,int> pii; const int N=1050; const int INF=0x3f3f3f3f; const int dx[]={-1,0,1,0}; const int dy[]={0,1,0,-1}; int n,m,num,a[12][12],dp[12][12][N]; struct PRE{ int x,y,s; } pre[12][12][N]; queue<pii> q; bool inq[12][12]; void spfa(int now) { //同层dpfa松弛 while (!q.empty()) { pii u=q.front(); q.pop(); for (int i=0;i<4;i++) { int x=u.first+dx[i],y=u.second+dy[i]; if (x<1 || x>n || y<1 || y>m) continue; if (dp[x][y][now]>dp[u.first][u.second][now]+a[x][y]) { dp[x][y][now]=dp[u.first][u.second][now]+a[x][y]; pre[x][y][now]=(PRE){u.first,u.second,now}; if (!inq[x][y]) q.push(make_pair(x,y)),inq[x][y]=1; } } inq[u.first][u.second]=0; } } void dfs(int x,int y,int now) { inq[x][y]=1; if (!x || !y) return; PRE tmp=pre[x][y][now]; dfs(tmp.x,tmp.y,tmp.s); if (x==tmp.x && y==tmp.y) dfs(x,y,now-tmp.s); //(x,y,now)是由子集转移过来的 } int main() { scanf("%d%d",&n,&m); memset(dp,0x3f,sizeof(dp)); num=0; //点集的点个数 for (int i=1;i<=n;i++) for (int j=1;j<=m;j++) { scanf("%d",&a[i][j]); if (!a[i][j]) dp[i][j][1<<num]=0,num++; } int ALL=(1<<num)-1; for (int sta=0;sta<=ALL;sta++) { //遍历每一层 for (int i=1;i<=n;i++) for (int j=1;j<=m;j++) { for (int s=sta;s;s=(s-1)&sta) { //枚举子集 if (dp[i][j][sta]>dp[i][j][s]+dp[i][j][sta-s]-a[i][j]) { dp[i][j][sta]=dp[i][j][s]+dp[i][j][sta-s]-a[i][j]; pre[i][j][sta]=(PRE){i,j,s}; //记录前驱状态用于输出方案 } } if (dp[i][j][sta]<INF) q.push(make_pair(i,j)),inq[i][j]=1; //松弛别人 } spfa(sta); } int rx,ry,Min=INF; for (int i=1;i<=n;i++) for (int j=1;j<=m;j++) if (!a[i][j] && dp[i][j][ALL]<Min) Min=dp[i][j][ALL],rx=i,ry=j; printf("%d ",dp[rx][ry][ALL]); memset(inq,0,sizeof(inq)); dfs(rx,ry,ALL); //dfs沿着前驱状态走输出方案 for (int i=1;i<=n;i++,puts("")) for (int j=1;j<=m;j++) if (a[i][j]==0) printf("x"); else if (inq[i][j]) printf("o"); else printf("_"); return 0; }
HDU-4085 Peach Blossom Spring
斯坦纳树经典题。但要注意的是这题只要求点对相互连通,并不要求所有点连通!我们的解决办法是:还是先把所有关键点做一次最小斯坦纳树得到所有连接状态下的答案,然后我们要想办法根据这些树变成森林。我们再思考森林其实是什么东西?森林其实就是分割的多棵树或者说是不连通的几棵树组合而成,那么我们就仿照森林的定义从几个树的答案组合成森林!
设ans[sta]为连接状态为sta的最小森林权值为ans[sta],初始ans[sta]就等于所有的dp[i][sta]中的最小值,为什么?因为此时我们换成森林的角度去思考,我们只关心连接状态而不关心这个森林的根(当然森林并没有根,只是这么个原理)。初始化之后我们开始转移,比较易得转移方程为ans[sta]=min(ans[s]+ans[sta-s]) (s是sta的子集),这里的转移我们也要从森林的角度思考,我们只关心连接状态,所以这个两个森林连不连接根本就没有关系,同样可以组合成目标连接状态sta。所以我们从小到大枚举sta,然后枚举sta的子集进行转移。最后得到的就是最小森林。
这个还有一个小细节是:因为题目的特殊性要求必须人(1-k点)和房子(k+1-2k点)个数相同才被看作合法状态,才能进行转移。
#include<bits/stdc++.h> using namespace std; const int N=1050; const int INF=0x3f3f3f3f; int n,m,k,ALL,dp[55][N],ans[N]; int cnt,head[55],nxt[2010],to[2010],len[2010]; void add_edge(int x,int y,int z) { nxt[++cnt]=head[x]; to[cnt]=y; len[cnt]=z; head[x]=cnt; } queue<int> q; bool inq[55]; void spfa(int now) { while (!q.empty()) { int x=q.front(); q.pop(); for (int i=head[x];i;i=nxt[i]) { int y=to[i]; if (dp[y][now]>dp[x][now]+len[i]) { dp[y][now]=dp[x][now]+len[i]; if (!inq[y]) q.push(y),inq[y]=1; } } inq[x]=0; } } bool check(int sta) { int ret=0; for (int i=1;i<=2*k;i++) if (sta&(1<<(i-1))) if (i<=k) ret++; else ret--; return ret==0; } int main() { int T; cin>>T; while (T--) { scanf("%d%d%d",&n,&m,&k); cnt=1; memset(head,0,sizeof(head)); for (int i=1;i<=m;i++) { int x,y,z; scanf("%d%d%d",&x,&y,&z); add_edge(x,y,z); add_edge(y,x,z); } ALL=(1<<(2*k))-1; memset(inq,0,sizeof(inq)); for (int i=1;i<=n;i++) for (int j=0;j<=ALL;j++) dp[i][j]=INF; for (int i=1;i<=k;i++) dp[i][1<<(i-1)]=0; for (int i=n-k+1;i<=n;i++) dp[i][1<<(i-n+2*k-1)]=0; for (int sta=0;sta<=ALL;sta++) { for (int i=1;i<=n;i++) { for (int s=sta;s;s=(s-1)&sta) dp[i][sta]=min(dp[i][sta],dp[i][s]+dp[i][sta-s]); if (dp[i][sta]<INF) q.push(i),inq[i]=1; } spfa(sta); } for (int sta=0;sta<=ALL;sta++) { ans[sta]=INF; for (int i=1;i<=n;i++) ans[sta]=min(ans[sta],dp[i][sta]); } for (int sta=0;sta<=ALL;sta++) if (check(sta)) for (int s=sta;s;s=(s-1)&sta) if (check(s)) ans[sta]=min(ans[sta],ans[s]+ans[sta-s]); if (ans[ALL]>=INF) puts("No solution"); else printf("%d ",ans[ALL]); } return 0; }
ZOJ-3613 Wormhole Transport
这道题跟HDU4085差不多。因为是工厂和资源点连接就可以了,那就是最小森林,做法像HDU4085一样,先把所有点做最小斯坦纳树然后dp求最小森林,同时把最多点对最小权值的答案记录下来。
这里与HDU4085不同的是,这题的合理状态要求的是工厂数量>=资源点数量,这是为什么呢?因为我们在做完最小斯坦纳树后用dp合成森林的时候ans[sta]=min(ans[s]+ans[sta-s])这里必须要注意到s这棵森林和sta-s这棵森林必须是要分开的,不能出现s的资源点和sta-s的工厂连接这种情况!所以我们只把工厂>=资源点的状态才看作合法的话就能保证每个资源点只在自己这颗森林连接不会练到其他森林去。
总的来说规定合法状态其实是为了不让两棵森林相互影响从而合并得到错误的答案。
那么为什么不令资源点>=工厂呢?好像理论上也可以阻止两颗森林相互影响合并,但是因为这题的特殊性,每个星球的工厂个数能多于1,而资源点只能小于等于1,如果令资源点>=工厂为合法状态的话,一旦出现每个星球工厂个数都>资源点个数的情况,那么每一个状态都被视为不合法,那没法转移了。但是反过来就不会出现这种情况。所以工厂>=资源这个是一个十分巧妙的办法,它即能避免森林影响,也不会漏掉答案方案。其实我们可以这样想我们想要的只是工厂==资源点的方案,但是我们不能像上一题一样把工厂==资源点定义为合法,因为这里不是每个点权值都为1,这样定义会出现没法转移的情况,所以只能让资源点>=工厂,而且这样的定义会包含工厂==资源点的情况,所以是可以的。(到这里你也许会想那上一题把==改成>=是不是也可以,博主测过确实可以,因为这样同样阻止了森林影响)
想到这里蒟蒻博主就有疑问了:那岂不是如果一个星球工厂和资源都能大于1岂不是无解了?只是按照我上面的思考好像是没有什么好的办法。
#include<bits/stdc++.h> using namespace std; const int N=1050; const int M=5e3+10; const int INF=0x3f3f3f3f; int n,m,num,ALL,id[210],w[210],p[210],dp[210][N],ans[N]; int cnt,head[210],nxt[M<<1],to[M<<1],len[M<<1]; void add_edge(int x,int y,int z) { nxt[++cnt]=head[x]; to[cnt]=y; len[cnt]=z; head[x]=cnt; } queue<int> q; bool inq[210]; void spfa(int now) { while (!q.empty()) { int x=q.front(); q.pop(); for (int i=head[x];i;i=nxt[i]) { int y=to[i]; if (dp[y][now]>dp[x][now]+len[i]) { dp[y][now]=dp[x][now]+len[i]; if (!inq[y]) q.push(y),inq[y]=1; } } inq[x]=0; } } int getp(int sta) { int ret=0; for (int i=1;i<=num;i++) if (sta&(1<<(i-1))) ret+=p[id[i]]; return ret; } int getw(int sta) { int ret=0; for (int i=1;i<=num;i++) if (sta&(1<<(i-1))) ret+=w[id[i]]; return ret; } int main() { while (scanf("%d",&n)==1) { num=0; for (int i=1;i<=n;i++) { scanf("%d%d",&p[i],&w[i]); if (p[i] || w[i]) id[++num]=i; } scanf("%d",&m); cnt=1; memset(head,0,sizeof(head)); for (int i=1;i<=m;i++) { int x,y,z; scanf("%d%d%d",&x,&y,&z); add_edge(x,y,z); add_edge(y,x,z); } ALL=(1<<num)-1; memset(inq,0,sizeof(inq)); for (int i=1;i<=n;i++) for (int j=0;j<=ALL;j++) dp[i][j]=INF; for (int i=1;i<=num;i++) dp[id[i]][1<<(i-1)]=0; for (int sta=0;sta<=ALL;sta++) { for (int i=1;i<=n;i++) { for (int s=sta;s;s=(s-1)&sta) dp[i][sta]=min(dp[i][sta],dp[i][s]+dp[i][sta-s]); if (dp[i][sta]<INF) q.push(i),inq[i]=1; } spfa(sta); } for (int sta=0;sta<=ALL;sta++) { ans[sta]=INF; for (int i=1;i<=n;i++) ans[sta]=min(ans[sta],dp[i][sta]); } int Max=0,Min=INF; for (int sta=0;sta<=ALL;sta++) if (getp(sta)>=getw(sta)) { for (int s=sta;s;s=(s-1)&sta) if (getp(s)>=getw(s) && getp(sta-s)>=getw(sta-s)) ans[sta]=min(ans[sta],ans[s]+ans[sta-s]); if (getw(sta)>Max || getw(sta)==Max && ans[sta]<Min) Max=getw(sta),Min=ans[sta]; } printf("%d %d ",Max,Min); } return 0; }
2018-2019 ACM-ICPC Brazil Subregional Programming Contest J - Joining Capitals
题意:在二维平面上,给出n个点(n<=100)的坐标,其中前k个是特殊点,后面n-k个不是特殊点,任两点间边权是距离。问这k个特殊点组成的最小斯坦纳树并且这k个点必须是叶子。
解法:一看数据100个点10个特殊点,斯坦纳树没跑了。暴力建图然后套斯坦纳树模板。但是注意怎么才能满足这k个点必须是叶子的要求呢?答案是spfa更新的时候不更新特殊点。这是什么道理,因为这样写的话,只有特殊点出去更新别人没有别人更新特殊点,这样就是导致这棵树只会由几个 特殊点出发的 组件 组成,组成的这棵树当然特殊点只是叶子啦。
#include<bits/stdc++.h> using namespace std; const int N=1050,M=1e6+10; const int INF=0x3f3f3f3f; int n,m,k,ALL; double x[N],y[N],dp[110][N],ans; int cnt,head[N],nxt[M],to[M]; double len[M]; void add_edge(int x,int y,double z) { nxt[++cnt]=head[x]; to[cnt]=y; len[cnt]=z; head[x]=cnt; } queue<int> q; bool inq[N]; void spfa(int now) { while (!q.empty()) { int x=q.front(); q.pop(); for (int i=head[x];i;i=nxt[i]) { int y=to[i]; if (y<=k) continue; //不去更新指定点 if (dp[y][now]>dp[x][now]+len[i]) { dp[y][now]=dp[x][now]+len[i]; if (!inq[y]) q.push(y),inq[y]=1; } } inq[x]=0; } } int main() { scanf("%d%d",&n,&k); cnt=1; memset(head,0,sizeof(head)); for (int i=1;i<=n;i++) scanf("%lf%lf",&x[i],&y[i]); for (int i=1;i<=n;i++) for (int j=i+1;j<=n;j++) { if (i<=k && j<=k) continue; double t=sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j])); add_edge(i,j,t); add_edge(j,i,t); } ALL=(1<<(k))-1; memset(inq,0,sizeof(inq)); for (int i=1;i<=n;i++) for (int j=0;j<=ALL;j++) dp[i][j]=INF; for (int i=1;i<=k;i++) dp[i][1<<(i-1)]=0; ans=INF; for (int sta=0;sta<=ALL;sta++) { for (int i=1;i<=n;i++) { for (int s=sta;s;s=(s-1)&sta) dp[i][sta]=min(dp[i][sta],dp[i][s]+dp[i][sta-s]); if (dp[i][sta]<INF) q.push(i),inq[i]=1; if (sta==ALL) ans=min(ans,dp[i][sta]); } spfa(sta); } printf("%.5lf ",ans); return 0; }