dp问题

本文深入探讨动态规划,特别是背包问题(包括完全背包和0-1背包)以及线性DP的应用,例如最长上升子序列和股票交易问题。通过实例详细解释了dp数组的更新规则和优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

动态规划: 最优(最大或最小); 分割成子问题(自下而上,整体最优依赖于子最优);小问题还有相互重叠的更小的子问题;每一步都可能面临多种选择。

一、背包dp 每一步有多种选择,选或不选,组合最优

for(int i = 1; i <= amount; i++) {

for(int j = 0; j < coins.length; j++) {

if(coins[j] <= i) {

dp[i] = Math.min(dp[i - coins[j]] + 1, dp[i]);

}

}

}

1.完全背包

1.零钱兑换:
零钱: [1, 2, 3, 6, 8]
amount = 10;
组合成amount的零钱个数最小 零钱可重复

四要素:
dp[i] 代表到amount=i为止,组成i的最少硬币数
dp[i] = Math.min(dp[i - coins[j]] + 1, dp[i]);
自底向上:dp[0] = 0 (不知道零钱数额,当金额为0时,硬币数为0)
初始,方便比较:dp[i] = amount+1,amount+1为了在满足不了刚刚好组成的amount的情况下,输出-1

class Solution {
    public int coinChange(int[] coins, int amount) {

        // dp[i] 到面额i为止,组成i的零钱数
        // dp[i] = min(dp[i - Cj] + 1, dp[i])  自下而上 
        // dp[i] = amount + 1  初始(为解决2 3 -1),
        // 底: dp[0] = 0;

        int[] dp = new int[amount+1];
        Arrays.fill(dp, amount+1);
        dp[0] = 0;

        for(int i = 1; i <= amount; i++) {
            for(int j = 0; j < coins.length; j++) {
                if(coins[j] <= i) {
                    dp[i] = Math.min(dp[i - coins[j]] + 1, dp[i]);
                }
            }
        }

        return dp[amount] > amount ? -1 : dp[amount];
    }
}

特殊:如果组合小于,即满足不了刚刚组合amount的情况,
用以上解决, Arrays.fill(dp, amount+1); dp[0] = 0; return dp[amount] > amount ? -1 : dp[amount];

2.完全平方数 dp[i] = Math.min(dp[i - sqaure_nums[j]] + 1, dp[i]);
完全平方数:[1 4 9 16 25] 自己生成的
n:100
组合成数n的完全平方数的个数最小 完全平方数可重复

四要素:
dp[i] 代表到n=i为止,组成i的最少完全平方数
dp[i] = dp[i] = Math.min(dp[i - sqaure_nums[j]] + 1, dp[i]);
自底向上:dp[0] = 0 dp[1] = 1
初始,方便比较:dp[i] = i; 每一个完全平方数都是1,最大

class Solution {
    public int numSquares(int n) {
        // dp[i] 到数i时,组成的完全平方数个数最小
        // 自己生成完全平方数的数组后,就和零钱问题一样
        // dp[i] 代表数i的组成的完全平方数个数最小
        // dp[i] = i  
        // dp[i] = min(dp[i - sqaure_nums[j] + 1, dp[i])

        int sqaure_nums_len = (int) Math.sqrt(n) + 1;
        int[] sqaure_nums = new int[sqaure_nums_len];
        
        for(int i = 1; i < sqaure_nums_len; i++) {
            sqaure_nums[i] = i * i;
        } 

        int[] dp = new int[n+1];
        for(int i = 0; i <= n; i++) {
            dp[i] = i;
        }

        for(int i = 1; i <= n; i++) {
            for(int j = 1; j < sqaure_nums_len; j++) {
                if(sqaure_nums[j] <= i) {
                    dp[i] = Math.min(dp[i - sqaure_nums[j]] + 1, dp[i]);
                }
            }
        }

        return dp[n];
 
    }
}

注意:
int sqaure_nums_len = (int) Math.sqrt(n) + 1;
if(sqaure_nums[j] <= i)

3.钢管切割 dp[i] = Math.max(dp[i - gangDuan[j]] + prices[j], dp[i]);
gangDuan = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
钢管长度 = length
段的价格prices = [1,5,8,9,10,17,17,20,24,30]

