1. 双指针
算法中的双指针使用,有时候会觉得很巧妙,解决了很多的问题。首先双指针也是个很宽泛的概念,它类似于遍历中的i和j。但区别是,这两个指针是同时移动的
1.1 题型归纳
这里将题型归纳为三种,分别如下:
快慢指针(前后按不同步调的两个指针)
前后双端指针(一个在首,一个在尾部,向中间靠拢)
固定间隔的指针(以
i,i + k的间距的两个指针)
1.2 比较版本号
给出两个版本号version1和version2,请比较它们的大小
版本号由一个或多个修订号组成,各修订号由一个'.'连接。每个修订号由多位数字组成,可能包含前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从0开始,最左边的修订号下标为0,下一个修订号下标为1,以此类推。例如:2.5.33和0.1都是有效的版本号
比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较忽略任何前导零后的整数值 。也就是说,修订号1和修订号001相等。如果版本号没有指定某个下标处的修订号,则该修订号视为0。例如:版本1.0小于版本1.1,因为它们下标为0的修订号相同,而下标为1的修订号分别为0和1,即:0 < 1
返回规则如下:
如果
version1 > version2返回1如果
version1 < version2返回-1除此之外返回
0
示例1:
输入:version1 = "1.01", version2 = "1.001"输出:0
- 忽略前导零,
01和001都表示相同的整数1
示例2:
输入:version1 = "1.0", version2 = "1.0.0"输出:0
version1没有指定下标为2的修订号,即视为0
示例3:
输入:version1 = "0.1", version2 = "1.1"输出:-1
version1中下标为0的修订号是0,version2中下标为0的修订号是1。由于0 < 1,所以version1 < version2
解题思路:
使用双指针,i负责记录遍历v1字符串的索引,j负责记录遍历v2字符串的索引。由它们控制外层循环的停止条件
内部实现两个内循环,分别遍历v1和v2中的字符。使用x = x * 10 + c算法,将字符格式的版本号变为真正的数字,遇到.停止本次内循环
当两个内循环停止后,比较本次的两个版本号。如果不相等,直接返回最终结果。如果相等,继续执行外层循环,由此开启下一轮的内循环,完成后续版本号的对比
如果版本号一直相等,只有当v1和v2中全部字符遍历结束时,外层循环才会停止,并返回结果相等
代码实现:
class Solution {func compareVersion(_ version1: String, _ version2: String) -> Int {var i = 0;var j = 0;while(i < version1.count || j < version2.count) {var x = 0;while(i < version1.count){let c = version1[i];i += 1;if(c == "."){break;}x = x * 10 + Int(c)!;}var y = 0;while(j < version2.count){let c = version2[j];j += 1;if(c == "."){break;}y = y * 10 + Int(c)!;}if(x > y){return 1;}if(x < y){return -1;}}return 0;}}
时间复杂度:O(n + m)
空间复杂度:O(1)
2. 动态规划
动态规划(DP):Dynamic Programming,实际上是一种思想,通过将优化问题分解为更简单的⼦问题,并利⽤整体问题的最优解取决于其⼦问题的最优解,这⼀事实来解决优化问题的算法技术
动态规划的两个关键点:
重叠⼦问题
最优⼦结构
2.1 解决问题的⽅式
动态规划提供了两种解决问题的⽅法:
⾃顶向下的记忆化
在这种⽅法中,尝试通过递归找到较⼩⼦问题的解决⽅案,从而解决较⼤的问题
每当我们解决⼀个⼦问题时,我们都会缓存它的结果,这样如果它被多次调⽤,我们就不会重复解决它
相反,我们可以只返回保存的结果。这种存储已解决⼦问题结果的技术称为记忆化
⾃底向上的制表法
制表与⾃上⽽下的⽅法相反,避免了递归。在这种⽅法中,我们“⾃下⽽上”的解决问题,即:⾸先解决所有相关的⼦问题
通常会填写⼀个
n维表来完成。根据表中的结果,然后计算顶部(原始问题)的解决⽅案
使⽤动态规划解决的问题有个明显的特点:
⼀旦⼀个⼦问题的求解得到结果,以后的计算过程就不会修改它,这样的特点叫做⽆后效性
动态规划只解决每个⼦问题⼀次,具有天然剪枝的功能,从⽽减少计算量
动态规划解题思路分为两步:
状态定义:定义动态规划过程中涉及的变量状态
转移⽅程:归纳和抽象出⼦问题对应的数学⽅程
2.2 最长公共子序列
给出两个字符串text1和text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列 ,返回0
一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串
例如:”ace“ 是 “abcde“ 的子序列,但 “aec“ 不是 “abcde“ 的子序列。两个字符串的公共子序列是这两个字符串所共同拥有的子序列
示例1:
输入:text1 = "abcde", text2 = "ace"输出:3
- 最长公共子序列是”
ace“ ,它的长度为3
示例2:
输入:text1 = "abc", text2 = "abc"输出:3
- 最长公共子序列是”
abc“ ,它的长度为3
示例3:
输入:text1 = "abc", text2 = "def"输出:0
- 两个字符串没有公共子序列,返回
0
2.2.1 思路分析
我们将两个字符串中的字符进行逐一对比,就可以得到最终的结果。而字符对比的结果,只存在两种情况,相等与不对
如果字符相等:
将相同子序列的长度
+1分别获取
t1和t2的下一个字符,进行下一轮对比
当字符不相等:
使用
t1当前字符,与t2中后续的字符依次对比。遇到相等字符,触发上面的相对逻辑。否则,从t1中下一个字符,循环往复,对比完t1与t2的所有字符,得到按t1顺序排列的相同子序列长度t2同样需要这个过程,逐一获取t2中字符与t1进行对比,得到按t2顺序排列的相同子序列长度将
t1和t2中相同子序列长度,返回较大值即可
我们按照动态规划解题思路进行分析,首先找到子问题,即:字符对比。然后进行以下两个步骤:
【步骤一】找到状态定义:
相等
不等
【步骤二】找到转移⽅程:定义getResult方法,传入的i和j分别表示两个字符串中,即将对比的字符索引位置,该方法最终返回对比结果
当
t1[i] == t2[j],则1 + getResult(i + 1, j + 1)当
t1[i] != t2[j],则Max(getResult(i, j + 1), getResult(i + 1, j))
2.2.2 递归解法
递归解法,从所需结果出发不断回溯前一运算,直到回到初值再递推得到所需结果。属于自顶向下的解决方案,从未知到已知
代码实现:
class Solution {fileprivate var _t1 : String?;fileprivate var _t2 : String?;func longestCommonSubsequence(_ text1: String, _ text2: String) -> Int {_t1 = text1;_t2 = text2;return getResult(i: 0, j: 0);}fileprivate func getResult(i : Int, j : Int) -> Int {if(i == _t1!.count || j == _t2!.count){// 所有字符都不匹配// 相同子序列长度+0return 0;}if(_t1![i] == _t2![j]){// 将相同子序列的长度+1// 分别获取t1和t2的下一个字符,进行下一轮对比return 1 + getResult(i: i + 1, j: j + 1);}// 使用t1当前字符,与t2中后续的字符依次对比let result1 = getResult(i: i, j: j + 1);// 使用t2当前字符,与t1中后续的字符依次对比let result2 = getResult(i: i + 1, j: j);// 将t1和t2中相同子序列长度,返回较大值即可return result1 > result2 ? result1 : result2;}}
如果m == n,则时间复杂度:O(2 ^ n)
- 求解过程中,会出现一些重复子问题。而这些子问题每次都会被重复计算,所以算法的执行效率非常低下
2.2.3 缓存优化
算法时间复杂度的优化,可以使⽤⼀个数组来存储⼦问题结果。当想要⼀个⼦问题的解决⽅案时,⾸先查看数组中是否已经存在解决⽅案。如果有,取出。否则,执⾏计算并存储结果
代码实现:
class Solution {fileprivate var _t1 : String?;fileprivate var _t2 : String?;fileprivate var _arr : [[Int]]?;func longestCommonSubsequence(_ text1: String, _ text2: String) -> Int {_t1 = text1;_t2 = text2;_arr = [[Int]].init(repeating: [Int].init(repeating: -1, count: _t2!.count + 1), count: _t1!.count + 1);let result = getResult(i: 0, j: 0);return result;}fileprivate func getResult(i : Int, j : Int) -> Int {// ⾸先查看数组中是否已经存在解决⽅案if(_arr![i][j] == -1) {// 未知结果,计算并存储结果if(i == _t1!.count || j == _t2!.count){// 所有字符都不匹配// 相同子序列长度+0_arr![i][j] = 0;}else if(_t1![i] == _t2![j]){// 将相同子序列的长度+1// 分别获取t1和t2的下一个字符,进行下一轮对比_arr![i][j] = 1 + getResult(i: i + 1, j: j + 1);}else {// 使用t1当前字符,与t2中后续的字符依次对比let result1 = getResult(i: i, j: j + 1);// 使用t2当前字符,与t1中后续的字符依次对比let result2 = getResult(i: i + 1, j: j);_arr![i][j] = result1 > result2 ? result1 : result2;}}// 已知结果,直接返回return _arr![i][j];}}
时间复杂度:O(m * n)
空间复杂度:O(m * n)
2.2.4 递推解法
递推解法:从初值出发反复进行某一运算得到所需结果。属于自底向上的解决方案,从已知到未知
创建二维数组的行、列长度时,在两个字符串原有长度基础上各自+1。多创建出来的行与列,元素用0填充。这样做的目的:当遇到字符串中最后一个字符的对比时,不会因为i + 1或j + 1导致数组越界,统一逻辑的处理
代码实现:
class Solution {fileprivate var _t1 : String?;fileprivate var _t2 : String?;fileprivate var _arr : [[Int]]?;func longestCommonSubsequence(_ text1: String, _ text2: String) -> Int {_t1 = text1;_t2 = text2;// 两个字符串原有长度基础上各自+1,元素用0填充_arr = [[Int]].init(repeating: [Int].init(repeating: 0, count: _t2!.count + 1), count: _t1!.count + 1);let result = getResult();return result;}fileprivate func getResult() -> Int {// 从t1与t2字符串的结尾字符开始对比for i in (0..<_t1!.count).reversed() {for j in (0..<_t2!.count).reversed() {if(_t1![i] == _t2![j]){// 字符相等,在上一次结果上+1// 因为虚拟行列,不用担心数组越界_arr![i][j] = 1 + _arr![i + 1][j + 1];continue;}// 字符不相等,获取[i][j + 1]和[i + 1][j]的结果let result1 = _arr![i][j + 1];let result2 = _arr![i + 1][j];// 选择较大的值,记录在[i][j]中_arr![i][j] = result1 > result2 ? result1 : result2;}}// 返回最终的计算结果return _arr![0][0];}}
时间复杂度:O(m * n)
空间复杂度:O(m * n)
示例:
let text1 = "nematode knowledge";let text2 = "empty bottle";let solution = Solution();print("\(solution.longestCommonSubsequence(text1, text2))");
使用递推法求解后,会生成以下矩阵:
上述代码是从字符串结尾处开始对比,如果从字符串的起始字符开始,需要进行简单的修改:
class Solution {fileprivate func getResult() -> Int {for i in 1..._t1!.count {for j in 1..._t2!.count {if(_t1![i - 1] == _t2![j - 1]){_arr![i][j] = 1 + _arr![i - 1][j - 1];continue;}let result1 = _arr![i][j - 1];let result2 = _arr![i - 1][j];_arr![i][j] = result1 > result2 ? result1 : result2;}}return _arr![_t1!.count][_t2!.count];}}
从
索引1位置开始遍历字符串,将索引0当成虚拟行列使用对比字符时,则需要
t1[i - 1] == t2[j - 1],通过-1进行索引值的修正当前
[i][j]位置的值,使用i - 1和j - 1配合计算最终计算结果,在数组的
[t1.count][t2.count]中存储
2.2.5 空间优化
以上算法的空间复杂度为O(n * m),但通过代码可以得知,只要有上一行的结果,即可推算出当前行的结果。所以我们可以定义两个一维数组,分别存储上一行与当前行的结果
按照这个思路,我们将长度较短的字符串作为列,可以让数组所需的空间更小,从而进一步优化空间
class Solution {fileprivate var _row : String?;fileprivate var _col : String?;// 当前行fileprivate var _currRow : [Int]?;// 上一行fileprivate var _prevRow : [Int]?;func longestCommonSubsequence(_ text1: String, _ text2: String) -> Int {// 较长字符串作为行_row = text1.count >= text2.count ? text1 : text2;// 较短字符串作为列_col = text1.count >= text2.count ? text2 : text1;_currRow = [Int].init(repeating: 0, count: _col!.count + 1);_prevRow = [Int].init(repeating: 0, count: _col!.count + 1);let result = getResult();return result;}fileprivate func getResult() -> Int {for i in 1..._row!.count {for j in 1..._col!.count {if(_row![i - 1] == _col![j - 1]){// _prevRow为上一行,j - 1为上一列,等同于二维数组的arr[i - 1][j - 1]_currRow![j] = 1 + _prevRow![j - 1];continue;}//等同于二维数组的arr[i - 1][j]let result1 = _prevRow![j];//等同于二维数组的arr[i][j - 1]let result2 = _currRow![j - 1];// 将较大的结果,更新当前行的j列上_currRow![j] = result1 > result2 ? result1 : result2;}// 将当前行的结果赋值给上一行,准备下一轮的循环使用_prevRow = _currRow;}return _currRow![_col!.count];}}
时间复杂度:O(m * n)
空间复杂的:O(2 * min(m, n))
2.2.6 公共子序列
通过矩阵,反推出两个字符串的公共子序列内容:
class Solution {fileprivate var _t1 : String?;fileprivate var _t2 : String?;fileprivate var _arr : [[Int]]?;func commonSubsequence(_ text1: String, _ text2: String) -> [String]? {_t1 = text1;_t2 = text2;_arr = [[Int]].init(repeating: [Int].init(repeating: 0, count: _t2!.count + 1), count: _t1!.count + 1);let result = getResult();return result;}fileprivate func getResult() -> [String]? {// 得到公共子序列的最大长度var k = get_lcs_lenght();if(k == 0){return nil;}// 按最大长度分配结果数组的空间var result = [String].init(repeating: "", count: k);// 从最终的结果,向初始位置推算var i = _t1!.count;var j = _t2!.count;k -= 1;while(k >= 0){if(_t1![i - 1] == _t2![j - 1]){result[k] = _t1![i - 1];k -= 1;i -= 1;j -= 1;continue;}if(_arr![i][j - 1] > _arr![i - 1][j]){j -= 1;}else{i -= 1;}}return result;}fileprivate func get_lcs_lenght() -> Int {for i in 1..._t1!.count {for j in 1..._t2!.count {if(_t1![i - 1] == _t2![j - 1]){_arr![i][j] = 1 + _arr![i - 1][j - 1];continue;}let result1 = _arr![i][j - 1];let result2 = _arr![i - 1][j];_arr![i][j] = result1 > result2 ? result1 : result2;}}return _arr![_t1!.count][_t2!.count];}}
2.3 背包问题
给定一个背包容量target,再给定一个物品数组nums,能否按一定方式选取nums中的元素得到target,每个元素选一次/每个元素选多次/选元素进行排列组合
根据上述需求,可以分为四种背包问题:
0/1背包问题:每个元素最多选取一次
完全背包问题:每个元素乐意重复选择
组合背包问题:背包中的物品要考虑顺序
分组背包问题:不止一个背包,需要遍历每个背包
2.3.1 0/1背包
以0/1背包问题为例,使用动态规划的思想解决
给定n个物品的重量和价值,从中选择物品放在容量为w的背包中(表示选择物品重量不超过w),同时每个物品最多允许选择1个,使得背包中物品价值之和最高
输入:
int Values[n]:物品的价值int Weights[n]:物品的重量int LimitWeight:背包容量剩余大小输出:
MaxValue:背包里的物品最大价值
2.3.2 思路分析
拆解为子问题:物品的选取。每个物品只能选一个,在不超过背包容量的情况下,保证背包中物品总价值最高
状态定义:
选取物品
不选取物品
转移⽅程:
选取物品:
value + getResult(index: index + 1, limit: limit - weight)- 当前物品价值 + 即将获取的后续物品价值,预先将背包剩余重量 - 当前物品重量
不选取物品:
getResult(index: index + 1, limit: limit)- 跳过本次物品,直接获取的后续物品价值
2.3.3 递归解法
class Solution {fileprivate var _weights: [Int]?;fileprivate var _values: [Int]?;func knapsack(_ weights: [Int], _ values: [Int], _ limit: Int) -> Int {_weights = weights;_values = values;return getResult(index: 0, limit: limit);}fileprivate func getResult(index : Int, limit : Int) -> Int {var max = 0;if(limit <= 0 || index >= weights.count){// 背包没有空间,或者物品已经取完,本次选取的物品价值返回0return max;}// 获取当前物品重量let weight = _weights![index];// 获取当前物品价值let value = _values![index];if(weight > limit){// 当前物品重量,超过背包剩余重量,放弃本次物品,继续选取下一个物品max = getResult(index: index + 1, limit: limit);// 返回可选取的后续物品的总价值return max;}// 选取本次物品,背包中的物品价值增加var selectedValue = value;// 选取本次物品,背包中的剩余重量减少let selectedWeight = limit - weight;// 继续选取下一个物品selectedValue += getResult(index: index + 1, limit: selectedWeight);// 放弃本次物品,直接选取下一个物品let skipValue = getResult(index: index + 1, limit: limit);// 比较两种选取方式中,可获得更大的物品价值max = selectedValue > skipValue ? selectedValue : skipValue;// 返回更大的物品价值return max;}}
- 时间复杂度:O(m ^ n)
2.3.4 缓存优化
class Solution {fileprivate var _weights: [Int]?;fileprivate var _values: [Int]?;fileprivate var _arr: [[Int]]?;func knapsack(_ weights: [Int], _ values: [Int], _ limit: Int) -> Int {_weights = weights;_values = values;// 创建二维数组作为缓存,i和j分别为物品数量和背包限制的总重量_arr = [[Int]].init(repeating: [Int].init(repeating: -1, count: limit + 1), count: weights.count + 1);return getResult(index: 0, limit: limit);}fileprivate func getResult(index : Int, limit : Int) -> Int {if(limit <= 0 || index >= weights.count){// 背包没有空间,或者物品已经取完,本次选取的物品价值返回0return 0;}if(_arr![index][limit] == -1) {// 获取当前物品重量let weight = _weights![index];// 获取当前物品价值let value = _values![index];if(weight > limit){// 当前物品重量,超过背包剩余重量,放弃本次物品,继续选取下一个物品_arr![index][limit] = getResult(index: index + 1, limit: limit);// 返回可选取的后续物品的总价值return _arr![index][limit];}// 选取本次物品,背包中的物品价值增加var selectedValue = value;// 选取本次物品,背包中的剩余重量减少let selectedWeight = limit - weight;// 继续选取下一个物品selectedValue += getResult(index: index + 1, limit: selectedWeight);// 放弃本次物品,直接选取下一个物品let skipValue = getResult(index: index + 1, limit: limit);// 比较两种选取方式中,可获得更大的物品价值_arr![index][limit] = selectedValue > skipValue ? selectedValue : skipValue;// 返回更大的物品价值return _arr![index][limit];}// 命中缓存return _arr![index][limit];}}
时间复杂度:O(m * n)
空间复杂度:O(m * n)
2.3.5 递推解法
class Solution {fileprivate var _weights: [Int]?;fileprivate var _values: [Int]?;fileprivate var _arr: [[Int]]?;func knapsack(_ weights: [Int], _ values: [Int], _ limit: Int) -> Int {_weights = weights;_values = values;// 创建二维数组作为缓存,i和j分别为物品数量和背包限制的总重量_arr = [[Int]].init(repeating: [Int].init(repeating: 0, count: limit + 1), count: weights.count + 1);return getResult(limit: limit);}fileprivate func getResult(limit : Int) -> Int {for i in 1..._weights!.count {for j in 1...limit{// 获取当前物品重量let weight = _weights![i - 1];// 获取当前物品价值let value = _values![i - 1];if(weight > j){// 当前物品重量,超过背包剩余重量,放弃本次物品,使用上一次的物品总价值填充_arr![i][j] = _arr![i - 1][j];continue;}// 选取本次物品,背包中的物品价值增加var selectedValue = value;// 选取本次物品,背包中的剩余重量减少let selectedWeight = j - weight;// 在剩余重量相同的条件下,加入上一次物品选择后的总价值selectedValue += _arr![i - 1][selectedWeight];// 放弃本次物品,在相同重量的条件下,得到上一次物品选择后的总价值let skipValue = _arr![i - 1][j];// 比较两种选取方式,保存更大的物品价值_arr![i][j] = selectedValue > skipValue ? selectedValue : skipValue;}}// 返回最终物品选取的总价值return _arr![_weights!.count][limit];}}
时间复杂度:O(m * n)
空间复杂度:O(m * n)
2.2.6 空间优化
class Solution {fileprivate var _weights: [Int]?;fileprivate var _values: [Int]?;fileprivate var _curRow: [Int]?;fileprivate var _preRow: [Int]?;func knapsack(_ weights: [Int], _ values: [Int], _ limit: Int) -> Int {_weights = weights;_values = values;// 初始化当前行_curRow = [Int].init(repeating: 0, count: limit + 1);// 初始化上一行_preRow = [Int].init(repeating: 0, count: limit + 1);return getResult(limit: limit);}fileprivate func getResult(limit : Int) -> Int {for i in 0..<_weights!.count {// 获取当前物品重量let weight = _weights![i];// 获取当前物品价值let value = _values![i];for j in 1...limit{if(weight > j){// 当前物品重量,超过背包剩余重量,放弃本次物品,使用上一次的物品总价值填充_curRow![j] = _preRow![j];continue;}// 选取本次物品,背包中的物品价值增加var selectedValue = value;// 选取本次物品,背包中的剩余重量减少let selectedWeight = j - weight;// 在剩余重量相同的条件下,加入上一次物品选择后的总价值selectedValue += _preRow![selectedWeight];// 放弃本次物品,在相同重量的条件下,得到上一次物品选择后的总价值let skipValue = _preRow![j];// 比较两种选取方式,保存更大的物品价值_curRow![j] = selectedValue > skipValue ? selectedValue : skipValue;}for j in 1...limit{print("\(_curRow![j]),");}print("\n");// 将当前行的结果赋值给上一行,准备下一轮的循环使用_preRow = _curRow;}// 返回最终物品选取的总价值return _curRow![limit];}}
时间复杂度:O(m * n)
空间复杂的:O(2n)
2.2.6 空间再优化
class Solution {fileprivate var _weights: [Int]?;fileprivate var _values: [Int]?;// 当前行,同时也作为上一行的数据fileprivate var _curRow: [Int]?;func knapsack(_ weights: [Int], _ values: [Int], _ limit: Int) -> Int {_weights = weights;_values = values;// 初始化当前行_curRow = [Int].init(repeating: 0, count: limit + 1);return getResult(limit: limit);}fileprivate func getResult(limit : Int) -> Int {for i in 0..<_weights!.count {// 获取当前物品重量let weight = _weights![i];// 获取当前物品价值let value = _values![i];// 由于当前总价值,依赖于上一行的总价值,所以剩余空间需要从大到小遍历,避免上一行的数据提前被覆盖为当前行for j in (1...limit).reversed() {if(weight > j){// 当前物品重量,超过背包剩余重量,放弃本次物品,使用上一次的物品总价值填充// 使用curRow存储当前总价值,同时它也作为上一行的总价值在使用// 所以不去修改curRow[j]的值,相当于使用了上一次的物品总价值continue;}// 选取本次物品,背包中的物品价值增加var selectedValue = value;// 选取本次物品,背包中的剩余重量减少let selectedWeight = j - weight;// 在剩余重量相同的条件下,加入上一次物品选择后的总价值selectedValue += _curRow![selectedWeight];// 放弃本次物品,在相同重量的条件下,得到上一次物品选择后的总价值// 只要curRow[j]没有提前被覆盖,拿到的就是上一次的物品总价值let skipValue = _curRow![j];// 比较两种选取方式,保存更大的物品价值_curRow![j] = selectedValue > skipValue ? selectedValue : skipValue;}}// 返回最终物品选取的总价值return _curRow![limit];}}
时间复杂度:O(m * n)
空间复杂的:O(n)
总结
双指针题型归纳:
快慢指针(前后按不同步调的两个指针)
前后双端指针(一个在首,一个在尾部,向中间靠拢)
固定间隔的指针(以
i,i + k的间距的两个指针)
动态规划:
动态规划(
DP):Dynamic Programming,实际上是一种思想,通过将优化问题分解为更简单的⼦问题,并利⽤整体问题的最优解取决于其⼦问题的最优解,这⼀事实来解决优化问题的算法技术动态规划的两个关键点:
重叠⼦问题
最优⼦结构
动态规划提供了两种解决问题的⽅法:
⾃顶向下的记忆化
⾃底向上的制表法
使⽤动态规划解决的问题有个明显的特点:
⼀旦⼀个⼦问题的求解得到结果,以后的计算过程就不会修改它,这样的特点叫做⽆后效性
动态规划只解决每个⼦问题⼀次,具有天然剪枝的功能,从⽽减少计算量
动态规划解题思路分为两步:
状态定义:定义动态规划过程中涉及的变量状态
转移⽅程:归纳和抽象出⼦问题对应的数学⽅程
