在公司个一次team building中 马小哈同学提出了一个问题。
问题描述:
棋盘被分成n*n的格子,每个格子有若干米粒,有一只小鸡从左上角出发,移动到右下角,每次只能向右或者向下移动。
求一个算法,算法的输入是每个格子的米粒数,输出是一个“向右走”/“向下走”的指令序列,使得小鸡吃到的米粒数最大化。
不正真的绅士,很快给出求解, 博君一乐。
解答:
这题是一个中等难度的二维动态规划题。
(动态规划是一种有趣的解题技术,用来解运筹学中的最优化问题。有一类问题,用递归、回溯或者穷举来解会导致时间复杂度爆炸,而用贪心又可能陷入局部最
优。用动态规划解它
只需要多项式时间,且得到的是全局最优。用动态规划,一般把原问题划分成阶段,并且每个阶段都是一个一个同类型的但是规模更小的问题,从当前阶段做一个决
策后,就到了下一个阶段。首先,把从一个阶段到下一个阶段的状态转移情况用一个“状态转移方程”写出来。然后从最小的问题(也就是最终的阶段)出发,倒回
头来推原问题的解,把状态转移方程变化成一个规划方程,这样就把一个递归的问题变化成了递推的问题。解题原理和递归算法很像,区别是动态规划算法里得由程
序员维护一个表来保存和查询计算的中间状态,并且要手动的把递归问题变化成递推问题,因此动态规划比递归快很多,是一种用空间换时间的技术。建模往往很费
脑子,但是按照规划方程写出来的程序往往很短。)
推荐一本神书《How to Solve It》,需要的数学基础为0,讲解各种解题技巧,中间穿插各种趣味故事和智力题,完暴《编程之美》,曾经在不正真的绅士幼小的心灵中留下不可磨灭的痕迹,实为茶余饭后放松类读物的上佳选择。
英国女婿版 http://book.douban.com/subject/1456890/
踹你斯版 http://book.douban.com/subject/2124114/
举个例子,假设有一个5*5的矩阵
3 5 5 2 1
4 8 9 0 3
5 6 7 1 2
5 6 3 4 8
9 3 2 1 4
从(1,1)走到(5,5),每次只能向右或向下移动,累计走过的数字,求最大化累计数字的走法。
假设现在在(1,1),当前数字是3,可能的移动是到(1,2)的5或者(2,1)的4。然而,如果向右移动,就再也不可能吃到左下角的9了,似乎向下移动更好。移动到(2,1),当前是4,如果向下移动,就吃不到右边的8和9了,如果向右移动,就吃不到左下角的9了,似乎往右移动收益才更大。然而如果这回往右移动,就和刚才的第一步移动的理由矛盾了,第一步要往下移动的理由是要吃左下角的9,但是现在往右的话,就根本没去吃它,那第一步就没必要往下移动去吃4了,而是应该往右移动吃5才对。实际上,这个阵的最优移动方式是,3 右 5 下 8 右 9 下 7 下 3 右 4 右 8 下,最大收益是51,并没有吃到左下角的9。
逆向思考,从右下角的终点开始思考。现在从原矩阵的右下角开始划,分出一个1*1的子矩阵,
3 5 5 2 1
4 8 9 0 3
5 6 7 1 2
5 6 3 4 8
---
9 3 2 1 |4
假设我们走在这个子阵的左上角,要到右下角并且积累数字最大化,显然只有一种走法,最多能积累的数字和为4。
现在从原矩阵的右下角开始划,分出一个2*2的子矩阵,
3 5 5 2 1
4 8 9 0 3
5 6 7 1 2
----
5 6 3 |4 8
9 3 2 |1 4
单独考察这个子阵,假设当前在数字8的位置,因此下一步只能往下走,最终收益12,假设在数字1的位置,下一步只能往右,最终收益5。假设在4的位置,可以往右或者往下,往右的最终收益是4 + 12 = 16,比较大,所以应该往右。
现在,我们就得到了一个2*2的收益矩阵。
16 12
5 4
这个收益矩阵的每个元素表示从刚才划出的子矩的对应的某格子出发,可能吃掉的数字之和的最大值。
现在从原矩阵的右下角开始划,分出一个3*3的子矩阵,
3 5 5 2 1
4 8 9 0 3
------
5 6 |7 1 2
5 6 |3 4 8
9 3 |2 1 4
对应的收益矩阵是,
? ? ?
? 16 12
? 5 4
和刚才的计算方法一样,首先算右上角,等于子阵的右上角元素2加上其下元素8所可能带来的最大收益,而这个最大收益已经在2*2的收益矩阵里算过了,为12。所以收益阵的右上角是2 + 12 = 14。
? ? 14
? 16 12
? 5 4
收益阵的元素(1,2)应该等于子阵的元素(1,2) + max{收益阵(2,2)=16, 收益阵(1,3)=14},结果为17。
? 17 14
? 16 12
? 5 4
接着就按照类似的道理把3*3的收益阵算完,得到
26 17 14
19 16 12
7 5 4
接着再用同样的办法算4*4的子阵和收益阵,一直算下去。最后得到5*5的收益阵如下:
51 48 40 20 18
47 43 35 17 17
37 32 26 17 14
30 25 19 16 12
19 10 7 5 4
收益阵的每个元素代表了从原矩阵对应元素开始走,可能吃到的数字和的最大值。因此,在举例的5*5矩阵中,能吃到的数字和最大是51。现在就可以很简单的根据收益阵来输出小鸡行动的指令,从左上角开始,选数字大的格子往右或者往下走就行了。
Python程序chicken.py:
def init_nn_matrix(n):
''' Return an n*n zeroed matrix. '''
return [[0] * n for i in range(0, n)]
def potential_gain(m):
''' Calculate max potention gain for each grid. '''
n = len(m) # assume m is an n*n matrix
r = init_nn_matrix(n)
r[n-1][n-1] = m[n-1][n-1]
for s in range(2, n + 1):
# deal with an s*s sub-matrix
si = sj = n - s
r[si][n-1] = m[si][n-1] + r[si+1][n-1]
for j in range(n - 2, sj, -1):
r[si][j] = m[si][j] + max(r[si][j+1], r[si+1][j])
r[n-1][sj] = m[n-1][sj] + r[n-1][sj+1]
for i in range(n - 2, si, -1):
r[i][sj] = m[i][sj] + max(r[i+1][sj], r[i][sj+1])
r[si][sj] = m[si][sj] + max(r[si][sj+1], r[si+1][sj])
return r
def walk_matrix(gain):
directions = []
n = len(gain)
i = j = 0
while i != n - 1 or j != n - 1:
if i == n - 1:
directions.append('-')
j = j + 1
continue
if j == n - 1:
directions.append('|')
i = i + 1
continue
if gain[i+1][j] > gain[i][j+1]:
directions.append('|')
i = i + 1
else:
directions.append('-')
j = j + 1
return directions
def solve_matrix(m):
print 'matrix'
for row in m:
for col in row:
print col,
gain = potential_gain(m)
print 'potential gain matrix'
for row in gain:
for col in row:
print col,
print "Directions:"
print ' '.join(walk_matrix(gain))
print 'Total:', gain[0][0]
if __name__ == "__main__":
m = [[3, 5, 5, 2, 1, ],
[4, 8, 9, 0, 3, ],
[5, 6, 7, 1, 2, ],
[5, 6, 3, 4, 8, ],
[9, 3, 2, 1, 4, ], ]
solve_matrix(m)
m = [[0, 0, 0, 0, 0, ],
[0, 0, 0, 0, 0, ],
[0, 0, 0, 0, 0, ],
[0, 0, 0, 0, 0, ],
[0, 0, 0, 0, 0, ], ]
solve_matrix(m)
m = [[1, 0, 0, 0, 0, ],
[0, 1, 0, 0, 0, ],
[0, 0, 1, 0, 0, ],
[0, 0, 0, 1, 0, ],
[0, 0, 0, 0, 1, ], ]
solve_matrix(m)
m = [[1, 1, 1, 1, 1, ],
[0, 0, 0, 0, 1, ],
[0, 0, 0, 0, 1, ],
[0, 0, 0, 0, 1, ],
[0, 0, 0, 0, 1, ], ]
solve_matrix(m)
m = [[1, 0, 0, 0, 0, ],
[1, 0, 0, 0, 0, ],
[1, 0, 0, 0, 0, ],
[1, 0, 0, 0, 0, ],
[1, 1, 1, 1, 1, ], ]
solve_matrix(m)
m = [[3, 4, 5, 2, 1, ],
[4, 8, 9, 0, 3, ],
[5, 6, 7, 1, 2, ],
[5, 6, 3, 4, 8, ],
[99, 3, 2, 1, 4, ], ]
solve_matrix(m)
运行
$
python chicken.py
输出结果中,“-”表示向右移动,“|”表示向下移动。
matrix
3 5 5 2 1
4 8 9 0 3
5 6 7 1 2
5 6 3 4 8
9 3 2 1 4
potential gain matrix
51 48 40 20 18
47 43 35 17 17
37 32 26 17 14
30 25 19 16 12
19 10 7 5 4
Directions:
- | - | | - - |
Total: 51
matrix
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
potential gain matrix
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
Directions:
- - - - | | | |
Total: 0
matrix
1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 1 0
0 0 0 0 1
potential gain matrix
5 4 3 2 1
4 4 3 2 1
3 3 3 2 1
2 2 2 2 1
1 1 1 1 1
Directions:
- | - | - | - |
Total: 5
matrix
1 1 1 1 1
0 0 0 0 1
0 0 0 0 1
0 0 0 0 1
0 0 0 0 1
potential gain matrix
9 8 7 6 5
4 4 4 4 4
3 3 3 3 3
2 2 2 2 2
1 1 1 1 1
Directions:
- - - - | | | |
Total: 9
matrix
1 0 0 0 0
1 0 0 0 0
1 0 0 0 0
1 0 0 0 0
1 1 1 1 1
potential gain matrix
9 4 3 2 1
8 4 3 2 1
7 4 3 2 1
6 4 3 2 1
5 4 3 2 1
Directions:
| | | | - - - -
Total: 9
matrix
3 4 5 2 1
4 8 9 0 3
5 6 7 1 2
5 6 3 4 8
99 3 2 1 4
potential gain matrix
126 47 40 20 18
123 43 35 17 17
119 32 26 17 14
114 25 19 16 12
109 10 7 5 4
Directions:
| | | | - - - -
Total: 126