算法篇:动态规划(二)

206 阅读10分钟

1. leetcode62 不同路径

一个机器人位于一个 m x n **网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

输入: m = 3, n = 7
输出: 28

示例 2:

输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

输入: m = 7, n = 3
输出: 28

示例 4:

输入: m = 3, n = 3
输出: 6
func uniquePaths(m int, n int) int {
   dp := make([][]int, m)
   for i := 0; i < m; i++ {
      dp[i] = make([]int, n)
      dp[i][0] = 1
   }
   for j := 0; j < n; j++ {
      dp[0][j] = 1
   }
   for i := 1; i < m; i++ {
      for j := 1; j < n; j++ {
         dp[i][j] = dp[i-1][j] + dp[i][j-1]
      }
   }
   return dp[m-1][n-1]

}

这段代码的逻辑如下:

  1. 初始化一个二维数组dp,其中dp[i][j]表示到达网格中位置(i, j)的不同路径数量。
  2. 因为机器人只能向右或向下移动,所以第一行和第一列的所有位置的路径数都是1,因为机器人只有一种方式到达这些位置。
  3. 然后,使用两层循环遍历整个网格,除了第一行和第一列之外的每一个位置。
  4. 对于网格中的每一个位置(i, j),机器人可以从上方(i-1, j)或左侧(i, j-1)到达该位置。因此,到达(i, j)的路径数量就是到达(i-1, j)的路径数量加上到达(i, j-1)的路径数量。
  5. 最后,返回到达右下角位置(m-1, n-1)的路径数量。

这个算法的时间复杂度和空间复杂度都是O(m*n),因为需要遍历整个网格并且存储每个位置的路径数量。这个算法是有效的,并且能够正确计算不同的路径数量。

2. leetcode 64 最小路径和

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明: 每次只能向下或者向右移动一步。

示例 1:

输入: grid = [[1,3,1],[1,5,1],[4,2,1]]
输出: 7
解释: 因为路径 13111 的总和最小。

示例 2:

输入: grid = [[1,2,3],[4,5,6]]
输出: 12
func minPathSum(grid [][]int) int {
   if len(grid) == 0 {
      return 0
   }
   m,n := len(grid),len(grid[0])
   dp := make([][]int,m)
   for i:= 0 ; i< m;i++ {
      dp[i] = make([]int,n)
   }
   dp[0][0] = grid[0][0]
   for i := 0; i <m;i++ {
      for j:= 0;j < n;j++ {
         if i == 0 && j == 0 {
            continue
         } else if i == 0 {
            dp[i][j] = dp[i][j-1]+grid[i][j]
         } else if j == 0{
            dp[i][j] = dp[i-1][j]+grid[i][j]
         } else {
            dp[i][j] = min(dp[i][j-1],dp[i-1][j])+grid[i][j]
         }
      }
   }
   return dp[m-1][n-1]
}
func min(x,y int) int {
   if x <y {
      return x
   }
   return y
}

代码逻辑如下:

  1. 首先检查输入矩阵grid是否为空,如果为空,则直接返回0。
  2. 获取矩阵的行数m和列数n
  3. 初始化一个二维数组dp,其中dp[i][j]将会存储到达位置(i, j)的最小路径和。
  4. 将起始位置(0, 0)的最小路径和设为grid[0][0]
  5. 使用双重循环遍历整个矩阵。
  6. 对于每个位置(i, j),有两种可能的前一个位置:从上方(i-1, j)来或从左侧(i, j-1)来。我们要取这两个位置的最小路径和加上当前位置的值grid[i][j]作为当前位置的最小路径和。
  7. 特殊情况处理:如果在第一行(i == 0),则只能从左侧来;如果在第一列(j == 0),则只能从上方来。
  8. 最终,返回到达右下角位置(m-1, n-1)的最小路径和。

min函数是一个辅助函数,用于比较两个数并返回较小的一个。

3. leettcode 322 零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

示例 1:

输入: coins = [1, 2, 5], amount = 11
输出: 3 
解释: 11 = 5 + 5 + 1

示例 2:

输入: coins = [2], amount = 3
输出: -1

示例 3:

输入: coins = [1], amount = 0
输出: 0

示例 4:

输入: coins = [1], amount = 1
输出: 1

示例 5:

输入: coins = [1], amount = 2
输出: 2
func coinChange(coins []int, amount int) int {
   dp := make([]int,amount+1)
   for i := 1;i <= amount;i++ {
      minCoinNum := math.MaxInt
      for _,coin := range coins {
         if i-coin >= 0 && dp[i-coin] >= 0 {
            minCoinNum = min(minCoinNum,dp[i-coin])
         }
      }
      if minCoinNum < math.MaxInt {
         dp[i] = minCoinNum+1
      } else {
         dp[i] = -1
      }
   }
   return dp[amount]
}

func min(x,y int) int {
   if x < y {
      return x
   }
   return y
}

