算法-动态规划(1)最大子序和问题
算法-动态规划(2)最长公共子串
深刻理解动态规划的思路, 动态规划里面的三个规则
- 确定状态
- 确定边值
- 确定状态转换方程
思考动态规划的思维方式
- 第一步: 需要理解dp,
dp是一维数组还是二维数组还是只是一个变量, 数组的话下标表示什么 - 第二步 : 确定状态
确定状态需要两个意识
1. 最后一步
2. 子问题 - 第三步: 确定状态转移方程
- 第四步: 确定边值
练习一:
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
分析
第一步: 需要理解dp,
dp是一维数组还是二维数组还是只是一个变量, 数组的话下标表示什么
这里的变量是 给定n, n 阶你才能到达楼顶, n个台阶, 一个可变变量, 定义一维数组.
dp[n] 对应多少种方法 , 也就是就是最优解
第二步 : 确定状态
确定状态需要两个意识
1. 最后一步
2. 子问题
最后一步, 如果我当前是dp[n], 那么我这个dp[n]是怎么来的. 也就是通过上一步dp[n-1] + 1 或者是dp[n-2] + 2 .
简单粗暴理解, 我不需要关心dp[n-2] 和dp[n-1]. 我只知道, 我要是需要达到
dp[n], 那么我无非就是dp[n-1]再加一步, 和dp[n-2]再加两步. 那就是所有的情况都考虑到了.这个时候就确定了状态转移方程.
也就是从最后一步, 推出了子问题, 从而退出了转移方程
第三步: 确定状态转移方程
dp[n] = dp[n-2] + dp[n-1]
举个简单例子简单校验, 比如n = 3, 总共有3个台阶, 那么
dp[3] = dp[1] + dp[2]
d[1] = 1, 1个台阶, 只走1步
d[2] = 2, 2个台阶, 走1,1或者走2
dp[3] = dp[1] + dp[2] = 3, 走1,1,1 或者 1, 2 或者 2,1
第四步: 确定边值
当n = 0的时候, 直接返回0,
当n = 1的时候, 直接返回1,
代码:
static func climbStairs(_ n: Int) -> Int {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
// 确定状态, dp[n] 就是最优解 , 对于问题, 有多少个变量就开多少个数组
var dp : [Int] = [Int](repeating: 0, count: n)
// 确定边值
dp[0] = 1
dp[1] = 2
var i = 2;
while(i < n) {
// 状态转移方程
dp[i] = dp[i-1]+dp[i-2]
i += 1;
}
return dp[n - 1]
}
练习二:
打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
分析
第一步: 需要理解dp,
dp是一维数组还是二维数组还是只是一个变量, 数组的话下标表示什么
这里的变量是 给定i, i是第i间房间, 因为可能这一条街总共有total个房间, 小偷从0到i, 到第i间的时候, 是对应最高金额的房间
第二步 : 确定状态
确定状态需要两个意识
1. 最后一步
2. 子问题
最后一步, 如果我当前是dp[i], 那么我这个dp[i]是怎么来的. 也就是通过上一步dp[i-1] 或者是dp[i-2]. 这里跟爬楼梯思路一直, 因为我可能偷, 也可能不偷, 不偷的情况是我上一间已经偷过了.
简单粗暴理解, 我不需要关心dp[i-2] 和dp[i-1]. 我只知道, 我要是需要达到
dp[i], 那么我无非就是dp[i-2]再加当前房间的金额或者就是dp[i-1], 因为我只要能偷, 就能在当前金额上叠加金额,达到最大.
也就是从最后一步, 推出了子问题, 从而退出了转移方程
第三步: 确定状态转移方程
dp[i] = max(dp[i-2] + 当前金额 , dp[i-1] )
第四步: 确定边值
例如 : 输入:[1,2,3,1] 假设数组nums = [1,2,3,1]
d[0] = nums[0]
d[1] = max( nums[1], dp[0])
代码:
static func rob(_ nums: [Int]) -> Int {
if (nums.count == 0) {
return 0
}
if (nums.count == 1) {
return nums[0]
}
let len = nums.count;
var dp: [Int] = [Int](repeating: 0, count: len);
var i = 2;
var maxResult = 0;
// 边界值处理
dp[0] = nums[0];
dp[1] = max( dp[0], nums[1])
maxResult = max( dp[0], dp[1] )
while (i < len ) {
dp[i] = max( dp[i-1] , dp[i-2] + nums[i])
maxResult = max(maxResult, dp[i])
i += 1
}
return maxResult
}
练习三:
最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
分析
第一步: 需要理解dp
这里求的是最大和, 也就是最优解, 那么就是dp就是最大和, 最优解. 因为需要找到连续的数组, 开始和结束的位置不一定,所以dp记录一个常数, 作为一个变量.
第二步 : 确定状态
确定状态需要两个意识
1. 最后一步
2. 子问题
最后一步, 假设当前的dp是最优解. 对应上一步就是 dp = dp + nums[i], 我不关心上一步的dp怎么来的, 我只关心当前的dp这个是最优解.
这里面要关心的条件是 dp + nums[i] 比 nums[i] 本身要大就可以. 因为如果我 dp + nums[i] 发现比 nums[i] 还要小, 那么就应该直接取dp = nums[i] , 因为, 如果本身dp加上下一个都没有下一个自身大, 就没有必要是dp + nums[i], 直接去nums[i]就可以了
所以取max(nums[i], dp+nums[i]), 所以推导出状态转移方程
第三步: 确定状态转移方程
dp=max(nums[i], dp+nums[i])
第四步: 确定边值
确定nums的边值状态. 如果i= 0, 那么dp = nums[0];
代码
static func maxSubArray(_ nums: [Int]) -> Int {
if (nums.count == 0) {
return 0
}
var dp = nums[0]
var i = 1
let len = nums.count
var maxResult = dp
while (i < len) {
dp = max(nums[i], dp + nums[i])
maxResult = max(dp, maxResult)
i += 1
}
return maxResult;
}