1.打家劫舍
解题思路:
动态规划的的四个解题步骤是:
- 定义子问题
- 写出子问题的递推关系
- 确定 DP 数组的计算顺序
- 空间优化(可选)
下面我们一步一步地进行讲解。
标签:动态规划
动态规划方程:dp[n] = MAX( dp[n-1], dp[n-2] + num )
由于不可以在相邻的房屋闯入,所以在当前位置 n 房屋可盗窃的最大值,要么就是 n-1 房屋可盗窃的最大值,要么就是 n-2 房屋可盗窃的最大值加上当前房屋的值,二者之间取最大值
举例来说:1 号房间可盗窃最大值为 3 即为 dp[1]=3,2 号房间可盗窃最大值为 4 即为 dp[2]=4,3 号房间自身的值为 2 即为 num=2,那么 dp[3] = MAX( dp[2], dp[1] + num ) = MAX(4, 3+2) = 5,3 号房间可盗窃最大值为 5
时间复杂度:O(n),n 为数组长度
class Solution {
public int rob(int[] nums) {
if(nums.length==0){
return 0;
}
int N =nums.length;
int[] dp =new int[N+1];
dp[0]=0;
dp[1]=nums[0];
for(int k =2;k<=N;k++){
dp[k]=Math.max(dp[k-1],nums[k-1]+dp[k-2]);
}
return dp[N];
}
}
这里我们可以又快一下空间复杂度:
- 我们发现 dp[n] 只与 dp[n−1] 和 dp[n−2] 有关系,因此我们可以设两个变量
cur
和pre
交替记录,将空间复杂度降到 O(1) 。
空间优化是动态规划问题的进阶内容了。对于初学者来说,可以不掌握这部分内容。
空间优化的基本原理是,很多时候我们并不需要始终持有全部的 DP 数组。对于小偷问题,我们发现,最后一步计算 f(n) 的时候,实际上只用到了 f(n−1) 和 f(n−2) 的结果。n−3 之前的子问题,实际上早就已经用不到了。那么,我们可以只用两个变量保存两个子问题的结果,就可以依次计算出所有的子问题。下面的图比较了空间优化前和优化后的对比关系:
class Solution {
public int rob(int[] nums) {
int pre =0,cur =0,tmp;
for(int num:nums){
tmp=cur;
cur=Math.max(pre+num,cur);
pre =tmp;
}
return cur;
}
}
2.地下城游戏
解法一(超时!):
f(1,0)+dungeon[1][0]=f(1,1)
f(0,1)+dungeon[0][1]=f(1,1)dungeon[1][0] 是已经确定了的,那么 f(1,1) 是多少?
我们知道最后一个格子是 -3,那么最小耗费的生命值就是 3 了, f(1, 1) = 3,其实这个就是 退出条件 了。当我们到了最后一个格子的时候,就是代表着我们已经到了终点了,就可以退出了,
so ...
f(1,0)=f(1,1)−dungeon[1][0]=7
f(0,1)=f(1,1)−dungeon[0][1]=8
f(0,0)=min(f(1,0),f(0,1))−dungeon[0][0]=8
所以起始点的最小耗费生命值为8。
class Solution {
public int calculateMinimumHP(int[][] dungeon) {
return dfs(dungeon, dungeon.length, dungeon[0].length, 0, 0);
}
private int dfs(int[][] dungeon, int m, int n, int i, int j) {
// 到达终点,递归终止。
if (i == m - 1 && j == n - 1) {
return Math.max(1 - dungeon[i][j], 1);
}
// 最后一行,只能向右搜索。
if (i == m - 1) {
return Math.max(dfs(dungeon, m, n, i, j + 1) - dungeon[i][j], 1);
}
// 最后一列,只能向下搜索。
if (j == n - 1) {
return Math.max(dfs(dungeon, m, n, i + 1, j) - dungeon[i][j], 1);
}
// 向下搜索 + 向右搜索,得到(i, j)点的后续路径所要求的最低血量 Math.min(dfs(i + 1, j), dfs(i, j + 1)),
// 又因为(i, j)点本身提供血量dungeon[i][j], 因此从(i, j)开始所需的最低血量为 Math.min(dfs(i + 1, j), dfs(i, j + 1)) - dungeon[i][j]
// 因为骑士的血量不能小于1,因此要和1取个max。
return Math.max(Math.min(dfs(dungeon, m, n, i + 1, j), dfs(dungeon, m, n, i, j + 1)) - dungeon[i][j], 1);
}
}
解法2:DFS + 记忆化(0ms,100%,搜索时会有大量重复计算的分支,加上记忆化即可解决)
3.课程表
算法流程:
- 统计课程安排图中每个节点的入度,生成 入度表 indegrees。
- 借助一个队列 queue,将所有入度为 0 的节点入队。
- 当 queue 非空时,依次将队首节点出队,在课程安排图中删除此节点 pre:
- 并不是真正从邻接表中删除此节点 pre,而是将此节点对应所有邻接节点 cur 的入度 −1,即 indegrees[cur] -= 1。
- 当入度 −1后邻接节点 cur 的入度为 0,说明 cur 所有的前驱节点已经被 “删除”,此时将 cur 入队。
- 在每次 pre 出队时,执行 numCourses--;
- 若整个课程安排图是有向无环图(即可以安排),则所有节点一定都入队并出队过,即完成拓扑排序。换个角度说,若课程安排图中存在环,一定有节点的入度始终不为 0。
- 因此,拓扑排序出队次数等于课程个数,返回 numCourses == 0 判断课程是否可以成功安排。
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
int[] indegrees = new int[numCourses];
List<List<Integer>> adjacency = new ArrayList<>();
Queue<Integer> queue = new LinkedList<>();
for(int i = 0; i < numCourses; i++)
adjacency.add(new ArrayList<>());
// Get the indegree and adjacency of every course.
for(int[] cp : prerequisites) {
indegrees[cp[0]]++;
adjacency.get(cp[1]).add(cp[0]);
}
// Get all the courses with the indegree of 0.
for(int i = 0; i < numCourses; i++)
if(indegrees[i] == 0) queue.add(i);
// BFS TopSort.
while(!queue.isEmpty()) {
int pre = queue.poll();
numCourses--;
for(int cur : adjacency.get(pre))
if(--indegrees[cur] == 0) queue.add(cur);
}
return numCourses == 0;
}
}