斯坦纳树

遇到一道题,给定一个图,求联通指定点的最小花费,刚开始,还以为floyd求任意两点最短路,然后对给定点求最小生成树,
样例:
u v w
1 2 10
3 2 10
4 2 10 求联通1 3 4 点最少消费,都不能解
百度题解,斯坦纳树!!!!!!!
斯坦纳树问题是组合优化问题,与最小生成树相似,是最短网络的一种。最小生成树是在给定的点集和边中寻求最短网络使所有点连通。而最小斯坦纳树允许在给定点外增加额外的点,使生成的最短网络开销最小。
       首先我们知道,最优解必然是一棵树,然后这棵树又是由若干棵子树合并成的,于是我们可以状态压缩,把k个节点的连通状态用一个二进制数j表示,dp[i][j]表示以i为根和对应状态为j的节点连通的子树的最小权值。有两种转移方法:

       枚举子树的形态:dp[ i ][ j ]=min{ dp[ i ][ j ],dp[ i ][ k ]+dp[ i ][ l ] },其中k和l是对j的一个划分。
       按照边进行松弛:dp[ i ][ j ]= min{ dp[ i ][ j ],dp[ i' ][ j ]+w[ i ][ i' ] },其中i和i'之间有边相连。
       
       对于第一种转移,我们直接枚举子集就行了。对于第二种转移,我们仔细观察可以发现这个方程和最短路的约束条件是很类似的,于是我们可以用spfa或者dij来进行状态转移。枚举子集的复杂度=n*sum{C(k,i)*2^i,0<i=k}=n*3^k,spfa的复杂度为n*2^k。所以总复杂度为O(n*3^k)。
       
       具体实现的时候我试了好几种不同的方法,一开始是直接把两种转移都看成图中的边,一遍spfa得出结果,大概如下所示:
void spfa(){
    while(!Q.empty()){
        int x=Q.front()/10000,y=Q.front()%10000;
        in[x][y]=0;
        Q.pop();
        for(edge *i=Adj[x];i;i=i->nxt)       //对当前节点的每条边都进行松弛操作
            update(i->v,s[i->v]|y,d[x][y]+i->w);
        int t=nn-1-y;
        for(int i=t;i;i=(i-1)&t)            //枚举补集的所有子集,进行松弛操作
            update(x,y|i,d[x][y]+d[x][i|s[x]]);//s[x]指x的二进制表示
    }
}

   这么做的复杂度是没有变的,但是常数非常大,hdu上跑了2500ms才过,几乎是倒数了。仔细一想,我们发现第二松弛操作其实做了很多无用功,考虑能不能进行优化。
      第二种松弛操作非常的耗时间,所以我们就不把它加到spfa里面进行转移,直接在外面进行枚举,实现更新,避免大量的重复计算。先枚举连通性j,对于所有的1<=i<=n,我们先进行第一种转移,既枚举子集进行更新。如果dp[i][j]被更新了,我们就把它加到队列里,最后再进行spfa(),这样按j分层的进行转移,大概如下:
for(int y=0;y<nn;y++)                             //枚举连通性
            for(int x=1;x<=n;x++){
                bool flag=0;
                for(int i=(y-1)&y;i;i=(i-1)&y)      //枚举所有子集,进行第一种转移
                    flag|=update(x,y,d[x][i|s[x]]+d[x][(y-i)|s[x]]);
                if(flag) Q.push(x*10000+y);       //如果节点被更新则加入队列
                spfa();       //spfa进行第二种转移
            }

我本来以为这样会更快一些,结果尼玛跑了4700ms = =!顿时吐槽无力。

       为啥这样会更慢呢?我觉的大概是由于spfa()的次数过多,所以导致很多节点被重复的更新了很多次,又产生了大量了重复计算,所以反而更慢了。那么就没有什么好办法吗?仔细一想,我发现进行spfa的时候只需要对当前层的节点进行spfa就行了,不需要整个图完全松弛一遍,因为更高的层都可以通过枚举子集而变成若干个更低的层,这样一次spfa的复杂度一下就降了下来,变成了O(n)级别,大概如下:
