🌠作者:@阿亮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];
    }
};
通过这道题目可以看出,状态定义的不同,状态转移方程也会不同,代码量和简洁程度也会有所不同。
👉总结👈
本篇博客主要讲解了什么是动态规划以及几道动态规划的题目:斐波那契数列、拆分词句和三角矩阵。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️






















