数据结构与算法-DP/动态规划

动态规划

三要素:

状态
状态转移方程
空间换时间:保存每一步的递推结果

1. leetcode 300.最长递增子串 (LIS)
给定一个数列,长度为N,
求这个数列的最长上升(递增)子数列(LIS)的长度.
以1 7 2 8 3 4为例。
这个数列的最长递增子数列是 1 2 3 4,长度为4;
次长的长度为3, 包括 1 7 8; 1 2 3 等.

什么是状态转移方程
上述状态定义好之后,状态和状态之间的关系式,就叫做状态转移方程。
对于LIS这个问题,状态转移方程为:

F1 = 1
Fn =max(Fi +1 | Ak > Ai, i 属于(1 .. k-1)) (k>1)

用文字解释一下是:
以第k项结尾的LIS的长度是:保证第i项比第k项小的情况下,以第i项结尾的LIS长度加一的最大值,取遍i的所有值(i小于k)。

#代码,开一个同样空间的F列表来保存每一个子状态当前的最大长度
#对于每一个当前位置i,要比较小于i的所有位置F上的值
#复杂度 O(n^2)

def lis_dp(nums):
    F = [1] * len(nums)
    for i in range(1,len(nums)):
        tmp_max = 1
        for j in range(i):
            if nums[i] > nums[j]:
                if F[j] + 1 > tmp_max:
                    tmp_max = F[j] + 1
        F[i] = tmp_max
    print(F)
    return max(F)

2. leetcode 70. 爬楼梯
# 爬楼梯问题,一个人每次可以爬一阶或两阶楼梯,问爬上N阶楼梯有多少种爬法
# 问题可以转化为子问题N-1阶楼梯的爬法与N-2阶楼梯的爬法之和(分别再爬一阶或两阶)
状态转移方程:
dp(i) = dp(i-1) + dp(i-2)
# N = 1 时候有一种爬法
# N = 2 时候有两种爬法(2或1+1)

def palouti(num):
    dp = [0] * num 
    dp[0] = 1
    dp[1] = 2
    for i in range(2,num):
        dp[i] = dp[i-1] + dp[i-2]

    return dp[num - 1]

# 和斐波那契数列很像,为了减少计算复杂度,使用DP的思想,拿一个数组保存之前子问题的计算结果
3. leetcode 53 最大子数组

Maximum Subarray

# 给定一个整数数组,找到一个具有最大和的子数组,返回其最大和。
# 给出数组[−2,2,−3,4,−1,2,1,−5,3],符合要求的子数组为[4,−1,2,1],其最大和为6

def maxSubArray(nums):
    tmp_max = nums[0] # tmp_max 全局到当前位置的最大保存值
    cur_max = nums[0] # 当前连续序列的最大值
    for i in range(1,len(nums)):
        cur_max = max(cur_max + nums[i],nums[i]) # 判断当前的数组元素是否要替换之前连续的数组元素
        #比如下面开头的[-4,5],到5当前最大[5]为5,大于[-4,5]的1,因此新的最大子数组一定以5开头
        if cur_max > tmp_max:
            tmp_max  = cur_max
    return tmp_max

if __name__ == '__main__':
    a = [-4,5,-4,5,-4,5,-4,5,-4,5,-4,5,-4,5,-4,5,-4,5,-1000]
    print(maxSubArray(a))
4.最大子数组 II -lintcode42
# 给定一个整数数组,找出两个 不重叠 子数组使得它们的和最大。
每个子数组的数字在数组中的位置应该是连续的。
返回最大的和。
# 给出数组 [1, 3, -1, 2, -1, 2]
这两个子数组分别为 [1, 3] 和 [2, -1, 2] 或者 [1, 3, -1, 2] 和 [2],它们的最大和都是 7

做法1:
遍历该数组,对于每个分割点,用上面最大子数组的方法分别对左边合右边求最大子数组的和,取最大的和,复杂度O(n^2)

