[Leetcode] [Tutorial] Programación dinámica


70. Sube las escaleras

Supongamos que estás subiendo escaleras. Se necesitan n pasos para llegar a la cima del edificio.

Puedes subir 1 o 2 escalones a la vez. ¿De cuántas maneras diferentes puedes subir a la cima de un edificio?

Ejemplo:
Entrada: n = 3
Salida: 3

Solución

Primero pensemos en el problema de manera intuitiva: para un número determinado de pasos n, subimos 1 o 2 pasos cada vez, por lo que hay dos opciones, que se pueden descomponer en los siguientes subproblemas:

  • Sube al nivel n-1, luego sube al nivel 1.
  • Sube hasta n-2 escalones, luego sube hasta 2 escalones

Entonces la solución es la suma de los dos subproblemas anteriores.

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)

La desventaja de la recursividad directa anterior son los cálculos repetidos, luego podemos guardar los resultados de los subproblemas ya calculados.

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]

La recursividad memorizada todavía usa estructuras recursivas, y las estructuras recursivas se pueden convertir en estructuras iterativas. Podemos comenzar desde abajo y construir la solución paso a paso hasta alcanzar n.

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]

Al observar el código anterior, encontramos que en cada cálculo solo se utilizan los valores de los dos primeros estados. Por lo tanto, no hay absolutamente ninguna necesidad de guardar todos los valores, sólo los dos últimos estados.

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. Triángulo Yang Hui

Dado un número entero no negativo de numRows, genere las primeras filas de numRows del "Triángulo Yang Hui".

En el "Triángulo de Yang Hui", cada número es la suma de los números en la parte superior izquierda y superior derecha.

Ejemplo:
Entrada: numRows = 5
Salida: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]

Solución

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. Robo

Eres un ladrón profesional que planea robar casas en la calle. Hay una cierta cantidad de efectivo escondida en cada habitación. El único factor de restricción que afecta su robo es que las casas adyacentes están equipadas con sistemas antirrobo interconectados. Si dos casas adyacentes son asaltadas por ladrones la misma noche, el sistema Alarmará automáticamente.

Dada una serie de números enteros no negativos que representan la cantidad de dinero almacenada en cada casa, calcula la cantidad máxima de dinero que puedes robar en una noche sin activar el dispositivo de alarma.

Ejemplo:
Entrada: [2,7,9,3,1]
Salida: 12

Solución

Para la casa i-ésima, hay dos opciones:

  • Robar: El monto en este momento es el monto máximo de las primeras casas i-2 más el monto de la casa i-ésima.
  • No robar: El monto en este momento es el monto máximo de las casas i-1 anteriores.
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. Cambio de cambio

Se le proporciona una matriz de números enteros, que representa monedas de diferentes denominaciones; y una cantidad entera, que representa la cantidad total.

Calcule y devuelva la cantidad mínima de monedas necesarias para completar el monto total. Si ninguna combinación de monedas puede completar el monto total, se devuelve -1.

Puedes pensar que el número de cada tipo de moneda es infinito.

Ejemplo:
Entrada: monedas = [1, 2, 5], cantidad = 11
Salida: 3

Solución

Para el problema del cambio de monedas, la estrategia codiciosa que se te ocurre es: en cada paso, elige la denominación de moneda más grande que no exceda la cantidad restante.

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

Sin embargo, para este problema, el algoritmo codicioso no siempre funciona. Sin embargo, no existe una "selección codiciosa de atributos" para este problema. Es decir, hacer una elección óptima local en cada paso no conduce necesariamente a una solución óptima global.

Es posible que la solución óptima deba considerar no solo la información del paso actual, sino también cómo obtener la solución óptima a través de la consideración y coordinación generales, que es exactamente lo que puede proporcionar el método de programación dinámica.

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

Simplemente inicialice dp[0] a 0. Esto se debe a que, para cualquier moneda (donde moneda es el valor nominal de la moneda), su valor dp se calcula automáticamente como 1 en el bucle.

279. Números cuadrados perfectos

Dado un número entero n, devuelve el menor número de cuadrados perfectos cuya suma es n.

Un número cuadrado perfecto es un número entero cuyo valor es igual al cuadrado de otro número entero, es decir, su valor es igual al producto de un número entero multiplicado por sí mismo. Por ejemplo, 1, 4, 9 y 16 son números cuadrados perfectos, pero 3 y 11 no lo son.

