python 实现动态规划

动态规划(Dynamic programming)

是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推的方式去解决,当前子问题的解将由上一个子问题的解推出。使用动态规划来解题只需要多项式时间复杂度,因此它比回溯法、暴力法等要快许多。

动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。
动态规划中的子问题往往不是相互独立的(即子问题重叠)。在求解的过程中,许多子问题的解被反复地使用。为了避免重复计算,动态规划算法采用了填表来保存子问题解的方法,即带备忘录的递归,当需要某个子问题的解时,直接取值即可,从而避免重复计算。

适用问题
符合“一个模型三个特征”的问题。
“一个模型”:指 多阶段决策最优解模型;
“三个特征”:分别是最优子结构、无后效性和重复子问题。
(1)最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2)无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势。

这类问题的求解步骤通常如下:
初始状态→│决策1│→│决策2│→…→│决策n│→结束状态
(1)划分:按照问题的特征,把问题分为若干阶段。注意:划分后的阶段一定是有序的或者可排序的
(2)确定状态和状态变量:将问题发展到各个阶段时所处的各种不同的客观情况表现出来。状态的选择要满足无后续性
(3)确定决策并写出状态转移方程:状态转移就是根据上一阶段的决策和状态来导出本阶段的状态。根据相邻两个阶段状态之间的联系来确定决策方法和状态转移方程
(4)边界条件:状态转移方程是一个递推式,因此需要找到递推终止的条件

动态规划三要素:
(1)问题的阶段
(2)每个阶段的状态
(3)相邻两个阶段之间的递推关系
整个求解过程可以用一张最优决策表来描述,最优决策表是一张二维表(行:决策阶段,列:问题的状态)表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。

经典问题1:斐波那契数列

def fibonacci(n):
    if n < 2:
        return n
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)


def dyna_fibonacci(n):
    if n < 2:
        return n
    else:
        a, b = 0, 1
        for _ in range(n - 1):
            a, b = b, a + b
        return b


if __name__ == '__main__':
    t1 = time.time()
    fibonacci(100)
    t2 = time.time()
    dyna_fibonacci(100)
    t3 = time.time()
    print("Time required for using recursion:", t2 - t1)
    print("Dynamic Planning Duration:", t3 - t2)

经典问题2:最长公共子序列(Longest Common Subsequence)

给定两个字符串,求它们的最长公共子序列的长度。

def longest_subst(str1, str2):
    maxlen, index = 0, 0
    arr = [[0 for _ in range(len(str2) + 1)] for _ in range(len(str1) + 1)]
    for i in range(1, len(str1) + 1):
        for j in range(1, len(str2) + 1):
            if str1[i - 1] == str2[j - 1]:
                arr[i][j] = arr[i - 1][j - 1] + 1
            else:
                arr[i][j] = 0
            if arr[i][j] > maxlen:
                maxlen = arr[i][j]
                index = j
    return str2[index - maxlen:index]


print(longest_subst("1AB2345CD", "12345EF"))

使用了一个二维数组来存储子问题的最优解,通过两层循环遍历两个字符串的所有字符,当当前字符相等时,我们可以构成更长的公共子串,因此将 arr[i][j] 设置为左上角元素 arr[i - 1][j - 1] 的值加1。同时,我们不断更新最长公共子串的长度和结束位置。

1)初始化maxlen和index,分别用来记录最长公共子串的长度和在str2中的结束位置。
2)创建一个二维数组arr来存储每个位置的最长公共子串的长度。
3)使用双重循环遍历两个字符串str1和str2,从第一个字符开始比较,直到最后一个字符。
4)对于每一对字符,如果它们相等,则当前位置的最长公共子串的长度是前一个位置的最长公共子串长度加1;否则,最长公共子串的长度为0(因为不相等,不存在公共子串)。
5)在每次更新arr数组时,检查当前最长公共子串的长度是否超过了之前记录的最大长度maxlen,如果是,则更新maxlen和index。index被设置为j,表示当前最长公共子串在str2中的结束位置。

index的作用是记录最长公共子串在str2中的结束位置,最终返回的是从str2中index - maxlen到index位置的子串,这就是最长的公共子串。

经典问题3:背包问题

背包问题(Knapsack Problem)是一个经典的组合优化问题,在计算机科学和运筹学中被广泛研究和应用。该问题描述如下:给定一个背包容量和一组物品,每个物品都有一个重量和一个价值,我们的目标是选择一些物品放入背包中,使得放入背包的物品总重量不超过背包容量,并且物品的总价值最大化。