for(edge *i=Adj[x];i;i=i->nxt)
    if(update(i->v,y|s[i->v],d[x][y]+i->w)&&y==(y|s[i->v])&&!in[i->v][y]) //只把处于相同层的节点加到队列中
        in[i->v][y]=1,Q.push(i->v*10000+y); 

这样修改以后效果果然非常明显,1000ms就AC了。但还是不够快,别人最快的能够达到500ms。于是我baidu了一下,膜拜神牛 500ms的做法,发现他们没有用spfa!大概就是把第二种转移表示成了另外一种形式:

       dp[ i ][ j ]=min{ dp[ i ][ j ] , dp[ k ][ j ]+d[ k ][ i ] },其中d[ k ][ i ]表示k到i的最短路。
       
       很容易就能证明这样写方程也是对的,于是我们就可以先用floyed预处理出任意两点间的最短路,然后直接DP。这样做的总复杂度为O(n^3+n^2*2^k+n*3^k),这个复杂度并不比上面的方法低,但由于hdu4085的n比较小,所以这样写反而比上一种方法要快上不少。但对于  [WC2008]游览计划、ZOJ 3613 Wormhole Transport这两道题就不行了,n都达到了100甚至200的大小,这种方法要比前面一种慢。所以最后得出结论,还是前一种方法最稳定 = ^ =

       11年北京赛区的E题,这题有点不同的地方在于,最后的答案可能是一个森林,所以我们要先求出斯坦纳树后进行DP。转移的时候要注意一点,只有人的个数和房子的个数相等的时候才算合法状态,所以我们要加一个check()函数进行检查。
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<algorithm>
#define N 60
#define INF 2000000
using namespace std;
struct edge{
    int v,w;
    edge *nxt;
}E[2009],*Adj[N],*cur;
int n,m,K,nn;
int s[N],in[N][1<<10];
int d[N][1<<10],dp[1<<10];
queue<int> Q;
void addedge(int u,int v,int w){cur->v=v,cur->w=w,cur->nxt=Adj[u],Adj[u]=cur++;}
bool check(int x){
    int r=0;
    for(int i=0;x;i++,x>>=1)
        r+=(x&1)*(i<K?1:-1);
    return r==0;
}
inline bool update(int x,int y,int w){
    if(w<d[x][y]) return d[x][y]=w,true;
    return false;
}
void spfa(){
    while(!Q.empty()){
        int x=Q.front()/10000,y=Q.front()%10000;
        in[x][y]=0;
        Q.pop();
        for(edge *i=Adj[x];i;i=i->nxt)
            if(update(i->v,y|s[i->v],d[x][y]+i->w)&&y==(y|s[i->v])&&!in[i->v][y])
                in[i->v][y]=1,Q.push(i->v*10000+y);
                
    }
}
void init(){
    cur=E;
    memset(Adj,0,sizeof(Adj));
    memset(s,0,sizeof(s));    
    scanf("%d%d%d",&n,&m,&K);
    nn=1<<(2*K);
    for(int i=1;i<=n;i++)
        for(int j=0;j<nn;j++)
            d[i][j]=INF;
    while(m--){
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        addedge(u,v,w);
        addedge(v,u,w);
    }    
    for(int i=1;i<=K;i++){
        s[i]=1<<(i-1),d[i][s[i]]=0;                
        s[n-i+1]=1<<(K+i-1),d[n-i+1][s[n-i+1]]=0;        
    }    
}
int main(){    
    int T;
    scanf("%d",&T);
    while(T--){        
        init();
        for(int y=0;y<nn;y++){
            for(int x=1;x<=n;x++){                
                for(int i=(y-1)&y;i;i=(i-1)&y)
                    d[x][y]=min(d[x][y],d[x][i|s[x]]+d[x][(y-i)|s[x]]);
                if(d[x][y]<INF) Q.push(x*10000+y),in[x][y]=1;
            }
            spfa();
        }
        for(int j=0;j<nn;j++){
            dp[j]=INF;
            for(int i=1;i<=n;i++) dp[j]=min(dp[j],d[i][j]);
        }
        for(int i=1;i<nn;i++)
            if(check(i))
                for(int j=i&(i-1);j;j=(j-1)&i)
                    if(check(j))
                        dp[i]=min(dp[i],dp[j]+dp[i-j]);
        if(dp[nn-1]>=INF) puts("No solution");
        else printf("%d\n",dp[nn-1]);
    }
}


       这题要求一棵满足要求的斯坦纳树,基本上按照上面的做法写就行了,不过有一点恶心的就是要输出一组可行方案,所以DP的时候还要记录一下路径。
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<algorithm>
#define INF 2000000
#define N 10
using namespace std;
int dx[]={0,1,0,-1},
    dy[]={1,0,-1,0};
