文章目录
前言
动态规划是编程面试中的热门话题,如果面试题是求解一个问题的最优解(通常是求最大值或者最小值),而且该问题能够分解成若干个子问题,并且子问题之间还有重叠的更小的子问题,就可以考虑用动态规划的方法来求解这个问题。
关键名词:
(1) 无后效性:即“未来与过去无关”。(严格定义:如果给定某一阶段的状态,则在这一阶段以后过程的发展不受这阶段以前各段状态的影响。)
(2) 最优子结构性质:即大问题的最优解可以由小问题的最优解推出。
如何判断一个问题能否使用DP解决:能将大问题拆成几个小问题,且满足无后效性、最优子结构性质。
DP的核心思想:尽量缩小可能解空间。
算法题
1. LeetCode 64 : 最小路径和
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
/*
* 1
* LeetCode 64 : 最小路径和
* https://leetcode-cn.com/problems/minimum-path-sum/
*/
int minPathSum(vector<vector<int>>& grid)
{
int m = grid.size();
int n = grid[0].size();
vector<vector<int> > dp(m,vector<int>(n,0));
dp[0][0] = grid[0][0];
for(int i = 1; i < m; i++)
dp[i][0] = dp[i-1][0] + grid[i][0];
for(int j = 1; j < n; j++)
dp[0][j] = dp[0][j-1] + grid[0][j];
for(int i = 1; i < m; i++)
for(int j = 1; j < n; j++)
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
return dp[m-1][n-1];
}
2. LeetCode 53 : 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
/*
* 2
* LeetCode 53 : 最大子序和
* https://leetcode-cn.com/problems/maximum-subarray/
*/
int maxSubArray(vector<int> &nums)
{
#if 0
//dp[i]表示nums中以nums[i]结尾的最大子序和
if(nums.empty())
{
return -1;
}
vector<int> dp(nums.size());
dp[0] = nums[0];
int res = nums[0];
for (int i = 1; i < nums.size(); i++)
{
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
res = max(res, dp[i]);
}
return res;
#endif
#if 1
// 因为dp[i]只与dp[i-1]有关,所以可以只用只用一个变量 pre 来维护对于当前dp[i]的dp[i-1]的值是多少
if(nums.empty())
{
return -1;
}
int pre = nums[0];
int res = nums[0];
for (int i = 1; i < nums.size(); i++)
{
pre = max(pre + nums[i], nums[i]);
res = max(res, pre);
}
return res;
#endif
}
3. LeetCode 198 : 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 400
/*
* 3
* LeetCode 198 : 打家劫舍
* https://leetcode-cn.com/problems/house-robber/
*/
int rob(vector<int>& nums)
{
/*
* dp[i] 表示前 i 间房屋能偷窃到的最高总金额
* dp[i] = max(dp[i−2]+nums[i], dp[i−1])
* dp[0]=nums[0] 只有一间房屋,则偷窃该房屋
* dp[1]=max(nums[0],nums[1]) 只有两间房屋,选择其中金额较高的房屋进行偷窃
*/
if(nums.empty())
return 0;
if(nums.size() == 1)
{
return nums[0];
}
vector<int> dp(nums.size(), -1);
//memo[i]表示抢劫nume[0,....i]所能获得的最大收益
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for(int i = 2; i < nums.size(); i++)
{
dp[i] = max(dp[i-2]+nums[i], dp[i-1]);
}
return dp[nums.size()-1];
}
4. LeetCode 213 : 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入:nums = [0]
输出:0
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 1000
/*
* 4
* LeetCode 213 : 打家劫舍 II
* https://leetcode-cn.com/problems/house-robber-ii/
*/
int getRob(const vector<int>& nums, int start, int end)
{
int len = end-start+1;
vector<int> dp(len, -1);
if(len == 0)
return 0;
if(len == 1)
return nums[start];
dp[0] = nums[start];
dp[1] = max(nums[start], nums[start+1]);
for(int i = 2; i < len; i++)
{
dp[i] = max(nums[start+i]+dp[i-2], dp[i-1]);
}
return dp[len-1];
}
int rob(vector<int>& nums)
{
/*
* 因为第一个element 和最后一个element不能同时出现. 则分两次call House Robber I.
* case 1: 不包括最后一个element.
* case 2: 不包括第一个element.
* 两者的最大值即为全局最大值
*/
if(nums.empty())
return 0;
if(nums.size() == 1)
return nums[0];
int a = getRob(nums, 0, nums.size() - 2);
int b = getRob(nums, 1, nums.size() - 1);
return max(a, b);
}
5. Leetcode 931 : 下降路径最小和
给定一个方形整数数组 A,我们想要得到通过 A 的下降路径的最小和。
下降路径可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列。
示例:
输入:[[1,2,3],[4,5,6],[7,8,9]]
输出:12
解释:
可能的下降路径有:
[1,4,7], [1,4,8], [1,5,7], [1,5,8], [1,5,9]
[2,4,7], [2,4,8], [2,5,7], [2,5,8], [2,5,9], [2,6,8], [2,6,9]
[3,5,7], [3,5,8], [3,5,9], [3,6,8], [3,6,9]
和最小的下降路径是 [1,4,7],所以答案是 12。
提示:
1 <= A.length == A[0].length <= 100
-100 <= A[i][j] <= 100
/*
* 5
* Leetcode 931 : 下降路径最小和
* https://leetcode-cn.com/problems/minimum-falling-path-sum/
*/
int minFallingPathSum(vector<vector<int> >& A)
{
/*
* dp(r,c) : 从第r层,第c列开始的最小路径和。
* 当r为最后一层时,dp(r,c) = A[r][c];
* 当r不为最后一层时,dp(r,c) = A[r][c] + 下一层的子问题的解的最小值。
* 状态转移方程 (在不考虑边界的情况下)
* dp(r,c) = A[r][c] + min(dp[r+1][c-1], dp[r+1][c], dp[r+1][c+1])
*/
int row = A.size();
int col = A[0].size();
for(int r = row-2; r >= 0; r--)
{
for(int c = 0; c < col; c++)
{
// best = min(A[r+1][c-1], A[r+1][c], A[r+1][c+1])
int best = A[r+1][c];
if(c > 0)
best = min(best, A[r+1][c-1]);
if(c+1 < col)
best = min(best, A[r+1][c+1]);
A[r][c] += best;
}
}
int res = INT_MAX;
for(auto x : A[0])
res = min(res, x);
return res;
}
6. LeetCode 322 : 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
示例 4:
输入:coins = [1], amount = 1
输出:1
示例 5:
输入:coins = [1], amount = 2
输出:2
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 2^31 - 1
0 <= amount <= 10^4
/*
* 6
* LeetCode 322 : 零钱兑换
* https://leetcode-cn.com/problems/coin-change/
*/
int coinChange(vector<int>& coins, int amount)
{
/*
* dp(i) 为组成金额 i 所需最少的硬币数量
* dp(i) = min{dp(i - c_j)} + 1 (j = 0, ... n-1)
* 其中 c_j 代表的是第 j 枚硬币的面值
*/
int Max = amount + 1;
vector<int> dp(amount + 1, Max);
dp[0] = 0;
for(int i = 1; i <= amount; ++i)
{
for(int j = 0; j < (int)coins.size(); ++j)
{
if(coins[j] <= i)
{
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
7. LeetCode 62 : 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
例如,上图是一个7 x 3 的网格。有多少可能的路径?
示例 1:
输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
示例 2:
输入: m = 7, n = 3
输出: 28
提示:
1 <= m, n <= 100
题目数据保证答案小于等于 2 * 10 ^ 9
/*
* 7
* LeetCode 62 : 不同路径
* https://leetcode-cn.com/problems/unique-paths/
*/
int uniquePaths(int m, int n)
{
/*
* dp[i][j] 是到达 (i, j) 最多路径
* 动态方程:dp[i][j] = dp[i-1][j] + dp[i][j-1]
* 对于第一行 dp[0][j],或者第一列 dp[i][0],由于都是在边界,所以只能为 1
*/
vector<vector<int> > memo(n, vector<int>(m, 0) );
for(int i = 0; i < n; i++)
memo[i][0] = 1;
for(int j = 0; j < m; j++)
memo[0][j] = 1;
for(int i = 1; i < n; i++)
for(int j = 1; j < m; j++)
memo[i][j] = memo[i][j-1] + memo[i-1][j];
return memo[n-1][m-1];
}
#if 0
int uniquePaths(int m, int n)
{
/* 排列组合
* 因为机器到底右下角,向下几步,向右几步都是固定的,
* 所以有 C(m+n-2,m-1)(或C(m+n-2,n-1))
*/
long ans = 1;
for(int i = 0; i < min(m-1, n-1); i++)
{
// 乘以分子
ans *= m+n-2-i;
// 直接除以分母 将阶乘拆开 防止溢出
ans /= i+1;
}
return (int)ans;
}
#endif
8. LeetCode 面试题 08.02 : 迷路的机器人
设想有个机器人坐在一个网格的左上角,网格 r 行 c 列。机器人只能向下或向右移动,但不能走到一些被禁止的网格(有障碍物)。设计一种算法,寻找机器人从左上角移动到右下角的路径。
网格中的障碍物和空位置分别用 1 和 0 来表示。
返回一条可行的路径,路径由经过的网格的行号和列号组成。左上角为 0 行 0 列。如果没有可行的路径,返回空数组。
示例 1:
输入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
输出: [[0,0],[0,1],[0,2],[1,2],[2,2]]
解释:
输入中标粗的位置即为输出表示的路径,即
0行0列(左上角) -> 0行1列 -> 0行2列 -> 1行2列 -> 2行2列(右下角)
说明:r 和 c 的值均不超过 100。
/*
* 8
* LeetCode 面试题 08.02 : 迷路的机器人
* https://leetcode-cn.com/problems/robot-in-a-grid-lcci/
*/
vector<vector<int>> pathWithObstacles(vector<vector<int> > &obstacleGrid)
{
/*
* 动态规划
* 移动方向决定了当前坐标只能够从垂直向上的一个坐标或者左边的一个坐标达来到
* 左边的第一列,只能从上往下 dp[i][0] = !obstacleGrid[i-1][0] && dp[i-1][0]
* 上面的第一行,只能从左往右 dp[0][j] = !obstacleGrid[0][j-1] && dp[0][j-1]
* 状态转移方程 :dp[i][j] = dp[i-1][j] || dp[i][j-1]
*
* dp[row][col] == 1, 说明存在路径从起点到终点,根据dp矩阵就能逆推回去得到一条路径
*/
int row = obstacleGrid.size();
int col = obstacleGrid[0].size();
vector<vector<int> > res;
//起点和终点是障碍
if(obstacleGrid[0][0] || obstacleGrid[row-1][col-1])
return res;
vector<vector<bool> > dp(row, vector<bool>(col, false));
dp[0][0] = true;
//初始化首列
for(int r = 1; r < row; r++)
{
dp[r][0] = !obstacleGrid[r][0] && dp[r - 1][0];
}
//初始化首行
for(int c = 1; c < col; c++)
{
dp[0][c] = !obstacleGrid[0][c] && dp[0][c - 1];
}
for(int i = 1; i < row; i++)
{
for(int j = 1; j < col; j++)
{
dp[i][j] = !obstacleGrid[i][j] && (dp[i - 1][j] || dp[i][j - 1]);
}
}
//如果终点不可达
if(!dp[row-1][col-1])
return res;
int r = row-1, c = col-1;
while(r > 0 || c > 0)
{
res.push_back({r, c});
//先考虑上面的坐标, 不管是先上面还是左边,只要坐标可用,最终肯定能回到起点(这是前面动态递推的结果)
if(r > 0 && dp[r - 1][c])
r--;
else if(c > 0 && dp[r][c - 1])
c--;
}
res.push_back({0, 0});
reverse(res.begin(), res.end());
return res;
}
总结
以上是动态规划相关面试算法题汇总,个别题目也给出了解题算法描述和注释。以上所有代码都可以去我的网站GitHub查看,后续也将继续补充其他算法方面的相关题目。