背包问题可以分为两种类型:0-1背包问题和无限背包问题。

0-1背包问题(0/1 Knapsack Problem):每个物品最多只能选择一次放入背包中。即物品的选择是二进制的,要么放入背包,要么不放入。
无限背包问题(Unbounded Knapsack Problem):每个物品可以选择多次放入背包中,即物品的选择是非负整数的。
下面是一个使用Python实现0-1背包问题的示例代码:

def knapsack_01(values, weights, capacity):
    n = len(values)

    # 创建一个二维数组来存储子问题的最优解
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        for j in range(1, capacity + 1):
            # 如果当前物品的重量超过背包容量,则无法放入背包
            if weights[i - 1] > j:
                dp[i][j] = dp[i - 1][j]
            else:
                # 考虑将当前物品放入背包和不放入背包两种情况,选择价值最大的
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1])

    # 获取最优解的价值
    max_value = dp[n][capacity]

    # 回溯找出选择的物品
    selected_items = []
    j = capacity
    for i in range(n, 0, -1):
        if dp[i][j] != dp[i - 1][j]:
            selected_items.append(i - 1)
            j -= weights[i - 1]

    return max_value, selected_items

# 测试示例
values = [60, 100, 120]
weights = [10, 20, 30]
capacity = 50
max_value, selected_items = knapsack_01(values, weights, capacity)
print("Max Value:", max_value)
print("Selected Items:", selected_items)

上述代码实现了0-1背包问题的动态规划解法。通过定义一个二维数组 dp 来存储子问题的最优解,然后使用两层循环遍历所有可能的物品和背包容量组合,计算出每个子问题的最优解。最后,通过回溯找出选择的物品。

在上述示例中,我们测试了一个简单的示例,包含3个物品,每个物品的重量分别为10、20、30,价值分别为60、100、120,背包容量为50。程序输出了最大价值以及选择的物品索引,使用二维数组 dp 来存储子问题的最优解,其中 dp[i][j] 表示在考虑前 i 个物品,且背包容量为 j 的情况下的最优解。通过填充整个 dp 数组,我们可以逐步计算出问题的最优解。

最终的最优解将存储在 dp[n][capacity] 中,其中 n 是物品的总数,capacity 是背包的容量。

背包问题中动态规划算法的核心部分dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]),用于计算子问题的最优解。

dp[i][j] 表示在考虑前 i 个物品,且背包容量为 j 的情况下的最优解,即背包能够装下的物品的最大总价值。

dp[i - 1][j] 表示不选择第 i 个物品时的最优解,即当前背包容量为 j,不考虑第 i 个物品时的最大总价值。

dp[i - 1][j - weights[i - 1]] + values[i - 1] 表示选择第 i 个物品时的最优解,即当前背包容量为 j,考虑第 i 个物品时的最大总价值。weights[i - 1] 是第 i 个物品的重量,values[i - 1] 是第 i 个物品的价值。

所以,这行代码的含义是:选择第 i 个物品时,比较不选择第 i 个物品和选择第 i 个物品的两种情况,取其中的最大值作为当前背包容量为 j 时的最优解。

通过比较这两种情况,我们可以确定是否将第 i 个物品放入背包中,以使得背包的总价值最大化。
回溯具体
从最后一个物品开始,逐步向前回溯。具体的操作如下:

初始化一个空列表 selected_items,用于存储选择的物品的索引。

初始化变量 j 为背包的剩余容量,初始值为总容量 capacity。

使用 for 循环从最后一个物品开始向前遍历,即从 n 到 1,每次减 1。

在每一次循环中,判断当前物品是否被选择。如果 dp[i][j] 不等于 dp[i - 1][j],说明当前物品被选择放入背包中。

将当前物品的索引 i - 1 添加到 selected_items 列表中,表示选择了该物品。

更新剩余容量 j,减去当前物品的重量 weights[i - 1]。

检查剩余容量 j 是否为 0,如果为 0,则已经没有剩余容量,即背包已经装满,可以终止循环。

循环结束后,selected_items 列表中存储了被选择的物品的索引,即最优解中的物品选择。

通过这样的回溯操作,我们可以找到最优解中选择的物品索引。

经典问题4:矩阵求和

题目主要信息:
给定一个矩阵,从矩阵左上角到右下角,每次只能向下或者向右
从左上角到右下角路径上经过的所有数字之和为路径和,求该路径和的最小值
矩阵不为空,每个元素值都为非负数

