《动态规划路径问题》


动态规划路径问题

核心思想

  • 拆分子问题,记住过往,减少重复计算

就下面9道题而言,个人总结出来的解题经验主要在于:

  • 明确状态量;
  • 确定状态转移方程;
  • 确定初始状态或初始值以及边界条件;

1. 不同路径

题目描述:一个 mxn 的棋盘,每次只能向右走或向下走,问到达右下角有多少种走法?

状态转移方程: f(m,n) = f(m-1,n)+f(m,n-1),其中 f(0,n) = 1,f(m,0) = 1;

class Solution {
public:
    int uniquePaths(int m, int n) {
        std::vector<int> dp(n);
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (i == 0 || j == 0) {
                    dp[j] = 1; // f(0,n) 与 f(m,0) 都为 1
                } else {
                    dp[j] += dp[j - 1]; // f(m,n) = f(m-1,n)+f(m,n-1) 此处将二维数组优化为了1维数组
                }
            }
        }
        return dp[n - 1];
    }
};

2. 不同路径 II

题目描述:一个 mxn 的棋盘,每次只能向右走或向下走,但是现在棋盘上有了障碍物,arr[i,j]=1 表示障碍物,arr[i,j]=0 表示可通行,问到达右下角有多少种走法?

状态转移方程: f(m,n) = f(m-1,n)+f(m,n-1), f(m,0) = f(m-1,0)、f(0,n) = f(0,n-1)

如果 arr[i,j] == 1 则 f(m,n) = 0

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>> &obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
        std::vector<int> dp(n);
        dp[0] = 1; // 初始化默认为 1
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                // 存在障碍物,则此格为 0
                if (obstacleGrid[i][j] == 1) {
                    dp[j] = 0;
                    continue;
                }
                if (i > 0 && j > 0) {
                    dp[j] += dp[j - 1]; // f(m,n) = f(m-1,n)+f(m,n-1)
                } else if (i > 0) {
                    dp[j] = dp[0]; // j = 0 则只能向下走,f(m,n) = f(m-1,0)
                } else if (j > 0) {
                    dp[j] = dp[j - 1]; // i = 0 则只能向右走,f(m,n) = f(0,n-1)
                }
            }
        }
        return dp[n - 1];
    }
};

3. 最小路径和

题目描述:一个包含非负整数的 mxn 的网格,每次只能向右走或向下走,找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

状态转移方程:

f(m,n) = min(f(m-1,n), f(m,n-1)) + arr[m,n]

f(0,n) = f(0,n-1) + arr[0,n]

f(m,0) = f(m-1,0) + arr[m,0]

class Solution {
public:
    int minPathSum(vector<vector<int>> &grid) {
        int m = grid.size();
        int n = grid[0].size();
        std::vector<int> dp(n);
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (i > 0 && j > 0) {
                    // f(m,n) =  min(f(m-1,n), f(m,n-1)) + arr[m,n]
                    dp[j] = std::min(dp[j - 1], dp[j]) + grid[i][j];
                } else if (i > 0) {
                    // j = 0 则只能向下走,f(m,n) = f(m-1,0) + arr[m,n]
                    dp[j] = dp[j] + grid[i][j];
                } else if (j > 0) {
                    // i = 0 则只能向右走,f(m,n) = f(0,n-1) + arr[m,n]
                    dp[j] = dp[j - 1] + grid[i][j];
                } else {
                    // i j 都为0,则 f(m,n) = arr[0,0]
                    dp[j] = grid[i][j];
                }
            }
        }
        return dp[n - 1];
    }    
};

4. 三角形最小路径和

题目描述:现在将网格变为三角形,求出最小路径和?图解转换:

   2       		 	    2
  3 4      -------->    3 4
 6 5 7     transfrom    6 5 7
4 1 8 3    			    4 1 8 3 

也就是说,当前元素可以连接左上方的元素或上方元素;所以状态方程:

f(i,j) = min(f(i-1,j-1), f(i-1,j)) + arr(i, j)

只能连接上方元素:f(i,0) = f(i-1,0) + arr(i, j)

只能连接左上方元素:f(i,j) = f(i-1,j-1) + arr(i, j)

注意:更新 f(i,j) 时从右向左更新

