算法小结之动态规划
前言
在上篇讨论 递归 的文章中,最后留了一道八皇后的问题,我们很自然的可以想到一种思路:顺着棋盘的每一行或每一列,我们找到一个可以放置皇后的位置,连续找到 8 个位置就是一组解。
但和之前的三道递归问题有着明显的不同,每一次放置的棋子,不仅依赖于之前放置的棋子,同时也会影响剩下来的棋子,而且每次可以放置的位置并不都唯一。
八皇后
问题描述
在 8×8 格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
解题思路
按照前言中的思路,定义棋盘 bool[,] chessboard
,我们尝试一下:
先定义一个方法
bool SetQueens(bool[,] chessboard, int row)
, 这个方法表示在第row
行,放置一个皇后,如果找不到可以放置皇后的位置,则方法返回False
对于每一行而言,每一个格子都需要尝试放置皇后,我们再定义一个方法
bool CheckValid(bool[,] chessboard, int row, int col)
用来判断在row
行col
列放置的皇后是否合法如果找到一个合法位置,那么很显然我们要做的事情就是在下一行继续尝试放置皇后,同时,因为我们是在每一行的所有格子都要做一遍测试,所以在这一行的下一次尝试之前,我们需要取回这一次在这一行放置的棋子
1
2
3
4
5
6
7
8
9for (int col = 0; col < chessboard.GetLength(1); col++)
{
chessboard[row, col] = true;//放置皇后
if (CheckValid(chessboard, row, col))
{
SetQueens(chessboard, row + 1);//放置合法,到下一行放置皇后
}
chessboard[row, col] = false;//取回皇后,避免干扰同一行的下一次尝试
}不停的尝试下一行,直到放完所有行的时候,我们就得到了一个可行的解法
1
2
3
4if (row == chessboard.GetLength(0))
{
Result.Add((bool[,])chessboard.Clone());
}
整合一下,bool SetQueens(bool[,] chessboard, int row)
的方法体就是
1 | void SetQueens(bool[,] chessboard, int row = 0) |
当然,之前我们定义了 bool CheckValid(bool[,] chessboard, int row, int col)
,根据规则很容易理解就是横竖撇捺上不能有其他棋子。
- 横:由于我们是一行一行放置的,所以横一定是唯一的,就不用判断了;
- 竖:把这一列遍历一次,做判断;
- 撇:斜率为 1 且经过
(row, col)
这个点的直线,在正方形内的整数坐标,做判断; - 捺:斜率为 -1 且经过
(row, col)
这个点的直线,在正方形内的整数坐标,做判断;
根据这个思路,很容易写出方法体了:
1 | bool CheckValid(bool[,] chessboard, int row, int col) |
问题小结
代码:HT.Algorithm的 EightQueens
在这里不考虑对称的问题,八皇后有 92 组解。
八皇后这个问题在网上很容易找到许多简短的算法,像用一维数组做记录、位运算之类的,但本质上都是需要回溯的,其他算法的精妙之处在这里按下不表。
双蛋问题(鸡蛋掉落、鹰蛋问题)
为了能够自然的引入这个问题,又写递归,又写八皇后的,都感觉有点买椟还珠了。😂
其实是看了李永乐老师的一个视频,复工复产找工作?先来看看这道面试题:双蛋问题,想记录一下解答过程。
问题描述
问题大意:蛋在某层楼以下扔都不会碎,有 T
层楼,N
个蛋,在运气最差的情况下,找到临界楼层需至少要 M
次,求 M
的值。
定义 M(T,N)
表示这个答案。
视频中 11 分钟的表格我照抄一份:
T\N | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
3 | 3 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
4 | 4 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
5 | 5 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
6 | 6 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
7 | 7 | 4 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
8 | 8 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 |
9 | 9 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 |
解题思路
在 T
层楼中选择第 k
层扔蛋:
- 如果碎了,在
(1, k - 1)
之间,找出临界楼层还需要M(k - 1, N - 1)
次 - 如果没碎,在
(k, T)
之间,找出临界楼层还需要M(T - k, N)
次
考虑最坏情况,在 k
层开始扔,两种情况需要取次数较多的一种,加上在第 k
层的一次,即
Mk(T, N) = Max{ M(k - 1, N - 1), M(T - k, N) } + 1
那么,对于 k 而言,可以是 1 到 T 层中的任意一层,所以对应 M 的值有以下选择
k | 1 | 2 | 3 | … | T |
---|---|---|---|---|---|
M | M1 | M2 | M3 | … | MT |
根据题意,我们所求的 M,就是在 k 取[1,T]之间所对应的[M1,MT]中的最小值,即
M(T, N) = Min{ M1, M2, M3, …, MT }
那么,根据这个思路,就可以写出如下代码
1 | static int LeastTimes(int floor, int egg) |
问题小结
代码:HT.Algorithm的 EggDrop
顺着李老师的思路,这类问题应该不难理解,虽然我在大学里始终参不透玄机。
同款问题在LeetCodeCN-887. 鸡蛋掉落,很显然你不能直接用上面的算法妄想去 AC,脑补一下时间复杂度,应该是过不了任何一家 OJ 的。
鳄鱼小岛(与本文无关)
李老师留的课后习题,一并写了吧。网上也有说猫捉老鼠。
问题描述
李永乐老师:假如有一个圆形的小岛,然后有一条鳄鱼在圆形小岛周围游弋,鳄鱼的速度是人的四倍,而且鳄鱼总是希望找到一个离人最近的位置,而这个人最开始在这个小岛的正中心,那么我问这个人该如何运动,才能够比鳄鱼先到达这个岛的边缘,从而逃离这个岛呢?
解题思路
先做些设定,这个岛的半径为 r
米,人的速度为 v
米/秒,那么鳄鱼的速度为 4v
米/秒。
假设我就是那个被鳄鱼困在岛上的倒霉蛋,站在圆心,鳄鱼在正南方。
我想往正南方跑,那么我需要
r/v
秒的时间才能到,鳄鱼只需要(πr)/(4v)
秒,很显然鳄鱼比我快,这样跑是不行的冷静想一下,根据题意,鳄鱼一定会选择劣弧,我沿着半径直线跑不行,那我始终沿着距离鳄鱼最远的点跑吧,也就是说,我需要更换我的目标点,这个点使得鳄鱼必须要再跑
πr
的路程假设我更换目标点时,我已经跑了
m
米远,鳄鱼跑了n
米远,我因为目的地转换而产生的圆心角为α
,那么很显然,当α ≥ π/4
时,我接下来要跑的路程比半径还远,鳄鱼一定比我快
那么,换个思路想,是否存在一个角度,这个角度可以使得我沿半径方向在逐步靠近边缘,而始终让鳄鱼需要再走
πr
的距离,也就是,我和圆心和鳄鱼共线在同一条直径上很容易想到,在同一个圆心,半径为
r/4
的圆里,我是完全办得到的,因为在这个圆里,即便鳄鱼的速度是我的四倍,我跑的角速度也是大于或等于鳄鱼的角速度,使得三点共线那么,在我跑到
r/4
处,只要沿着圆心相反的方向再跑(3r)/4
的距离就可以跑到边缘,对于我而言,我所需要的时间是(3r)/(4v)
秒,而鳄鱼需要(πr)/(4v)
秒,显然我更快,那么我就成功逃离了
问题小结
这道题,算数学问题还是算物理问题呢?
后记
用递归的方法去解决问题,代码看起来很优雅,但到了像比较复杂的动态规划里,普通递归的代码就带来肉眼可见的时空间复杂度,这往往是不可接受的。
对于学习理解来说,这种写法尚可,但在实际解决问题时,需要去做优化,这种优化有的是基于强大的数学功底,例如
斐波那契数列:Math is fun
有的则是记忆、剪枝等等,罢了,不说了,接着留坑吧。