代码逻辑如下:

  1. 初始化一个长度为amount+1的数组dpdp[i]代表组成金额i所需的最小硬币数。数组中的每个元素初始化为0
  2. 1开始遍历到amount,对于每个金额i,找出可以凑成该金额的最小硬币数。
  3. 对于每个硬币coin,检查当前金额i减去该硬币面额后的剩余金额是否大于等于0dp[i-coin]不为-1(表示这个剩余金额是可以被凑齐的)。
  4. 如果满足条件,更新最小硬币数minCoinNumdp[i-coin]的值(表示剩余金额所需的最小硬币数)。
  5. 如果最终找到了至少一种方法可以凑齐当前金额i,则设置dp[i]minCoinNum+1(加上当前这枚硬币);否则设置为-1,表示无法凑齐。
  6. 最终返回dp[amount],即为凑齐总金额所需的最小硬币数。

leetcode 300 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入: nums = [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

输入: nums = [0,1,0,3,2,3]
输出: 4

示例 3:

输入: nums = [7,7,7,7,7,7,7]
输出: 1
func lengthOfLIS(nums []int) int {
   dp:= make([]int,len(nums))
   for i := 0; i < len(dp);i++ {
      dp[i] =1
   }
   for i := 0 ; i < len(nums);i++ {
      for j := 0; j < i ; j++ {
         if nums[i] > nums[j] {
            dp[i] = max(dp[j]+1, dp[i])
         }
      }
   }
   maxRes := 0
   for i := 0; i < len(dp); i++ {
      maxRes = max(maxRes,dp[i])
   }
   return maxRes
}
func max(x,y int) int {
   if x > y {
      return x
   }
   return y
}

代码逻辑是这样的:

  1. 初始化一个与输入数组 nums 等长的 dp 数组,dp[i] 用于存储以 nums[i] 结尾的最长递增子序列的长度。初始时,每个元素的 LIS 长度至少为 1(它自己)。
  2. 使用两层循环,外层循环遍历 nums 中的每个元素,内层循环在当前元素之前的元素中寻找可以构成递增子序列的元素。
  3. 如果 nums[i] 大于 nums[j](即可以构成递增序列),则检查以 nums[j] 结尾的子序列长度加上当前元素 nums[i] 是否会产生更长的递增子序列。如果是,更新 dp[i]
  4. 通过内层循环的比较和更新,dp[i] 最终会存储以 nums[i] 结尾的最长递增子序列的长度。
  5. 为了找到整个数组的 LIS,需要在 dp 数组中找到最大值,这是通过遍历 dp 数组并更新 maxRes 来实现的。
  6. 最后返回 maxRes,它代表整个数组的最长递增子序列的长度。

max 函数是一个辅助函数,用于返回两个整数中的最大值。

这个动态规划解法的时间复杂度是 O(n^2),其中 n 是输入数组 nums 的长度。这是因为需要双重循环遍历所有的子序列对。

leetcode 198 打家劫舍问题

一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响小偷偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组 nums ,请计算 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入: nums = [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4

示例 2:

输入: nums = [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12
func rob(nums []int) int {
	if len(nums) == 0 {
		return 0
	}
	if len(nums) == 1 {
		return nums[0]
	}
	dp := make([][]int, len(nums))
	for i := range dp {
		dp[i] = make([]int, 2)
	}
	dp[0][0] = 0
	dp[0][1] = nums[0]
	for i := 1; i < len(nums); i++ {
		dp[i][0] = max(dp[i-1][0], dp[i-1][1]) // 不偷当前房子
		dp[i][1] = dp[i-1][0] + nums[i]        // 偷当前房子
	}
	return max(dp[len(nums)-1][0], dp[len(nums)-1][1])
}

func max(x, y int) int {
	if x > y {
		return x
	}
	return y
}

代码逻辑如下:

  1. 首先,检查输入数组nums是否为空。如果为空,直接返回0,因为没有房屋可供偷窃。

  2. 如果数组只有一个元素,那么只有一间房屋可供偷窃,所以返回这个元素的值。

  3. 初始化一个二维的动态规划数组dp,其中dp[i][0]表示不偷第i个房子时所能获得的最大金额,dp[i][1]表示偷第i个房子时所能获得的最大金额。

  4. 对于第一间房屋,dp[0][0](不偷)的值为0,dp[0][1](偷)的值为nums[0](即第一间房屋中的金额)。

  5. 从第二间房屋开始遍历,更新每一间房屋对应的dp数组值:

    • dp[i][0](不偷第i间房屋)的最大金额将是前一间房屋偷或不偷时的最大金额,即max(dp[i-1][0], dp[i-1][1])
    • dp[i][1](偷第i间房屋)的最大金额将是前一间房屋不偷时的最大金额加上当前房屋中的金额,即dp[i-1][0] + nums[i]
  6. 遍历完成后,返回最后一间房屋偷或不偷时的最大金额,即max(dp[len(nums)-1][0], dp[len(nums)-1][1])

max函数是一个辅助函数,用来比较两个整数值并返回较大的那个值。

这个动态规划方法的时间复杂度为O(n),其中n是数组nums的长度,因为它只需遍历一次数组即可计算出所有的状态。空间复杂度为O(n),因为需要一个二维数组来存储状态。然而,实际上可以优化空间复杂度到O(1),方法是只存储前一间房屋的状态而不是整个数组,因为每次更新状态时只依赖于前一间房屋的状态。