LCP 38-守卫城堡
城堡守卫游戏的胜利条件为使恶魔无法从出生点到达城堡。游戏地图可视作 2*N
的方格图,记作字符串数组 grid
,其中: - "."
表示恶魔可随意通行的平地; - "#"
表示恶魔不可通过的障碍物,玩家可通过在 平地 上设置障碍物,即将 "."
变为 "#"
以阻挡恶魔前进; - "S"
表示恶魔出生点,将有大量的恶魔该点生成,恶魔可向上/向下/向左/向右移动,且无法移动至地图外; - "P"
表示瞬移点,移动到 "P"
点的恶魔可被传送至任意一个 "P"
点,也可选择不传送; - "C"
表示城堡。
然而在游戏中用于建造障碍物的金钱是有限的,请返回玩家最少需要放置几个障碍物才能获得胜利。若无论怎样放置障碍物均无法获胜,请返回 -1
。 注意:
- 地图上可能有一个或多个出生点 - 地图上有且只有一个城堡 示例 1 >输入:grid = ["S.C.P#P.", ".....#.S"]
> >输出:3
> >解释:至少需要放置三个障碍物 ![image.png](https://pic.leetcode-
cn.com/1614828255-uuNdNJ-image.png) 示例 2: >输入:grid = ["SP#P..P#PC#.S", "..#P..P####.#"]
> >输出:-1
> >解释:无论怎样修筑障碍物,均无法阻挡最左侧出生的恶魔到达城堡位置
示例
3: >输入:grid = ["SP#.C.#PS", "P.#...#.P"]
> >输出:0
> >解释:无需放置障碍物即可获得胜利
示例
4: >输入:grid = ["CP.#.P.", "...S..S"]
> >输出:4
> >解释:至少需要放置 4
个障碍物,示意图为放置方法之一 ![image.png](https://pic.leetcode-cn.com/1614828218-sIAYkb-
image.png) 提示: - grid.length == 2
- 2 <= grid[0].length == grid[1].length <= 10^4
- grid[i][j]
仅包含字符 "."
、"#"
、"C"
、"P"
、"S"
前言
由于本题是「力扣杯」的竞赛题,因此只会给出提示、简要思路以及代码,不会对算法本身进行详细说明,希望读者多多思考。
方法一:拆点 + 最小割
提示 1
我们希望「恶魔」无法到达城堡,即「城堡」和「恶魔」之间不连通,对应到图论模型上就是「割」问题。
思路
使用最小割建模:
- 将每一个非「障碍物」的格子拆分成两个点,一个作为「起点」一个作为「终点」。
- 如果该格子是「空地」,连接一条流量为 1 的边。
- 否则,连接一条流量为 \infty 的边。
- 相邻两个格子之间,从一个格子的「终点」到另一个格子的「起点」连接一条流量为 \infty 的边。
- 对于所有「传送门」,它们的「终点」向「超级传送门节点」连接一条流量为 \infty 的边,并从该节点连回「起点」一条流量为 \infty 的边。
- 所有「恶魔」的「终点」向「超级恶魔节点」连接一条流量为 \infty 的边。
以「城堡」的「起点」为源点,「超级恶魔节点」为汇点,求出最小割。根据最小割最大流定理,求出最大流即可。如果最大流为 \infty,说明答案为 -1。
代码
下面的最大流模板参考自 AtCoder Library 。
1 | namespace atcoder { |
方法二:动态规划
提示 1
如果「恶魔」能够走到「传送门」,那么「恶魔」就会支配所有的「传送门」。
因此,我们可以考虑两种情况:即允许「恶魔」走到「传送门」,或者不允许「恶魔」走到「传送门」。
- 对于第一种情况,我们可以将所有的「传送门」全部看成「恶魔」;
- 对于第二种情况,我们可以将所有的「传送门」全部看成「城堡」。
此时,网格中就只有「城堡」「恶魔」「空地」「障碍物」了,我们需要做的就是把「城堡」和「恶魔」之间互相分开。
提示 2
使用两次动态规划解决上述问题,每一次动态规划考虑一种情况。
思路
在动态规划之前(将「传送门」替换成「恶魔」或「城堡」之后),我们首先需要判断网格中是否有相邻的「城堡」和「恶魔」。如果有,那么显然是无法将它们分开的,因此无解;如果没有,那么把所有的「空地」都放上「障碍物」,显然可以将它们分开的,因此存在解。
用 f[i][s_1][s_2] 表示当前处理到第 i 列,并且第 i 列的两个格子的状态分别是 s_1 和 s_2 时,最小需要将「空地」放上「障碍物」的操作次数。状态有 4 种:
- 0 表示「空地」
- 1 表示「城堡」或者之前的列存在「城堡」可以到达此位置
- 2 表示「恶魔」或者之前的列存在「恶魔」可以到达此位置
- 3 表示「障碍物」
任意两种状态之间都是相互独立的。
使用该状态的表示方法进行动态规划即可,其中的细节较为复杂,希望读者可以结合下面的细节部分以及代码部分自行思考。
细节
这里介绍一种相对容易实现的状态转移方法。
设第 i-1 列的 f 状态为 s_1 和 s_2,第 i 列本身的两个格子(不考虑第 i-1 列的影响)的状态为 t_1 和 t_2,我们需要判断 s_1, s_2 是否可以和 t_1, t_2 成为相邻的两列。如果可以,那么 s_1, s_2 会对 t_1, t_2 产生影响(例如 s_1=1 且 t_1=0,那么「城堡」就可以到达 t_1 的位置,会将 t_1 的值更新为 1)。设影响后的状态为 t_1’, t_2’,那么就有状态转移方程:
f[i][t_1’][t_2’] = \min\big{ f[i-1][s_1][s_2] + \Delta(t_1, t_2) \big}
其中 \Delta(t_1, t_2) 表示我们额外在第 i 列放置的「障碍物」数量。
代码
1 | int f[10010][4][4]; |