­

动态规划之棋盘路径问题

  • 2019 年 10 月 6 日
  • 筆記

动态规划之棋盘路径问题

1.对比

DP vs 回溯 vs 贪心

  • 回溯(递归) – 重复计算
  • 贪心 – 永远局部最优
  • DP – 记录局部最优子结构/多种记录值

2.棋盘路径问题

问题描述:

如下图所示,小人从左上角移动到右下角,总共有多少种走法.

约束条件为:粉红色为石头,走不通,小人走路方向只有向下与向右.

该问题可以分解f(0,0)=0,f(1,0)=1,f(0,1)=1 f(m,n)=f(m-1,n)+f(m,n-1)(此时m,n不为0)。

0(A)

1

1

2(B)

如上表所示为从棋盘中取出的左上角4个格子,填充的数据中第二行第二列(index假设从1开始)为2,表示从A到B有2种路径,依次往下走,最终得到f(m,n)=f(m-1,n)+f(m,n-1)(此时m,n不为0)。

因此该问题是递归问题,同时可以通过动态规划解决。

3.判断是石头还是空地

上述棋盘有个很强的约束条件,就是棋盘的约束问题,粉红色假设为石头,没有粉红色为空地,那我们就是要找到空地,从空地进行行走,下面来编写这样的函数。

棋盘假设为grid的二维数组,共有m行,n列。生成的二维数组中1表示石头,0表示空位。

棋盘生成代码:

import numpy as np  m,n = 8,8  grid = np.zeros((m,n),dtype=np.int)  for i in range(m):      for j in range(n):          if (i==1 and (j==2 or j==6)) or (i==2 and j==4) or (i==3 and (j==0 or j==2 or j==5))                  or (i==4 and j==2) or (i==5 and (j==3 or j==4 or j==6)) or (i==6 and (j==1 or j==5)):              grid[i][j]=1  print(grid)  

输出:

[[0 0 0 0 0 0 0 0]   [0 0 1 0 0 0 1 0]   [0 0 0 0 1 0 0 0]   [1 0 1 0 0 1 0 0]   [0 0 1 0 0 0 0 0]   [0 0 0 1 1 0 1 0]   [0 1 0 0 0 1 0 0]   [0 0 0 0 0 0 0 0]]  

判断该棋盘中某行某列是否是空地:

# 判断是否是空地  def validSquare(grid,m,n):      if grid[m][n]==1:          return False      return True  

4.递归法

  • 每次先判断当前是否是空地
  • 左上角处理
  • 每次递归都可以划分为(1,0)或(0,1)子问题
  • 第一行与第一列递归
  • 中间行列数据递归
def countPath(grid,m,n):      # 是否为空地,grid中0为空地,1为非空地      if not validSquare(grid,m,n):          return 0         # 左上角特殊处理      if (m==0 and n==0):          return 0      # (0,1)与(1,0)为1      if ((m == 0 and n == 1) or (m == 1 and n == 0)):          return 1      # 第一行,第一列特殊处理      elif ((m == 0) and (n>1)):          return countPath(grid, m, n - 1)      elif ((n == 0) and (m>1)):          return countPath(grid,m-1,n)      # 其他则递归      return countPath(grid,m, n - 1) + countPath(grid,m - 1, n)  # 申请dp数组  dp = np.zeros((m,n),dtype=np.int)  print(countPath(grid,m-1,n-1))  

5.动态规划

从左上角到右下角直接使用递推式,找到动态规划的状态转移方程,然后返回最后的一个数据即可。

我们知道当第一行或者第一列中有石头的时候,随之后面的行或者列就走不通了,那么直接设为0即可。

所以第一步先堆特殊的第一行与第一列进行处理。

然后堆中间行与列处理。

def countPath2(grid,m,n,dp):      # 处理每一行      for i in range(m):          if not validSquare(grid,i,0):              for j in range(i,m):                  dp[j][0]=0              break          dp[i][0]=1      # 处理每一列      for i in range(n):          if not validSquare(grid,0,i):              for j in range(i,m):                  dp[0][j]=0              break          dp[0][i]=1      # 左上角处理      dp[0][0]=0      for i in range(1,m):          for j in range(1,n):              if validSquare(grid,i,j):                  dp[i][j]=dp[i-1][j]+dp[i][j-1]              else:                  dp[i][j]=0      return dp[m-1][n-1]  

由于从左上角到右下角与从右下角到左上角路径对称,那么我们可以再写一个从右下角到左上角来验证之前的代码正确性,如果正常,则两次结果是一样的!

下面为从右下角到左上角的动态规划代码:

def countPath1(grid,m,n,dp):      # 处理每一行      for i in range(m-1,-1,-1):          if not validSquare(grid,i,n-1):              for j in range(i,m):                  dp[j][n-1]=0              break          dp[i][n-1]=1      # 处理每一列      for i in range(n-1,-1,-1):          if not validSquare(grid,m-1,i):              for j in range(i,m):                  dp[m-1][j]=0              break          dp[m-1][i]=1      dp[m-1][n-1]=0      for i in range(m-2,-1,-1):          for j in range(n-2,-1,-1):                  if validSquare(grid,i,j):                      dp[i][j]=dp[i+1][j]+dp[i][j+1]                  else:                      dp[i][j]=0      return dp[0][0]