[Leetcode] [Tutorial] Dynamic Programming


70. Climb the stairs

Suppose you are climbing stairs. It takes n steps for you to reach the top of the building.

You can climb 1 or 2 steps at a time. How many different ways can you climb to the top of a building?

Example:
Input: n = 3
Output: 3

Solution

Let's first think about the problem in an intuitive way: for a given number of steps n, we climb 1 or 2 steps each time, so there are two options, which can be decomposed into the following sub-problems:

  • Climb up to level n-1, then climb up to level 1
  • Climb up to n-2 steps, then climb up to 2 steps

So the solution is the sum of the above two sub-problems.

class Solution:
    def climbStairs(self, n: int) -> int:
        if n == 1:
            return 1
        if n == 2:
            return 2
        return self.climbStairs(n - 1) + self.climbStairs(n - 2)

The disadvantage of the above direct recursion is repeated calculations, then we can save the results of the already calculated subproblems.

class Solution:
    def climbStairs(self, n: int) -> int:
        memo = [0] * (n + 1)
        return self.climb_with_memoization(n, memo)

    def climb_with_memoization(self, n: int, memo: list) -> int:
        if n == 1:
            return 1
        if n == 2:
            return 2
        if memo[n] > 0:
            return memo[n]
        memo[n] = self.climb_with_memoization(n - 1, memo) + self.climb_with_memoization(n - 2, memo)
        return memo[n]

Memoized recursion still uses recursive structures, and recursive structures can be converted into iterative structures. We can start from the bottom and build the solution step by step until n is reached.

class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 2:
            return n
        dp = [0] * (n + 1)
        dp[1], dp[2] = 1, 2
        for i in range(3, n + 1):
            dp[i] = dp[i - 1] + dp[i - 2]
        return dp[n]

Observing the above code, we find that only the values ​​of the first two states are used in each calculation. Therefore, there is absolutely no need to save all values, just the last two states.

class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 2:
            return n
        a, b = 1, 2
        for _ in range(3, n + 1):
            a, b = b, a + b
        return b

118. Yang Hui Triangle

Given a non-negative integer numRows, generate the first numRows rows of "Yang Hui Triangle".

In "Yang Hui's Triangle", each number is the sum of the numbers on its upper left and upper right.

Example:
Input: numRows = 5
Output: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]

Solution

class Solution:
    def generate(self, numRows: int) -> List[List[int]]:
        triangle = [[1] * i for i in range(1, numRows + 1)]
        for i in range(2, numRows):
            triangle[i][0], triangle[i][i] = 1, 1
            for j in range(1, i):
                triangle[i][j] = triangle[i - 1][j - 1] + triangle[i - 1][j]
        return triangle

198. Robbery

You are a professional thief planning to steal houses along the street. There is a certain amount of cash hidden in each room. The only restriction factor that affects your theft is that the adjacent houses are equipped with interconnected anti-theft systems. If two adjacent houses are broken into by thieves on the same night, the system will automatically alarm .

Given an array of non-negative integers representing the amount of money stored in each house, calculate the maximum amount of money you can steal in one night without triggering the alarm device.

Example:
Input: [2,7,9,3,1]
Output: 12

Solution

For the i-th house, there are two options:

  • Steal: The amount at this time is the maximum amount of the first i-2 houses plus the amount of the i-th house.
  • No stealing: The amount at this time is the maximum amount of the previous i-1 houses.
class Solution:
    def rob(self, nums: List[int]) -> int:
        if not nums:
            return 0

        if len(nums) == 1:
            return nums[0]

        dp = [0] * len(nums)
        dp[0] = nums[0]
        dp[1] = max(nums[0], nums[1])
        for i in range(2, len(nums)):
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
        return dp[len(nums) - 1]

322. Change exchange

Give you an integer array coins, representing coins of different denominations; and an integer amount, representing the total amount.

Calculate and return the minimum number of coins required to make up the total amount. If no coin combination can complete the total amount, -1 is returned.

You can think of the number of each type of coin as infinite.

Example:
Input: coins = [1, 2, 5], amount = 11
Output: 3

Solution

For the coin change problem, the greedy strategy you might think of is: at each step, choose the largest coin denomination that does not exceed the remaining amount.

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        coins.sort(reverse=True)  # 将硬币按降序排序
        total_coins = 0
        for coin in coins:
            num_coins = amount // coin  # 使用尽可能多的当前面额硬币
            amount -= num_coins * coin
            total_coins += num_coins

            if amount == 0:
                return total_coins
        
        return -1

However, for this problem, the greedy algorithm does not always work. However, there is no "greedy selection of attributes" for this problem. That is, making a local optimal choice at each step does not necessarily lead to a globally optimal solution.

The optimal solution may need to consider not only the information of the current step, but also how to obtain the optimal solution through overall consideration and coordination, which is exactly what the dynamic programming method can provide.

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [amount + 1] * (amount + 1)
        dp[0] = 0
        
        for i in range(1, amount + 1):
            for coin in coins:
                if coin <= i:
                    dp[i] = min(dp[i], dp[i - coin] + 1)
                    
        return dp[amount] if dp[amount] <= amount else -1

Just initialize dp[0] to 0. This is because, for any coin (where coin is the face value of the coin), its dp value is automatically calculated as 1 in the loop.

279. Perfect square numbers

Given an integer n, return the smallest number of perfect squares whose sum is n.

A perfect square number is an integer whose value is equal to the square of another integer; in other words, its value is equal to the product of an integer multiplied by itself. For example, 1, 4, 9, and 16 are all perfect square numbers, but 3 and 11 are not.

Example:
Input: n = 13
Output: 2
Explanation: 13 = 4 + 9

Solution