切割完后,得到的钢管加起来的价值最大

四要素:
dp[i] 代表到length=i为止,组成i的最大钢段价格和数
dp[i] = Math.max(dp[i - gangDuan[j]] + prices[j], dp[i]);
自底向上:dp[0] = 0
初始,方便比较:dp[i] = 0; 每一个长度价值为0

    public static int cuttingGangTiao(int length) {
        // dp[i] 代表的是钢管长度为i时,切割所获利润最大
        // dp[i] = Math.max(dp[i - gangDuan[j]] + prices[j], dp[i]);
        // dp[i] = 0 最小
        int[] gangDuan = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};  // 每一步有10种选择(在gangDuan[j] < i 的前提下)
        int[] prices = {1,5,8,9,10,17,17,20,24,30}; 

        int[] dp = new int[length+1];  //

        for (int i = 1; i <= length; i++) {
            for (int j = 0; j < gangDuan.length; j++) {
                if(gangDuan[j] <= i) {
                    dp[i] = Math.max(dp[i - gangDuan[j]] + prices[j], dp[i]);
                }
            }
        }
        
        return dp[length];
    }

4.剪绳子
绳子段长度:[1 2 3 4]
绳子长度:length
减后,绳子各段相乘的乘积最大

四要素:
dp[i] 绳子长度为 i 时,绳子减后,各段绳子长度乘积最大值

dp[i] = Math.max(Math.max(j * dp[i-j], j * (i - j)), dp[i]); // 根据递推等式,dp[i-j]只可能由两段及以上组成,j * dp[i-j] 段数 >= 3, 所以还要考虑一种只有两段组成的情况 j * (i - j),所以是三个比较

自底向上: dp[0] = 0; dp[1] = 1; dp[2] = 1; dp[3] = 2 只有0 1 2 3 只有一种减的情况

初始:dp[i] = 1 所有绳子段长度 = 1 ,乘积最小。
在这里插入图片描述

class Solution {
    public int cuttingRope(int n) {
        // 每一步 可以在1 -- i-1的位置上减
        // dp[i] = Math.max(Math.max(dp[j]*dp[i-j], i*(i-j)), dp[i])
        // dp[i] = 1; // 最小,都切一段
        // if(n == 0) return 0; if(n == 1) return 0; if(n == 2) return 1; if(n == 3) return 2
        // 自底向上:dp[0] = 0; dp[1] = 1; dp[2] = 1; dp[3] = 2

        if(n == 0) return 0;
        if(n == 1) return 0;  
        if(n == 2) return 1;  // 只能分成1和1
        if(n == 3) return 2;  // 只能分成1和2

        int[] dp = new int[n+1];
        Arrays.fill(dp, 1);
        dp[0] = 0;
        dp[1] = 1;
        dp[2] = 1;
        dp[3] = 2;

        for(int i = 4; i <= n; i++) {
            for(int j = 1; j <= i / 2; j++) {
                    dp[i] = Math.max(Math.max(j * dp[i-j], j * (i - j)), dp[i]);
            }
        }

        return dp[n];
    }
}

注意:if(n == 1) return 0; dp[1] = 1; 方便灵活

2. 0-1背包 和完全背包相比,每一步选择过后的选项就不能再选

1.宽背包

宽度背包 0-1 dp
lengths = {1,2,3,4,5,6,7}
prices = {10,9,8,7,6,5,4}
length = 10

四要素
dp[i] 代表货物柜长度为i时,所摆放商品的最大价格
dp[i] = Math.max(dp[i - lengths[j]] + prices[j], dp[i]);
自顶向下:dp[0] = 0 // 长度为0时,利润为0
初始:dp[i] = 0 最小

https://www.zhihu.com/search?type=content&q=Threadocal%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF

    public static int kuanDuBeiBao(int length) {
        // dp[i] 代表货物柜长度为i时,所摆放商品的最大价格
        // dp[i] = Math.max(dp[i - lengths[j]] + prices[j], dp[i]);
        // 自顶向下:dp[0] = 0 // 长度为0时,利润为0
        // 初始:dp[i] = 0 最小

        int[] lengths = {1,2,3,4,5,6,7};
        int[] prices = {10,9,8,7,6,5,4};

        int[] dp = new int[length+1];

        for (int j = 0; j < lengths.length; j++) {
            for (int i = length; i >= 1; i--) {
                if(lengths[j] <= i) {
                    dp[i] = Math.max(dp[i - lengths[j]] + prices[j], dp[i]);
                }
            }
        }

        return dp[length];
    }

