🌠作者:@阿亮joy.
🎆专栏:《数据结构与算法要啸着学》
🎇座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
目录
- 👉什么是动态规划👈
- 👉斐波那契数列👈
- 👉拆分词句👈
- 👉三角矩阵👈
- 👉总结👈
👉什么是动态规划👈
动态规划(Dynamic Programming,简称为 DP)是分治思想的延伸,通俗来说就是大事化小,小事化了的艺术。在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果。
动态规划具备了以下三个特点:
- 把原来的问题分解成了几个相似的子问题
- 所有的子问题都只需要解决一次
- 储存子问题的解
动态规划的本质,是对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系),动态规划问题一般从以下四个角度来考虑:状态定义、状态间的转移方程定义、状态的初始化和返回结果。
状态定义的要求:定义的状态一定能够形成递推关系。动态规划适用的场景:最大最小值问题、可不可行、是不是以及方案个数等问题。
👉斐波那契数列👈
斐波那契数列不管是递归版本和循环版本的解法,都是非常简单的,但是它仍然是非常经典的一道动态规划的题目。它能够让我们很好去熟悉动态规划状态的定义、状态转移方程的定义、状态的初始化以及返回结果。
状态:F(i) 第 i 项斐波那契数
状态转移方程:F(i) = F(i - 1) + F(i - 2)
初始状态:F(0) = 0,F(1) = 1,F(2) = 1
返回结果:F(n) 第 n 项斐波那契数
以上的分析过程是非常重要的,特别是对一些难题,这也是解决动态规划题目的难点所在。
class Solution
{
public:
int Fibonacci(int n)
{
// 创建数组,保存中间状态的解
int* F = new int[n + 1];
// 初始化F(0)和F(1)
F[0] = 0, F[1] = 1;
// 状态转移方程F(i) = F(i-1) + F(i-2)
for(int i = 2; i <= n; ++i)
{
F[i] = F[i - 1] + F[i - 2];
}
// 返回结果
int ret = F[n];
delete[] F;
return ret;
}
};
其实我们求解当前第 i 项的斐波那契数,只需要前两项的斐波那契数,所以我们可以对空间复杂度进行进一步的优化。
class Solution
{
public:
int Fibonacci(int n)
{
int f0 = 0;
int f1 = 1;
int fn = 1;
for(int i = 2; i <= n; ++i)
{
// f0为F(i-2),f1为F(i-1)
fn = f0 + f1;
f0 = f1;
f1 = fn;
}
return fn;
}
};
👉拆分词句👈
给定一个字符串s和一组单词dict,判断s是否可以用空格分割成一个单词序列,使得单词序列中所有的单词都是dict中的单词(序列可以包含一个或多个单词)。例如:
给定s=“nowcode”;dict=[“now”, “code”]。返回true,因为"nowcode"可以被分割成"now code"。
如果我们想一下采用暴力的方式去分割字符串的话,很明显是行不通的。这时候,我们可以尝试一下采用动规。动态最难的就是如何定义状态以及状态定义好后,如何确定状态转移方程。完成了这两部,题目也差不多可以解决了。
那状态(子问题)如何确定呢?其实状态一般都是从问题中抽象出来的。本道题的问题是字符串 s 是否可以被分割,那么状态 F(i) 是不是就是字符串 s 的前 i 个字符是否可以被分割。那接下来就确定状态转移方程,见下图。
class Solution
{
public:
bool wordBreak(string s, unordered_set<string> &dict)
{
if(dict.empty()) // 词典为空
{
return false;
}
vector<bool> canBreak(s.size() + 1, false);
// 初始状态
canBreak[0] = true;
for(int i = 1; i <= s.size(); ++i)
{
// j < i && F(j) && 第[j + 1, i]字符组成的单词是否在词典里
for(int j = 0; j < i; ++j)
{
if(canBreak[j] && (dict.find(s.substr(j, i - j)) != dict.end()))
{
canBreak[i] = true;
break;
}
}
}
return canBreak[s.size()];
}
};
通过拆分词句这道题,我们也可以看出状态方程不一定是一个等式,且需要辅助状态(实际不存在的状态)。
👉三角矩阵👈
思路一:从上向下推
class Solution
{
public:
int minimumTotal(vector<vector<int> > &triangle)
{
if(triangle.empty())
{
return 0;
}
int row = triangle.size();
int col = triangle[0].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];
}
else if(j == i)
{
triangle[i][j] += triangle[i - 1][j - 1];
}
else
{
triangle[i][j] += min(triangle[i - 1][j - 1], triangle[i - 1][j]);
}
}
}
// 找出从顶部到底部的最小路径和
int ret = triangle[row - 1][0];
for(int j = 1; j < row; ++j)
{
if(triangle[row - 1][j] < ret)
{
ret = triangle[row - 1][j];
}
}
return ret;
}
};
思路二:从下向上推
class Solution
{
public:
int minimumTotal(vector<vector<int> > &triangle)
{
if(triangle.empty())
{
return 0;
}
int row = triangle.size();
int col = triangle[0].size();
for(int i = row - 2; i >= 0; --i)
{
for(int j = 0; j <= i; ++j)
{
triangle[i][j] += min(triangle[i + 1][j], triangle[i + 1][j + 1]);
}
}
return triangle[0][0];
}
};
通过这道题目可以看出,状态定义的不同,状态转移方程也会不同,代码量和简洁程度也会有所不同。
👉总结👈
本篇博客主要讲解了什么是动态规划以及几道动态规划的题目:斐波那契数列、拆分词句和三角矩阵。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️