最近碰到一个算法题,开始觉得很简单,后来觉得有点难,后来做了做发现其实没有那么难,感觉有点意思。在这里分享一下这道题和解题思路。 这道题的题目如下:
一个n*m的网格,每个格子上有三个参数pD,pR,pS。pD表示下一步走到下方格子的概率,pR表示下一步走到右方格子的概率,pS表示下一步仍然在原地的概率。0<= pD, pR, pS <=1,pD+pR+pS = 1。从左上角格子到右下角格子的步数期望是多少?
看到网格,从左上角走到右下角,第一反应这就是一道常规的动态规划。这个反应没错,那么接下来就是怎么去推导状态转移方程。
如果设为从左上角到的步数期望,那么需要找到一种函数关系,使得: 注意题目中有个条件:就算停留在原地一次,也算走了一步。如果用传统的dp思维,那么从和到的状态转移方程就十分复杂,因为可能在格子上无限停留,得先求和再求极限。并且从到的步数期望并不为0(因为可能在左上角一直停留,也算步数),这个问题就难解了。所以,要再想想有没有简单的方法。 如果我们设期望为从到右下角的步数期望,那么问题就转换成从递推到,而是肯定为0的,问题突然开始出现了转机。 那么,考虑到会原地不动,我们先不急着得到状态转移方程,先列一下和什么相关。首先,思路是从右下角开始往左上递推,那么和有关的就是和了,可以得出如下的方程: 这个方程中,连自身都在等号右侧,我刚列出来的时候也感觉有点别扭,但这个题的核心就在这里要绕个弯。根据上面的方程,可以求出的通项为: 对于处于边缘区域的最下面的一行和最右边的一列,是分别没有和的,那么他们就没有这两项。所以总结出来通项就是: 如果i=n-1且j=m-1, 则 如果i=n-1,则 如果j=m-1,则 如果0<=i<n-1且0<=j<m-1,则
有了上面的思考和dp状态转移方程,代码就比较容易写了,这里我用Golang写了一个:
func solve(grid [][][3]float64) float64 {
n := len(grid)
if n == 0 {
return 0
}
m := len(grid[0])
dp := make([][]float64, n)
for i:=n-1; i>=0; i-- {
dp[i] = make([]float64, m)
if i == n-1 {
for j := m-2; j>=0; j-- {
dp[n-1][j] = (dp[n-1][j+1]*grid[n-1][j][1] + 1) / (1-grid[n-1][j][2])
}
} else {
dp[i][m-1] = (dp[i+1][m-1]*grid[i][m-1][0] + 1) / (1 - grid[i][m-1][2])
}
}
for i:=n-2; i>=0; i-- {
for j:=m-2; j>=0; j-- {
toDown := grid[i][j][0]
toRight := grid[i][j][1]
stop := grid[i][j][2]
dp[i][j] = (toDown*dp[i+1][j] + toRight*dp[i][j+1] + 1) / (1-stop)
}
}
return dp[0][0]
}
其实按照上面的思路,正推也可以推出来。如果设为从左上角到的步数期望,就会有: 会满足这个条件: 按照上文的思路,把的通项求出,也能从左上角推导到右下角。
写完不得不感叹,动态规划的算法题真的是千变万化,需要开动脑筋多想想。