int max_s,n,m;
int mat[N][N],st[N][N],vis[N][N],cnt;
int d[N][N][1<<N],pre[N][N][1<<N];
bool in[N][N][1<<N];
queue<int> Q;
void spfa(){
    int x,y,s,tx,ty,ts;
    while(!Q.empty()){
        x=Q.front()/100000;
        y=(Q.front()-x*100000)/10000;
        s=Q.front()-x*100000-y*10000;
        Q.pop();
        in[x][y][s]=0;
        for(int i=0;i<4;i++){
            tx=x+dx[i],ty=y+dy[i];
            if(tx>=n||ty>=m||tx<0||ty<0) continue;
            ts=s|st[tx][ty];
            if(d[x][y][s]+mat[tx][ty]<d[tx][ty][ts]){
                d[tx][ty][ts]=d[x][y][s]+mat[tx][ty];
                pre[tx][ty][ts]=x*100000+y*10000+s;
                if(!in[tx][ty][ts]&&s==ts) in[tx][ty][ts]=1,Q.push(tx*100000+ty*10000+ts);
            }                
        }
    }
}
void go(int x,int y,int s){
    vis[x][y]=1;
    int t=pre[x][y][s],tx,ty,ts;
    if(!t) return;
    tx=t/100000;
    ty=(t-tx*100000)/10000;
    ts=t-tx*100000-ty*10000;
    go(tx,ty,ts);
    if(x==tx&&y==ty) go(x,y,(s-ts)|st[x][y]);
}
int main(){
    //freopen("in.in","r",stdin);
    scanf("%d%d",&n,&m);    
    for(int i=0;i<n;i++)
        for(int j=0;j<m;j++){
            scanf("%d",&mat[i][j]);
            if(!mat[i][j]) st[i][j]=1<<(cnt++);
        }    
    max_s=1<<cnt;
    for(int i=0;i<n;i++)
        for(int j=0;j<m;j++){
            for(int k=0;k<max_s;k++)
                d[i][j][k]=INF;
            if(st[i][j]) d[i][j][st[i][j]]=0;
        }
    for(int k=1;k<max_s;k++){
        for(int i=0;i<n;i++)
            for(int j=0;j<m;j++){
                if(st[i][j]&&!(st[i][j]&k)) continue;                
                for(int x=(k-1)&k;x;x=(x-1)&k){
                    int t=d[i][j][x|st[i][j]]+d[i][j][(k-x)|st[i][j]]-mat[i][j];
                    if(t<d[i][j][k]) d[i][j][k]=t,pre[i][j][k]=i*100000+j*10000+(x|st[i][j]);
                }
                if(d[i][j][k]<INF) Q.push(i*100000+j*10000+k),in[i][j][k]=1;
            }
        spfa();
    }
    for(int i=0;i<n;i++)
        for(int j=0;j<m;j++)
            if(st[i][j]){
                printf("%d\n",d[i][j][max_s-1]);
                go(i,j,max_s-1);
                for(int x=0;x<n;x++){
                    for(int y=0;y<m;y++){
                        if(st[x][y]) putchar('x');
                        else if(vis[x][y]) putchar('o');
                        else putchar('_');
                    }
                    puts("");
                }
                return 0;
            }
}



       ZOJ Monthly, June 2012的C题。和HDU 4085差不多,有一点不同的是一个星球可能有很多个工厂,但是含有资源和含有工厂的星球个数都不超过4。还是先状态压缩,然后DP求出斯坦纳树。最优的方案有可能是森林,所以我们还要DP,dp[ i ]表示对应的工厂节点和资源节点组成的斯坦树森林的最优值。那么:

       dp[ i ]=min{ dp[ i ],dp[ j ]+dp[ k ] },其中j和k为i的一个划分。

       这里要注意一点,所有的状态i、j、k都要满足一个条件,就是连通的星球上工厂的个数要大于等于资源的个数,这样才是一个合法的状态,所以要加一个check()函数。最后再找到所含资源最多,花费最小的合法方案就是答案。
