BZOJ 2595 wc2008 浏览计划 [斯坦纳树]

50 篇文章 0 订阅

 

斯坦纳树裸题

但是本题的斯坦纳树是点权,与大多数图的边权不同,下边讲解一般的边权做法,然后再将本题的题解。

 

斯坦纳树:给出一些点,选出若干条边使得这些点连通,求总边权的最值。

斯坦纳树是NP问题,不存在多项式时间内的解法,求解方法是状压dp。

设 f[i][j]表示选择若干条边,使得状态为 ii 的给定点连通,并且当前可以选择下一条边的端点为 jj 的最小边权和。初始状态 f[2k][pos[k]]=0,其中 pos[k] 为第 kk 个给定点的编号。

那么我们对于每个 i 和 j ,首先枚举 i的子集 k ,用 f[k][j]+f[i−k][j] 更新 f[i][j]f[i][j] 。

然后再考虑同层转移:如果 x 与 y 边权为 z ,用f[i][x]+z 更新f[i][y] ,用f[i][y] 更新 f[i][x] 。容易发现这个转移就是最短路,因此使用堆优化Dijkstra跑一遍得出所有的 f[i][j] 。

最终答案就是 min{f[2p−1][i]}

这个dp的理解比较显然,时间复杂度O(3p·n+2p·mlog⁡n) ,其中 p 是给定点的数目。

 

那么对于本题,由于权值在点上,因此枚举子集转移时,当前点会重复选择,需要减去代价。

在同层转移时,求的就是点权最短路,边长为目标点的点权。

设 f[i][j][k]表示选定若干条边,使得状态为 ii 的给定点连通,并且当前可以选择下一条边的相邻点为 (j,k)(j,k) 的最小点权和。初始状态 f[2k][posx[k]][posy[k]]=0 .

首先枚举子集转移:f[i][j][k]=minl⊆if[l][j][k]+f[i−l][j][k]−a[j][k] ,然后同层转移,与 (j,k)相邻的点的 f 加上 a[j][k] 可以转移到f[i][j][k] 。

输出方案的话直接记录路径,记录从哪种方式的哪个状态转移过来,dfs一遍即可知道选定的点。

时间复杂度 O(3p⋅nm+2p⋅nmlogn)

/*
    斯坦纳树
*/

#include <bits/stdc++.h>
#define mp make_pair
#define fi first
#define se second
#define PII pair<int,int>

using namespace std;
const int maxn = 1050;
const int INF  = 1e9;
int N, M, tot = 0 ;
int a[12][12], f[12][12][maxn];
int dir[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}};
int vis[12][12];
struct PRE{
    int x, y, S;
}pre[12][12][maxn];
queue<PII> q;

void SPFA(int cur){
    while (q.size() != 0){
        PII p = q.front();q.pop();
        vis[p.fi][p.se] = 0;
        for (int i = 0 ; i < 4; i++){
            int nxtx = p.fi + dir[i][0], nxty = p.se + dir[i][1];
            if (nxtx < 1 || nxtx > N || nxty < 1 || nxty > M) continue;
            if (f[nxtx][nxty][cur] > f[p.fi][p.se][cur] + a[nxtx][nxty]){
                f[nxtx][nxty][cur] = f[p.fi][p.se][cur] + a[nxtx][nxty];
                pre[nxtx][nxty][cur] = (PRE){p.fi, p.se, cur};
                if (!vis[nxtx][nxty]){
                    vis[nxtx][nxty] = 1, q.push(mp(nxtx, nxty));
                }
            }
        }
    }
}

void dfs(int x, int y, int now){
    vis[x][y] = 1;
    PRE tmp = pre[x][y][now];
    if (tmp.x == 0 && tmp.y == 0) return ;
    dfs(tmp.x, tmp.y, tmp.S);
    if (tmp.x == x && tmp.y == y) dfs(tmp.x, tmp.y, now - tmp.S);
}

