Bootstrap

数据结构与算法-09贪心算法&动态规划

贪心算法&动态规划

1 贪心算法介绍

贪心算法(Greedy Algorithm)是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。贪心算法通常用于解决优化问题,如最小化成本、最大化收益等。然而,贪心算法并不总是能够得到全局最优解,但它具有直观、高效、易于实现等优点,因此在许多实际问题中得到了广泛应用。

基本思想

  • 贪心算法总是从问题的某一个初始解出发。
  • 在每一步决策中,它总是做出在当前状态下最好或最优的选择,即局部最优解。
  • 通过每一步的局部最优选择,希望能够导致全局最优解。

贪心算法实现步骤

  • 建立数学模型来描述问题。
  • 把求解的问题分成若干个子问题。
  • 对每一子问题求解,得到子问题的局部最优解。
  • 把子问题的局部最优解合成原来问题的一个解。
  • 示例:找零问题
    • 问题描述:
      • 假设一个自动售货机只能接受1元、5元和10元的纸币或硬币,现在顾客给了售货机N元,售货机需要尽可能多地使用10元、5元和1元的纸币或硬币来找零给顾客。
    • 建立数学模型:
      • 设顾客给出的金额为 N 元,10元、5元和1元的纸币或硬币数量分别为 x, y, z。
      • 我们需要求解的是: x * 10 + y * 5 + z * 1 = N
      • x, y, z 均为非负整数,并且希望 x 尽可能大(因为10元的面值最大),然后在 x 确定的情况下 y 尽可能大,最后 z 用来补足剩余的金额。
    • 把求解的问题分成若干个子问题
      • 尽可能多地使用10元纸币,即 x = N // 10(整除运算)。
      • 接下来,从剩余的金额(N % 10)中尽可能多地使用5元纸币,即 y = (N % 10) // 5。
      • 最后,使用1元纸币来补足剩余的金额,即 z = N % 10 - y * 5。
    • 把子问题的局部最优解合成原来问题的一个解
      • 将 x, y, z 的值组合起来,就得到了找零的纸币或硬币组合。

贪心算法的适用范围

  • 贪心算法适用于那些具有贪心选择性质和最优子结构性质的问题。
  • 贪心选择性质:所求问题的整体最优解可以通过一系列局部最优的选择来达到。
  • 最优子结构性质:问题的最优解所包含的子问题的解也是最优的。

局限性

  • 贪心算法并不总是能够得到全局最优解。对于某些问题,贪心算法可能会陷入局部最优解而无法达到全局最优。
  • 贪心算法的正确性需要证明。对于每一个贪心选择,都需要证明其能够导致全局最优解。

优化

  • 对于一些贪心算法无法直接求解的问题,可以通过一些优化策略或与其他算法结合来求解。
  • 例如,在求解整数背包问题时,可以使用动态规划算法来求解全局最优解,或者将贪心算法与回溯算法结合使用来求解近似最优解。

2 贪心算法例题

2.1 会议问题

题目

某天早上公司领导找你解决一个问题,明天公司有N个同等级的会议需要使用同一个会议室,现在给你这个N个会议的开始和结束时间,你怎么样安排才能使会议室最大利用?即安排最多场次的会议?

示例

会议1:0点~9点:9点之前开始的会议都不行了。

会议2:8点~10点

会议3:10点~12点:12点

会议4:8点~20点

结果:最多可以安排两场会议【会议1、会议3】

import java.util.ArrayList;
import java.util.List;

/**
 * 题目:某天早上公司领导找你解决一个问题,明天公司有N个同等级的会议需要使用同一个会议室,现在给你这个N个会议的开始和结束时间,你怎么样安排才能使会议室最大利用?即安排最多场次的会议?
 *
 * eg:
 * 会议1:0点~9点:9点之前开始的会议都不行了。
 * 会议2:8点~10点
 * 会议3:10点~12点:12点
 * 会议4:8点~20点
 * 结果:最多可以安排两场会议【会议1、会议3】
 *
 * 思路:
 * 1、将会议根据结束时间进行排序
 * 2、遍历会议,如果当前会议开始时间大于等于前一个会议的结束时间,则说明可以安排会议,否则不能安排会议
 */
public class MeetingTest {

    public static void main(String[] args) {
        List<Meeting> meetings = initInfo(5);
        meetings.sort(null);
        int currentTime = 0;
        for (Meeting meeting : meetings) {
            if (meeting.getStart() >= currentTime){
                System.out.println(meeting);
                currentTime = meeting.getEnd();
            }
        }
    }

    public static List<Meeting> initInfo(int size){
        List<Meeting> list = new ArrayList<>();
        Meeting meeting1 = new Meeting(0, 9);
        list.add(meeting1);
        Meeting meeting2 = new Meeting(8, 10);
        list.add(meeting2);
        Meeting meeting3 = new Meeting(10, 12);
        list.add(meeting3);
        Meeting meeting4 = new Meeting(8, 20);
        list.add(meeting4);
        return list;
    }
}
class Meeting implements Comparable<Meeting>{
    private int start;
    private int end;

    public Meeting(int start, int end) {
        this.start = start;
        this.end = end;
    }

