题干: https://leetcode.com/problems/minimum-cost-to-make-at-least-one-valid-path-in-a-grid/
又是这类grid的题目,虽然心里知道大概用类似动态规划的思路去做,但是做着就发现一个问题,stack overflow了,究其原因,是这题的直接子问题非常难以划分,例如我一开始尝试从某坐标[i,j]找其cost值时,我问自己,这个cost值是否跟该坐标的邻居处的cost值有相关性?看起来很直接啊,当然有,如果邻居的cost值都知道了,再根据本处的坐标指向(代表上下左右),就可以确定本处的最小cost值了,这看起来非常合理啊。可是再一细想就有问题,就是任意一处坐标其邻居最大有4个,最小有两个,并且这些邻居坐标并不必然保证问题规模递减,例如求[0,1]的邻居有[1,1],但同时也有[0,0]啊,而[0,0]坐标处的问题规模不比[0,1]小啊。
这样我们就没法像求fibonacci数列那样,用类似这种弄个dp[n]=dp[n-1]+dp[n-2]来算了,因为fibonacci的这个递推公式有着明显的规模递减的方向性,而本题没有,如果强行用这种递推公式,就会产生stack overflow,因为大小规模问题互相循环依赖嘛。
苦思冥想依然无果,后来索性在纸上画图找灵感,发现一个点,我们要找【0,0】处的cost,正着推比较难,会有上面的stackoverflow问题,那么反着来呢?例如我已经知道最右下角的点[m-1,n-1], 到自己的path的cost是0,那么我们就可以很容易地顺着指向该位置的箭头找出所有cost为0的坐标,找出了所有cost为0的坐标后,下一步,就可以通过找所有cost为0的位置的直接邻居,如果这些邻居的cost还未定的话,我们就可以确定这些邻居处的cost一定为1,因为最多只要改一下箭头反向指向自己为0的邻居,就可以顺着链条达到【m-1,n-1】点了,理解这一点光分析数字是不行的,需要画图,看几何意义,其实也不难理解,只要想象说箭头像流水一样,是否有一条连续的流水指向最终的[m-1,n-1]点即可。
找到了这些cost为1的邻居后并不代表着所有cost为1的点都找到了,还需要找出流水指向任意这些点的上游点,这样就找出来了所有cost为1的点。然后依次类推,可以找出所有cost为2的点,3的点等等,可以确定这样做下去,一定能找到某个cost为level的点集合是包含[0][0]处的点的,这样就可以停止计算了。
基于以上方法,只要写两个核心函数即可:
1,基于cost为k的点标记其所有未被标记的邻居的cost值为k+1
2, 找某点的上游流水中的所有点,并标记为跟当前点相同的cost(可以形象地描述为向上游染色)
代码如下:
package com.example.demo.leetcode;
/**
* leetcode 1368
*/
public class MinimumCostValidPath {
/**
* left upper grid[0][0]
* right bottom grid[m-1][n-1]
* direction: 1: right , 2: left, 3: down, 4: up, might start from boarder so could go out of board
* @param grid
* @return
*/
public int minCost(int[][] grid) {
int rowCnt = grid.length;
int colCnt = grid[0].length;
int[][] costtable = new int[rowCnt][colCnt];
for(int i = 0;i<rowCnt; i++){
for(int j=0;j<colCnt;j++){
costtable[i][j]=-1;
}
}
costtable[rowCnt-1][colCnt-1] = 0;
dye(costtable, grid);
for(int i=0;i<rowCnt;i++){
for(int j=0;j<colCnt;j++){
System.out.print(costtable[i][j]+" ");
}
System.out.println("");
}
System.out.println("======");
return costtable[0][0];
};
private void dyeDirectNeighbor(int[][] grid, int r, int c, int[][] costtable, int level){
int rowCnt = grid.length;
int colCnt = grid[0].length;
//上,如果存在,且costtable未设值,则设值其为level
if(r>0 && costtable[r-1][c]==-1){
costtable[r-1][c]=level;
}
//下
if(r<rowCnt-1 && costtable[r+1][c]==-1){
costtable[r+1][c]=level;
}
//左
if(c>0 && costtable[r][c-1]==-1){
costtable[r][c-1]=level;
}
//右
if(c<colCnt-1 && costtable[r][c+1]==-1){
costtable[r][c+1]=level;
}
}
/**
* find all those costtable elements that equals level, do dyechain for all of them
* @param grid
* @param costtable
* @param level
*/
private void bulkDyeChain(int[][] grid, int[][] costtable, int level){
int rowCnt = grid.length;
int colCnt = grid[0].length;
for(int i=0;i<rowCnt;i++){
for(int j = 0;j<colCnt;j++){
if(costtable[i][j]==level){
dyeChain(i,j,costtable, grid);
}
}
}
}
//找出所有cost=level的grid, 进行染色(只是个比喻,其实就是设置costtable相应坐标处的值
//如果这个过程中,计算值已经包含了[0][0]坐标,则返回true,说明计算结束了
//重要前提: 计算level=k时需要确保level=k-1的值已经计算过
private boolean innerDye(int[][] costtable, int[][] grid, int level){
int rowCnt = grid.length;
int colCnt = grid[0].length;
if(level==0){
dyeChain(rowCnt-1, colCnt-1, costtable, grid);
}else{
// 找出所有costtable值为level-1的坐标位置,染色其未被设值的costtable直接邻居坐标为level
for(int i=0;i<rowCnt;i++){
for(int j=0;j<colCnt;j++){
if(costtable[i][j]==level-1){
dyeDirectNeighbor(grid, i, j, costtable, level);
}
}
}
bulkDyeChain(grid, costtable, level);
}
if(costtable[0][0]==-1){
return false;
}else{
return true;
}
}
/**
*/
public void dye(int[][] costtable, int[][] grid){
int dyeLevel = 0;
boolean contains00 = false;
while(true){
contains00 = innerDye(costtable, grid, dyeLevel);
dyeLevel++;
if(contains00){
break;
}
}
}
// assume know costtable[r][c], for those pointing to [r][c], set the same value, recursively, only hande those point to me
public void dyeChain(int r, int c, int[][] costtable, int[][] grid){
int rowCnt = grid.length;
int colCnt = grid[0].length;
// check upper direction pointing downwards and not set
if(r>0 && grid[r-1][c]==3){
if(costtable[r-1][c]==-1){
costtable[r-1][c] = costtable[r][c];
dyeChain(r-1,c, costtable, grid);
}
}
// check bottom direction pointing upwards and not set
if(r<rowCnt-1 && grid[r+1][c]==4){
if(costtable[r+1][c]==-1){
costtable[r+1][c] = costtable[r][c];
dyeChain(r+1,c, costtable, grid);
}
}
// check left direction point right and not set
if(c>0 && grid[r][c-1]==1){
if(costtable[r][c-1]==-1){
costtable[r][c-1] = costtable[r][c];
dyeChain(r,c-1, costtable, grid);
}
}
// check right direction point left and not set
if(c<colCnt-1 && grid[r][c+1]==2){
if(costtable[r][c+1]==-1){
costtable[r][c+1] = costtable[r][c];
dyeChain(r,c+1, costtable, grid);
}
}
}
// todo think about edge case
public static void main(String[] args) {
MinimumCostValidPath demo = new MinimumCostValidPath();
int[][] grid = {{1,1,1,1},{2,2,2,2},{1,1,1,1},{2,2,2,2}};
int[][] grid2 = {{1,1,3},{3,2,2},{1,1,4}};
int[][] grid3 = {{1,2},{4,3}};
int[][] grid4 = {
{3,4,3},
{2,2,2},
{2,1,1},
{4,3,2},
{2,1,4},
{2,4,1},
{3,3,3},
{1,4,2},
{2,2,1},
{2,1,1},
{3,3,1},
{4,1,4},
{2,1,4},
{3,2,2},
{3,3,1},
{4,4,1},
{1,2,2},
{1,1,1},
{1,3,4},
{1,2,1},
{2,2,4},
{2,1,3},
{1,2,1},
{4,3,2},
{3,3,4},
{2,2,1},
{3,4,3},
{4,2,3},
{4,4,4}};
int ret = demo.minCost(grid);
int ret2 = demo.minCost(grid2);
int ret3 = demo.minCost(grid3);
int ret4 = demo.minCost(grid4);
System.out.println("Min cost 1: "+ret);
System.out.println("Min cost 2: "+ret2);
System.out.println("Min cost 3: "+ret3);
System.out.println("Min cost 4: "+ret4);
}
}
反思:
碰到容易产生循环依赖,stackoverflow的题目,就尝试从一端开始想起,想办法通过约束使得事情的进展朝着一个方向前进,而不是四处杂乱无章的方向