class Solution {
public:
    int minimumTotal(vector<vector<int>> &triangle) {
        int n = triangle[triangle.size() - 1].size();
        std::vector<int> dp(n);
        // 本题在仅用一维数组存储时更新dp要从右向左,从左向右的话存在覆盖现象;
        for (int i = 0; i < n; ++i) {
            for (int j = triangle[i].size() - 1; j >= 0; --j) {
                if (i > 0 && j > 0 && j < triangle[i].size() - 1) {
                    // 可以连接正上方与左上方元素,f(i,j) = min(f(i-1,j-1), f(i-1,j)) + arr(i, j)
                    dp[j] = std::min(dp[j - 1], dp[j]) + triangle[i][j];
                } else if (i > 0 && j == 0) {
                    // 只能连接正上方元素,f(i,0) = f(i-1,0) + arr(i, j)
                    dp[j] = dp[j] + triangle[i][j];
                } else if (i > 0 && j > 0 && j == triangle[i].size() - 1) {
                    // 只能连接左上方元素,f(i,j) = f(i-1,j-1) + arr(i, j)
                    // 从左向右的话,这里的[j-1]不再是上一层的了,是本层更新后的值了,就产生了覆盖
                    dp[j] = dp[j - 1] + triangle[i][j];
                } else {
                    // i j 都为0,则 f(m,n) = arr(0, 0)
                    dp[j] = triangle[i][j];
                }
            }
        }
        return *std::min_element(dp.begin(), dp.end());
    }  
};

5. 下降路径最小和

题目描述:方形网格(nxn),找出下降路径的最小和。下降路径指当前元素可以连接左上方、正上方、右上方三个元素中的一个。状态转移方程:

f(i,j) = min(f(i-1,j-1), f(i-1,j), f(i-1,j+1)) + arr[i,j]

最左侧(j=0):f(i,0) = min(f(i-1,0), f(i-1,1)) + arr[i,j]

最右侧(j=n):f(i,n) = min(f(i-1,n-1), f(i-1,n)) + arr[i,j]

class Solution {
public:
    int minFallingPathSum(vector<vector<int>> &matrix) {
        int n = matrix.size();
        /*
         * 此处没有再用一维数组,因为一维数组存在覆盖现象,需要用一个变量去记录当前状态左侧未更新前的值。
         * 也没有在原数组上直接更新数值,是因为传参为引用,为了避免修改原始数据,所以用了一个二维数组。
         */
        std::vector<std::vector<int>> dp(2, std::vector<int>(n));

        for (int i = 0; i < n; ++i) {
            // int pre = 0;
            for (int j = 0; j < n; ++j) {
                if (i > 0 && j > 0 && j < n - 1) {
                    // f(i,j) = min(f(i-1,j-1), f(i-1,j), f(i-1,j+1)) + arr[i,j]
                    dp[i % 2][j] = std::min(dp[(i - 1) % 2][j - 1], std::min(dp[(i - 1) % 2][j], dp[(i - 1) % 2][j + 1])) + matrix[i][j];
                } else if (i > 0 && j == 0) {
                    // 最左侧(j=0):f(i,0) = min(f(i-1,0), f(i-1,1))  + arr[i,j]
                    dp[i % 2][j] = std::min(dp[(i - 1) % 2][j], dp[(i - 1) % 2][j + 1]) + matrix[i][j];
                } else if (i > 0 && j == n - 1) {
                    // 最右侧(j=n):f(i,n) = min(f(i-1,n-1), f(i-1,n))  + arr[i,j]
                    dp[i % 2][j] = std::min(dp[(i - 1) % 2][j - 1], dp[(i - 1) % 2][j]) + matrix[i][j];
                } else {
                    dp[i % 2][j] = matrix[i][j];
                }
                // pre = dp[j]; 此处记录左侧的状态值,当下次循环时dp[j-1]用pre替代即可
            }
        }
        return *std::min_element(dp[(n + 1) % 2].begin(), dp[(n + 1) % 2].end());
    }
};

6. 下降路径最小和 II

题目描述:方形网格(nxn),找出 非零偏移下降路径 的最小和。非零偏移下降路径指从 grid 数组中的每一行选择一个数字,且按顺序选出来的数字中,相邻数字不在原数组的同一列 。状态转移方程:

f(i,j) = min(f(i-1,0), …, f(i-1,j-1), f(i-1,j+1), …, f(i-1,n)) + arr[i,j]

