背景, 基本思路,案例模板
背景
1 动态规划在算法中,可以说是占半壁江山,其中重要的原因是,通过该算法,将直来直去的暴力搜索法,其计算量直接降低一个数量级,其能力是十分显著的;
2 其利用的数学技术就是最优化原理,这一先进的效能工具;
3 本文梳理各种优秀博客及书籍中,对动态规划模板的总结,相互映照,取长补短,形成模式化思维;然后再结合具体的案例,进行应用实践,以期能够养成解决这一类问题的思维;
一、基本思路
1.1 触发条件
是否可用动态规划方法进行处理,一般需要遵循如下几种条件。
1.1.1 算法的乐趣定义
1 最优化原理
1)问题的最优子结构的性质
2)不管之前是否是最优决策,都必须保证从现在开始的决策是在之前决策基础上的最优决策,那么这样的最优子结构就符合最优化原理;
2 无后向性
1)之前的决策只影响当前阶段的决策,当时对之后各阶段的决策不产生影响;
1.2 实现步骤
1.2.1 五分钟学算法
动态规划问题,一般分为四个步骤,分别是:
1 问题分解
2 状态定义
3 转移方程推导
4 算法实现
1.2.2 算法的乐趣
1 定义最优子问题
1)确定问题的优化目标及最优解判定;
2)对决策过程分阶段;可按照时间空间顺序或者问题演化状态;
2 定义状态
1)既是决策对象,也是决策的结果;
2)对起始状态施加决策,状态发生变化,得到决策的结果状态;
3)状态的定义是建立在子问题定义的基础上,因此状态必须满足“无后向性”。必要时,可以增加状态的维度,引入更多的约束条件,使得状态定义满足“无后向性”条件;(什么案例?装配线问题,背包问题中的剩余容量约束)
3 定义决策与状态转移方程
1)从n-1阶段,到n阶段的演化规律;
2)如果状态选择不合适,导致子问题之间没有重叠,也就不存在状态转移关系,会退化为类似分治法那样的朴素递归搜索算法;
4 确定边界条件
1)递归终结条件;
2)对于递推关系直接实现的动态规划方法,需要确定状态转移方程递归式的初始条件或者边界条件;
1.2.3 mu
muyids
动态规划包含三个重要概念:最优子结构,边界,状态转移公式
1 最优子结构
2 边界:边界是最简单的最优子结构,无需再简化便可得到结果;如果一个问题没有边界,将无法得到有限的结果;
3 状态转移方程;
针对本段话,我认为边界这块强调的好,此外,我认为边界有两个维度的含义,一是广义上的最优子结构是一类边界,二是狭义上的每个元素的取值范围,也可以看做是一种边界;
而将两者综合在一起看,后者就是前者细分后的值;
1.3 案例模板
基于上述1.1和1.2 进行描述,上面有,下面再写;
1.4 其他博文一针见血的话
1 动态规划说起来很高大上,说白了是以空间换时间,将计算结果封存起来,避免重复计算;
二、案例汇总
2.1 二维空间接雨水
2.1.1 触发条件
1 最优化原理
1)每一列的最大盛水量之和相加,即为总的盛水量;
2 无后向性
1)当某一列盛水量确定之后,不会发生改变,并且对之后决策不会产生影响;
2.1.2 实现步骤
1 问题分解
1)子问题是哪一个?
a 每一列的盛水量,是一个子问题;
b 当前列左(右)侧的最大高度;然后借用木桶原理计算,即会演化为子问题a
2)最优解
所有子问题的和,就是全局最优;
2 状态定义
1)当前列左(右)侧最大高度--maxLeft, maxRight;
2)当前列的高度--height[i]
3)当前列的储水量;
3 转移方程推导
1)maxLeft[i] = max(maxLeft[i-1], height[i-1])
4 边界条件
1)maxLeft/maxRight的i边界是[0, length - 1];
2) 初始条件:maxLeft[0] = 0, maxRight[length - 1] = 0; 因为左右两侧不存在储水的情况;
2.1.3 代码
见题目代码
#define fmax(a, b) (((a) > (b)) ? (a) : (b))
#define fmin(a, b) (((a) > (b)) ? (b) : (a))
int trap(int* height, int heightSize){
// 基于列进行查找处理;
// 1 求解每列的左右两侧的最大值;
// 2 针对每一个位置的值,基于木桶原理,进行求解;
int i, totalNum, min;
int *maxLeft = malloc(heightSize * sizeof(int));
int *maxRight = malloc(heightSize * sizeof(int));
totalNum = 0;
for (i = 1; i < heightSize - 1; i++) {
maxLeft[i] = fmax(maxLeft[i - 1], height[i - 1]);
}
for (i = heightSize - 2; i > 0; i--) {
maxRight[i] = fmax(maxRight[i + 1], height[i + 1]);
}
for (i = 1; i < heightSize - 1; i++) {
min = fmin(maxLeft[i], maxRight[i]);
if (min > height[i]) {
totalNum = totalNum + min - height[i];
}
}
free(maxLeft);
maxLeft = NULL;
free(maxRight);
maxRight = NULL;
return totalNum;
}
2.2 最长回文字符串
2.2.1 触发条件
1 最优化原理
1)结合中心扩展法,每一个字母为中心的最长回文字符中,选择其中最大的;
2 无后向性
1)当前字符为中心的求解,对其他字符没有影响
2.2.2 实现步骤
1 问题分解
1)求解每个字符的最长回文字符;
2)每个字符的求解方式:当前字符为中心,向外扩展;
2 状态定义
1)中心点的左边坐标;--left
2)右边坐标;--right
3)当前字符位置的最长回文长度--length
4)中心点--i;
5)当前求出的最长回文字符,初始值是1--maxLen;
3 状态转移方程
1)dp[left, right] = dp[left + 1, right - 1] 与 s[left] ?= s[right];
2)注意中心点起始位置,存在与中心点相同的情况
if (s.charAt(l) == s.charAt(r) && (r - l <= 2 || dp[l + 1][r - 1]))
3)状态转移方程为当前阶段,然后for循环驱动变化;
2.2.3 代码
public String longestPalindrome(String s) {
if (s == null || s.length() < 2) {
return s;
}
int strLen = s.length();
int maxStart = 0; //最长回文串的起点
int maxEnd = 0; //最长回文串的终点
int maxLen = 1; //最长回文串的长度
boolean[][] dp = new boolean[strLen][strLen];
for (int r = 1; r < strLen; r++) {
for (int l = 0; l < r; l++) {
if (s.charAt(l) == s.charAt(r) && (r - l <= 2 || dp[l + 1][r - 1])) {
dp[l][r] = true;
if (r - l + 1 > maxLen) {
maxLen = r - l + 1;
maxStart = l;
maxEnd = r;
}
}
}
}
return s.substring(maxStart, maxEnd + 1);
}
作者:reedfan
链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-fa-he-dong-tai-gui-hua-by-reedfa/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2.3 跳石板
2.3.1 触发条件
1 最优化原理
1)从前往后推导,每一步的最优,达到终点的最优;
2 无后向性
1)达到当前步骤是最优后,那么从当前步开始的之后,都取决于之后的决定,而与之前的步骤无关;
2.3.2 实现步骤
1 问题分解
1)求解到达每一步的最小步数;
2)然后以此类推,找出到达最后终点的步数;
2 状态定义
1)当前的石板位置--i;
2)到达当前石板的最小步数--dp[i];
3)下一步能够走的步数--k;
4)存储所能到达的下一个节点的最小步数--dp[i + k];
3 状态转移方程推导
1)dp[i + k] = min(dp[i + k], dp[i] + 1);
4 边界条件
0)step = m - n + 1;
1)i属于[0, step - 1]
2.4 把数字翻译成字符串
2.4.1 触发条件
2.4.2 实现步骤
1 问题分解
1)子问题为求出【1-n】中的k位翻译数字的最优,然后不断拼接直到获取n位字符串的最优;
2 状态定义
1)当前字符长度--i--[0, count - 1]
2)当前位置的最大长度--dp[i]
3 状态转移方程
分为两种情况,在‘a’-'z'之间,则说明可以使用字母表示两位数,否则,要么不能跳转,或者高位数是‘0’,本质上仍旧为一位数;
1)dp[i] = dp[i-1];
2)dp[i] = dp[i-1] + dp[i-2];
4 边界条件及实现
1)dp[0] = 1;
2)dp[1]的求解,需要单独拎出来,因为它不适用于dp[i - 2];
3)其他取值范围,在状态定义时已经写过;
2.4.3 其他
1 我在本题花费时间比较长的非智力因素是:起始位置为零或者为一,没有规划好;
2 关于数字转化为字符数组的两种方法,位于csdn的另一篇文章的[2.2小节](https://blog.youkuaiyun.com/qq_31382031/article/details/105344489)进行记录;