    public int getStart() {
        return start;
    }

    public int getEnd() {
        return end;
    }

    @Override
    public int compareTo(Meeting o) {
        return this.end == o.end ? 0 : this.end < o.end ? -1 : 1;
    }

    @Override
    public String toString() {
        return "Meeting{" +
                "start=" + start +
                ", end=" + end +
                '}';
    }
}

3 动态规划介绍

动态规划(Dynamic Programming,简称DP)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式来求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。

基本思想

  1. 重叠子问题:在求解过程中,动态规划算法会将每个子问题的解存储起来,当再次需要求解此子问题时,直接返回之前存储的结果,而不是重新计算。这是动态规划算法能够显著提高效率的关键。
  2. 最优子结构:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构性质。动态规划利用这一性质,通过求解子问题的最优解来构造原问题的最优解。

动态规划实现步骤

  1. 划分阶段:按照问题的时间或空间特征,将问题划分为若干个阶段。每个阶段对应一个决策过程,决策过程的选择不是任意的,它依赖于当前的状态,又影响以后的发展。
  2. 定义状态:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。状态的选择要满足无后效性,即“将来与过去无关”,只与当前的状态有关。
  3. 状态转移方程:根据上一阶段状态和决策来导出本阶段的状态的转移方程,即状态之间的递推关系。
  4. 求解:按照阶段从前往后(或自底向上)逐步计算各个状态及对应的最优值(或最优策略),直至得到初始状态的最优值(或最优策略)为止。

示例说明

  • 题目:背包问题

    • 是一个经典的组合优化问题,其描述如下:给定一组物品,每种物品都有自己的重量和价值。在限定的总重量(背包容量)内,如何选择物品放入背包,使得背包内物品的总价值最大,同时保证每种物品只能选择放入或不放入背包中一次,即不能选择物品的一部分。
  • 问题描述

    • 物品:有n种物品,每种物品的重量为w[i],价值为v[i],其中i为物品的编号,从1到n。
    • 背包容量:背包的总容量为W。
    • 目标:找出一种方案,使得在不超过背包容量W的前提下,放入背包的物品总价值最大。
  • 解决方法

    • 动态规划

      • 动态规划是解决0-1背包问题的常用方法。其关键在于将问题分解为更小的子问题,并保存这些子问题的解,以避免重复计算。
    • 定义状态:设f[i] [j]表示前i个物品中选取若干件放入容量为j的背包所能获得的最大价值。

    • 状态转移方程

      • 如果不放入第i个物品,则f[i][j] = f[i-1][j]。
      • 如果放入第i个物品,则f[i][j] = f[i-1][j-w[i]] + v[i](前提是j >= w[i])。

      综合两种情况,可以得到状态转移方程:

      • f*[i][j]=max{f[i−1][j],if j<w[i];f[i−1][jw[i]]+v[i],otherwise}
    • 初始化:f[0] [j] = 0(j从0到W),表示没有物品可选时,背包的价值为0。

    • 求解:从f[1] [0]到f[n] [W]逐步计算,最终f[n] [W]即为所求的最大价值。

优点

  • 能够显著提高求解效率,特别是对于具有重叠子问题和最优子结构性质的问题。
  • 思路清晰,易于理解和实现。

局限性

  • 对于没有重叠子问题和最优子结构性质的问题,动态规划可能并不适用。
  • 需要存储中间结果,可能导致空间复杂度较高。
  • 对于一些复杂问题,状态的定义和状态转移方程的设计可能比较困难。

4 动态规划例题

4.1 背包问题

题目

经典问题:背包问题
小偷去某商店盗窃,背有一个背包,容量是50kg,现在有以下物品(物品不能切分,且只有一个),请问小偷应该怎么拿才能得到最大的价值?

示例

物品1 10kg 60元

物品2 20kg 100元

物品3 30kg 120元

结果:

30+20(kg)=120+100=220

解题思路

状态定义:

物品\背包空间(kg) 10 20 30 40 50

物品1 60 60 60 60 60

物品2 60 100 160 160 160

物品3 60 100 160 180 220

得到状态转移公式:res = MAX(WEIGHT(N) + res(N-1, W-WEIGHT(N)), res(N-1,W)) = MAX(WEIGHT(3) + res(3-1, 5-3), res(3-1,3)) = MAX(120 + 100), 160) = 220

/**
 * 经典问题:背包问题
 * 小偷去某商店盗窃,背有一个背包,容量是50kg,现在有以下物品(物品不能切分,且只有一个),请问小偷应该怎么拿才能得到最大的价值?
 *
 * 示例
 * 物品1   10kg    60元
 * 物品2   20kg    100元
 * 物品3   30kg    120元
 *
 * 结果:
 * 40+10(kg)=120+60=180
 *
 * 解题思路:
 *  物品\背包空间(kg)  10     20    30    40    50
 *  物品1              60     60    60    60    60
 *  物品2              60    100   160   160   160
 *  物品3              60    100   160   180   220
 *
 *  得到解题公式:res = MAX(WEIGHT(N) + res(N-1, W-WEIGHT(N)),  res(N-1,W)) = MAX(WEIGHT(3) + res(3-1, 5-3),  res(3-1,3)) = MAX(120 + 100),  160) = 220
 */
