动态规划经典例题一

动态规划经典例题二

文章目录

  • 动态规划(Dynamic Programming)
    • 概念
      • DP定义:
      • 动态规划具备了以下三个特点
      • 动态规划的本质
      • 从四个角度考虑动态规划问题
      • 状态定义的要求
    • 第一题 Fibonacci
      • 动态规划方法
    • 第2题 变态青蛙跳台阶(Climbing Stairs)
      • 动态规划方法
    • 第3题 最大连续子数组和(Maximum Subarray)
      • 动态规划方法
    • 第4题 字符串分割(Word Break)
      • 动态规划方法
    • 第5题 三角矩阵(Triangle)
      • 动态规划方法
    • 第6题 路径总数(Unique Paths)
      • 动态规划方法

动态规划(Dynamic Programming)

概念

DP定义:

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

动态规划具备了以下三个特点

  1. 把原来的问题分解成了几个相似的子问题。
  2. 所有的子问题都只需要解决一次。
  3. 储存子问题的解。

动态规划的本质

对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系)

从四个角度考虑动态规划问题

  1. 状态定义
  2. 状态间的转移方程定义
  3. 状态的初始化
  4. 返回结果

状态定义的要求

定义的状态一定要形成递推关系

一句话概括:三特点四要素两本质
适用场景: 最大值/最小值,可不可行,是不是,方案个数


第一题 Fibonacci

大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39

斐波那契数列定义:F(n)=F(n-1)+F(n-2)(n>=2,n∈N*),其中F(1)=1,F(2)=1

方法一:递归

  • 递归的方法时间复杂度为O(2^n),随着n的增大呈现指数增长,效率低下
  • 当输入比较大时,可能导致栈溢出
  • 在递归过程中有大量的重复计算
class Solution {
public:
    int Fibonacci(int n) {
      if(n<=0)
        return 0;
      if(n==1||n==2)
        return 1;
      return Fibonacci(n-2)+Fibonacci(n-1);
    }
};

动态规划方法

方法二:数组

  • 状态:F(n)
  • 状态递推:F(n)=F(n-1)+F(n-2)
  • 初始值:F(1)=F(2)=1
  • 返回结果:F(N)
class Solution {
public:
    int Fibonacci(int n) {
      vector<int> arr(n+1);
      arr[0]=0;
      arr[1]=1;
      for(int i=2;i<=n;i++)
        arr[i]=arr[i-1]+arr[i-2];
      return arr[n];
    }
};

上述解法的空间复杂度为O(n),其实F(n)只与它相邻的前两项有关,所以没有必要保存所有子问题的解,只需要保存两个子问题的解就可以。
下面方法的空间复杂度将为O(1)
方法三:交换

class Solution {
public:
    int Fibonacci(int n) {
      int a=0;
      int b=1;
      int c;
      if(n<=0)
        return 0;
      if(n==1||n==2)
        return 1;
      for(int i=2;i<=n;++i)
      {
        c=a+b;
        a=b;
        b=c;
      }
      return c;
    }
};

第2题 变态青蛙跳台阶(Climbing Stairs)

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
状态递推:

状态:
F(n):跳上n级台阶的方法
F(n)=F(n-1)+F(n-2)+F(n-3)+······F(1)+F(0)
F(n-1)=F(n-2)+F(n-3)······+F(1)+F(0)
右上可得:
F(n)=2*F(n-1)

初始值:

F(1) = 1
F(2) = 2F(1) = 2
F(3) = 2
F(2) = 4
F(4) = 2*F(3) = 8
所以它是一个等比数列
F(n) = 2^(n-1)

方法一:递归

class Solution {
public:
    int jumpFloorII(int number) {
        if(number<2)
        {
            return number;
        }
        else
            return 2*jumpFloorII(number-1);
    }
};

动态规划方法

方法二:排列
每个台阶看成一个位置,除过最后一个位置,其它位置都有两种可能性,所以总的排列数为2^(n-1)*1 = 2^(n-1)

class Solution {
public:
    int jumpFloorII(int number) {
      int ret=1;
      for(int i=2;i<=number;i++)
      {
        ret=2*ret;
      }
      return ret;
    }
};

扩展:降低时间复杂度

  • 上述实现的时间复杂度:O(N)
  • O(1)的实现:使用移位操作
    方法三:位操作
class Solution {
public:
    int jumpFloorII(int number) {
      if(number==0)
        return 0;
      return 1<<(number-1);
    }
};

总结:
此题看似复杂,通过抽象和归纳,可以找出问题的内在规律
定义问题的状态,以及状态间的递推关系,找到问题的答案

扩展1:

