算法_动态规划(Dynamic Programming)

动态规划 (dynamic programming)

定义
动态规划是运筹学的一个分支,是求解决策过程最优化的数学方法,把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解。分治算法的基本思想是将一个规模为 N 的问题分解为 K 个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。即一种分目标完成程序算法。动态规划是分治思想的延伸,通俗一点来说就是大事化小,小事化无的艺术。
在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果。

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

特点

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

考虑的角度

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

适用场景
大值/小值, 可不可行, 是不是,方案个数

应用举例

01.青蛙跳台阶

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

方法一:动态规划

  1. 状态:
    子状态:跳上1级,2级,3级,…,n级台阶的跳法数
    f(n):还剩n个台阶的跳法数
  2. 状态递推:
    n级台阶,第一步有n种跳法:跳1级、跳2级、到跳n级
    跳1级,剩下n-1级,则剩下跳法是f(n-1)
    跳2级,剩下n-2级,则剩下跳法是f(n-2)
    f(n) = f(n-1)+f(n-2)+…+f(n-n)
    f(n) = f(n-1)+f(n-2)+…+f(0)
    f(n-1) = f(n-2)+…+f(0)
    f(n) = 2*f(n-1)
  3. 初始值:
    f(1) = 1
    f(n)是一个首项为1,公比为2的等比数列
  4. 返回结果:
    f(n)
//递归
class Solution {
public:
    int jumpFloorII(int number) {
        if(number <= 0) 
        {           
            return 0;   
        }
        else if(number == 1)
        {
            return 1;
        }
        else
        {
            return 2*jumpFloorII(number-1);
        }
    }
};

//动态规划
class Solution {
public:
    int jumpFloorII(int number) {
        if(number <= 0)
        {
            return 0;
        }
        if(number == 1)
        {
            return 1;
        }
        //申请一个数组保存子问题的解
        int* record = new int[number-1]; 
        record[0] = 1;
        for (int i = 1; i <= number; i++)
        {
            record[i] = 2*record[i - 1];
        }        
        return record[number-1];
        delete[] record; 
    }
};

//上述解法的空间复杂度为O(n) 
//其实f(n)只与它相邻的前一项有关,只需要保存一个子问题的解就可以 
//下面方法的空间复杂度将为O(1) 
class Solution {
public:
    int jumpFloorII(int number) {
        if(number <= 0)
        {
            return 0;
        }
        if(number == 1)
        {
            return 1;
        }
        int fn = 1;
        int result = 0; 
        for (int i = 2; i <= number; i++)
        {           
            result = 2*fn;  
            fn = result;      
        }        
        return result; 
    }
};

方法二:排列
以台阶为研究对象,除了最后一个台阶只有一种情况,即必须到达,其它台阶都有两种可能性,即青蛙跳到这个台阶或者不跳到这个台阶,所以总的排列数为2^(n-1)。

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

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

总结:

  • 此题看似复杂,通过抽象和归纳,可以很容易找出问题的内在规律。

  • 解题的关键是定义问题的状态,以及状态间的递推关系。

    扩展1:
    上述问题为变态青蛙跳台阶,太疯狂,这只青蛙像是吃了大力丸身上充满了无穷的力量。现在让它变成一个正常的青蛙,限制它一次只能跳1阶或者2阶,又该如何解答?
    扩展2:
    矩形覆盖,用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2 * 1 的小矩形无重叠地覆盖一个 2 * n 的大矩形,总共有多少种方法?

02. 最大连续子数组和(Maximum Subarray)

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

方法:动态规划

  1. 状态:
    子状态:长度为1,2,3,…,n 的子数组和的大值
    F(i):长度为 i 的子数组和的大值,这种定义不能形成递推关系,舍弃
    F(i):以 array[i] 为末尾元素的子数组和的大值
  2. 状态递推:
    F(i) = max(F(i-1) + array[i],array[i])
    F(i) = (F(i-1) > 0)? F(i-1) + array[i] : array[i]
  3. 初始值:
    F(0) = array[0]
  4. 返回结果:
    maxsum:所有F(i)中的大值
    maxsum = max(maxsum,F(i))
class Solution {
public:
    int FindGreatestSumOfSubArray(vector<int> array) {
        if (array.empty())
        {
            return -1;
        }
        int sum = array[0];
        int maxsum = array[0];
        for (int i = 1; i < array.size(); i++)
        {
            sum = (sum > 0) ? sum + array[i] : array[i];
            maxsum = (sum < maxsum) ? maxsum : sum;
        }
        return maxsum;
    }
};


03.字符串分割(Word Break)

题目描述:
Given a string s and a dictionary of words dict determine if s can be segmented into a space-separated sequence of one or more dictionary words.
For example, given
s =“leetcode”,
dict =[“leet”, “code”]
Return true because"leetcode"can be segmented as"leet code".
(给定一个字符串 s 和一个词典 dict,确定 s 是否可以根据词典中的词分成 一个或多个单词。
比如,给定
s = “leetcode”
dict = [“leet”, “code”]
返回true,因为"leetcode"可以被分成"leet code"。)

