【动态规划】常用算法:背包问题, 最长递增子序列(LIS), 最长公共子序列, 最小编辑距离, 最短路径问题, 区间调度问题, 最长回文子序列, 切割钢条问题详解与代码示例


目录

1.0/1 背包问题:

2 最长递增子序列(LIS):

3  最长公共子序列(LCS):

 4 最小编辑距离(Edit Distance):

5 最短路径问题(单源最短路径):

6 区间调度问题:

 7 最长回文子序列:

8 切割钢条问题:


        动态规划是一种通过将原问题分解为相互重叠的子问题,并通过已解决的子问题的解来求解原问题的方法。动态规划算法通常用于优化问题,其中需要做出一系列决策,每个决策都会影响到后续的决策。以下是一些常见的动态规划算法:

1.0/1 背包问题:

目标是选择一组物品放入背包,使得总重量不超过背包容量且总价值最大。

 题目:给定n个物品,每个物品有自己的重量和价值,要求从中选择一些物品放入容量为C的背包中,使得放入的物品总重量不超过C且总价值最大。

思路:定义一个二维数组dp[i][j]表示在前i个物品中选择总重量不超过j的情况下的最大总价值。

状态转移方程为:

dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])

其中w[i]为第i个物品的重量,v[i]为其价值。

示例代码:

def knapsack(weight, value, C):
    n = len(weight)
    dp = [[0] * (C+1) for _ in range(n+1)]
    
    for i in range(1, n+1):
        for j in range(1, C+1):
            if j >= weight[i-1]:
                dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i-1]] + value[i-1])
            else:
                dp[i][j] = dp[i-1][j]
    
    return dp[n][C]

# 测试
weight = [2, 3, 4, 5]
value = [3, 4, 5, 6]
C = 8
result = knapsack(weight, value, C)
print(result)  # 输出:10

 

2 最长递增子序列(LIS):

题目:给定一个序列,找出最长的递增子序列的长度。

思路:定义一个一维数组dp,其中dp[i]表示以第i个元素结尾的最长递增子序列的长度。

状态转移方程为:

dp[i] = max(dp[j]+1)

其中0 <= j < i且nums[j] < nums[i]。

示例代码:

def lengthOfLIS(nums):
    n = len(nums)
    dp = [1] * n
    
    for i in range(n):
        for j in range(i):
            if nums[j] < nums[i]:
                dp[i] = max(dp[i], dp[j]+1)
    
    return max(dp)

# 测试
nums = [10, 9, 2, 5, 3, 7, 101, 18]
result = lengthOfLIS(nums)
print(result)  # 输出:4

3  最长公共子序列(LCS):

题目:给定两个序列,找出它们的最长公共子序列的长度。

思路:定义一个二维数组dp,其中dp[i][j]表示序列s1的前i个字符和序列s2的前j个字符的最长公共子序列的长度。

状态转移方程为:

dp[i][j] = dp[i-1][j-1] + 1,if  s1[i-1] == s2[j-1]

dp[i][j] = max(dp[i-1][j], dp[i][j-1]),if  s1[i-1] != s2[j-1] 。

示例代码:

def longestCommonSubsequence(s1, s2):
    m, n = len(s1), len(s2)
    dp = [[0] * (n+1) for _ in range(m+1)]
    
    for i in range(1, m+1):
        for j in range(1, n+1):
            if s1[i-1] == s2[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]

# 测试
s1 = "abcde"
s2 = "ace"
result = longestCommonSubsequence(s1, s2)
print(result)  # 输出:3

 4 最小编辑距离(Edit Distance):

题目:给定两个字符串s1和s2,通过一系列插入、删除、替换操作,将s1转换为s2,求最小的编辑距离。

思路:定义一个二维数组dp,其中dp[i][j]表示将s1的前i个字符转换为s2的前j个字符所需的最小编辑距离。

状态转移方程为:

dp[i][j] = dp[i-1][j-1],如果s1[i-1] == s2[j-1]

dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1

如果s1[i-1] != s2[j-1]

示例代码:

def minEditDistance(s1, s2):
    m, n = len(s1), len(s2)
    dp = [[0] * (n+1) for _ in range(m+1)]
    
    for i in range(1, m+1):
        dp[i][0] = i
    for j in range(1, n+1):
        dp[0][j] = j
        
    for i in range(1, m+1):
        for j in range(1, n+1):
            if s1[i-1] == s2[j-1]:
                dp[i][j] = dp[i-1][j-1]
            else:
                dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
    
    return dp[m][n]

# 测试
s1 = "intention"
s2 = "execution"
result = minEditDistance(s1, s2)
print(result)  # 输出:5

5 最短路径问题(单源最短路径):

题目:给定一个加权有向图和起点S,求从S到所有其他节点的最短路径。

思路:使用Dijkstra算法或Bellman-Ford算法来解决。这里以Dijkstra算法为例。

示例代码:

import heapq