#include<cstdio>
#include<cstring>
#include<queue>
#include<vector>
#include<algorithm>
#define N 209
using namespace std;

struct edge{int v,w;edge *nxt;}E[10009],*Adj[N],*cur;
int n,m,nn;
int d[N][1<<8],dp[1<<8];
bool in[N][1<<8];
int S[N],P[N],st[N],fac[4],cf,cs;
queue<int> Q;
void addedge(int u,int v,int w){cur->v=v,cur->w=w,cur->nxt=Adj[u],Adj[u]=cur++;}
void up(int &a,int b){if(a==-1||a>b) a=b;}
void spfa(){
    while(!Q.empty()){
        int x=Q.front()/1000,y=Q.front()%1000;
        Q.pop();
        in[x][y]=0;
        for(edge *i=Adj[x];i;i=i->nxt)
            if(d[i->v][y|st[i->v]]==-1||d[x][y]+i->w<d[i->v][y|st[i->v]]){
                d[i->v][y|st[i->v]]=d[x][y]+i->w;
                if(y==(y|st[i->v])&&!in[i->v][y]) in[i->v][y]=1,Q.push(i->v*1000+y);
            }                
    }
}
bool check(int x){
    int t=0;
    for(int i=0;x;i++,x>>=1)
        t+=(x&1)*(i<cf?fac[i]:-1);
    return t>=0;
}
int cnt(int x){
    int r=0;
    for(int i=0;x;i++,x>>=1)
        r+=(x&1)*(i<cf?0:1);
    return r;
}
int main(){
    while(scanf("%d",&n)+1){
        cur=E;
        cf=cs=0;
        memset(Adj,0,sizeof(Adj));
        memset(st,0,sizeof(st));
        memset(d,-1,sizeof(d));
        memset(dp,-1,sizeof(dp));        
        int ans=0;
        for(int i=1;i<=n;i++){            
            scanf("%d%d",P+i,S+i);
            if(S[i]&&P[i]) P[i]--,S[i]=0,ans++;
            if(P[i]) st[i]=1<<cf,fac[cf++]=P[i],d[i][st[i]]=0;
        }        
        for(int i=1;i<=n;i++)
            if(S[i])
                st[i]=1<<(cf+cs++),d[i][st[i]]=0;
        nn=1<<(cf+cs);
        
        scanf("%d",&m);
        while(m--){
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            addedge(u,v,w);
            addedge(v,u,w);
        }
        
        for(int y=1;y<nn;y++){
            for(int x=1;x<=n;x++){
                if(st[x]&&!(st[x]&y)) continue;
                for(int i=(y-1)&y;i;i=(i-1)&y)
                    if(d[x][i|st[x]]!=-1&&d[x][(y-i)|st[x]]!=-1)
                        up(d[x][y],d[x][i|st[x]]+d[x][(y-i)|st[x]]);
                if(d[x][y]!=-1) Q.push(x*1000+y),in[x][y]=1;
            }
            spfa();
        }
        for(int i=1;i<=n;i++)
            for(int j=0;j<nn;j++)
                if(d[i][j]!=-1)
                    up(dp[j],d[i][j]);
        int num=0,cost=0;
        for(int i=1;i<nn;i++)
            if(check(i)){
                for(int j=(i-1)&i;j;j=(j-1)&i)
                    if(check(j)&&check(i-j)&&dp[j]!=-1&&dp[i-j]!=-1)
                        up(dp[i],dp[j]+dp[i-j]);
                int t=cnt(i);
                if(dp[i]!=-1&&(t>num||(t==num&&dp[i]<cost)))
                    num=t,cost=dp[i];
            }
        printf("%d %d\n",num+ans,cost);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值