【算法】传说中的“动态规划”真的很难吗?——动态规划详解呈上(方法总结+手写板书)(上篇)

目录

(一)动态规划的简介及解题思路

1.什么是动态规划?

2.动态规划的一般思路

(二)动态规划的常见题型及方法总结

1.斐波那契数列(Fibonacci)

2.字符串分割(Word Break)

3.三角矩阵(Triangle)


(一)动态规划的简介及解题思路

1.什么是动态规划?

DP定义:
动态规划是分治思想的延伸,通俗一点来说就是大事化小,小事化无的艺术。
在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果。

动态规划具备了以下三个特点
1. 把原来的问题分解成了几个相似的子问题
2. 所有的子问题都只需要解决一次
3. 储存子问题的解。

2.动态规划的一般思路

动态规划的本质,是对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系)
动态规划问题一般从以下四个角度考虑:
1. 状态定义——目前的定义一个中间状态下,需要考虑的问题
2. 状态间的转移方程定义 ——在上一个状态一步转移到这个状态
3. 状态的初始化——在程序开始的阶段,我们需要怎么样的设定,来进入这个转移方程
4. 返回结果——返回最终的结果
状态定义的要求:定义的状态一定要形成递推关系。
一句话概括:三特点四要素两本质
适用场景:最大值/最小值, 可不可行, 是不是,方案个数。

       

     可能你看到这里因为过于抽象,理解有些难度,大概总结一下,首先我们要定义一个F(i)(或者一元状态无法描述时,定义二元F(i,j)),在这里假设我们已经知道了上一步的结果,我们在这里想如何让上一步进行某些操作来转化到这一步,通过这个转化关系,我们定义出状态转移方程。

    这个方程需要适应题目的规律,什么意思呢?就是在任意一个状态下(代入任意i值),都可以用这个状态转移方程来得到答案。而如何来写状态转移方程,这就是一个难点了,就是随机找到一个过程,上个过程的结果F(i-1)已知,至于上个过程如何得到的结果,这里我们不关心(有点像贪心算法)然后再进行一定的验证,没有问题之后,来给这个公式的开始状态带入初始的、确切的数据(初始化状态),使它根据初始值 和利用状态转移方程把后面的数据求出来,并返回最后的一项(返回值),即答案。

总结一下:(1)根据题目描述获取问题 F(n),并先将 F(n)拆解成小部分,如下图所受

                                                          

 F(2):定义状态转移方程,保证在这个方程中,任意相邻的两个状态,从左到右,这个方程都适用。

      补充理解:

    关于上面所提到的“只关心这一步的值”我们怎么理解呢?其实就是根据状态转移方程,在前面i-1个过程中所得到当前结果已经是i-1个的过程已经求得,我们只要关心如何利用前i-1个状态转移到第i个状态即可。

    每个过程都这么考虑,最后的得到的F(n)就是最终的前n个(即全部)状态的结果 。

                                            

(二)动态规划的常见题型及方法总结

1.斐波那契数列(Fibonacci)

题目网址链接:

力扣链接:力扣

牛客链接:

斐波那契数列_牛客题霸_牛客网

经典类型问题,有许多种解法,也有不同的时间、空间复杂度。在这里,我们考虑使用动态规划的方法来分析这个问题。

       在这里首先定义状态,所谓的状态就是你要对其中一步,得到该步骤答案的过程,在这里的状态是F(i)表示i的值是多少的一种状态,然后就根据斐波那契数列公式来定义它的转移方程,并按照题目种已经给出的F(0)F(1)来定义初始值。

根据上述分析 来进行 首先3步走:

状态:F(i):第i项的值是多少。

状态转移方程:F(i)=F(i-1)+F(i-2)

初始状态:F(0)=0   F(1)=0

返回值:F(n)

根据上述内容画出表格:

 代码实现:

class Solution {
public:
    int Fibonacci(int n) {
    int* F=new int [n+1];
        F[0]=0;
        F[1]=1;
        for(int i=2;i<=n;i++){
            F[i]=F[i-1]+F[i-2];
        }
        return F[n];  
    }
};

附传统递归算法进行参考:(栈溢出)

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); } };

2.字符串分割(Word Break)

题目链接:

力扣链接:力扣

牛客链接:拆分词句_牛客题霸_牛客网

