JZOJ 3252. 【GDOI三校联考】炸弹(树形DP)

JZOJ 3252. 【GDOI三校联考】炸弹

题目

Description

在一个N行M列的二维网格里,有些格子是空地(用字符‘.’表示),有些格子是障碍物(用字符‘#’表示)。每个空地格子都有一只虫子,虫子不会移动。FJ打算用最少的炸弹把所有的虫子消灭。FJ每次可以选择在一个空地格子(不妨假设是格子a)放置一个炸弹,这个炸弹爆炸后,格子a的虫子会被消灭,假设有另一个空地格子b,如果空地格子b同时满足如下两个条件,那么空地b格子的虫子也会被该炸弹消灭:

1.格子a和格子b在同一行或者在同一列。

2.格子a和格子b之间没有障碍物格子。

有趣的是,任意两个不同的空地格子都有且只有一条由空地格子构成的路径,即所有空地格子构成一棵树形结构。注意:炸弹并不能毁灭障碍物!

Input

第一行,两个整数,n和m。1 <= n, m<=50。

接下来是n行m列的二维网格。

Output

输出最少的炸弹数。

Sample Input

输入1:
3 4
#…
…##
#.##

输入2:
3 7
.#.#.#.

.#.#.#.

Sample Output

输出1:
2

输出2:
4

Data Constraint

30%的数据满足空地格子数量小于20

Hint

样例解释1:
#.B.
.B##
#.##

样例解释2:
.#.#.#.
B.B.B.B
.#.#.#.
字符B表示放置炸弹

题解

  • 这道题的限制很像二分图匹配然后跑网络流,但是发现好像建不出图,一脸GG,
  • 由于出题人良心的给出了空地是构成了一棵树的限制,所以又自然地想到了树形DP。
  • 首先我想到的是缩点,把每行和每列连续的一段(为方便理解,需保证这种段的长度大于 1 1 1,以下称之为“段”)看成树上的一个点,有交叉的地方则连一条边,
  • 最坏的情况是每个段单独自己炸,而实际上可以通过在某两段的交叉处放一个炸弹,一次炸完两个段,
  • 那么很自然地设出状态: f [ i ] [ 0 / 1 ] f[i][0/1] f[i][0/1]表示点 i i i当前有或没有与它儿子节点用一个炸弹
  • (这里如果和儿子共用了,那就不能和父亲共用,否则可以和父亲共用)
  • 转移则是:
  • 如果当前点不和儿子共用,答案即为所有儿子的 m i n ( f [ s o n ] [ 0 / 1 ] ) min(f[son][0/1]) min(f[son][0/1])再加上给当前点炸的一次 1 1 1
  • 否则,可以由某个儿子的 f [ s o n ] [ 0 ] f[son][0] f[son][0]和另外所有的 m i n ( f [ s o n ] [ 0 / 1 ] ) min(f[son][0/1]) min(f[son][0/1])得来,因为这里炸的次数在DP到和它共用的儿子时已经计算过了,所以不用 + 1 +1 +1
  • 于是以为这样就可以轻松AC了,其实并不是,
  • 仔细想想,发现这样转移后最终的放置方案会使得图中每一段至少有一个炸弹来炸(交叉处相当于给两个段上都放了炸弹),
  • 但实际上,可以有一整行或整列都没有炸弹的情况,只要一整段的每个位置都与其他段相交,那么就可以通过其他那些段上全放炸弹而自己这段完全不放炸弹,
    JZOJ 3252. 【GDOI三校联考】炸弹(树形DP)_第1张图片
  • 如果,只需要在标红点的地方放炸弹, 而框出来的整段都没有放炸弹。
    那么再设一种状态 f [ i ] [ 2 ] f[i][2] f[i][2]表示这个段完全靠所有父亲和儿子来炸,但不是所有的段都可以有这个状态使用,必须满足它的度数 d [ i ] d[i] d[i]等于它的大小 s [ i ] s[i] s[i],现在的转移是:
  • f [ i ] [ 0 ] f[i][0] f[i][0]:在上面的基础上,如果儿子的 2 2 2状态可用的话,可以在 0 / 1 / 2 0/1/2 0/1/2三个状态中取最小值,最后记得 + 1 +1 +1
  • f [ i ] [ 1 ] f[i][1] f[i][1]:因为这种需要至少一个儿子来炸它,所以先枚举是哪个儿子选了 0 0 0,再在剩下的儿子中按 0 0 0状态的方式取最小值;
  • f [ i ] [ 2 ] f[i][2] f[i][2]:如果这个点全靠儿子和父亲,那么儿子的 2 2 2状态就不能用了,所以在所有儿子的 0 / 1 0/1 0/1状态中取较小值求和。
  • 既然这样,最早我们定义段的长度必须大于 1 1 1也就没有用了,也可以把它看做是一个段,只是它可能是由它儿子和父亲来炸的,因此所有的段都有 2 2 2状态可用,因为这样每个段的每一位都会和其他段相交。
  • 那么前面判断 2 2 2状态是否可用的部分可以删去,直接转移即可(不过点数会变多,未必更快)。
  • 仔细想一想其实不难推,时间复杂度 O ( n 2 ) O(n^2) O(n2)
  • 另:这题还有不用缩点的树形DP,不过转移比较奇怪??还有可以贪心?!?!

代码

#include
#include
#include
using namespace std;
#define N 55
char a[N][N];
int b1[N][N], b2[N][N], vi[N * N];
struct {
	int l, r, u, d;
}c[N][N];
int last[N * N], nxt[N * N * 2], to[N * N * 2], len = 0;
int f[N * N][3];
void add(int x, int y) {
	to[++len] = y;
	nxt[len] = last[x];
	last[x] = len;
}
void dfs(int k, int fa) {
	vi[k] = 1;
	f[k][2] = 0, f[k][0] = 1;
	int sum = 0;
	for(int i = last[k]; i; i = nxt[i]) if(to[i] != fa) {
		int x = to[i];
		dfs(x, k);
		f[k][2] += min(f[x][0], f[x][1]);
		sum += min(min(f[x][0], f[x][1]), f[x][2]);
	}
	f[k][0] += sum;
	f[k][1] = f[k][0];
	for(int i = last[k]; i; i = nxt[i]) if(to[i] != fa) {
		int x = to[i];
		f[k][1] = min(f[k][1], sum - min(min(f[x][0], f[x][1]), f[x][2]) + f[x][0]);
	}
}
int main() {
	int n, m, i, j;
	scanf("%d%d\n", &n, &m);
	for(i = 1; i <= n; i++) scanf("%s\n", a[i] + 1);
	for(i = 1; i <= n; i++) {
		for(j = 1; j <= m; j++) if(a[i][j] == '.') {
			if(j > 1 && a[i][j - 1] == '.') c[i][j].l = c[i][j - 1].l + 1; else c[i][j].l = 1;
		}
		for(j = m; j; j--) if(a[i][j] == '.') {
			if(j < m && a[i][j + 1] == '.') c[i][j].r = c[i][j + 1].r + 1; else c[i][j].r = 1;
		}
	}
	for(j = 1; j <= m; j++) {
		for(i = 1; i <= n; i++) if(a[i][j] == '.') {
			if(i > 1 && a[i - 1][j] == '.') c[i][j].u = c[i - 1][j].u + 1; else c[i][j].u = 1;
		}
		for(i = n; i; i--) if(a[i][j] == '.') {
			if(i < n && a[i + 1][j] == '.') c[i][j].d = c[i + 1][j].d + 1; else c[i][j].d = 1;
		}
	}
	int tot = 0;
	for(i = 1; i <= n; i++) {
		for(j = 1; j <= m; j++) if(a[i][j] == '.') {
			int u = b1[i][j - c[i][j].l + 1];
			if(!u) u = b1[i][j - c[i][j].l + 1] = ++tot;
			int v = b2[j][i - c[i][j].u + 1];
			if(!v) v = b2[j][i - c[i][j].u + 1] = ++tot;
			add(u, v), add(v, u);
		}
	}
	dfs(1, 0);
	printf("%d", min(min(f[1][0], f[1][1]), f[1][2]));
	return 0;
}

你可能感兴趣的:(题解,动态规划,JZOJ,树形DP,动态规划)