int main(){
    scanf("%d%d", &N, &M);
    memset(f, 0x3f, sizeof(f));
    for (int i = 1; i <= N; i++)
        for (int j = 1; j <= M; j++){
            scanf("%d", &a[i][j]);
            if (a[i][j] == 0) f[i][j][1<<tot] = 0, tot++;
        }
    int limit = (1 << tot) - 1;
    for (int sta = 0; sta <= limit; sta++){
        for (int i = 1; i <= N; i++)
            for (int j = 1; j <= M; j++){
                for (int s = sta & (sta - 1); s ; s = (s - 1) & sta){
                    if (f[i][j][s] + f[i][j][sta - s] - a[i][j] < f[i][j][sta]){
                        f[i][j][sta] = f[i][j][s] + f[i][j][sta - s] - a[i][j];
                        pre[i][j][sta] = (PRE){i,j,s};
                    }
                }
                if (f[i][j][sta] < INF) q.push(mp(i,j)), vis[i][j] = 1;
            }
        SPFA(sta);
    }
    int ansx, ansy, flag = 0;
    for (int i = 1; i <= N && !flag; i++)
            for (int j = 1; j <= M; j++)
                if (!a[i][j]){
                    ansx = i;
                    ansy = j;
                    flag = 1;
                    break;
                }
    printf("%d\n", f[ansx][ansy][limit]);
    memset(vis, 0, sizeof(vis));
    dfs(ansx, ansy, limit);
    for (int i = 1; i <= N; i++, puts("")) {
        for (int j = 1; j <= M; j++){
            if (a[i][j] == 0) putchar('x');
            else if (vis[i][j]) putchar('o');
            else putchar('_');
        }
    }
    return 0;
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目描述 有一个 $n$ 个点的棋盘,每个点上有一个数字 $a_i$,你需要从 $(1,1)$ 走到 $(n,n)$,每次只能往右或往下走,每个格子只能经过一次,路径上的数字和为 $S$。定义一个点 $(x,y)$ 的权值为 $a_x+a_y$,求所有满足条件的路径中,所有点的权值和的最小值。 输入格式 第一行一个整数 $n$。 接下来 $n$ 行,每行 $n$ 个整数,表示棋盘上每个点的数字。 输出格式 输出一个整数,表示所有满足条件的路径中,所有点的权值和的最小值。 数据范围 $1\leq n\leq 300$ 输入样例 3 1 2 3 4 5 6 7 8 9 输出样例 25 算法1 (树形dp) $O(n^3)$ 我们可以先将所有点的权值求出来,然后将其看作是一个有权值的图,问题就转化为了在这个图中求从 $(1,1)$ 到 $(n,n)$ 的所有路径中,所有点的权值和的最小值。 我们可以使用树形dp来解决这个问题,具体来说,我们可以将这个图看作是一棵树,每个点的父节点是它的前驱或者后继,然后我们从根节点开始,依次向下遍历,对于每个节点,我们可以考虑它的两个儿子,如果它的两个儿子都被遍历过了,那么我们就可以计算出从它的左儿子到它的右儿子的路径中,所有点的权值和的最小值,然后再将这个值加上当前节点的权值,就可以得到从根节点到当前节点的路径中,所有点的权值和的最小值。 时间复杂度 树形dp的时间复杂度是 $O(n^3)$。 C++ 代码 算法2 (动态规划) $O(n^3)$ 我们可以使用动态规划来解决这个问题,具体来说,我们可以定义 $f(i,j,s)$ 表示从 $(1,1)$ 到 $(i,j)$ 的所有路径中,所有点的权值和为 $s$ 的最小值,那么我们就可以得到如下的状态转移方程: $$ f(i,j,s)=\min\{f(i-1,j,s-a_{i,j}),f(i,j-1,s-a_{i,j})\} $$ 其中 $a_{i,j}$ 表示点 $(i,j)$ 的权值。 时间复杂度 动态规划的时间复杂度是 $O(n^3)$。 C++ 代码

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值