具体做法:
step 1:我们可以构造一个与矩阵同样大小的二维辅助数组,其中dp[i][j]表示以(i,j)位置为终点的最短路径和,则dp[0][0]=matrix[0][0]
step 2:很容易知道第一行与第一列,只能分别向右或向下,没有第二种选择,因此第一行只能由其左边的累加,第一列只能由其上面的累加。
step 3:边缘状态构造好以后,遍历矩阵,补全矩阵中每个位置的dp数组值:如果当前的位置是(i,j),上一步要么是(i−1,j)往下,要么就是(i,j−1)往右,那么取其中较小值与当前位置的值相加就是到当前位置的最小路径和,因此状态转移公式为dp[i][j]=min(dp[i−1][j],dp[i][j−1])+matrix[i][j]
step 4:最后移动到(n−1,m−1)的位置就是到右下角的最短路径和。

def max_Matrix(arr):
    rows = len(arr)
    cols = len(arr[0])
    # dp[i][j]表示以当前i,j位置为终点的最短路径长度
    dp = [[0 for _ in range(cols)] for _ in range(rows)]
    dp[0][0] = arr[0][0]
    # 处理第一行
    for j in range(1, cols):
        dp[0][j] = arr[0][j] + dp[0][j - 1]
    # 处理第一列
    for j in range(1, rows):
        dp[j][0] = arr[j][0] + dp[j - 1][0]
    print(dp)
    for i in range(1, rows):
        for j in range(1, cols):
            if dp[i - 1][j] < dp[i][j - 1]:
                dp[i][j] = dp[i - 1][j] + arr[i][j]
            else:
                dp[i][j] = dp[i][j - 1] + arr[i][j]
    return dp[rows - 1][cols - 1]

经典问题5 最长递增子序列(Longest Increasing Subsequence)

给定一个整数序列,求其中最长的递增子序列的长度。

def max_increasing_subsequence(nums):
    n = len(nums)
    if n == 0:
        return []

    # dp[i] 表示以 nums[i] 结尾的最大递增子序列的长度
    dp = [1] * n

    # 记录最大递增子序列的长度和结束位置
    max_length = 1
    end_index = 0

    # 动态规划求解最大递增子序列的长度
    for i in range(1, n):
        for j in range(i):
            if nums[i] > nums[j]:
                dp[i] = max(dp[i], dp[j] + 1)
                if dp[i] > max_length:
                    max_length = dp[i]
                    end_index = i

    # 回溯查找最大递增子序列
    max_subsequence = []
    max_subsequence.append(nums[end_index])
    for i in range(end_index - 1, -1, -1):
        if nums[i] < nums[end_index] and dp[i] == dp[end_index] - 1:
            max_subsequence.append(nums[i])
            end_index = i

    return max_subsequence[::-1]


# 示例用法
nums = [1, 3, 5, 2, 4, 6, 8]
print("最大递增子序列:", max_increasing_subsequence(nums))

这段代码首先定义了一个函数 max_increasing_subsequence(nums),用于求解给定整数列表 nums 的最大递增子序列。算法使用动态规划的方法,在 O(n^2) 的时间复杂度内完成计算。

1)初始化一个长度为 n 的动态规划数组 dp,其中 dp[i] 表示以 nums[i] 结尾的最大递增子序列的长度,初始值都为 1。
2)通过双重循环遍历列表,更新 dp 数组的值,同时记录最大递增子序列的长度和结束位置。
3)使用回溯的方法,根据 dp 数组的值回溯出最大递增子序列。
4)返回找到的最大递增子序列。

经典例题6 编辑距离(Edit Distance)

给定两个字符串,通过插入、删除、替换操作,将一个字符串转换成另一个字符串所需的最少操作次数。
设字符串 1 为 s1,长度为 m,字符串 2 为 s2,长度为 n。

def min_distance(word1, word2):
    m, n = len(word1), len(word2)
    # 初始化 dp 矩阵
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    
    # 初始化边界条件
    for i in range(m + 1):
        dp[i][0] = i
    for j in range(n + 1):
        dp[0][j] = j
    
    # 动态规划计算最小编辑距离
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if word1[i - 1] == word2[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]

# 示例用法
word1 = "intention"
word2 = "execution"
print("编辑距离:", min_distance(word1, word2))

