@bzoj


@description@

从未来过绍兴的小D有幸参加了Winter Camp 2008,他被这座历史名城的秀从未来过绍兴的小D有幸参加了Winter Camp 2008,他被这座历史名城的秀丽风景所吸引,强烈要求游览绍兴及其周边的所有景点。

主办者将绍兴划分为N行M列(NXM)个方块,景点含于方块内,且一个方块至多有一个景点。无景点的方块视为路。

为了保证安全与便利,主办方依据路况和治安状况,在非景点的一些方块内安排不同数量的志愿者:在景点内聘请导游(导游不是志愿者)。在选择旅游方案时,保证任意两个景点之间,存在一条路径,在这条路径所经过的每一个方块都有志愿者或者该方块为景点。既能满足选手们游览的需要,又能够让志愿者的总数最少。

现在,希望你能够帮助主办方找到一种最好的安排方案。

原题传送门。

@solution@

斯坦纳树经典题。

斯坦纳树的定义是:给定一个图与 k 个特殊点,求将这 k 个特殊点连通的最小生成树。
当 k = 点数时,就是常见的最小生成树的定义。

状压 dp:记 dp[i][s] 表示当前点为 i,已经包含了 k 个特殊点的集合为 s,最小连通代价。
第一类转移:dp[i][s] = min{dp[i][t] + dp[i][s xor t]},即两个集合取并集。
第二类转移:dp[i][s] = min{dp[j][s] + dis(i, j)},即任意加一条边。
正确性很显然,因为每一种可行方案都会被上面的过程考虑到。

第二类转移带环,但是注意到这个转移很像最短路的 “松弛” 操作,于是我们直接写个 spfa 即可。
时间复杂度(如果把 spfa 算作 O(nm) 的上界的话)为 O(n*3^k + nm*2^k)。

本题因为是点权,为了不使第一种转移出现重复,dp[i][s] 不包含 i 的点权。

@accepted code@

#include <queue>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;

typedef pair<int, int> pii;
#define mp make_pair
#define fi first
#define se second

const int INF = (1 << 30);
const int dx[] = {0, 1, -1, 0, 0};
const int dy[] = {0, 0, 0, -1, 1};

bool inq[10][10];
int A[10][10], id[10][10], N, M, K, S;
int dp[10][10][1 << 10], pre[10][10][1 << 10];
queue<pii>que;
void run(int s) {
	for(int i=0;i<N;i++)
		for(int j=0;j<M;j++)
			que.push(mp(i, j)), inq[i][j] = true;
	while( !que.empty() ) {
		pii p = que.front(); que.pop(); inq[p.fi][p.se] = false;
		for(int i=1;i<=4;i++) {
			int x0 = p.fi + dx[i], y0 = p.se + dy[i];
			if( x0 >= N || y0 >= M || x0 < 0 || y0 < 0 ) continue;
			if( dp[x0][y0][s] > dp[p.fi][p.se][s] + A[p.fi][p.se] ) {
				dp[x0][y0][s] = dp[p.fi][p.se][s] + A[p.fi][p.se];
				pre[x0][y0][s] = i;
				if( !inq[x0][y0] ) {
					que.push(mp(x0, y0));
					inq[x0][y0] = true;
				}
			}
		}
	}
}
void solve() {
	for(int s=1;s<S;s++) {
		for(int i=0;i<N;i++)
			for(int j=0;j<M;j++) {
				int t = s & (s - 1);
				while( t ) {
					if( dp[i][j][s] > dp[i][j][t] + dp[i][j][s ^ t] ) {
						dp[i][j][s] = dp[i][j][t] + dp[i][j][s ^ t];
						pre[i][j][s] = -t;
					}
					t = s & (t - 1);
				}
			}
		run(s);
	}
}
bool tag[10][10];
void get(int x, int y, int s) {
	tag[x][y] = true;
	if( pre[x][y][s] > 0 )
		get(x - dx[pre[x][y][s]], y - dy[pre[x][y][s]], s);
	else if( pre[x][y][s] < 0 ) {
		pre[x][y][s] = -pre[x][y][s];
		get(x, y, pre[x][y][s]), get(x, y, s^pre[x][y][s]);
	}
}

int main() {
	scanf("%d%d", &N, &M);
	for(int i=0;i<N;i++)
		for(int j=0;j<M;j++) {
			scanf("%d", &A[i][j]);
			if( A[i][j] == 0 ) id[i][j] = (K++);
		}
	S = (1 << K);
	for(int i=0;i<N;i++)
		for(int j=0;j<M;j++) {
			for(int s=0;s<S;s++) dp[i][j][s] = INF;
			if( A[i][j] == 0 ) dp[i][j][1 << id[i][j]] = 0;
		}
	solve();
	int x, y, ans = INF;
	for(int i=0;i<N;i++)
		for(int j=0;j<M;j++)
			if( ans > dp[i][j][S - 1] + A[i][j] )
				ans = dp[i][j][S - 1] + A[i][j], x = i, y = j;
	printf("%d
", ans);
	get(x, y, S - 1);
	for(int i=0;i<N;i++) {
		for(int j=0;j<M;j++) {
			if( tag[i][j] ) {
				if( A[i][j] == 0 )
					putchar('x');
				else putchar('o');
			}
			else putchar('_');
		}
		puts("");
	}
}
/*
8 8
1 4 1 3 4 2 4 1
4 3 1 2 0 1 2 3
3 2 1 3 0 3 1 2
2 6 5 0 2 4 1 0
5 1 2 1 3 4 2 5
5 1 3 1 5 0 1 4
5 0 6 1 4 5 3 4
0 2 2 2 3 4 1 1
*/

@details@

关于第二类转移,其实也可以用 floyd 先预处理出所有点对的最短路,然后直接点对两两 O(n^2) 转移。

因为 spfa 的复杂度上界是 O(NM),对于 M 比较大的稠密图用 floyd 更快。

当然对于本题这种网格图 spfa 的复杂度和 floyd 没差,spfa 的常数说不定还要小一些。

原文地址:https://www.cnblogs.com/Tiw-Air-OAO/p/12418981.html