blog-web/source/_posts/算法/动态规划(1).md
2018-09-02 00:08:03 +08:00

147 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 动态规划(1)
date: 2018-8-27 02:17:07
tags:
- 算法
- 动态规划
categories:
- 算法
---
### 楔子
最大子序和 ( leetcode题目 )
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
> 示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
<!-- more -->
题目只需要求最大的和是多少, 而不需要知道这个子数组的起止位置
首先需要明确的是, 如果这个子数组的边缘位置(第一个或最后一个)元素是一个负数
那么这个子数组的和肯定不是最大的, 因为舍掉这个元素, 之后的和肯定会更大, 负数是会让总和减小的
比如[-3,4,-1,2] 这个子数组肯定不是最大, 因为把-3舍掉会更大
更进一步, 如果这个子数组边缘位置连续多个数的和是负数, 那么这多个数也可以舍掉, 让总和更大
比如[1,-3,4,-1,2] 这个子数组肯定不是最大, 因为把1,-3舍掉, 可以让总和更大
以示例当中给出的数组进行推演
[-2,1,-3,4,-1,2,1,-5,4]
( 下面的每个序号代表推进到第几个元素 )
1. 总和初始值是0 , 先加第一个数此时总和是`-2`
2. 前面的总和-2, 加上它会使总和 **变小**, 所以不加, 总和 **归零**, 然后加1, 此时总和是`1`
3. 前面的总和1, 加上它会使总和 **变大**, 所以加上, 此时总和是`-2`
4. 前面的总和-2, 加上它会使总和 **变小**, 所以不加, 总和 **归零**, 然后加4, 此时总和是`4`
5. 前面的总和4, 加上它会使总和 **变大**, 所以加上, 此时总和是`3`
.....
每一步是否舍弃前面总和的判断条件就是前面的总和是正还是负, 正数则加, 负数则舍弃
依次进行下去, 从每一步得到的总和里面找出最大的就可以了
每一步的总和分别是`-2 1 -2 4 3 5 6 1 5`
显然最大的是6
#### 代码实现
```java
class Solution {
public int maxSubArray(int[] nums) {
if(nums.length == 0) { //特殊情况判断
return 0;
}
int sum = nums[0], maxSum = nums[0];
for(int i=1 ; i<nums.length ; i++) {
sum = (sum < 0 ? nums[i] : sum + nums[i]);
if(sum > maxSum) {
maxSum = sum;
}
}
return maxSum;
}
}
```
空间复杂度是`O(1)`, 因为使用了常数个变量, 没有开辟长度为n的新数组
时间复杂度是`O(n)`, 因为要逐个遍历传入的数组当中的元素
### 不同路径问题
一个机器人位于一个 m x n 网格的左上角 起始点在下图中标记为“Start”
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角在下图中标记为“Finish”
问总共有多少条不同的路径?
![不同路径问题](/images/算法/robot_maze.png)
#### 解决方式
1. 首先最左上角的格子的到达方式肯定只有1种, 因为必须要从这个格子开始走
2. 在每个格子当中只能向右或者向下移动, 所以每个对于每个格子来说, 到达这个格子的时候, 只能从左侧或者上方到达
3. 左边缘的格子无法从左侧到达, 上边缘的格子无法从上方到达
所以对于每个格子来说, 到达这个格子的路径的数量 = 到达左侧格子的数量 + 到达上方格子的数量
左边缘的格子前者为0, 上边缘的格子后者为0
根据这个原则, 就可以把到达每个格子的路径数量递推出来了
#### 代码实现
```java
public int uniquePaths(int m, int n) {
if(m<=0 || n<=0) {
return 0;
}
int[][] nums = new int[m+1][n+1];
nums[1][1] = 1; // 起点方格的走法
for(int i=1 ; i<=m ; i++) {
for(int j=1 ; j<=n ; j++) {
if(i==1 && j==1) {
continue;
}
nums[i][j] = nums[i-1][j] + nums[i][j-1];
}
}
return nums[m][n];
}
```
整体思路就是创建一个整数二维数组, m+1和n+1是为了留出第一行和第一列数值都是0
方便进行计算, 当然这个也不是必须的, 在循环当中判断也可以, 但是不影响时间和空间复杂度
时间复杂度`O(mn)`
空间复杂度`O(mn)`
#### 进阶
如果某些方格存在障碍物, 机器人不能经过这些格子
有多少走法该如何计算
传入的参数是一个整数二维数组, 其中0代表该位置无障碍, 1代表该位置有障碍
这个问题同样可以沿用上一题的解法
需要补充的就是在障碍物的位置, 右侧和下方的格子
其对应的来自左侧和上面的走法应该是0, 因为无法从障碍物走过来
代码实现
```java
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
if(m == 0) {
return 0;
}
int n = obstacleGrid[0].length;
// 需要排除宽度是0以及高度是0的情况, 这种情况下走法是0
// 还需要排除起点位置有障碍和终点位置有障碍的情况, 这种情况下走法也是0
if(n == 0 || obstacleGrid[0][0] == 1 || obstacleGrid[m-1][n-1] == 1) {
return 0;
}
int[][] nums = new int[m+1][n+1];
nums[1][1] = 1;
for(int i=1 ; i<=m ; i++) {
for(int j=1 ; j<=n ; j++) {
if(i == 1 && j == 1) {
continue;
}
// 判断左侧是否有障碍
int left = (i > 1 && obstacleGrid[i-2][j-1] == 0) ? nums[i-1][j] : 0;
// 判断上方是否有障碍
int top = (j > 1 && obstacleGrid[i-1][j-2] == 0) ? nums[i][j-1] : 0;
nums[i][j] = left + top;
}
}
return nums[m][n];
}
```
### 总结
动态规划的主要应用于使用穷举去解决时间复杂度过高的问题
将大问题化解为小问题, 并且记住前面小问题的求解结果, 避免在穷举过程中对前面小问题的重复求解
从而降低时间复杂度