做法2: 
DP方法,提前记录好每个位置上之前的数组的最大子数组之和
把该数组从左到右遍历,记录每一个位置上之前数组的最大子数组之和
再从右往左遍历,同样记录
之后对于每一个i到len(nums)-1的index,计算从左到右和从右到左对应位置最大子数组之和,最大的为结果,复杂度O(n)
class Solution:
    """
    @param: nums: A list of integers
    @return: An integer denotes the sum of max two non-overlapping subarrays
    """
    

    def subArray(self,nums):
        F = [0] * len(nums)
        tmp_max = nums[0]
        cur_max = nums[0]
        F[0] = nums[0]
        for i in range(1,len(nums)):
            cur_max = max(cur_max + nums[i],nums[i])
            tmp_max = max(tmp_max,cur_max)
            F[i] = tmp_max
        return F


    def maxTwoSubArrays(self, nums):
        all_max = nums[0] + nums[1]
        F = self.subArray(nums) # 从左到右
        nums.reverse()
        F1 = self.subArray(nums) # 从右到左
        
        for i in range(1,len(nums)-1):
            tmp_max = F[i] + F1[len(nums)-2-i] # 这里下标需要注意
            all_max = max(tmp_max,all_max)
        
        return all_max

leetcode 62 Unique Paths

在一个M * N的矩阵中,start为左下角(1,1)点,end为右上角(m,n),只能向上或者向右走,有多少种独立的走法?

DFS法:
树结构走,每次走[0,1]代表上和右,dfs结束条件为 up + right = m + n -2 (注意是从1,1开始所以要-2)
dfs返回条件是up >=m or n >= n
该种方法树剪枝的效率一般,leetcode上会超时,但是方便打印所有走的路线。

class Solution(object):
    def uniquePaths(self, m, n):
        
        def dfs(up,right,m,n,path,nums,nb):
            if up >= m or right >= n:
                return 
            if len(path) ==  m + n - 2:
                nb.append(path) 
                return 
            for i in nums:
                if i == 0:
                    dfs(up + 1,right,m,n,path + [0],nums,nb)
                if i == 1:
                    dfs(up, right + 1,m,n,path+[1],nums,nb)
        
        nums = [0,1]
        path,up,right,nb = [],0,0,[]
        dfs(up,right,m,n,path,nums,nb)
        return len(nb)

DP法
状态转移方程:
dp(m,n) = dp(m-1,n) + dp(m,n-1)
走到当前位置的走法相当于走到左边的走法加上走到右边的走法数量。

class Solution(object):
    def uniquePaths(self, m, n):
        """
        :type m: int
        :type n: int
        :rtype: int
        """
        dp = [[1 for x in range(n)] for x in range(m)]
        for i in range(1,m):
            for j in range(1,n):
                dp[i][j] = dp[i-1][j] + dp[i][j-1]
        return dp[m-1][n-1]

leetcode 63 Unique Path2

从矩阵左下角走到右上角(或叫左上角走到右下角)的独立path个数,只能走up或者right,一次走一步,矩阵里是有block的,block表示为1,通路表示为0

如
0 0 0 
0 1 0
0 0 0
从左下走到右上,由于正中间有一个block,因此只有两条path
即 [up,up,right,right] 或者 [right,right,up,up]

状态转移方程

两个一样大小的矩阵M和dp,M保存矩阵中碰到阻碍的情况(1是阻碍)
dp中保存矩阵中起点到当前位置的不同path数量
1. 初始dp[0][0] = 1 - M[0][0] ,因为M=0是畅通,M=1是阻碍
2. 当i == 0 or j ==0 时: #两条边
     if M[i][0] == 1:
        dp[i][0] = 0
     else:
         dp[i][0] = dp[i-1][0] 
对dp[0][j]同理
3. 当i >= 1 and j >= 1时:
  if M[i][j] == 1:
      dp[i][j] = 0
  elif M[i][j] == 0:
      dp[i][j] = dp[i-1][j] + dp[i][j-1]

最终代码:
这种解法可读性较强,空间复杂度O(m*n), leetcode discuss上也有空间复杂度O(n)和in-place的,可见https://discuss.leetcode.com/topic/19795/python-different-solutions-o-m-n-o-n-in-place (其中O(m*n)和这个解法一模一样,代码更trick一些)

class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid):
        """
        :type obstacleGrid: List[List[int]]
        :rtype: int
        """
        m = len(obstacleGrid)
        n = len(obstacleGrid[0])
        dp = [[0 for i in range(n)] for j in range(m)]
        dp[0][0] = 1 - obstacleGrid[0][0]
        for i in range(1,m): # lower edge
            if obstacleGrid[i][0] == 1:
                dp[i][0] = 0
            else:
                dp[i][0] = dp[i-1][0]

        for i in range(1,n): # left edge
            if obstacleGrid[0][i] == 1:
                dp[0][i] = 0
            else:
                dp[0][i] = dp[0][i-1]

        for i in range(1,m):
            for j in range(1,n):
                if obstacleGrid[i][j] == 1:
                    dp[i][j] = 0
                else:
                    dp[i][j] = dp[i-1][j] + dp[i][j-1]

        return dp[m-1][n-1]

