【算法】动态规划
1.概念主要思想及场景用法
Dynamic Programming
DP定义:动态规划是分治思想的延伸,通俗一点来说就是大事化小,小事化无的艺术。在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果。
动态规划具备了以下三个特点:
- 把原来的问题分解成了几个相似的子问题。
- 所有的子问题都只需要解决一次。
- 储存子问题的解。
动态规划的本质,是对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系)
动态规划问题一般从以下四个角度考虑:
- 状态定义
- 状态间的转移方程定义
- 状态的初始化
- 返回结果
状态定义的要求:定义的状态一定要形成递推关系。
一句话概括:三特点四要素两本质
适用场景:最大值/最小值, 可不可行, 是不是,方案个数
2.题目练习
动态规划主要是一种思想,通过刷题才能更有经验。
2.1第1题 Fibonacci
题目描述
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。
n<=39
斐波拉契数列是一道很简单的题目,我想大家第一时间都会想递归
class Solution{
public:
int Fibonacci(int n){
// 初始值
if (n <= 0){
return 0;
}
if (n == 1 || n == 2) {
return 1;
}
// F(n)=F(n-1)+F(n-2)
return Fibonacci(n - 2) + Fibonacci(n - 1);
}
};
递归的方法时间复杂度为O(2^n),随着n的增大呈现指数增长,效率低下当输入比较大时,可能导致栈溢出
在递归过程中有大量的重复计算。我们来通过动态规划的方法来分析题目:
动态规划的四个要素:
1.状态:第i项的值为F(i)
2.状态初始化:F(0) = 0,F(1) = 1
3.转义方程:F(n)=F(n-1)+F(n-2)
4.返回值:F(n)
class Solution3{
public:
int Fibonacci(int n){
// 初始值
if (n <= 0){
return 0;
}
if (n == 1 || n == 2) {
return 1;
}
int fn1 = 1;
int fn2 = 1;
int result = 0;
for (int i = 3; i <= n; i++){
// F(n)=F(n-1)+F(n-2)
result = fn2 + fn1;
// 更新值
fn1 = fn2;
fn2 = result;
}
return result;
}
};
2.2第2题 字符串分割(Word Break)
题目描述
给定一个字符串s和一组单词dict,判断s是否可以用空格分割成一个单词序列,使得单词序列中所有的单词都是dict中的单词(序列可以包含一个或多个单词)。
例如:
给定s=“leetcode”;
dict=[“leet”, “code”].
返回true,因为"leetcode"可以被分割成"leet code".
这个题如果使用暴力方法查分会很麻烦,我们来用动态规划的思想解决:
还是通过题目找到四要素:
这个题的状态和状态转义方程是比较难找的,这也是动态规划的难点。
分析问题:字符串s能否被分割 ----->从而抽象出状态
状态:F(i):字符串前i个字符是否可以分割
分析问题:
eg:LeetCode可以被分成的可能性有
F(0)和[8,8] 状态:true
F(1)和[2,8] 状态:
F(2)和[3,8] 状态:
F(3)和[4,8] 状态:
F(4)和[5,8] 状态:true
F(5)和[6,8] 状态:
。。。。。。
通过分析:我们可以得出它的状态方程
状态方程:F(i):j < i && F(j) && [j+1,i]是否能在词典中找到,若能状态方程为true.
状态初始化:F(0) :true
返回值:F(s.size())
代码实现:
class Solution {
public:
bool wordBreak(string s, unordered_set<string> &dict) {
if(s.empty())
return false;
if(dict.empty())
return false;
//开数组初始化它的大小和状态
vector<bool> array(s.size()+ 1,false);
//动态规划四要素:状态初始化
array[0] = true;
for(int i = 1;i <= s.size();i++)
{
//状态转化方程:
for(int j = 0;j < i;j++)
{
// F(i): true{j <i && F(j) && substr[j+1,i]能在词典中找到} OR false
// 第j+1个字符的索引为j
if(array[j] && dict.find(s.substr(j, i - j))!= dict.end())
{
//满足条件,可以被分割
array[i] = true;
break;
}
}
}
return array[s.size()];
}
};
第3题 三角矩阵(Triangle)
题目描述
给出一个三角形,计算从三角形顶部到底部的最小路径和,每一步都可以移动到下面一行相邻的数字,
例如,给出的三角形如下:
[↵ [2],↵ [3,4],↵ [6,5,7],↵ [4,1,8,3]↵]
最小的从顶部到底部的路径和是2 + 3 + 5 + 1 = 11。
注意:
如果你能只用O(N)的额外的空间来完成这项工作的话,就可以得到附加分,其中N是三角形中的行总数。
分析问题:从顶部到底部的最小路径和
状态:涉及到位置使用二维数组
F(i,j):从(0,0)到(i,j)的最小路径和
状态转换方程:
分析:
情况一:
到达F(i,j)只有两条路,F(i-1,j-1)和F(i-1,j)选择一条最短的,再加上我们自己
即:
F(i)(j) = min(F(i-1,j-1),F(i-1,j)) + array[i][j]
eg:
情况2:
对于边上的点,从上边节点到达它的路径只有一条
即:
if(j == 0 ):F(i-1,0)+array[i][0];
if(j == i ):F(i-1,j -1)+array[i][j];
初始状态:
F(0,0) = array[0][0]
**返回结果:**最后一行的最短路径
min(F(row - 1,j))
class Solution {
public:
int minimumTotal(vector<vector<int> > &triangle) {
int row = triangle.size();
int col = triangle[row - 1].size();
//遍历二维数组,算出它到达底部所有值的最短路径和
for(int i = 1;i < row;i++)
{
for(int j = 0;j <= i;j++)
{
if(j == 0)
triangle[i][j] = triangle[i - 1][j] + triangle[i][j];
else if(j == i)
triangle[i][j] = triangle[i - 1][j - 1] + triangle[i][j];
else
triangle[i][j] = min(triangle[i - 1][j - 1],triangle[i-1][j]) + triangle[i][j];
}
}
int minsum = triangle[row - 1][0];
for(int j = 1;j < col;j++)
{
minsum = min(minsum,triangle[row-1][j]);
}
return minsum;
}
};
总结:感觉现在笔试动态规划越来越多了,要多多练题才能更好的理解动态规划。