最左侧(j=0):f(i,0) = min(f(i-1,1), …, f(i-1,n)) + arr[i,j]

最右侧(j=n):f(i,n) = min(f(i-1,0), …, f(i-1,n-1)) + arr[i,j]

class Solution {
public:
	int minFallingPathSum(vector<vector<int>> &grid) {
        int n = grid.size();
        std::vector<std::vector<int>> dp(2, std::vector<int>(n));
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                if (i > 0 && j > 0 && j < n - 1) {
                    // f(i,j) = min(f(i-1,0), …, f(i-1,j-1), f(i-1,j+1), …, f(i-1,n)) + arr[i,j]
                    dp[i % 2][j] = std::min(*std::min_element(dp[(i + 1) % 2].begin(), dp[(i + 1) % 2].begin() + j),
                                            *std::min_element(dp[(i + 1) % 2].begin() + j + 1, dp[(i + 1) % 2].begin() + n))
                                   + grid[i][j];
                } else if (i > 0 && j == 0) {
                    // 最左侧(j=0):f(i,0) = min(f(i-1,1), …, f(i-1,n))  + arr[i,j]
                    dp[i % 2][j] = *std::min_element(dp[(i + 1) % 2].begin() + 1, dp[(i + 1) % 2].begin() + n) + grid[i][j];
                } else if (i > 0 && j == n - 1) {
                    // 最右侧(j=n):f(i,n) = min(f(i-1,0), …, f(i-1,n-1))  + arr[i,j]
                    dp[i % 2][j] = *std::min_element(dp[(i + 1) % 2].begin(), dp[(i + 1) % 2].begin() + n - 1) + grid[i][j];
                } else {
                    dp[i % 2][j] = grid[i][j];
                }
            }
        }
        return *std::min_element(dp[(n + 1) % 2].begin(), dp[(n + 1) % 2].end());
    }
};

7. 统计所有可行路径

题目描述:给你一个互不相同的整数数组,其中 locations[i] 表示第 i 个城市的位置。同时给你 start,finish 和 fuel 分别表示出发城市、目的地城市和你初始拥有的汽油总量。

每一步中,如果你在城市 i ,你可以选择任意一个城市 j ,满足 j != i 且 0 <= j < locations.length ,并移动到城市 j 。从城市 i 移动到 j 消耗的汽油量为 |locations[i] - locations[j]||x| 表示 x 的绝对值。

注意:fuel 任何时刻都不能为负,且可以经过任意城市超过一次(包括 start 和 finish )。

请返回从 start 到 finish 所有可能路径的数目。

思路一:

递归+回溯:没有缓存重复运算,过不了复杂度。

void dfs(vector<int> &locations, int start, int finish, int fuel, int &res) {
    if (fuel < 0) {
        return;
    } else if (fuel >= 0 && start == finish) {
        res += 1;
    }
    for (int i = 0; i < locations.size(); i++) {
        if (i == start) { continue; }
        fuel -= std::abs(locations[i] - locations[start]);
        dfs(locations, i, finish, fuel, res);
        fuel += std::abs(locations[i] - locations[start]);
    }
}
int countRoutes(vector<int> &locations, int start, int finish, int fuel) {
    int res = 0;
    dfs(locations, start, finish, fuel, res);
    return res;
}

思路二:

记忆化搜索:状态量有起始位置与剩余燃料,用dp[i, j]表示当前位置与剩余燃料

int dfs(vector<int> &locations, int start, int finish, int fuel, vector<vector<int>> &memo) {
    if (fuel < 0) {
        return 0;
    }
    // 如果当前这个位置,当前剩余燃料值出现过,直接返回数组中记录的值
    if (memo[start][fuel] != -1) {
        return memo[start][fuel];
    }
    // 起始位置==结束位置,表示走到了,res = 1;
    int res = (start == finish) ? 1 : 0;
    for (int i = 0; i < locations.size(); ++i) {
        if (i != start) {
            int cost = std::abs(locations[i] - locations[start]);
            res = (res + dfs(locations, i, finish, fuel - cost, memo)) % 1000000007;
        }
    }
    // 记录当前这种状态的结果
    memo[start][fuel] = res;
    return res;
}
int countRoutes(vector<int> &locations, int start, int finish, int fuel) {
    int n = locations.size();
    vector<vector<int>> memo(n, vector<int>(fuel + 1, -1)); // 初始化状态表,默认用-1填充
    return dfs(locations, start, finish, fuel, memo);
}