def dijkstra(graph, S):
    n = len(graph)
    distance = [float('inf')] * n
    distance[S] = 0
    
    pq = [(0, S)]  # 使用优先队列按距离从小到大选择节点
    
    while pq:
        d, node = heapq.heappop(pq)
        
        if d > distance[node]:
            continue
        
        for neighbor, weight in graph[node]:
            new_distance = distance[node] + weight
            if new_distance < distance[neighbor]:
                distance[neighbor] = new_distance
                heapq.heappush(pq, (new_distance, neighbor))
    
    return distance

# 测试
graph = [[(1, 4), (2, 2)],  # 节点0的邻接节点和边权重
         [(3, 3)],          # 节点1的邻接节点和边权重
         [(1, 1), (3, 5)],  # 节点2的邻接节点和边权重
         [(4, 2)],          # 节点3的邻接节点和边权重
         []]                # 节点4的邻接节点和边权重
S = 0
result = dijkstra(graph, S)
print(result)  # 输出:[0, 4, 2, 6, inf]

6 区间调度问题:

题目:给定一个包含n个区间的集合,找到最大的不重叠子集,即这个子集中的任意两个区间都没有重叠部分。

思路:对区间按照结束时间进行排序,选择结束时间最早的区间作为第一个区间,然后依次遍历剩余区间,如果当前区间的开始时间晚于前一个区间的结束时间,则将其加入不重叠子集。

示例代码:

def intervalSchedule(intervals):
    n = len(intervals)
    if n == 0:
        return 0
    
    intervals.sort(key=lambda x: x[1])  # 按结束时间进行排序
    count = 1
    end = intervals[0][1]
    
    for i in range(1, n):
        if intervals[i][0] >= end:
            count += 1
            end = intervals[i][1]
    
    return count

# 测试
intervals = [(1, 3), (2, 4), (3, 6), (5, 7), (6, 8)]
result = intervalSchedule(intervals)
print(result)  # 输出:3

 7 最长回文子序列:

题目:给定一个字符串,找出其中最长的回文子序列的长度。

思路:定义一个二维数组dp,其中dp[i][j]表示字符串从索引i到索引j的子串中的最长回文子序列的长度。

状态转移方程为:

dp[i][j] = dp[i+1][j-1] + 2,如果s[i] == s[j]

dp[i][j] = max(dp[i+1][j], dp[i][j-1]),如果s[i] != s[j]

示例代码:

def longestPalindromeSubseq(s):
    n = len(s)
    dp = [[0] * n for _ in range(n)]
    
    for i in range(n-1, -1, -1):
        dp[i][i] = 1
        for j in range(i+1, n):
            if s[i] == s[j]:
                dp[i][j] = dp[i+1][j-1] + 2
            else:
                dp[i][j] = max(dp[i+1][j], dp[i][j-1])
    
    return dp[0][n-1]

# 测试
s = "bbbab"
result = longestPalindromeSubseq(s)
print(result)  # 输出:4

8 切割钢条问题:

题目:给定一根长度为n的钢条和一个价格表,求切割钢条的方式,使得销售总收益最大。

思路:定义一个一维数组dp,其中dp[i]表示长度为i的钢条的最大收益。

状态转移方程为:

dp[i] = max(dp[i-j] + price[j])

其中0 <= j <= i,price[j]表示长度为j的钢条的价格。

示例代码:

def cutRod(length, price):
    n = len(price)
    dp = [0] * (length+1)
    
    for i in range(1, length+1):
        for j in range(1, min(i+1, n)):
            dp[i] = max(dp[i], dp[i-j] + price[j])
    
    return dp[length]

# 测试
length = 8
price = [0, 1, 5, 8, 9, 10, 17, 17, 20]
result = cutRod(length, price)
print(result)  # 输出:22

 

  1. 最长递增子序列(Longest Increasing Subsequence,LIS): 寻找给定序列中的最长递增子序列的长度。

  2. 最长公共子序列(Longest Common Subsequence,LCS): 给定两个序列,找到它们的最长公共子序列的长度。

  3. 最小编辑距离(Edit Distance): 通过一系列编辑操作(插入、删除、替换)将一个字符串转换为另一个字符串,求最小的编辑距离。

  4. 最短路径问题: 例如,单源最短路径(Dijkstra算法、Bellman-Ford算法)、全源最短路径(Floyd-Warshall算法)。

  5. 最大子数组和问题(Maximum Subarray Sum): 寻找给定数组中和最大的连续子数组。

  6. 背包问题的变种: 包括多重背包、完全背包、分数背包等。

  7. 区间调度问题: 给定一组区间,找到最大的不重叠子集。

  8. 最长回文子序列: 寻找给定字符串的最长回文子序列的长度。

  9. 切割钢条问题:给定一根长度为n的钢条和一个价格表,求切割钢条的方式,使得销售总收益最大。

这只是动态规划应用的一小部分,实际上,动态规划可以用于解决各种优化问题。每个动态规划问题都有其独特的状态定义、状态转移方程和初始条件。通常,动态规划问题可以分为自顶向下(递归加记忆化搜索)和自底向上(迭代)两种解法。

你可能感兴趣的:(Python算法30篇,算法,动态规划)