不懂:
为什么要for (int i = length; i >= 1; i–) 当length=10, ans = 34
for (int i = 1; i >= length; i++) 当length=10, ans = 100

二、线性dp

dp[i] = dp[i-1] + … 和前一个状态有关

for(i) { for(j) {dp[i] = dp[j] + } } 和前某几个状态有关

单串:最长上升子序列
双串:最长公共子序列
打家劫舍系列:
股票系列:

1.最大子序和 dp[i] = Math.max(dp[i-1], 0) + nums[i];
四要素:
dp[i] 到数组位置i为止,最大序列和
dp[i] = Math.max(dp[i-1], 0) + nums[i];
自底:dp[0] = nums[0], 从 i = 1 开始
初始化: dp[i] = 0 最小
res保留最优解,最优解很有可能在中间

class Solution {
    public int maxSubArray(int[] nums) {

        if(nums == null || nums.length == 0) return 0;

        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        int res = nums[0];

        for(int i = 1; i < nums.length; i++) {
            dp[i] = Math.max(dp[i-1], 0) + nums[i];
            res = Math.max(res, dp[i]);
        }

        return res;
    }
}

注意:
dp[0] = nums[0];
int res = nums[0];

dp[i]只和前一个状态有关,可以用一个currMax表示

class Solution {
    public int maxSubArray(int[] nums) {
    
        if(nums == null || nums.length == 0) return 0;

        int[] dp = new int[nums.length];

        int currMax = nums[0];

        int res = nums[0];

        for(int i = 1; i < nums.length; i++) {
            currMax = Math.max(currMax, 0) + nums[i];
            res = Math.max(res, currMax);
        }

        return res;
    }
}

2.单串:最长上升子序列 dp[i] = Math.max(dp[j] + 1, dp[i]);
四要素:
dp[i] 到数组位置i为止,的最长上升子序列
dp[i] = max(dp[j] + 1, dp[i])
dp[i] = 1 每个元素算一个

class Solution {
    public int lengthOfLIS(int[] nums) {
        if(nums == null || nums.length == 0) return 0;

        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);
        if(nums.length < 2) return nums.length;
        
        int res = 0;
        
        for(int i = 1; i < nums.length; i++) {
            for(int j = 0; j < i; j++) {
                if(nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[j] + 1, dp[i]);
                }
            }
            res = Math.max(res, dp[i]);        
        }
        return res;
    }
}

注意:考虑nums.length = 1的情况,直接返回1, 而不是res = 0;`

3.买卖股票(只能卖一次) dp[i] = Math.max(prices[i] - minPrice, dp[i-1]);

四要素:
dp[i] 代表到第 i 天为止,所获的最大利润
dp[i] = max(dp[i] - minPrice, dp[i-1])
自底向上:dp[0] = 0;
初始化:dp[i] = 0;

额外:
minPrice = nums[0]
res = 0

class Solution {
    public int maxProfit(int[] prices) {

        if(prices == null || prices.length == 0) return 0;

        int[] dp = new int[prices.length];
        int minPrice = prices[0];
        int res = 0;
        for(int i = 1; i < prices.length; i++) {
            if(prices[i] < minPrice) {
                minPrice = prices[i];
            }else if(prices[i] > minPrice) {
                dp[i] = Math.max(prices[i] - minPrice, dp[i-1]);
            }
            res = Math.max(res, dp[i]);
        }

        return res;
    }
    
}
class Solution {
    public int maxProfit(int[] prices) {
        if(prices == null || prices.length == 0) return 0;

        int currMax = 0;
        int minPrice = prices[0];

        for(int i = 1; i < prices.length; i++) {
            if(prices[i] < minPrice) {
                minPrice = prices[i];
            }else if(prices[i] > minPrice) {
                currMax = Math.max(currMax, prices[i] - minPrice);
            }
        }

        return currMax;
    }
    
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值