【算法】动态规划

概述

动态规划(Dynamic Programming)是一种通过将问题划分为相互重叠的子问题来解决问题的算法思想。其核心思想是通过保存已经计算过的子问题的解,避免重复计算,从而降低时间复杂度。

动态规划的适用条件包括:

问题具有最优子结构:问题的最优解可以通过子问题的最优解来构造。
子问题之间存在重叠:原问题的求解过程中,多次求解相同的子问题。
动态规划的基本步骤如下:

1、定义状态:明确问题的状态,并用状态变量表示。
2、确定状态转移方程:根据问题的最优子结构,确定状态之间的转移关系。
3、初始化:设置初始状态的值。
4、递推计算:根据状态转移方程,从初始状态逐步计算到目标状态。
5、求解目标:根据最终状态的值,得到问题的解。

背包问题

背包问题是一个经典的组合优化问题,在计算机科学和运筹学中具有广泛的应用。它的基本形式是:给定一个固定大小的背包,和一组物品,每个物品有对应的重量和价值。目标是在不超过背包容量的前提下,选择合适的物品放入背包,使得背包中物品的总价值最大化。

背包问题可以分为多个不同的变体,其中最常见的有01背包问题、部分背包、完全背包问题和多重背包问题。

01背包问题:

每个物品要么放入背包,要么不放入背包,不能拆分。
对于每个物品,只有两种选择,放入或者不放入背包。

代码示例

public class Knapsack {
    public static int knapsack(int[] weights, int[] values, int capacity) {
        int n = weights.length; // 物品个数
        int[][] dp = new int[n + 1][capacity + 1]; // 创建动态规划表

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= capacity; j++) {
                if (weights[i - 1] > j) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);
                }
            }
        }

        return dp[n][capacity];
    }

    public static void main(String[] args) {
        int[] weights = {2, 3, 4, 5}; // 物品重量
        int[] values = {3, 4, 5, 6};  // 物品价值
        int capacity = 8;            // 背包容量

        int max_value = knapsack(weights, values, capacity);
        System.out.println("最大价值: " + max_value);
    }
}

这段代码实现了 01 背包问题的动态规划解法。knapsack 方法接受物品重量数组 weights、物品价值数组 values 和背包容量 capacity 作为输入,并返回最大的背包价值。

代码中创建了一个二维数组 dp 来存储子问题的最优解。通过两层循环遍历每个子问题,并根据状态转移方程更新 dp 数组。最后,返回 dp[n][capacity],即表示最大的背包价值。

在上述示例中,测试样例中的物品重量为 [2, 3, 4, 5],物品价值为 [3, 4, 5, 6],背包容量为 8。输出结果为最大价值为 11。

部分背包

部分背包问题是一个在给定背包容量的限制下,选择物品放入背包以使得背包的总价值最大化的问题。与 0-1 背包问题不同的是,部分背包问题允许将物品分割成小块,并且可以选择装入一部分物品。

在部分背包问题中,每个物品都有一个重量和一个价值。目标是选择一些物品装入背包,使得被选中物品的总重量不超过背包的容量,同时使得它们的总价值最大化。

为了解决这个问题,我们可以根据物品的单位价值(即每单位重量的价值)进行排序,然后按照从高到低的顺序依次装入物品,直到背包无法再装入完整的物品为止。如果背包仍有剩余容量,则根据物品的单位价值将其分割成部分,并将部分装入背包,使得装入的部分物品价值最大化。

部分背包问题可以使用贪心算法来求解。通过按照单位价值排序并可根据限制条件逐步装入物品,贪心算法能够在较短的时间内得到一个近似最优解。

需要注意的是,部分背包问题的贪心算法并不一定能够得到最优解。在某些情况下,贪心选择可能会导致次优解或者错误的结果。因此,对于严格要求最优解的问题,可能需要使用其他算法来求解,如动态规划等。

代码示例

public class FractionalKnapsack {
    public static double fractionalKnapsack(int[] weights, int[] values, int capacity) {
        int n = weights.length;
        double[][] dp = new double[n + 1][capacity + 1];

        for (int i = 1; i <= n; i++) {
            int weight = weights[i - 1];
            int value = values[i - 1];
            for (int j = 1; j <= capacity; j++) {
                if (weight <= j) {
                    // 可以完整装入当前物品
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight] + value);
                } else {
                    // 只能装入一部分当前物品
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }

        return dp[n][capacity];
    }