上述问题为变态青蛙跳台阶,太疯狂,这只青蛙像是吃了大力丸身上充满了无穷的力量。现在让它变成一个正常的青蛙,限制它一次只能跳1阶或者2阶,现在该如何解答

扩展2:

牛客网上另一个题目:矩形覆盖
我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?

状态F(i):用i个21的小矩形无重叠地覆盖一个2i的大矩形的方法
递归:
不推荐,太占空间

class Solution {
public:
    int rectCover(int number) {
       if(number <=2)
            return number;
      return rectCover(number-2)+rectCover(number-1);
    }
};

迭代

class Solution {
public:
    int rectCover(int number) {
        if ( number < 1 ) 
          return 0;
        int a = 1, b = 2;
        while ( --number ) {
            b = b + a;
            a = b - a;
        }
        return a;
    }
};

上述两个题目都可以用斐波那契数列求解


第3题 最大连续子数组和(Maximum Subarray)

HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)

F(i):以第i项结尾的连续子序列的最大和
F(i):max(F(i-1)+a[i],a[i])

动态规划方法

方法一

class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
      if(array.empty())
        return 0;
      vector<int> maxSum(array.size(),0);
      //F[0]=array[0]
      maxSum[0]=array[0];
      for(int i=1;i<array.size();i++)
        //F[i]=max(F[i-1]+array[i],array[i])
        maxSum[i]=max(maxSum[i-1]+array[i],array[i]);
      //max(F[i])
      int ret=maxSum[0];
      for(int i=1;i<array.size();i++)
        ret=max(ret,maxSum[i]);
      return ret;
    }
};

方法二

class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
      int maxnum=array[0];
      int count=array[0];
      for(int i=1;i<array.size();i++)
      {
        if(count>=0)
          count+=array[i];
        else
          count=array[i];
        maxnum=max(count,maxnum);
      }
      return maxnum;
    }
};

第4题 字符串分割(Word Break)

给定一个字符串s和一组单词dict,判断s是否可以用空格分割成一个单词序列,使得单词序列中所有的单词都是dict中的单词(序列可以包含一个或多个单词)。
例如:
给定s=“leetcode”;
dict=[“leet”, “code”].
返回true,因为"leetcode"可以被分割成"leet code".

