La programació dinàmica és una tècnica de disseny d'algorismes que es basa en descompondre un problema complex en subproblemes més simples i resoldre'ls de manera òptima. Aquesta tècnica és especialment útil per a problemes d'optimització on les solucions es poden construir a partir de solucions òptimes de subproblemes més petits.

Conceptes Clau

  1. Descomposició en Subproblemes:

    • Dividir el problema original en subproblemes més petits i més manejables.
    • Cada subproblema es resol una vegada i el resultat es guarda per evitar càlculs repetits.
  2. Solapament de Subproblemes:

    • Els subproblemes es solapen, és a dir, el mateix subproblema es resol múltiples vegades en un algorisme naïf.
    • La programació dinàmica evita aquesta redundància emmagatzemant els resultats de subproblemes ja resolts.
  3. Propietat de Subestructura Òptima:

    • Una solució òptima del problema original es pot construir a partir de solucions òptimes dels seus subproblemes.

Estratègies de Programació Dinàmica

  1. Memòria (Top-Down)

En aquest enfocament, es comença resolent el problema original i es descompon en subproblemes més petits. Els resultats dels subproblemes es guarden en una estructura de dades (normalment una taula o diccionari) per evitar càlculs repetits.

Exemple: Fibonacci amb Memòria

def fibonacci_memo(n, memo={}):
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    memo[n] = fibonacci_memo(n-1, memo) + fibonacci_memo(n-2, memo)
    return memo[n]

# Exemple d'ús
print(fibonacci_memo(10))  # Sortida: 55

  1. Taula (Bottom-Up)

En aquest enfocament, es resolen primer els subproblemes més petits i es construeixen solucions per a subproblemes més grans a partir d'aquestes solucions. Normalment, es fa ús d'una taula per guardar els resultats dels subproblemes.

Exemple: Fibonacci amb Taula

def fibonacci_table(n):
    if n <= 1:
        return n
    fib = [0] * (n + 1)
    fib[1] = 1
    for i in range(2, n + 1):
        fib[i] = fib[i-1] + fib[i-2]
    return fib[n]

# Exemple d'ús
print(fibonacci_table(10))  # Sortida: 55

Exemples Clàssics de Programació Dinàmica

  1. Problema de la Motxilla (Knapsack Problem)

Donat un conjunt d'objectes, cadascun amb un pes i un valor, determinar el nombre d'objectes que es poden incloure en una motxilla de capacitat màxima de pes per tal de maximitzar el valor total.

Implementació

def knapsack(weights, values, capacity):
    n = len(values)
    dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)]
    
    for i in range(n + 1):
        for w in range(capacity + 1):
            if i == 0 or w == 0:
                dp[i][w] = 0
            elif weights[i-1] <= w:
                dp[i][w] = max(values[i-1] + dp[i-1][w-weights[i-1]], dp[i-1][w])
            else:
                dp[i][w] = dp[i-1][w]
    
    return dp[n][capacity]

# Exemple d'ús
weights = [1, 2, 3]
values = [10, 15, 40]
capacity = 5
print(knapsack(weights, values, capacity))  # Sortida: 55

  1. Problema del Camí Més Curt (Shortest Path Problem)

Donat un graf amb pesos no negatius, trobar el camí més curt des d'un node font a tots els altres nodes.

Implementació: Algorisme de Floyd-Warshall

def floyd_warshall(graph):
    n = len(graph)
    dist = [[float('inf')] * n for _ in range(n)]
    
    for i in range(n):
        for j in range(n):
            if i == j:
                dist[i][j] = 0
            elif graph[i][j] != 0:
                dist[i][j] = graph[i][j]
    
    for k in range(n):
        for i in range(n):
            for j in range(n):
                dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
    
    return dist

# Exemple d'ús
graph = [
    [0, 3, float('inf'), 5],
    [2, 0, float('inf'), 4],
    [float('inf'), 1, 0, float('inf')],
    [float('inf'), float('inf'), 2, 0]
]
print(floyd_warshall(graph))
# Sortida: [[0, 3, 7, 5], [2, 0, 6, 4], [3, 1, 0, 5], [5, 3, 2, 0]]

Exercicis Pràctics

Exercici 1: Subseqüència Comuna Més Llarga (Longest Common Subsequence)

Donades dues cadenes, trobar la longitud de la subseqüència comuna més llarga.

Implementació

def lcs(X, Y):
    m = len(X)
    n = len(Y)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    
    for i in range(m + 1):
        for j in range(n + 1):
            if i == 0 or j == 0:
                dp[i][j] = 0
            elif X[i-1] == Y[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
    
    return dp[m][n]

# Exemple d'ús
X = "AGGTAB"
Y = "GXTXAYB"
print(lcs(X, Y))  # Sortida: 4

Exercici 2: Problema de la Cadena de Matrius (Matrix Chain Multiplication)

Donada una seqüència de matrius, determinar la manera més eficient de multiplicar-les.

Implementació

def matrix_chain_order(p):
    n = len(p) - 1
    dp = [[0] * n for _ in range(n)]
    
    for l in range(2, n + 1):
        for i in range(n - l + 1):
            j = i + l - 1
            dp[i][j] = float('inf')
            for k in range(i, j):
                q = dp[i][k] + dp[k+1][j] + p[i]*p[k+1]*p[j+1]
                if q < dp[i][j]:
                    dp[i][j] = q
    
    return dp[0][n-1]

# Exemple d'ús
p = [1, 2, 3, 4]
print(matrix_chain_order(p))  # Sortida: 18

Resum

La programació dinàmica és una tècnica poderosa per resoldre problemes d'optimització que es poden descompondre en subproblemes més petits. Utilitzant estratègies com la memòria (top-down) i la taula (bottom-up), podem evitar càlculs repetits i millorar l'eficiència dels nostres algorismes. Els exemples clàssics com el problema de la motxilla i el problema del camí més curt il·lustren la utilitat d'aquesta tècnica en una àmplia varietat de problemes.

© Copyright 2024. Tots els drets reservats