leetcode 64. 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.

Example 1:
[[1,3,1],
 [1,5,1],
 [4,2,1]]
Given the above grid map, return 7. Because the path 1→3→1→1→1 minimizes the sum.
  1. DFS暴力解法,遍历全部路径,记录sum最短的那条,见函数minPathSum-dfs
  2. DP解法,见函数dp
状态转移方程:
if i == 0 or j == 0 (在两条边): 
    dp[i][0] = dp[i-1][0] + M[i][0]
    dp[0][j] = dp[0][j-1] + M[0][j]
else:
  dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + M[i][j] # 如果是sum最大路径,这里min换成max就可以

DFS在leetcode上会超时不能AC,一部分tests没法通过

#coding=utf-8
class Solution(object):
    def minPathSum(self, grid):
        path = [grid[0][0]]
        res = []
        self.res_final = []
        height = len(grid)
        length = len(grid[0])
        self.m = height 
        self.n = length
        self.max_tmp = 1e10
        self.dfs(grid,0,0,path,res)
        return sum(self.res_final)
    def dfs(self,grid,height,length,path,res):
    
        if len(path) == self.m + self.n - 1:
            # print(path)
            if sum(path) < self.max_tmp:
                self.max_tmp = sum(path)
                self.res_final = path
        for i in range(2):
            if i == 0 : # means go right
                if length + 1 > self.m - 1:
                    continue
                self.dfs(grid,height,length + 1,path + [grid[length+1][height]],res)
                
            if i == 1 : # means go down
                if height + 1 > self.n - 1:
                    continue
                self.dfs(grid,height + 1,length,path + [grid[length][height + 1]],res)
               

DP法

    def dp(self,grid):
        height = len(grid)
        length = len(grid[0])
        dp = [[0 for i in range(length)] for j in range(height)]
        dp[0][0] = grid[0][0]
        
        for i in range(1,height):
            dp[i][0] = dp[i-1][0] + grid[i][0]
        for j in range(1,length):
            dp[0][j] = dp[0][j-1] + grid[0][j]
        
        for i in range(1,height):
            for j in range(1,length):
                dp[i][j] = min(dp[i-1][j],dp[i][j-1]) + grid[i][j] # dp[1][1] = dp[1][0] + dp[0][1]
        
        return dp[height-1][length-1]

if __name__ == '__main__':
    Solution = Solution()
    grid = [[1,3,1,1],[4,2,1,1],[4,2,4,1]]#
    # grid = [[1,3,1],[1,5,1],[4,2,1]]
    print(Solution.minPathSum(grid))
    print(Solution.dp(grid))

leetcode 120. Triangle

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 is 11 (i.e., 2 + 3 + 5 + 1 = 11).
给定一个三角形,从顶往下走,每个点只能走下面的左右相邻的两点,求和为最小的路径的和的值

一般来说,求最后的值不用打印路径的都可以用DP完成,需要打印路径的可以用DFS完成。
显然状态转移方程为下:

T: 三角形的值数组,dp当前和最小路径的数组,T和dp大小完全一致。
1. dp[0][0] = T[0][0]
2. for i in range(1,len(dp)): # 从第二行开始
      对于每行第一个元素:dp[i][j] = dp[i-1][j] + T[i][j] (三角形,只能接收上一行最左元素)
      对于每行最后一个元素:dp[i][j] = dp[i-1][j-1] + T[i][j] (三角形,只能接收上一行最右元素)
      对于每行中间的元素:dp[i][j] = min(dp[i-1][j-1] ,dp[i-1][j]) + T[i][j]
  

code,空间O(m*n)

class Solution:
    
    def minimumTotal(self, triangle):
        """
        :type triangle: List[List[int]]
        :rtype: int
        """
        dp = [[i for i in j] for j in triangle]

        dp[0][0] = triangle[0][0]
        for i in range(1,len(triangle)):
            for j in range(len(triangle[i])):
                if j == 0 :
                    dp[i][j] = dp[i-1][j] + triangle[i][j]
                    print(dp[i][j])
                elif j == len(triangle[i]) - 1:
                    dp[i][j] = dp[i-1][j-1] + triangle[i][j]
                else:
                    dp[i][j] = min(dp[i-1][j-1] ,dp[i-1][j]) + triangle[i][j]
        
        return min(dp[len(dp)-1])

    原文作者:sylvainwang
    原文地址: https://www.jianshu.com/p/9e52899edd75
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