public class KnapsackProblem {
    public static void main(String[] args) {
        int[] weight = {10, 30, 20}; // 物品重量
        int[] value = {60, 120, 100}; // 物品价值
        int size = 50; // 背包容量
        int[][] res = new int[weight.length + 1][(size / 10) + 1]; // 创建二维数组,0行0列用来存放初始值
        String[][] resStr = new String[weight.length + 1][(size / 10) + 1]; // 用于存放路径
        for (int i = 1; i <= weight.length; i++) {
            for (int j = 1; j <= size / 10; j++) {
                if (j < weight[i - 1] / 10){
                    res[i][j] = res[i - 1][j];
                    resStr[i][j] = resStr[i - 1][j];
                }else{
                    res[i][j] = Math.max(value[i - 1] + res[i - 1][j - (weight[i - 1] / 10)], res[i - 1][j]);
                    resStr[i][j] = res[i - 1][j - (weight[i - 1] / 10)] + value[i - 1] > res[i - 1][j] ? resStr[i - 1][j - (weight[i - 1] / 10)] + " + " + i : resStr[i - 1][j];
                }
            }
        }
        // 打印各个阶段的结果
        for (int i = 0; i < res.length; i++) {
            for (int j = 0; j < res[i].length; j++) {
                System.out.print(res[i][j] + " ");
            }
            System.out.println();
        }
        // 打印各个阶段的路径
        for (int i = 0; i < resStr.length; i++) {
            for (int j = 0; j < resStr[i].length; j++) {
                System.out.print(resStr[i][j] + " | ");
            }
            System.out.println();
        }
        System.out.println("背包可以放的最大价值:"+res[weight.length ][size / 10]);
    }
}

4.2 购物车问题

题目

中了一个奖可以清空购物车5000元的东西(不能找零),每个东西只能买一件,那么她应该如何选择物品使之中奖的额度能最大利用呢?

示例

物品1 2000元

物品2 3000元

物品3 4000元

结果:物品1 + 物品2 = 5000元

解题思路

使用动态规划,动态规划的思路是:

values = {2000, 3000, 4000}

物品\价值 1000 2000 3000 4000 5000

1 0 2000 2000 2000 2000

2 0 2000 3000 3000 5000

3 0 2000 3000 4000 5000

得到公式:res[i][j] = max(values[i] + res[i] [V - i], res[i-1] [j])

/**
 * 题目:中了一个奖可以清空购物车5000元的东西(不能找零),每个东西只能买一件,那么她应该如何选择物品使之中奖的额度能最大利用呢?
 *
 * 示例:
 * 物品1  2000元
 * 物品2  3000元
 * 物品3  4000元
 *
 * 结果:物品1 + 物品2 = 5000元
 *
 * 思路:
 * 使用动态规划,动态规划的思路是:
 * values = {2000, 3000, 4000}
 * 物品\价值  1000     2000    3000    4000    5000
 * 1          0      2000     2000    2000    2000
 * 2          0      2000     3000    3000    5000
 * 3          0      2000     3000    4000    5000
 *
 * 得到公式:res[i][j] = max(values[i] + res[i][V - i], res[i-1][j])
 */
public class ShoppingCartProblem {

    public static void main(String[] args) {
        int v = 5000; // 优惠卷的价值
        int[] values = {2000, 3000, 4000}; // 物品的价值
        int[][] res = new int[values.length + 1][(v / 1000) + 1]; // 记录每一步获取到的最大价值结果
        String[][] resStr = new String[values.length + 1][(v / 1000) + 1]; // 记录每一步对应的路径
        for (int i = 0; i < values.length; i++) {
            for (int j = 1; j <= (v / 1000) ; j++) { // 遍历每一个价值
                if (j*1000 < values[i]){
                    res[i + 1][j] = res[i][j];
                    resStr[i + 1][j] = resStr[i][j];
                }else{
                    res[i + 1][j] = Math.max(values[i] + res[i][j - (values[i] / 1000)], res[i][j]);
                    if (res[i + 1][j] == values[i] + res[i][j - (values[i] / 1000)]){
                        resStr[i + 1][j] = resStr[i][j - (values[i] / 1000)] + " + " + values[i];
                    }else{
                        resStr[i + 1][j] = resStr[i][j];
                    }
                }
            }
        }
        // 打印每一个阶段的结果
        System.out.println("每一步的背包价值:");
        for (int i = 0; i < res.length; i++) {
            for (int j = 0; j < res[0].length; j++) {
                System.out.print(res[i][j] + " | ");
            }
            System.out.println();
        }
        // 打印每一个阶段的路径
        System.out.println("每一步的背包路径:");
        for (int i = 0; i < resStr.length - 1; i++) {
            for (int j = 0; j < resStr[0].length; j++) {
                System.out.print(resStr[i][j] + " | ");
            }
            System.out.println();
        }
        // 得到的最大价值
        System.out.println("背包可以放的最大价值:"+res[values.length ][v / 1000]);
    }
}

;