动态规划入门
动态规划 Dynamic Programing
一.动态规划问题常用场景:求最优值而非求最优解
经典凑硬币问题:
给你 k 种面值的硬币,面值分别为 c1, c2 ... ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
第一个问题问的是“最少需要几枚硬币”,第二个问题问的是“够偷窃到的最高金额”,求的都是最优值,而非最优解(即最优的具体解决方案),
此类问题都要在循环各种选择,然后求其中的max或min
二.动态规划三个概念
动态规划问题一般求最解,其核心就是 穷举
三个概念
1.“重叠子问题”
举个例子: 斐波那契问题
fbi(n)=fbi(n-1)+fbi(n-2)
穷举 fbi(n-1)=fbi(n-2)+fbi(n-3)时,会穷举到fbi(n-2),
而当回到 fbi(n)=fbi(n-1)+fbi(n-2)时,fbi(n-2)又会再次穷举一次,即子问题重叠。
2.最优子结构
要符合「最优子结构」,子问题间必须互相独立。
回到凑零钱问题,为什么说它符合最优子结构呢?比如你想求 amount = 11
时的最少硬币数(原问题),如果你知道凑出 amount = 10
的最少硬币数(子问题),你只需要把子问题的答案加一(再选一枚面值为 1 的硬币)就是原问题的答案。因为硬币的数量是没有限制的,所以子问题之间没有相互制,是互相独立的。
3.状态转移方程
原问题分解成子问题,原问题和子问题之间的联系
如fbi(n)=fbi(n-1)+fbi(n-2)
即斐波那契问题的状态转移方程。
三.从暴力递归升级到动态规划
动态规划问题的升级流程
暴力递归--->带备忘录的自顶向下的递归--->非递归的自底向上的动态规划
以凑硬币为例题
1.暴力递归
int coinChange(vector<int>& coins, int amount) {
if (amount == 0) return 0;
int ans = INT_MAX;
for (int coin : coins) {
// 金额不可达
if (amount - coin < 0) continue;
int subProb = coinChange(coins, amount - coin);
// 子问题无解
if (subProb == -1) continue;
ans = min(ans, subProb + 1);
}
return ans == INT_MAX ? -1 : ans;
}
int subProb = coinChange(coins, amount - coin);
通过这一句,每种情况逐层逐层的递归,工作量非常大
2.带备忘录的自顶向下的递归
int coinChange(vector<int>& coins, int amount) {
// 备忘录初始化为 -2
vector<int> memo(amount + 1, -2);
return helper(coins, amount, memo);
}
int helper(vector<int>& coins, int amount, vector<int>& memo) {
if (amount == 0) return 0;
if (memo[amount] != -2) return memo[amount];
int ans = INT_MAX;
for (int coin : coins) {
// 金额不可达
if (amount - coin < 0) continue;
int subProb = helper(coins, amount - coin, memo);
// 子问题无解
if (subProb == -1) continue;
ans = min(ans, subProb + 1);
}
// 记录本轮答案
memo[amount] = (ans == INT_MAX) ? -1 : ans;
return memo[amount];
}
vector<int> memo(amount + 1, -2);
创建备忘录时,给备忘录填满了“未备入”标识-2
memo[amount] = (ans == INT_MAX) ? -1 : ans;
在深究到对应的数据后,写入到备忘录
if (memo[amount] != -2) return memo[amount];
在查备忘录一开始时,检查备忘录中所查项目的“未备入”标识,如果不是未备入,直接返回备入的数据
总结:备忘录递归关键
-
创建备忘录时,给备忘录填满了“未备入”标识
-
查备忘录,先看是否为base,再看“未备入”标识看是否已经有备入过,如果有就不用再次深究,省去了查重叠问题的功夫
-
在深究查到原本没备入的数据后,将其写入备忘录,再返回该结果
3.非递归的自底向上的动态规划
用dp表/dp函数动态规划 Dynamic Programing
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i < dp.size(); i++) {
// 内层 for 在求所有子问题 + 1 的最小值
for (int coin : coins) {
if (i - coin < 0) continue;
dp[i] = min(dp[i], 1 + dp[i - coin]);
}
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
没有循环 只有迭代
- dp[i]代表什么
- dp[i]怎么得来
- dp[0]
- 输出什么
- 主要就是构建完整的dp[]数组
四.动态规划解题模板
第 1 步:设计状态
即dp数组或函数代表什么;dp[i]可以代表第i的值 或 [0,i]区间内的值 或 [0,i]]区间内的值但不一定用到第i或 [x,i]区间内的值
第 2 步:确定状态转移方程
dp[i]如何由dp[i-1]得来
第 3 步:考虑初始化
dp[0]
第 4 步:考虑输出
输出什么
例题: 一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。
——————————示例————————————————
输入: [2,7,9,3,1]
输出: 12
解释: 选择 1 号预约、 3 号预约和 5 号预约,总时长 = 2 + 9 + 1 = 12。
解题思路1:
1.dp[i]为选择i号为最后一个的最大收入
- dp(i )= max
- dp[0]=num[0]; dp[1]=num[1]
- 输出dp[i]数组中最大值
int message(int[] nums){
int[] dp = new int[length];
dp[0] = nums[0];
dp[1] = nums[1];
int MAX = 0;
for(int i = 0; i < length; i++){
int max = 0;
for(int j = 0; j < i - 1;j++){
if(dp[j] + num[i] >max){
max = dp[j] + num[i];
}
}
dp[i] = max;
MAX = MAX > max? Max: max;
}
return MAX;
}
解题思路2
- dp[i]/[0] 第i天不接,[0,i]区间内最大收益
dp[i]/[1] 第i天接客,[0,i]区间内最大收益
-
dp[i]/[0] = max( dp[i-1]/[0] ,dp[i-1]/[1] )
dp[i]/[1] = dp[i-1]/[0] + nums[i]
-
dp[0]/[0] = 0
dp[0]/[1] = nums[0]
-
输出 max( dp[length-1]/[1] , dp[length-1]/[0])
int[]/[] dp = new int[len][2];
dp[0]/[0] = 0;
dp[0]/[1] = nums[0];
for(i=1;i<len;i++){
dp[i]/[0] = max( dp[i-1]/[0] ,dp[i-1]/[1] )
dp[i]/[1] = dp[i-1]/[0] + nums[i]
}
return max( dp[length-1]/[1] , dp[length-1]/[0]);
总结:如果dp[i]需要考虑dp[i-1],可以设置二维的dp数组,在第二维中做决策。
解题思路3:
1.dp[i]为区间[0,i]赚到的最多钱
2.dp[i]=max( dp[i-1] , dp[i-2] + nums[i])
3.dp[0]=num[0] dp[1]=max( nusm[0] , nums[1] )
4.输出dp[len-1]
public int massage(int[] nums) {
int len = nums.length;
if (len == 0) {
return 0;
}
if (len == 1) {
return nums[0];
}
// dp[i]:区间 [0, i] 里接受预约请求的最大时长
int[] dp = new int[len];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < len; i++) {
// 今天在选与不选中,选择一个最优的
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[len - 1];
}