思路三:

动态规划:和记忆化搜索相同,状态还是当前位置与油量;状态转移方程为:

dp[pos, fuel] = dp[pos, fuel] + dp[d_pos, d_fuel]

int countRoutes(vector<int> &locations, int start, int finish, int fuel) {
    int n = locations.size();
    // 状态量:当前城市位置、所剩余的油量,dp[pos, fuel] 表示在pos这个城市油量为fuel时的路径数
    std::vector<std::vector<int>> dp(n, std::vector<int>(fuel + 1));
    // dp数组初始化,当前位置就是终点位置时,
    for (int i = 0; i < fuel + 1; ++i) { dp[finish][i] = 1; }

    // 状态转移方程:dp[pos, fuel] = dp[pos, fuel] + dp[d_pos, d_fuel]
    // 表示从 d_pos 移动到 pos,油量从 d_fuel 下降到 fuel
    for (int i = 0; i < fuel + 1; ++i) {
        for (int j = 0; j < n; ++j) {
            for (int k = 0; k < n; ++k) {
                if (j == k) { continue; }
                int need = std::abs(locations[k] - locations[j]);
                if (i >= need) {
                    dp[j][i] += dp[k][i - need];
                    dp[j][i] %= 1000000007;
                }
            }
        }
    }
    return dp[start][fuel];
}

8. 出界的路径数

题目描述:给你一个大小为 m x n 的网格和一个球。球的起始坐标为 [startRow, startColumn] 。你可以将球移到在四个方向上相邻的单元格内(可以穿过网格边界到达网格之外)。你 最多 可以移动 maxMove 次球。

给你五个整数 m、n、maxMove、startRow 以及 startColumn ,找出并返回可以将球移出边界的路径数量。因为答案可能非常大,返回对 109 + 7 取余 后的结果。

动态规划:

  • 状态量:当前位置、最大移动次数
  • 状态转移方程:dp[i,j][step] = dp[i-1,j][step-1]+dp[i,j+1][step-1]+dp[i+1,j][step-1]+dp[i,j-1][step-1];当 step 为 0 时,dp[i,j][step] = 0;
void dp_init(int i, int j, std::vector<std::vector<int>> &dp, int col) {
    int maxStep = dp[0].size();
    int idx = idx_encoder(i, j, col);
    for (int i = 1; i < maxStep; ++i) {
        dp[idx][i]++;
    }
}

int idx_encoder(int i, int j, int col) {
    int idx = i * col + j;
    return idx;
}

std::pair<int, int> idx_decoder(int idx, int col) {
    int x = idx / col;
    int y = idx % col;
    return std::make_pair(x, y);
}

int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
    // 将二维矩阵拉伸成一维矩阵,这样第一维表示位置,第二维表示最大移动次数
    // 创建dp数组,行表示每个格子的id,列表示移动次数,列数为 maxMove+1;
    std::vector<std::vector<int>> dp(m * n, std::vector<int>(maxMove + 1));
    // 初始化dp数组,四个角与四条边上的元素赋初值
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            if (i == 0) { dp_init(i, j, dp, n); }
            if (i == m - 1) { dp_init(i, j, dp, n); }
            if (j == 0) { dp_init(i, j, dp, n); }
            if (j == n - 1) { dp_init(i, j, dp, n); }
        }
    }
    // 定义转移方向:上左下右
    std::vector<std::pair<int, int>> directions = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
    /*
	 * 状态转移方程:
     *  dp[i,j][step] = dp[i-1,j][step-1]+dp[i,j+1][step-1]+dp[i+1,j][step-1]+dp[i,j-1][step-1]
     *  如果 step 为 0 时,dp[i,j][step] = 0;
     */         
    for (int step = 1; step <= maxMove; ++step) {
        for (int idx = 0; idx < m * n; ++idx) {
            // idx 反解
            auto xy = idx_decoder(idx, n);
            int x = xy.first, y = xy.second;
            for (auto direction : directions) {
                // 计算向 direction 方向移动一步后的 xy 坐标
                int prev_x = x + direction.first, prev_y = y + direction.second;
                // xy 坐标编码 idx
                int pre_idx = idx_encoder(prev_x, prev_y, n);
                if (prev_x >= 0 && prev_x < m && prev_y >= 0 && prev_y < n) {
                    dp[idx][step] += dp[pre_idx][step - 1];
                    dp[idx][step] %= 1000000007;
                }
            }
        }
    }
    return dp[idx_encoder(startRow, startColumn, n)][maxMove];
}