    public static void main(String[] args) {
        int[] weights = {2, 3, 4, 5};
        int[] values = {3, 4, 5, 6};
        int capacity = 8;

        double max_value = fractionalKnapsack(weights, values, capacity);
        System.out.println("最大总价值: " + max_value);
    }
}

这段代码使用动态规划算法解决部分背包问题。weights 数组存储物品的重量,values 数组存储物品的价值,capacity 表示背包的容量。

通过创建一个二维数组 dp,其中 dp[i][j] 表示在考虑前 i 个物品,并且背包容量为 j 时的最大总价值。

代码中,使用两层循环遍历所有物品和背包容量的组合。对于每个物品,分为两种情况进行处理:

如果当前物品的重量小于等于背包容量,可以选择完整装入该物品。此时,最大总价值等于不装入该物品时的最大总价值 dp[i-1][j] 和装入该物品后的总价值 dp[i-1][j-weight] + value 中的较大值。
如果当前物品的重量大于背包容量,无法完整装入该物品。此时,最大总价值与上一个物品时的价值相同,即 dp[i][j] = dp[i-1][j]。
最后,返回二维数组 dp 中最后一个元素的值,即表示在考虑所有物品时,背包可以装入的最大总价值。

在上述示例中,测试样例中的物品数组包含四个物品,每个物品的重量和价值分别为 2、3、4、5 和 3、4、5、6,背包容量为 8。输出结果为最大总价值为 11.0。

完全背包

完全背包问题是一个在给定背包容量的限制下,选择物品放入背包以使得背包的总价值最大化的问题。与 0-1 背包问题和部分背包问题不同的是,完全背包问题允许将物品无限次地装入背包中。

在完全背包问题中,每个物品都有一个重量和一个价值。与部分背包问题类似,目标是选择一些物品装入背包,使得被选中物品的总重量不超过背包的容量,同时使得它们的总价值最大化。

为了解决这个问题,我们可以使用动态规划算法。通过创建一个二维数组 dp,其中 dp[i][j] 表示在考虑前 i 个物品,并且背包容量为 j 时的最大总价值。

与部分背包问题不同的是,在完全背包问题中,对于每个物品,我们可以选择装入 0 个、1 个、2 个,… 直到 j/weight[i] 个(j/weight[i] 向下取整)个物品。

因此,对于每个物品 i,我们可以使用以下递推关系来计算 dp[i][j]:

dp[i][j] = max(dp[i-1][j-k*weight[i]] + k*value[i]),其中 0 <= k <= j/weight[i]

遍历所有物品和背包容量的组合,通过上述递推关系更新 dp 数组中的值。

最后,返回二维数组 dp 中最后一个元素的值,即表示在考虑所有物品时,背包可以装入的最大总价值。

需要注意的是,完全背包问题的动态规划解法时间复杂度较高,并且可能需要大量的内存空间。为了提高效率,我们可以使用一维数组进行优化,在遍历物品时从小到大的顺序更新 dp 数组。

代码示例

public class CompleteKnapsack {
    public static int completeKnapsack(int[] weights, int[] values, int capacity) {
        int n = weights.length;
        int[] dp = new int[capacity + 1];

        for (int i = 0; i < n; i++) {
            int weight = weights[i];
            int value = values[i];
            for (int j = weight; j <= capacity; j++) {
                dp[j] = Math.max(dp[j], dp[j - weight] + value);
            }
        }

        return dp[capacity];
    }

    public static void main(String[] args) {
        int[] weights = {2, 3, 4, 5};
        int[] values = {3, 4, 5, 6};
        int capacity = 8;

        int max_value = completeKnapsack(weights, values, capacity);
        System.out.println("最大总价值: " + max_value);
    }
}

这段代码使用动态规划算法解决完全背包问题。weights 数组存储物品的重量,values 数组存储物品的价值,capacity 表示背包的容量。

通过创建一个一维数组 dp,其中 dp[j] 表示在考虑前所有物品,并且背包容量为 j 时的最大总价值。

代码中,首先遍历所有物品,然后遍历所有可能的容量,对于每个容量 j,计算出背包可以装入的最大总价值。

递推公式为:

dp[j] = max(dp[j], dp[j - weight] + value)

其中,weights[i] 表示第 i 个物品的重量,values[i] 表示第 i 个物品的价值。

在更新 dp[j] 的值时,我们将 dp[j-weight]+value 的值与 dp[j] 的值比较,取两者中的最大值作为新的 dp[j] 值。

最后,返回一维数组 dp 中最后一个元素的值,即表示在考虑所有物品时,背包可以装入的最大总价值。

在上述示例中,测试样例中的物品数组包含四个物品,每个物品的重量和价值分别为 2、3、4、5 和 3、4、5、6,背包容量为 8。输出结果为最大总价值为 18。

多重背包

多重背包问题是在给定背包容量的限制下,选择物品放入背包以使得背包的总价值最大化的问题。与完全背包问题类似,多重背包问题允许将某些物品选择多次放入背包中,但是每个物品的选择次数是有限制的。

在多重背包问题中,每个物品都有一个重量、价值和一个数量限制。目标是选择一些物品放入背包,使得被选中物品的总重量不超过背包的容量,同时使得它们的总价值最大化。

为了解决多重背包问题,我们可以使用动态规划算法。通过创建一个二维数组 dp,其中 dp[i][j] 表示在考虑前 i 个物品,并且背包容量为 j 时的最大总价值。

与完全背包问题类似,对于每个物品 i,我们需要考虑选择物品 i 的次数。假设该物品的选择次数上限为 k,则选择次数的范围是 0 到 min(k, j/weight[i])。

因此,对于每个物品 i,我们可以使用以下递推关系来计算 dp[i][j]:

dp[i][j] = max(dp[i-1][j-k*weight[i]] + k*value[i]),其中 0 <= k <= min(k, j/weight[i])

遍历所有物品和背包容量的组合,通过上述递推关系更新 dp 数组中的值。

最后,返回二维数组 dp 中最后一个元素的值,即表示在考虑所有物品时,背包可以装入的最大总价值。

需要注意的是,多重背包问题的动态规划解法时间复杂度较高,并且可能需要大量的内存空间。为了提高效率,我们可以使用一维数组进行优化,在遍历物品时从大到小的顺序更新 dp 数组。

代码示例

public class MultipleKnapsack {
    public static int multipleKnapsack(int[] weights, int[] values, int[] counts, int capacity) {
        int n = weights.length;
        int[] dp = new int[capacity + 1];

        for (int i = 0; i < n; i++) {
            int weight = weights[i];
            int value = values[i];
            int count = counts[i];
            for (int j = capacity; j >= weight; j--) {
                for (int k = 1; k <= count && k * weight <= j; k++) {
                    dp[j] = Math.max(dp[j], dp[j - k * weight] + k * value);
                }
            }
        }

        return dp[capacity];
    }

    public static void main(String[] args) {
        int[] weights = {2, 3, 4};
        int[] values = {3, 4, 5};
        int[] counts = {2, 3, 1};
        int capacity = 8;

        int max_value = multipleKnapsack(weights, values, counts, capacity);
        System.out.println("最大总价值: " + max_value);
    }
}

这段代码使用动态规划算法解决多重背包问题。weights 数组存储物品的重量,values 数组存储物品的价值,counts 数组存储每个物品的数量限制,capacity 表示背包的容量。

通过创建一个一维数组 dp,其中 dp[j] 表示在考虑前所有物品,并且背包容量为 j 时的最大总价值。

代码中,首先遍历所有物品,然后通过两层循环遍历背包容量和物品的选择次数。对于每个物品,计算出选择不同次数情况下的最大总价值。

递推公式为:

dp[j] = max(dp[j], dp[j-k*weight]+k*value),其中 1 <= k <= min(count, j/weight)

在更新 dp[j] 的值时,我们将 dp[j-kweight]+kvalue 的值与 dp[j] 的值比较,取两者中的最大值作为新的 dp[j] 值。

最后,返回一维数组 dp 中最后一个元素的值,即表示在考虑所有物品时,背包可以装入的最大总价值。

在上述示例中,测试样例中的物品数组包含三个物品,每个物品的重量和价值分别为 2、3、4 和 3、4、5,数量限制分别为 2、3、1,背包容量为 8。输出结果为最大总价值为 20。

总结提升

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谷艳爽faye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值