动态规划(DP)

DP

1. 理论

  1. 每个大问题的子问题都是最优的,所以才可以直接记录下来
  2. 在下次寻找子问题的最优解时,直接使用
    与分治算法不同的是:
  • 适合 dp 请求的问题,经分解得到的子问题往往不是互相独立的
  • 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步求解

2. 最优子结构

原问题的最优解包含子问题的最优解

3. 状态转移方程

  • 写状态转移方程的程序,一般以递推的方式实现
  • 虽然时间复杂度和记忆化搜索一样
  • 而且在循环中会计算一些无意义的节点
  • 但是递推免去了调用时进栈出栈的操作,而且避免了在递归时层数过多时栈溢出的情况
    f(i,j)=max{f(i1,j),f(i1,jw(i))+v(i)}

4. 手办问题

手办名 价格 喜欢程度
兵长 10 24
蕾姆 4 9
小埋 4 9
和泉纱雾 5 10
空条承太郎 3 2

你现在 有 13

4.1 贪心算法

  • 算出每个物品的性价比
  • 优先买性价比高的物品
手办名 价格 喜欢程度 性价比
兵长 10 24 2.4
蕾姆 4 9 2.25
小埋 4 9 2.25
和泉纱雾 5 10 2
空条承太郎 3 2 0.67

最终的喜欢程度为:24 + 2 = 26

4.2 0-1 背包问题

背包问题 说明
0-1 背包 每个物品最多一个
完全背包 ==> 可以转化成 0-1 背包 每种物品无限件
手办是一个整体
  • 你不能只买🐻和大腿,按照比例给钱,所以不能用贪心来解决
普通递归实现
# f(i, j):当还剩 j 元钱时,前 i 个物品能达到的最大喜欢值

# max【不买这个商品的价值,买这个商品的价值】

f(i,j)=max{f(i1,j),f(i1,jw(i))+v(i)}

public class jianchi {
    static int[]  price = new int[]{10, 4, 4, 5, 3};
    static int[] value = new int[]{24, 9, 9, 10, 2};
    public static void main(String[] args) {
        System.out.println(f(5, 13));
    }

    public static int f(int i, int j){    // 当还剩 j 元钱时,前 i 个物品能达到的最大喜欢值
        //  baseCase
        if (i == 0){    //  当没有物品,价值为 0
            return 0;
        }
        if (j < price[i - 1]){  //  买不起当前物品,就无需比较
            return f(i - 1, j);
        }
        //  不买第 n 个物品
        //  买第 n 个物品,总钱数就会减少,接下来就用 j - price[i - 1]钱考虑剩下的 i - 1个物品喽
        return Math.max(f(i - 1, j), f(i - 1, j - price[i - 1]) + value[i - 1]);	//	下标从 0 开始
    }
}
递推(记忆化数组)

那么,这个 0/1 背包问题是否满足 最优子结构 呢???

能不能用 记忆化搜索 来优化这一算法???

已知条件
n 个物品 T 元钱
Wi 价格 Vi 喜欢值
Xiϵ{0,1} 是否买

这样问题可以转化为:

maxi=1nViXi(i=1nWiXiT)

证明:上述公式是具有 最优子结构 的。

  • (y1,y2yn)maxi=1nViXi 最优解
  • 那么 (y2,y3yn) 是子问题 maxi=2nViXi(i=2nWiXiTW1X1)
  • 使用 反证法
    • 假设子问题存在 (Z2,Z3Zn) 是最优解,那么 (y2,y3yn) 就不是它的最优解
    • 则:i=2nViyii=2nViZi
    • 两边同时加上 V1y1
    • i=2nViyi+V1y1i=2nViZi+V1y1 ===> 得出 (y1,y2yn) 并不是大问题的最优解,最优解是 (y1,Z2Zn)
    • 就与我们最开始的设定相矛盾了
  • 证明这个问题是有最优子结构的

使用记忆化搜索优化递推:

public class jianchi {
    static int[]  price = new int[]{10, 4, 4, 5, 3};
    static int[] value = new int[]{24, 9, 9, 10, 2};
    //  记忆数组 dp,第一个角标表示物品,第二个角标表示我们有多少钱
    //  防止数组越界,在开数据空间时,增加一个空间
    static int[][] dp = new int[6][14];
    public static void main(String[] args) {
        System.out.println(f(5, 13));
    }

    public static int f(int i, int j){    // 当还剩 j 元钱时,前 i 个物品能达到的最大喜欢值
        //  baseCase
        if (i == 0){    //  当没有物品,价值为 0
            return 0;
        }
        if (dp[i][j] != 0){
            return dp[i][j];
        }
        if (j < price[i - 1]){  //  买不起当前物品,就无需比较
            return dp[i][j] = f(i - 1, j);
        }
        //  不买第 n 个物品
        //  买第 n 个物品,总钱数就会减少,接下来就用 j - price[i - 1]钱考虑剩下的 i - 1个物品喽
        return dp[i][j] = Math.max(f(i - 1, j), f(i - 1, j - price[i - 1]) + value[i - 1]);
    }  
}