9. 最大得分的路径数目

题目描述:给你一个正方形字符数组 board ,你从数组最右下方的字符 ‘S’ 出发。你的目标是到达数组最左上角的字符 ‘E’ ,数组剩余的部分为数字字符 1, 2, …, 9 或者障碍 ‘X’。在每一步移动中,你可以向上、向左或者左上方移动,可以移动的前提是到达的格子没有障碍。

一条路径的 「得分」 定义为:路径上所有数字的和。

请你返回一个列表,包含两个整数:第一个整数是 「得分」 的最大值,第二个整数是得到最大得分的方案数,请把结果对 10^9 + 7 取余。

如果没有任何路径可以到达终点,请返回 [0, 0] 。

动态规划:

  • 状态量:当前位置,最大分数

  • 最大分数:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + board[i][j];

  • 最大路径数:当三个方向有相同的最大分数时,路径数是两个方向的和,路径dp数组初始化为 1;

    dp_road[i][j] = { (dp_road[i - 1][j] + dp_road[i][j - 1] + dp_road[i - 1][j - 1]) % 1000000007 if dp[i - 1][j] == dp[i][j - 1] == dp[i - 1[j - 1] }

void update(int n, int i, int j, int x, int y, std::vector<std::vector<std::pair<int, int>>> &dp) {
    // 边界处理,处理越界的情况,还需要考虑此路不通的情况,即[x,y]的最大分数为-1,表示不能通过xy对ij进行转移
    if (x >= n || y >= n || dp[x][y].first == -1) {
        return;
    }
    // 此时还没有通过board[i][j]的值更新dp[i][j].first的最大分数,所以dp[i][j].first表示的是右侧三个状态的最大分数
    if (dp[i][j].first < dp[x][y].first) {
        dp[i][j] = dp[x][y];
    } else if (dp[i][j].first == dp[x][y].first) {
        dp[i][j].second += dp[x][y].second;
        dp[i][j].second %= 1000000007;
    }
}

vector<int> pathsWithMaxScore(vector<string> &board) {
    // 状态量:当前位置,最大分数
    // dp[i][j] = max(dp[i + 1][j], dp[i][j + 1], dp[i + 1][j + 1]) + board[i][j];
    // 最大路径数:
    // dp_road[i][j] = {
    //      (dp[i + 1][j] + dp[i][j + 1] + dp[i + 1][j + 1]) % 1000000007
    //      if dp[i + 1][j] ==  dp[i][j + 1] == dp[i + 1][j + 1]
    // }
    int n = board.size();

    std::vector<std::vector<std::pair<int, int>>> dp(n, std::vector<std::pair<int, int>>(n, {-1, 0}));
    dp[n - 1][n - 1] = {0, 1};

    for (int i = n - 1; i >= 0; --i) {
        for (int j = n - 1; j >= 0; --j) {
            // 右下角的元素 'S' 跳过
            if (i == n - 1 && j == n - 1) { continue; }

            // 如果当前元素为 'X' 就跳过,当前格子的状态仍旧为初始值 {-1,0}
            if (board[i][j] == 'X') { continue; }

            // 状态转移,用右侧、右下侧、下侧的三个状态更新当前状态值
            update(n, i, j, i, j + 1, dp);     // 右侧
            update(n, i, j, i + 1, j + 1, dp); // 右下侧
            update(n, i, j, i + 1, j, dp);     // 下侧

            // 根据 board[i][j] 的值更新dp[i][j]的最大分数,此处要加上是否能到达判断;
            // 如果 dp[i][j].first == -1 表示三个位置都不能到达当前位置,那么就不更新,还是初始值 {-1,0}
            if (dp[i][j].first != -1) {
                // 如果是第一个元素(左上角)那最大分数就不变
                dp[i][j].first += (board[i][j] == 'E' ? 0 : board[i][j] - '0');
            }
        }
    }

    return dp[0][0].first == -1 ? vector<int>{0, 0} : vector<int>{dp[0][0].first, dp[0][0].second};
}

文章作者: LSJune
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LSJune !
评论
  目录