方法:动态规划

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

04. 三角矩阵

题目描述:
Given a triangle, find the minimum path sum from top to bottom.
Each step you may move to adjacent numbers on the row below.
For example, given the following triangle
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]

The minimum path sum from top to bottom is11(i.e., 2 + 3 + 5 + 1 = 11)
Note:
Bonus point if you are able to do this using only O(n) extra space, where n is the total number of rows in the triangle.

方法一:动态规划

  1. 状态:
    子状态:从(0,0)到(1,0),(1,1),(2,0),…(n,n)的短路径和
    F(i,j):从(0,0)到(i,j)的短路径和
  2. 状态递推:
    F(i,j) = min( F(i-1, j-1), F(i-1, j)) + triangle[i][j]
  3. 初始值:
    F(0,0) = triangle[0][0]
  4. 返回结果:
    min(F(n-1, i))

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

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

易错点:
只保留每一步的小值,忽略其他路径,造成最终结果错误,局部小不等于全局小。
总结:
遇到关于矩阵,网格,字符串间的比较,匹配的问题,单序列(一维)动态规划解决不了的情况下, 就需要考虑双序列(二维)动态规划。

05. 路径总数(Unique Paths)

题目描述:
A robot is located at the top-left corner of a m x n grid (marked ‘Start’ in the diagram below).
The robot can only move either down or right at any point in time.
The robot is trying to reach the bottom-right corner of the grid (marked ‘Finish’ in the diagram below).
How many possible unique paths are there?
算法_动态规划(Dynamic Programming)_第1张图片
Above is a 3 x 7 grid. How many possible unique paths are there?
Note:m and n will be at most 100.
(机器人位于M x N网格的左上角(下图中标记为“开始”)。机器人只能在任何时间点向下或向右移动。机器人正试图到达网格的右下角(在下图中标记为“完成”)。有多少可能的唯一路径? 上面是一个3 x 7的网格。有多少可能的唯一路径? 注:M和N最多为100。 )

方法:动态规划

  1. 状态:
    子状态:从(0,0)到达(1,0),(1,1),(2,1),…,(m-1,n-1)的路径数
    F(i,j):从(0,0)到达F(i,j)的路径数
  2. 状态递推:
    F(i,j) = F(i-1,j) + F(i,j-1)
  3. 初始化:
    特殊情况:
    第0行和第0列
    F(0,i) = 1
    F(i,0) = 1
  4. 返回结果:
    F(m-1,n-1)
06. 路径总数(Unique Paths II)

题目描述:
Follow up for “Unique Paths”:
Now consider if some obstacles are added to the grids. How many unique paths would there be?
An obstacle and empty space is marked as1and0respectively in the grid.
For example, There is one obstacle in the middle of a 3x3 grid as illustrated below.
[
[0,0,0],
[0,1,0],
[0,0,0]
]
The total number of unique paths is 2.
Note:m and n will be at most 100.

(独特路径”的后续行动:
现在考虑是否在网格中添加了一些障碍。有多少条独特的路?网格中的障碍物和空白区域分别标记为1和0。例如,如下图所示,3x3 网格中间有一个障碍物。
[
[0,0,0],
[0,1,0],
[0,0,0]
]
唯一路径的总数为2。 注:M和N最多为100。)

方法:动态规划

  1. 状态:
    子状态:从(0,0)到达(1,0),(1,1),(2,1),…(m-1,n-1)的路径数
    F(i,j): 从(0,0)到达F(i,j)的路径数
  2. 状态递推:
    F(i,j) = {F(i-1,j) + F(i,j-1)} OR {0, if obstacleGrid(i,j) = 1}
  3. 初始化:
    特殊情况:
    第0行和第0列
    F(0,i) = {1} OR {0, if obstacleGrid(0,j) = 1, j <= i}
    F(i,0) = {1} OR {0, if obstacleGrid(j,0) = 1, j <= i}
  4. 返回结果:
    F(m-1,n-1)
07. 最小路径和(Minimum Path Sum)

题目描述:
Given a m x n grid filled with non-negative numbers,find a path from top left to bottom right which minimizes the sum of all numbers along its path.
Note: You can only move either down or right at any point in time.
(给定一个由非负数填充的m x n网格,找到一条从左上到右下的路径,该路径使沿其路径的所有数字之和最小化。注意:只能在任何时间点向下或向右移动。)
方法:动态规划

  1. 状态:
    子状态:从(0,0)到达(1,0),(1,1),(2,1),…(m-1,n-1)的短路径
    F(i,j): 从(0,0)到达F(i,j)的短路径
  2. 状态递推:
    F(i,j) = min{F(i-1,j) , F(i,j-1)} + (i,j)
  3. 初始化: F(0,0) = (0,0)
    特殊情况:第0行和第0列
    F(0,i) = F(0,i-1) + (0,i)
    F(i,0) = F(i-1,0) + (i,0)
  4. 返回结果:
    F(m-1,n-1)

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