记忆化搜索,时间复杂度:O(nt)

  • n:物品个数
  • t:拥有的钱

下面用递推实现:

从已知 ---> 未知

  • 即:物品数从1个开始计算
  • 同时利用了数组未赋值时,初始值是0【我有 13 块钱,但是数组下标是从 0 开始的哦,所以大小要开成 14】
package com.alq.dp;

public class Bag {
    static int[]  price = new int[]{10, 4, 4, 5, 3};
    static int[] value = new int[]{24, 9, 9, 10, 2};
    //  记忆数组 dp,第一个角标表示物品,第二个角标表示我们有多少钱
    //  防止数组越界,在开数据空间时,增加一个空间
    static int[][] dp = new int[6][14];
    public static void main(String[] args) {
        System.out.println(f(5, 13));
    }

    public static int f(int i, int j){    // 当还剩 j 元钱时,前 i 个物品能达到的最大喜欢值
        //  递推实现
        //  遍历记忆数组
        for (int k = 1; k <= i ; k++) {    //  物品个数
            for (int l = j; l >= 0 ; l--) {   //  拥有的钱
                if ( l < price[k - 1]){
                    dp[k][l] = dp[k - 1][l];    //  买不起当前物品 【数组,未赋值时,默认值是 0】
                }else {
                    //	状态转移方程
                    dp[k][l] = Math.max(dp[k - 1][l], dp[k - 1][l - price[k - 1]] + value[k - 1]);
                }
            }
        }
        return dp[i][j];
    }
}

我们发现:当状态转移时

  • 当前状态只与上个状态有关
  • 只在 i - 1 或 i + 1 这一行中取需要的值
  • 再前面状态的值便没有用了

定义一个数组,存放放入物品信息

手办名 价格 喜欢程度
兵长 10 24
蕾姆 4 9
小埋 4 9
和泉纱雾 5 10
空条承太郎 3 2

image-20230425103347747

然后逐步向上查询:

image-20230425104234188

int i = path.length - 1;
int j = path[0].length - 1;
while (i > 0 && j > 0){
    if (path[i][j] == 1){
        System.out.println("第 " + i +  " " + j + "个被选中");
        j -= price[i - 1];
    }
    i--;
}
package com.alq.dp;

public class Bag {
    static int[]  price = new int[]{10, 4, 4, 5, 3};
    static int[] value = new int[]{24, 9, 9, 10, 2};
    //  记忆数组 dp,第一个角标表示物品,第二个角标表示我们有多少钱
    //  防止数组越界,在开数据空间时,增加一个空间
    static int[][] dp = new int[6][14];

    static int[][] path = new int[6][14];


    public static void main(String[] args) {
        System.out.println(f(5, 13));
        for (int[] ints : path) {
            for (int anInt : ints) {
                System.out.print(anInt + "\t");
            }
            System.out.println();
        }

        System.out.println("==================");


        for (int[] ints : dp) {
            for (int anInt : ints) {
                System.out.print(anInt+"\t");
            }
            System.out.println();
        }
        //  输出我们最后放入的是那些物品
        int i = path.length - 1;
        int j = path[0].length - 1;
        while (i > 0 && j > 0){
            if (path[i][j] == 1){
                System.out.println("第 " + i +  " " + j + "个被选中");
                j -= price[i - 1];
            }
            i--;
        }
    }

    public static int f(int i, int j){    // 当还剩 j 元钱时,前 i 个物品能达到的最大喜欢值
        //  递推实现
        //  遍历记忆数组
        for (int k = 1; k <= i ; k++) {    //  物品个数
            for (int l = j; l >= 0 ; l--) {   //  拥有的钱
                if ( l < price[k - 1]){
                    dp[k][l] = dp[k - 1][l];    //  买不起当前物品 【数组,未赋值时,默认值是 0】
                }else {
                    //	状态转移方程
                    // dp[k][l] = Math.max(dp[k - 1][l], dp[k - 1][l - price[k - 1]] + value[k - 1]);
                    if (dp[k - 1][l] < dp[k - 1][l - price[k - 1]] + value[k - 1]){ //  买
                        dp[k][l] = dp[k - 1][l - price[k - 1]] + value[k - 1];
                        //  将当前情况记录到 path
                        path[k][l] = 1;
                    }else {
                        dp[k][l] = dp[k - 1][l];
                    }
                }
            }
        }
        return dp[i][j];
    }
}
滚动数组

于是空间复杂度可以优化为:

O(nt)=O(t)

posted @   爱新觉罗LQ  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示