本篇为一些Dynamic Programming的一些题型。动态规划可以被称作记忆化的搜搜,它与分治算法很相似,都是将大的问题分解成性质相同的小问题,然后利用已经解决好小问题的解来解决大问题。它与分治算法的区别在于它可以将小问题的解储存起来,当再次需要时直接取出即可,不需要重复计算,一般储存在数组中,这一点会比分治算法具有更低的时间复杂度。另外,动规解法需要注意的往往有需要储存的状态(解/答案),初始的状态,和中间过程中的取舍等。
Triangle
题目为给定一个数字三角形,找到从顶部到底部的最小路径和。每一步可以移动到下面一行的相邻数字上。
public int minimumTotal(int[][] triangle) {
if (triangle == null || triangle.length == 0) {
return -1;
}
if (triangle[0] == null || triangle[0].length == 0) {
return -1;
}
// state: f[x][y] = minimum path value from 0,0 to x,y
int n = triangle.length;
int[][] f = new int[n][n];
// initialize
f[0][0] = triangle[0][0];
for (int i = 1; i < n; i++) {
f[i][0] = f[i - 1][0] + triangle[i][0];
f[i][i] = f[i - 1][i - 1] + triangle[i][i];
}
// top down
for (int i = 1; i < n; i++) {
for (int j = 1; j < i; j++) {
f[i][j] = Math.min(f[i - 1][j], f[i - 1][j - 1]) + triangle[i][j];
}
}
// answer
int best = f[n - 1][0];
for (int i = 1; i < n; i++) {
best = Math.min(best, f[n - 1][i]);
}
return best;
}
这道题目要可以进行存储空间的压缩,只使用一个一位数组即可,这里就不赘述。
K Sum
题目为:Given n distinct positive integers, integer k (k <= n) and a number target.
Find k numbers where sum is target. Calculate how many solutions there are?
我们使用f[i][j][k]来表示从前i个数中取出j个数得到的和为t的solution个数。关键代码:
for (int t = 1; t <= target; t++) {
f[i][j][t] = 0;
if (t >= A[i - 1]) {
f[i][j][t] = f[i - 1][j - 1][t - A[i - 1]];
}
f[i][j][t] += f[i - 1][j][t];
}
K Sum II
题目为:Given n unique integers, number k (1<=k<=n) and target.
Find all possible k integers where their sum is target.
此道题与K Sum I的不同在于去要给出所有的解决方案,一般这种需要给出所有方案而不是数量的题目不适合用动态规划方法,适合搜索类的DFS和BFS等。
我们这里使用DFS方法(也是一个递归的方法)来解决:
public void dfs(int A[], int K, int target, int index, ArrayList<Integer> tans){
if(K == 0 && target == 0) {
ans.add(new ArrayList<Integer>(tans));
return ;
}
if(K < 0 || target < 0 || index < 0)
return ;
dfs(A, K, target, index - 1, tans);
tans.add(A[index]);
dfs(A, K - 1, target - A[index], index - 1, tans);
tans.remove(tans.size() - 1);
}
这种方法与第一篇blog讲述的递归法类似,都是考虑添加当前值和不添加当前值。最后都需要remove当前值进行回溯。
Minimum Path Sum
题目是给定m×n的grid,要求从左上角走到右下角,求最少的格内数值总和。这里我们使用一个sum[i][j]来表示从左上角到grid[i][j]最少的格内数值总和,然后使用动规:sum[i][j] = Math.min(sum[i - 1][j], sum[i][j - 1]) + grid[i][j]。同时注意初始化。
Unique Paths
题目与上道题类似,给定m×n的grid,要求求从左上角到右下角的方案有多少种。这道题的sum[i][j]表示的则为从左上角到grid[i][j]有多少种方案。这里的动规为:sum[i][j] = sum[i - 1][j] + sum[i][j - 1]即可。
Unique Paths II
此题与Unique Path I区别在于grid中存在障碍物(grid内元素为1),这此时不能通过该格子往下走,需要绕路。我们换种思路,格子为0时,代表可以通过,与上题解题思路类似,当格子为1不能通过时,将sum设置为0即可,这样格子下边与右边的格子计算方案时表示上一个格子没有路可以通过来。注意初始化时,当前格子为1时,后面的sum均应该设置为0。这里就不贴出代码了。
Climbing Stairs
这道题为爬楼,每次只能爬一步或者两步,求爬到第n个阶梯有几种方案。这道题可以看成是一维的Unique Path。这里我们不使用以为数组来储存步数,进行一个数据的压缩,因为按照题意当前步数仅与上一次和上上一次的步数有关,因此我们只储存这三个数据就好了。这样空间复杂度仅为O(1):
public int climbStairs(int n) {
if (n <= 1) {
return 1;
}
int last = 1, lastlast = 1;
int now = 0;
for (int i = 2; i <= n; i++) {
now = last + lastlast;
lastlast = last;
last = now;
}
return now;
}
Jump Game
类似与上述的动规题型,题目为给定一个数组,数组里的数为从当前位置向后最远可到达的位数,判断是否能到达最后一个位置。我们使用一个can[i]来储存每个位置i是否是可达到的即可。
for (int i = 1; i < A.length; i++) {
for (int j = 0; j < i; j++) {
if (can[j] && j + A[j] >= i) {
can[i] = true;
break;
}
}
Jump Game II
与上道题目类似,要求给出到达最后位置的最少跳跃次数。此时我们使用的一位数组step[i]可用来储存到达i的最少跳跃次数了。对于每个位置i,我们对i之前的所有位置进行搜索看是否能到达i,我们要寻找尽量靠前的位置(可以看成越靠前花费的次数越少,是一种贪心策略)。
if (steps[j] != Integer.MAX_VALUE && j + A[j] >= i) {
steps[i] = steps[j] + 1;
break;
}