class Solution:
    def numSquares(self, n: int) -> int:
        squares = [i**2 for i in range(1, int(n**0.5) + 1)]
        dp = [float('inf')] * (n + 1)
        dp[0] = 0
        for i in range(1, n + 1):
            for num in squares:
                dp[i] = min(dp[i - num] + 1, dp[i])
        return dp[n]

If you think of each number as a node, and the difference between the two numbers is a perfect square, then the two numbers are connected. So our goal is to find the shortest path from 0 to n.

class Solution:
    def numSquares(self, n: int) -> int:
        # 获取所有小于 n 的完全平方数
        squares = [i**2 for i in range(1, int(n**0.5) + 1)]
        
        queue = deque([(n, 0)])  # (当前值, 步数)
        visited = set()

        while queue:
            num, step = queue.popleft()

            # 对于每一个完全平方数
            for square in squares:
                next_num = num - square
                if next_num < 0:
                    break
                if next_num == 0:
                    return step + 1
                if next_num not in visited:
                    visited.add(next_num)
                    queue.append((next_num, step + 1))

        return n  # 这行理论上不会执行,但为了完整性保留它

139. Word splitting

You are given a string s and a list of strings wordDict as a dictionary. Please judge whether you can use the words that appear in the dictionary to splice s.

Note: It is not required to use all the words that appear in the dictionary, and words in the dictionary can be used repeatedly.

Example 1:
Input: s = “applepenapple”, wordDict = [“apple”, “pen”]
Output: true

Example 2:
Input: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”] Output:
false

Solution

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        if not s:
            return True
        n = len(s)
        succ = [0]
        for i in range(1, n + 1):
            for j in succ:
                if s[j: i] in wordDict:
                    succ.append(i)
                    break
        return True if n in succ else False

300. Longest increasing subsequence

Given an array of integers nums, find the length of the longest strictly increasing subsequence in it.

A subsequence is a sequence derived from an array by removing (or not removing) elements from the array without changing the order of the remaining elements. For example, [3,6,2,7] is a subsequence of the array [0,3,1,6,2,2,7].

Example:
Input: nums = [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], so the length is 4.

Solution

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
            
        n = len(nums)
        dp = [1] * n
        for i in range(n):
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
        return max(dp)

This problem can also be solved using a greedy strategy.

When you encounter a number that is larger than the end of the current subsequence, try to add it to the subsequence. When we add numbers to a subsequence, it may also prevent subsequent numbers from being added. This means that we have to consider not only the length, but also the size of the last number of the subsequence. In fact, for a subsequence of a certain length, the smaller the ending number, the better, because then we have a greater chance of encountering a larger number in the future.

The greedy algorithm prompts us to always try to add the current number to the existing longest increasing subsequence. But how to quickly find where it should be inserted? This is where binary search comes in handy. We can use binary search to find the position where the number should be inserted and update the value at the current position if necessary.

The core idea of ​​the tails array is: for an increasing subsequence of length i, we want to know what is the minimum value at the end of all these subsequences, because this gives subsequent numbers a greater chance of being added. The index of tails represents the length of the subsequence, and the value represents the minimum ending value of the increasing subsequence of the corresponding length.

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        tails = []
        for num in nums:
            # 二分搜索
            left, right = 0, len(tails) - 1
            while left <= right:
                mid = (left + right) // 2
                if tails[mid] < num:
                    left = mid + 1
                else:
                    right = mid - 1
            
            # 如果num大于tails中的所有元素,则直接追加到tails中
            # 否则替换找到的位置
            if left == len(tails):
                tails.append(num)
            else:
                tails[left] = num
        
        return len(tails)

152. Product maximum subarray

Given an integer array nums, please find the non-empty continuous subarray with the largest product in the array (the subarray contains at least one number), and return the product corresponding to the subarray.

A subarray is a contiguous subsequence of an array.

Example:
Input: nums = [2,3,-2,4]
Output: 6

Solution

Since the product of a negative number and a negative number is positive, we also need to keep track of the maximum and minimum product of the subarrays that end with the current number. If the current number is negative, then when multiplied by the current number, the maximum value becomes the minimum value, and the minimum value becomes the maximum value.

So we need to record two states at each location:

max_product[i] represents the maximum product of subarrays ending with nums[i].
min_product[i] represents the minimum product of subarrays ending with nums[i].

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if not nums:
            return 0
        
        max_product = [0] * len(nums)
        min_product = [0] * len(nums)
        max_product[0], min_product[0] = nums[0], nums[0]
        for i in range(1, len(nums)):
            max_product[i] = max(nums[i], nums[i]*max_product[i-1], nums[i]*min_product[i-1])
            min_product[i] = min(nums[i], nums[i]*max_product[i-1], nums[i]*min_product[i-1])
        return max(max_product)

We can actually not use an array to save the result of each position, but only need to save the result of the previous position.

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if not nums:
            return 0

        prev_max = nums[0]
        prev_min = nums[0]
        global_max = nums[0]

        for i in range(1, len(nums)):
            # 使用临时变量来保存当前状态,因为我们需要前一个状态的prev_max和prev_min
            curr_max = max(nums[i], nums[i] * prev_max, nums[i] * prev_min)
            curr_min = min(nums[i], nums[i] * prev_max, nums[i] * prev_min)
            # 更新全局最大乘积
            global_max = max(global_max, curr_max)
            # 更新前一个数字的状态
            prev_max, prev_min = curr_max, curr_min

        return global_max

416. Split equal sum subsets

You are given a non-empty array nums containing only positive integers. Please determine whether this array can be divided into two subsets so that the sum of the elements of the two subsets is equal.

Example:
Input: nums = [1,5,11,5]
Output: true

Solution

Guess you like

Origin blog.csdn.net/weixin_45427144/article/details/132219846