定义状态: dp[i][j] 表示将 s1 的前 i 个字符编辑成 s2 的前 j 个字符所需的最少操作次数。
初始化状态: 当一个字符串为空时,将另一个字符串变为空串需要的操作次数即为其长度,即 dp[i][0] = i,dp[0][j] = j。
状态转移方程:
当 s1[i] == s2[j] 时,不需要进行操作,即 dp[i][j] = dp[i-1][j-1];
当 s1[i] != s2[j] 时,可以进行三种操作:
插入操作:将 s1 的前 i 个字符编辑成 s2 的前 j-1 个字符,然后在末尾插入一个字符,操作次数为 dp[i][j-1] + 1;
删除操作:将 s1 的前 i-1 个字符编辑成 s2 的前 j 个字符,然后删除末尾一个字符,操作次数为 dp[i-1][j] + 1;
替换操作:将 s1 的前 i-1 个字符编辑成 s2 的前 j-1 个字符,然后将末尾字符替换成相同,操作次数为 dp[i-1][j-1] + 1。
综合考虑以上三种情况,取最小值即可得到 dp[i][j]。
返回结果: 最终编辑完成后,dp[m][n] 即为所需的最小操作次数。

经典例题7 最大子数组和(Maximum Subarray)

给定一个整数数组,求其连续子数组中具有最大和的子数组的和

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

# 示例用法
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print("最大子数组和:", max_subarray(nums))

定义状态: dp[i] 表示以第 i 个元素结尾的最大子数组和。
初始化状态: dp[0] = nums[0]。
状态转移方程: 对于第 i 个元素,有两种选择:
将第 i 个元素加入当前子数组中,则最大子数组和为 dp[i-1] + nums[i];
不将第 i 个元素加入当前子数组中,以第 i 个元素为起点重新开始计算子数组和,即 dp[i] = nums[i]。
综合考虑以上两种情况,取较大的值作为 dp[i]。
返回结果: dp 数组中的最大值即为所求的最大子数组和。

经典例题8 最优二叉搜索树(Optimal Binary Search Tree)

给定一组有序关键字和对应的查找概率,构造一棵二叉搜索树,使得查找的平均代价最小。

def optimal_bst(keys, probs):
    n = len(keys)
    dp = [[0] * (n + 1) for _ in range(n + 1)]
    for i in range(n):
        dp[i][i] = probs[i]
    for length in range(1, n + 1):
        for i in range(n - length + 1):
            j = i + length - 1
            dp[i][j] = float('inf')
            for k in range(i, j + 1):
                cost = dp[i][k - 1] + dp[k + 1][j] + sum(probs[i:j + 1])
                dp[i][j] = min(dp[i][j], cost)
    return dp[0][n - 1]

# 示例用法
keys = [1, 2, 3, 4, 5]
probs = [0.2, 0.15, 0.1, 0.05, 0.3]
print("最小平均代价:", optimal_bst(keys, probs))

定义状态: dp[i][j] 表示以关键字 i 到关键字 j 为根节点的最优二叉搜索树的平均代价。
初始化状态: 对于只含一个关键字的子树,平均代价为其查找概率。
状态转移方程: 对于子树中的每一个关键字 k,以 k 为根节点的子树的平均代价为左子树的平均代价加上右子树的平均代价,再加上根节点 k 的查找概率。
返回结果: dp[1][n] 即为所求的最小平均代价。

经典例题9 打家劫舍问题(House Robber)

给定一组房屋,每个房屋中存放着一定金额的钱,相邻的房屋被安装了安全系统,如果相邻的两个房屋同时被盗,系统会自动报警。求在不触发警报的情况下能够获取的最大金额。

def rob(nums):
    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[-1]

# 示例用法
nums = [2, 7, 9, 3, 1]
print("能够获取的最大金额:", rob(nums))

定义状态: dp[i] 表示在第 i 家能够获取的最大金额。
初始化状态: dp[0] = nums[0],dp[1] = max(nums[0], nums[1])。
状态转移方程: 对于第 i 家,有两种选择:
偷取第 i 家,则最大金额为前两家的最大金额加上当前第 i 家的金额,即 dp[i-2] + nums[i];
不偷取第 i 家,则最大金额与前一家的最大金额相同,即 dp[i-1]。
综合考虑以上两种情况,取较大的值作为 dp[i]。
返回结果: dp 数组中的最后一个元素即为所求的最大金额。

经典例题10 最长回文子串(Longest Palindromic Substring)

给定一个字符串,求其最长的回文子串的长度。

