动态规划-状态压缩技巧总结(完全背包)

前文:状态压缩技巧总结(01背包)


前言

能够使用状态压缩技巧的动态规划都是二维dp问题,看它的状态转移方程,如果计算状态dp[i][j]需要的都是dp[i][j]相邻的状态,那么就可以使用状态压缩技巧,将二维的dp数组转化成一维,将空间复杂度从 O(N^2) 降低到 O(N)。

值得一提的是,状态压缩虽然能节省空间,但是会使代码的可读性变差,初次接触优化后的代码时,难免满头问号,我们首先要掌握好普通的动态规划问题的迭代解法,以此为基础,才能更好地理解压缩的方法和原理。


完全背包状态压缩

力扣518.零钱兑换Ⅱ

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。假设每一种面额的硬币有无限个。

输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

定义:dp[i][j] 表示从下标为[0-i]的物品里任意取,凑出金额j的方式数

class Solution {
    
    
    public int change(int amount, int[] coins) {
    
    
        int n=coins.length;
        int dp[][]=new int[n+1][amount+1];
        for(int i=0;i<=n;i++){
    
    
            dp[i][0]=1;
        }
        for(int i=1;i<=n;i++){
    
    
            for(int j=1;j<=amount;j++){
    
    
                dp[i][j]=dp[i-1][j]+(j>=coins[i-1]?dp[i][j-coins[i-1]]:0);
            }
        }
        return dp[n][amount];
    }
}

二维转一维

其中核心的递推式是:

dp[i][j]=dp[i-1][j]+dp[i][j-coins[i-1]];

按照前文状态压缩技巧总结(01背包)的状态压缩步骤,首先将直接二维变到一维;

class Solution {
    
    
    public int change(int amount, int[] coins) {
    
     
        int n=coins.length;
        int[] dp=new int[amount+1];
        dp[0] = 1;
        for(int i=1;i<=n;i++){
    
    
            for(int j=1;j<=amount;j++){
    
    
                if (j < coins[i-1]) continue; 
                dp[j]=dp[j]+dp[j-coins[i-1]];
            }
        }
        return dp[amount];
    }
}

分析数据覆盖问题

然后推测一下到底要不要改变遍历的顺序,这里是按照j由小增大的顺序遍历的,那这样是否会覆盖数据呢
请添加图片描述
如上图所示:
1.dp[i][j]的推导会用到第i行的0到j-1列的值,以及第i-1行第j列的值。
2.dp[0…j-1]存储的是第i行的数据dp[i][0…j-1],而最后一列存储的是dp[i-1][j]的值,所以我们能够用一个一维数组存储需要用到的所有数据。
3.j由小增大不会覆盖数据:
例如:求dp[2]会用到第i-1行dp[i-1][2],假设可能用到第i行dp[i][0]、dp[i][1]

dp[i][2]=dp[i-1][2]+dp[i][0]+dp[i][1];

将新求得的dp[2](即dp[i][2])填入压缩后的一维数组时会覆盖原来的dp[i-1][2]的值,而导致后面计算dp[3]、dp[4]等dp值时,若再要用到dp[2],拿到的是新求得的dp[i][2]值,符合完全背包的定义,即第i个物品可以重复选择,所以不会覆盖数据。

力扣322.零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。每种硬币的数量是无限的(暗示完全背包)。

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

定义:dp[i][j]表示只选择前i种硬币,凑出金额j所需的最少硬币数

class Solution {
    
    
    public int coinChange(int[] coins, int amount) {
    
    
        int[][] dp=new int[coins.length+1][amount+1];
        for(int i=1;i<=amount;i++){
    
    
            dp[0][i]=amount+1;
        }//一种都不选凑不出i>0的数,初始化为不可能达到的结果
        for(int i=1;i<=coins.length;i++){
    
    
            for(int j=1;j<=amount;j++){
    
    
                if(j>=coins[i-1]){
    
    
                    dp[i][j]=Math.min(dp[i-1][j],dp[i][j-coins[i-1]]+1);
                }else{
    
    
                    dp[i][j]=dp[i-1][j];
                }                
            }
        }
        return dp[coins.length][amount]>amount?-1:dp[coins.length][amount];
    }
}

二维转一维

状态压缩的代码同理

class Solution {
    
    
    public int coinChange(int[] coins, int amount) {
    
    
        int[]dp=new int[amount+1];
        for(int i=1;i<=amount;i++){
    
    
            dp[i]=amount+1;
        }//一种都不选凑不出i>0的数,初始化为不可能达到的结果
        for(int i=1;i<=coins.length;i++){
    
    
            for(int j=1;j<=amount;j++){
    
    
                if(j<coins[i-1])continue;
                dp[j]=Math.min(dp[j],dp[j-coins[i-1]]+1);                
            }
        }
        return dp[amount]>amount?-1:dp[amount];
    }
}

猜你喜欢

转载自blog.csdn.net/qq_41777702/article/details/125287151
今日推荐