Ejemplo:
Entrada: n = 13
Salida: 2
Explicación: 13 = 4 + 9

Solución

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]

Si piensas en cada número como un nodo y la diferencia entre los dos números es un cuadrado perfecto, entonces los dos números están conectados. Entonces nuestro objetivo es encontrar el camino más corto de 0 a 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. División de palabras

Se le proporciona una cadena sy una lista de cadenas wordDict como diccionario. Juzgue si puede usar las palabras que aparecen en el diccionario para unir s.

Nota: No es necesario utilizar todas las palabras que aparecen en el diccionario y las palabras del diccionario se pueden utilizar repetidamente.

Ejemplo 1:
Entrada: s = “applepenapple”, wordDict = [“apple”, “pen”]
Salida: verdadero

Ejemplo 2:
Entrada: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”] Salida:
false

Solución

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. Subsecuencia creciente más larga

Dada una matriz de números enteros, encuentre la longitud de la subsecuencia estrictamente creciente más larga que contiene.

Una subsecuencia es una secuencia derivada de una matriz eliminando (o no eliminando) elementos de la matriz sin cambiar el orden de los elementos restantes. Por ejemplo, [3,6,2,7] es una subsecuencia de la matriz [0,3,1,6,2,2,7].

Ejemplo:
Entrada: nums = [10,9,2,5,3,7,101,18]
Salida: 4
Explicación: La subsecuencia creciente más larga es [2,3,7,101], por lo que la longitud es 4.

Solución

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)

Este problema también se puede resolver mediante una estrategia codiciosa.

Cuando encuentre un número que sea mayor que el final de la subsecuencia actual, intente agregarlo a la subsecuencia. Cuando agregamos números a una subsecuencia, también puede impedir que se agreguen números posteriores. Esto significa que debemos considerar no solo la longitud, sino también el tamaño del último número de la subsecuencia. De hecho, para una subsecuencia de cierta longitud, cuanto menor sea el número final, mejor, porque entonces tendremos mayores posibilidades de encontrar un número mayor en el futuro.

El algoritmo codicioso nos pide que siempre intentemos agregar el número actual a la subsecuencia creciente más larga existente. Pero, ¿cómo encontrar rápidamente dónde insertarlo? Aquí es donde la búsqueda binaria resulta útil. Podemos usar la búsqueda binaria para encontrar la posición donde se debe insertar el número y actualizar el valor en la posición actual si es necesario.

La idea central de la matriz de colas es: para una subsecuencia creciente de longitud i, queremos saber cuál es el valor mínimo al final de todas estas subsecuencias, porque esto le da a los números posteriores una mayor probabilidad de ser sumados. El índice de colas representa la longitud de la subsecuencia y el valor representa el valor final mínimo de la subsecuencia creciente de la longitud correspondiente.

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. Subconjunto máximo de producto

Dado un número de matriz entera, busque el subarreglo continuo no vacío con el producto más grande en la matriz (el subarreglo contiene al menos un número) y devuelva el producto correspondiente al subarreglo.

Un subarreglo es una subsecuencia contigua de un arreglo.

Ejemplo:
Entrada: nums = [2,3,-2,4]
Salida: 6

Solución

Dado que el producto de un número negativo y un número negativo es positivo, también debemos realizar un seguimiento del producto máximo y mínimo de los subarreglos que terminan con el número actual. Si el número actual es negativo, cuando se multiplica por el número actual, el valor máximo se convierte en el valor mínimo y el valor mínimo se convierte en el valor máximo.

Entonces necesitamos registrar dos estados en cada ubicación:

max_product[i] representa el producto máximo de subarreglos que terminan en nums[i].
min_product[i] representa el producto mínimo de subarreglos que terminan en 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)

En realidad, no podemos usar una matriz para guardar el resultado de cada posición, solo necesitamos guardar el resultado de la posición anterior.

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. Dividir subconjuntos de suma igual

Se le proporciona una matriz de números no vacía que contiene solo números enteros positivos. Determine si esta matriz se puede dividir en dos subconjuntos para que la suma de los elementos de los dos subconjuntos sea igual.

Ejemplo:
Entrada: nums = [1,5,11,5]
Salida: verdadero

Solución

Supongo que te gusta

Origin blog.csdn.net/weixin_45427144/article/details/132219846
Recomendado
Clasificación