def longest_palindromic_substring(s):
    if not s:
        return ""
    n = len(s)
    dp = [[False] * n for _ in range(n)]
    start, max_len = 0, 1
    # 单个字符为回文子串
    for i in range(n):
        dp[i][i] = True
    # 相邻字符相同的子串为回文子串
    for i in range(n - 1):
        if s[i] == s[i + 1]:
            dp[i][i + 1] = True
            start = i
            max_len = 2
    # 动态规划,判断子串是否为回文子串
    for length in range(3, n + 1):
        for i in range(n - length + 1):
            j = i + length - 1
            if s[i] == s[j] and dp[i + 1][j - 1]:
                dp[i][j] = True
                start = i
                max_len = length
    return s[start:start + max_len]

# 示例用法
s = "babad"
print("最长回文子串:", longest_palindromic_substring(s))

定义状态: dp[i][j] 表示子串 s[i:j+1] 是否为回文子串,是则为 True,否则为 False。
初始化状态: 对角线上的单个字符为回文子串,即 dp[i][i] = True,相邻字符相同的子串为回文子串,即 dp[i][i+1] = (s[i] == s[i+1])
状态转移方程: 对于子串 s[i:j+1],如果 s[i] == s[j] 且 s[i+1:j] 为回文子串,则 s[i:j+1] 也为回文子串。
返回结果: 找到 dp[i][j] 为 True 且 j - i 最大的子串即为所求的最长回文子串。

经典例题11 硬币找零问题(Coin Change Problem)

给定一组硬币的面值和一个总金额,求出凑成该金额所需的最少硬币数量。

def coin_change(coins, amount):
    if amount == 0:
        return 0
    dp = [amount + 1] * (amount + 1)
    dp[0] = 0
    for i in range(1, amount + 1):
        for coin in coins:
            if i - coin >= 0:
                dp[i] = min(dp[i], dp[i - coin] + 1)
    return dp[amount] if dp[amount] != amount + 1 else -1

# 示例用法
coins = [1, 2, 5]
amount = 11
print("最少硬币数量:", coin_change(coins, amount))

定义状态: dp[i] 表示凑成金额 i 所需的最少硬币数量。
初始化状态: 将 dp[0] 初始化为 0,其他位置初始化为一个较大的值,比如金额数加 1。
状态转移方程: 对于金额 i,遍历每种硬币面值 j,如果 j 小于等于 i,则可以选择使用硬币 j,此时 dp[i] 的值为 dp[i - j] 加上一个硬币 j 的数量,取所有可能情况中的最小值。
返回结果: 返回 dp[amount] 即为所求的最少硬币数量。

  • 3
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
动态规划是一种常用的算法思想,可以用来解决很多问题,比如最长公共子序列、背包问题等。Python实现动态规划的步骤如下: 1. 定义状态:找到问题中的状态变量,定义状态表示方法。 2. 初始化状态:将状态初始化为问题的初始状态。 3. 状态转移:根据问题的状态转移方程,计算出每个状态的值。 4. 计算结果:根据问题的要求,计算出最终结果。 下面以背包问题为例,演示动态规划实现过程。 背包问题:有一个容量为C的背包和n个物品,每个物品有一个重量w和一个价值v,现在需要选择一些物品放入背包中,使得它们的总重量不超过C,同时总价值最大。 1. 定义状态:设f(i,j)表示前i个物品放入容量为j的背包中所能获得的最大价值。 2. 初始化状态:f(0,j) = 0, f(i,0) = 0。 3. 状态转移:对于第i个物品,有两种情况: - 不放入背包中,此时f(i,j) = f(i-1,j); - 放入背包中,此时f(i,j) = f(i-1,j-w[i]) + v[i]; 综合上述两种情况,状态转移方程为:f(i,j) = max(f(i-1,j), f(i-1,j-w[i]) + v[i])。 4. 计算结果:最终结果为f(n,C)。 下面是Python实现的代码: def knapsack(C, w, v): n = len(w) f = [ * (C+1) for _ in range(n+1)] for i in range(1, n+1): for j in range(1, C+1): if j < w[i-1]: f[i][j] = f[i-1][j] else: f[i][j] = max(f[i-1][j], f[i-1][j-w[i-1]] + v[i-1]) return f[n][C] # 测试 C = 10 w = [2, 3, 4, 5] v = [3, 4, 5, 6] print(knapsack(C, w, v)) # 输出:10

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值