BZOJ 2595 [Wc2008]游览计划

斯坦纳树DP。

斯坦纳树问题是组合优化问题,与最小生成树相似,是最短网络的一种。最小生成树是在给定的点集和边中寻求最短网络使所有点连通。而最小斯坦纳树允许在给定点外增加额外的点,使生成的最短网络开销最小
——百度百科

我是从这里学习的orz: http://www.cnblogs.com/lazycal/archive/2013/08/31/bzoj-2595.html

上文中提到斯坦纳树DP的两个方程

f[st][i]表示连通性至少为st,且经过i点的最小距离

·方程1.f[st][i] = Min{f[s][i] + f[st - s][i]}(s为st的子集)
·方程2.f[st][i] = Min{f[st][j] + w(i,j)}(i,j之间有边相连)

斯坦纳树的重点就在这里

方程1思路的正确性是显然的,方程1能够进行状态(子集)合并,但是缺点是可能有重复的点集

为此,方程2巧妙地依靠最短路算法消去了重复点集(至于怎么消去的,可以考虑状态合并时,总会有至少一个点没有被重复,于是从这个点往外推,就像松弛操作)。但只有方程2又无法合并子集,所以会有两个方程。

代码的画风和原文差不多

#include<cstdio>
#include<queue>
#include<cstring>
using namespace std;
queue<int> q;
bool vis[11*11];
const int INF = 1<<29;
int n, m, cnt=0, f[11][11][1<<11], a[11][11], from[11][11][1<<11];
int pack(const int x,const int y){return x*10 + y;}
int pack2(const int x,const int y,const int s){return x*100000 + y*10000 + s;}
void unpack(const int x,int &i,int &j){i = x/10; j = x%10;}
void unpack2(const int x,int &i,int &j,int &s){s = x%10000; j = (x/10000)%10; i = x/100000;}
bool update(int x, int y, int s1, int i, int j, int s2, int w)
{
    if(w<f[x][y][s1])
    {
        f[x][y][s1]=w;
        from[x][y][s1]=pack2(i,j,s2);
        return true;
    }
    return false;
}
int dx[4]={0,0,1,-1}, dy[4]={1,-1,0,0};
void SPFA(int sta)
{
    while(!q.empty())
    {
        int x, y;
        unpack(q.front(),x,y);
        vis[q.front()]=0;
        q.pop();
        for(int i = 0; i < 4; i++)
        {
            int nx=x+dx[i], ny=y+dy[i];
            if(nx<0||ny<0||nx>=n||ny>=m)continue;
            if(update(nx,ny,sta,x,y,sta,f[x][y][sta]+a[nx][ny]))
                if(!vis[pack(nx,ny)])
                {
                    vis[pack(nx,ny)]=1;
                    q.push(pack(nx,ny)); 
                }               
        }
    }
}
void mark(int x, int y, int s)
{
    if(a[x][y]!=0){a[x][y]=-1;}
    if(!from[x][y][s])return;
    int nx, ny, ns;
    unpack2(from[x][y][s],nx,ny,ns);
    mark(nx,ny,ns);
    if(nx==x && ny==y)mark(x,y,s-ns);
}
void print()
{
    for(int i = 0; i < n; i++, puts(""))
        for(int j = 0; j < m; j++)
        {
            if(!a[i][j])printf("x");
            else if(a[i][j]>0)printf("_");
            else printf("o");
        }
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 0; i < n; i++)
        for(int j = 0; j < m; j++)
            for(int k = 0, kk=1<<11; k < kk; k++)
                f[i][j][k]=INF;
    for(int i = 0; i < n; i++)
        for(int j = 0; j < m; j++)
        {
            scanf("%d",&a[i][j]);
            if(!a[i][j])
                f[i][j][1<<(cnt++)]=0;
        }
    int tot_sta = 1<<cnt;
    for(int sta = 1; sta < tot_sta; sta++)
    {
        for(int i = 0; i < n; i++)
        {
            for(int j = 0 ; j < m; j++)
            {
                for(int s = sta&(sta-1); s; s = (s-1)&sta)
                    update(i,j,sta,i,j,s,f[i][j][s]+f[i][j][sta-s]-a[i][j]);
                if(f[i][j][sta]<INF)
                {
                    q.push(pack(i,j));
                    vis[pack(i,j)]=1;
                }
            }
        }
        SPFA(sta);
    }
    for(int i = 0; i < n; i++)
        for(int j = 0; j < m;j++)
            if(!a[i][j])
            {
                printf("%d\n",f[i][j][tot_sta-1]);
                mark(i,j,tot_sta-1);
                print();
                return 0;
            }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值