简要分析: 

 我们还是按照原本的方式,首先定义状态,在这里状态不好定义,我们可以尝试的去想一下这个过程步骤,首先肯定是从头开始切,先切第一个字母,然后看第一个字母是否作为一个单词在词典中找到,然后如果不是的话,那我们就继续往下切,看看前两个字母组成的单词是否可以在词典中找到。如果前面的单词在词典中找到的话,那我们就要分析从切点往后的字母组成的单词是否可以找到,如果这个同时也可以找到,说明整个单词就可以被切分。

这里我们总结以下刚才的分析,我们可以定义F(i)表示前i个字符是可以被切割的,假设”leet“  ”code”是词典中的两个有效单词,这里我们以leet code为例,leetcode是“leetcodeaaabbb”字符串中中的一小部分,如何知道“leetcode”(该字符串中的前8个字符)是可以切分的呢,满足两点,第一(1-4字符)“leet”可以切分有效单词,第二“code(5-8字符)”为有效单词。

假设不是leetcode为前8个字符,假设是任意8个字母变为该字符串中的前8个字符,那么怎么判断前8个字符是否有效呢?

总结为表达式为:

 在这8中情况中,只要满足一种,说明前8个字符可以拆分,这个时候我们其实就可以想到了其整个过程,画图手写如下:

老样子,三步走:

 代码实现如下:

class Solution{ 
public: 
bool wordBreak(string s, unordered_set<string> &dict){ 
if (s.empty()){ return false; }
if (dict.empty()){ return false; }
vector<bool> can_break(s.size() + 1, false); // 初始化F(0) = true 
can_break[0] = true; 
for (int i = 1; i <= s.size(); i++){ 
   for (int j = i - 1; j >= 0; j--){ 
// F(i): true{j <i && F(j) && substr[j+1,i]能在词典中找到} OR false 
// 第j+1个字符的索引为j 
      if (can_break[j] && dict.find(s.substr(j, i - j)) != dict.end()){
      can_break[i] = true;
      break; } 
   } 
}return can_break[s.size()]; } 
 };

3.三角矩阵(Triangle)

题目链接:

牛客网:三角形_牛客题霸_牛客网

力扣:力扣力扣:力扣

 

   首先看到题目的时候,通过前两道题,我们应该掌握到了一定的规律,这里状态不难定义,就是F(i)为从顶到i当前的最小路径和,但是这个地方有一个问题,这是一个二维平面的数组,我们想要确定一个数不能以简单的i一个值来判定了,所以这里我们就引进了二维状态

   问: 什么是二维状态?

答:由 i 和 j 两个变量描述的状态。

 问: 什么时候用二维状态?

答:当一个变元已经没有办法确定地描述当前状态地时候,我们就引进两个变量。

这样,用 i j 表示 i 行 j 列的元素就可以了,好了,现在难点只有一个了,就是我怎么保证到第i行j列时,是从上一个i-1 ,j-1 取最小路径过来的呢?

我们可以观察一下,首先三角形边上的数据,到达他们的路径只有一条——沿着边走。

那么到达他们的只可能是F(i-1,j)(开头数据)或者F(i-1,j-1)

而对于非边的数据, 只能是F(i-1,j-1)和F(i-1,j)两者当中的最小值再加上当前值。例如:

 图示:

 代码如下:

class Solution { public: int minimumTotal(vector<vector<int>> &triangle) {
 if (triangle.empty()){ return 0; }// F[i][j], F[0][0]初始化 
vector<vector<int>> min_sum(triangle); 
int line = triangle.size(); 
for (int i = 1; i < line; i++){ 
   for (int j = 0; j <= i; j++){ // 处理左边界和右边界 
      if (j == 0){ 
          min_sum[i][j] = min_sum[i - 1][j]; 
            }
      else if (j == i){ 
          min_sum[i][j] = min_sum[i - 1][j - 1];   
          }
      else{
          min_sum[i][j] = min(min_sum[i - 1][j], min_sum[i - 1][j - 1]); 
          }// F(i,j) = min( F(i-1, j-1), F(i-1, j)) + triangle[i][j] 
       min_sum[i][j] = min_sum[i][j] + triangle[i][j]; 
      } 
     }int result = min_sum[line - 1][0]; 
     // min(F(n-1, i)) 
       for (int i = 1; i < line; i++){ 
       result = min(min_sum[line - 1][i], result); 
         }
      return result; }
     };

  • 17
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值