状态:
F(i):前i个字符能否被分割
F(1) “l”:dict.find(s(1,1)) false
F(2) “le”:F(1) && dict.find(s(2,2)) false
F(3) “lee”:F(2) && dict.find(s(3,3)) ,F(1) && dict.find(s(2,3)) false
F(4) “lee”:F(3) && dict.find(s(4,4)) ,F(2) && dict.find(s(3,4)),F(1) && dict.find(s(2,4)) ,F(0) && dict.find(s(1,4)) true
···········
F(i):F(j)&&dict.find(s(j+1,i)),(j

即s=“leetcode”;dict=[“leet”, “code”].

F(0)—>"": true
F(1)—>“l” :"" “l” F(0)&&dict.find(“l”) false
F(2)—>“le”:“l” “e”, “” “le” F(0)&&dict.find(“le”) false
F(3)—>“lee”:“le” “e” F(2)&&dict.find(“e”), “l” “ee”, “” “lee” false
F(4)—>“leet”:“lee” “t”,“le” “et”,“l” “eet”,“leet” F(0)&&dict.find(“leet”) true

动态规划方法

状态:
子状态:前1,2,3,…,n个字符能否根据词典中的词被成功分词
F(i): 前i个字符能否根据词典中的词被成功分词
状态递推:
F(i): true{j 在j小于i中,只要能找到一个F(j)为true,并且从j+1到i之间的字符能在词典
中找到,则F(i)为true
初始值:
对于初始值无法确定的,可以引入一个不代表实际意义的空状态,作为状态的起始
空状态的值需要保证状态递推可以正确且顺利的进行,到底取什么值可以通过简单
的例子进行验证
F(0) = true
返回结果:F(n)

class Solution {
public:
    bool wordBreak(string s, unordered_set<string> &dict) {
      if(s.empty())
        return false;
      if(dict.empty())
        return false;
      vector<bool> F(s.size()+1,false);
      //F(0)=true;
      F[0]=true;
      for(int i=1;i<=s.size();++i)
      {
        //F(i)=F(j)&&dict.find(s(j+1,i))  j
        for(int j=0;j<i;++j)
        {
          //1~j   j+1~i
          if(F[j]&&dict.find(s.substr(j,i-j))!=dict.end())
          {
            F[i]=true;
            break;
          }
        }
      }
      return F[s.size()];
    }
};

第5题 三角矩阵(Triangle)

给出一个三角形,计算从三角形顶部到底部的最小路径和,每一步都可以移动到下面一行相邻的数字,
例如,给出的三角形如下:

[
   [2],↵    
  [3,4],↵   
 [6,5,7],↵  
[4,1,8,3]↵
]

最小的从顶部到底部的路径和是2 + 3 + 5 + 1 = 11。
注意:
如果你能只用O(N)的额外的空间来完成这项工作的话,就可以得到附加分,其中N是三角形中的行总数。

动态规划方法

方法一:动态规划

(i,j)—>(i+1,j),(i+1,j+1)
(i,j)—>(i-1,j),(i-1,j-1)
状态:
F(i,j):从(0,0)到(i,j)的最短路径
F(i,j)=min(F(i-1,j),F(i-1,j-1))+a[i][j]
F(i,0)=F(i-1,0)+a[i][0]
F(i,i)=F(i-1,i-1)+a[i][i]
初始状态:
F(0,0)=a[0][0]
返回结果:
min(F(row-1,j))

class Solution {
public:
    int minimumTotal(vector<vector<int> > &triangle) {
       if(triangle.empty())
         return 0;
      //F(0,0)=a[0][0]
      vector<vector<int>> minsum(triangle);
      int row=triangle.size();
      for(int i=1;i<row;i++)
      {
        minsum[i][0]=minsum[i-1][0]+triangle[i][0];
      }
      for(int i=1;i<row;i++)
      {
        minsum[i][i]=minsum[i-1][i-1]+triangle[i][i];
      }
      for(int i=2;i<row;i++)
      {
        for(int j=1;j<i;j++)
        {
           //F(i,j)=min(F(i-1,j),F(i-1,j-1))+a[i][j]
            minsum[i][j]=min(minsum[i-1][j],minsum[i-1][j-1])+triangle[i][j]; 
        }
      }
      int minret=minsum[row-1][0];
      for(int i=1;i<row;i++)
      {
        minret=min(minsum[row-1][i],minret);
      }
      return minret;
    }
};

方法二:动态规划(反向思维)

状态:
子状态:从(n,n),(n,n-1),…(1,0),(1,1),(0,0)到最后一行的最短路径和
F(i,j): 从(i,j)到最后一行的最短路径和
状态递推:
F(i,j) = min( F(i+1, j), F(i+1, j+1)) + triangle[i][j]
初始状态:
F(n-1,0) = triangle[n-1][0], F(n-1,1) = triangle[n-1][1],…, F(n-1,n-1) = triangle[n1][n-1]
返回结果:
F(0, 0)
这种逆向思维不需要考虑边界,也不需要最后寻找最小值,直接返回F(0,0)即可

class Solution {
public:
    int minimumTotal(vector<vector<int> > &triangle) {
     if (triangle.empty())
       return 0;
    // F[n-1][n-1],...F[n-1][0]初始化
     vector<vector<int>> min_sum(triangle);
     int line = triangle.size();
     // 从倒数第二行开始
     for (int i = line - 2; i >= 0; i--)
     {
       for (int j = 0; j <= i; j++)
       {
       // F(i,j) = min( F(i+1, j), F(i+1, j+1)) + triangle[i][j]
       min_sum[i][j] = min(min_sum[i + 1][j], min_sum[i + 1][j + 1]) +triangle[i][j];
       }
     }
     return min_sum[0][0];
    }
};

注:易错点

  • 只保留每一步的最小值,忽略其他路径,造成最终结果错误
  • 局部最小不等于全局最小

总结:
遇到关于矩阵,网格,字符串间的比较,匹配的问题,单序列(一维)动规解决不了的情况下,就需要考虑双序列(二维)动规


第6题 路径总数(Unique Paths)

一个机器人在m×n大小的地图的左上角(起点,下图中的标记“start"的位置)。
机器人每次向下或向右移动。机器人要到达地图的右下角。(终点,下图中的标记“Finish"的位置)。
可以有多少种不同的路径从起点走到终点
动态规划经典例题一_第1张图片
上图是3×7大小的地图,有多少不同的路径?
备注:m和n小于等于100

动态规划方法

状态:
F(i,j):从左上角到达(i,j)的路径总数
F(i,j): 从(0,0)到达F(i,j)的路径数
状态递推:
F(i,j) = F(i-1,j) + F(i,j-1)
初始化:
特殊情况:第0行和第0列
F(0,i) = 1
F(i,0) = 1
返回结果:
F(m-1,n-1)

class Solution {
public:
    int uniquePaths(int m, int n) {
      if(m<1||n<1)
        return 0;
      vector<vector<int>> ret(m,vector<int>(n,1));
      for(int i=1;i<m;i++)
      {
        for(int j=1;j<n;j++)
        {
          ret[i][j]=ret[i-1][j]+ret[i][j-1];
        }
      }
      return ret[m-1][n-1];   
    }
};

你可能感兴趣的:(基础算法)