改变一组数的正负符号使其和为给定值

前序

在做这题之前,有必要先来看看最简单的0-1背包问题,因为这两道题目有很深的关联,有相似但也有差异。本文先介绍动态规划求解0-1背包,再详述动态规划求解数组变号问题,并说明两者不同点,最后给出另一种思路——递归求解该问题的方法。

0-1背包问题

问题描述:给定N件物品,它们的重量用数组weights[0…N-1]表示,价值用values[0…N-1]表示,现在给定背包所能承受的重量为W,求背包可装下物品的最大价值是多少?

解题思路:这是一道最经典的二维动态规划问题(经过优化也可以降维)。动态规划的题目都需要至少创建一个一维数组来保存中间解,因为这类题目每求一个值需要借助前面步骤的结果,其次也是最为重要的是,每道动态规划的题目都需要一个状态转移方程,找出这个方程是解题的关键。

状态转移式:对于这道0-1背包问题,为了方便理解,先不考虑优化问题。创建一个二维数组dp[0…N][0…W],给定i,j,0<i<N+1,0<j<W+1,对于dp[i][j]表示,背包在重量不超过j的情况下装下前i件物品的最大价值,dp[N][W]表示在重量不超过W的情况下装下前N件物品的最大价值,即为所求。假设,第i件物品的价值为v,重量为w,那么对于第i件物品:

  • 装得下的情况:dp[i][j] = max{ dp[i-1][j] , dp[i-1][j-w] + v };即当背包当前承载力为j,且j>=w(表示背包能装下重量为w的物品)时,可以考虑装第i件物品或者不装,这取决于装第i件物品时的价值:dp[i-1][j-w]+v 和 不装第i件物品的价值:dp[i-1][j]谁更大;
  • 装不下的情况:dp[i][j] = dp[i-1][j],即当背包当前承载力为j,且j<w(表示背包不能装下当前物品)时,对于前i件物品不超过重量j的最大价值dp[i][j]取值为前i-1件物品不超过重量j下的最大价值,即dp[i-1][j];

由以上,可以得出0-1背包的状态转移式为:

dp[i][j] = max{ dp[i-1][j] , dp[i-1][j-w] + v },当j>=w;(或j-w>=0
dp[i][j] = dp[i-1][j],当j<w;

0-1背包代码如下:

// W 为背包总体积
// N 为物品数量
// weights 数组存储 N 个物品的重量
// values 数组存储 N 个物品的价值

public int knapsack(int W, int N, int[] weights, int[] values) {
    int[][] dp = new int[N + 1][W + 1];
    for (int i = 1; i <= N; i++) {
        int w = weights[i - 1], v = values[i - 1];        //循环遍历N个物品
        for (int j = 1; j <= W; j++) {
            if (j >= w) {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v);
            } else {
                dp[i][j] = dp[i - 1][j];
            }
        }
    }
    return dp[N][W];
}

看完0-1背包问题,对于改变一数组的正负符号其和为给定值的问题也可以转化为0-1背包问题求解,需要注意的是使用动态规划,其dp[i][j]表达的含义有所变化,因此初始化dp数组以及状态转移方程也与0-1背包问题不同。

改变一组数的正负符号使其和为给定值

问题描述:对于给定的数组nums[0…n],其中数值为非负数(可以为0),可任意改变每个数的正负符号,求有多少种改变方法使得数组的和为给定值S?

输入示例:

nums[] = {1,1,1,1,1}
S = 3

输出示例:

5

表示有3种改变方法使得数组和为5:

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

解题思路①:这题可以变换为求数组中有多少个子集的和为某个给定值,从而使用0-1背包方法求解。变换方法:将数组看成X,Y两个子集,sum(nums) = sum(X)+sum(Y)则:

                               sum(X) - sum(Y) = S; 
             sum(X) + sum(Y) + sum(X) - sum(Y) = S + sum(X) + sum(Y);由1式两边同时加上sum(X)和sum(Y)
                                      2*sum(X) = S + sum(nums)

由上面3个等式可得:sum(X) = (S + sum(nums))/2 ; 上面的等式可以理解为X子集全部为正号,Y子集全部为负号,则可满足题意。那么由此原问题转化为求数组中某一子集X的和为(S + sum(nums))/2,这样的子集X有多少个?

0-1背包求解方法:二维数组dp[0…N][0…M],N为给定数组的长度加1,M为(S + sum(nums))/2+1;这里将nums数组看成是物品重量,求解背包重量为(S + sum(nums))/2+1的情况下的装法有几种。那么dp[i][j]的含义就应该是背包重量严格等于j的情况下前i中物品有多少种装法,dp[nums.length][(S+sum)/2]表示装下nums数组中的所有物品使得背包重量为(S+sum)/2的装法,即为所求。这里与0-1背包问题里的dp数组含义有很大差别,0-1背包中dp[i][j]表示背包重量不超过j的情况下前i件物品能装下的最大价值,重点应该关注的是,该问题求解的是严格等于j的情况,而0-1背包则是不超过j的情况,另外dp[i][j]的含义随题意变化就不必多言了。

状态转移式:由dp[i][j]的含义可分析出,在背包重量严格等于j的情况下讨论能不能装下第i件物品,假设第i件物品重量为w=nums[i]:

  • 当j>=w时,表示能装下第i件物品:dp[i][j] = dp[i-1][j] + dp[i-1][j-w],表示在背包重量严格等于j的情况前i种物品的装法等于背包重量严格等于j的情况下前i-1种物品装法,即dp[i-1][j],这表示没装第i件物品的装法,加上背包重量严格等于j-w的情况下前i-1种物品的装法,即dp[i-1][j-w],这表示装了第i件物品的装法;两种装法的和即为背包重量为j并且可以装下第i件物品时的装法。总结来说,就是在背包能装下第i件物品时,装满重量j的装法等于装下第i间物品(且背包重量为j)的装法加上不装第i件物品(且背包重量为j)的装法。

  • 当j<w时,表示不能装下第i件物品:dp[i][j] = dp[i-1][j],表示背包重量严格等于j的情况下前i种物品的装法等于背包重量严格等于j的情况下前i-1种物品的装法;

根据以上分析得到状态转移方程(w为第i件物品的重量即nums[i]):

dp[i][j] = dp[i-1][j] + dp[i-1][j-w],j>=w;
dp[i][j] = dp[i-1][j],j<w;

dp数组初始化:根据dp[i][j]的含义对dp进行初始化:dp[0][0]=1,装下前0件物品使得背包重量为0的装法有1种;那么对于每一件物品i的dp[i][0]有dp[i][0] = 1,表示装下前i件物品使得背包重量为0的装法有1种(就是不装),这里和状态转移方程中的dp[i][j] = dp[i-1][j]对应。该问题在dp的初始化上和0-1背包也不同,0-1背包中dp[i][0]表示不超过重量0时背包装下前i件物品的最大价值,显然dp[i][0]=0;而dp[0][j]表示不超过重量j时装下前0件物品的最大价值,显然dp[0][j]=0;

public int findTargetSumWays(int[] nums, int S) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        // 背包容量为整数,sum + S为奇数的话不满足要求
        if (((sum + S) & 1) == 1) {
            return 0;
        }
        // 目标和不可能大于总和
        if (S > sum) {
            return 0;
        }
        sum = (sum + S) >> 1;
        int len = nums.length;
        int[][] dp = new int[len + 1][sum + 1];
        dp[0][0] = 1;    //这步初始化不能少

        for (int i = 1; i <= len; i++) {
            for (int j = 0; j <= sum; j++) {
                // 装不下
                if (j - nums[i - 1] < 0) {
                    dp[i][j] = dp[i - 1][j];
                // 装得下 
                } else {
                    dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i - 1]];
                }
            }
        }
        return dp[len][sum];
    }

对于这个问题还有另一种解法是递归,也是最容易想到的解法。

解题思路②:对于数组中的每个元素num要么给该元素正号,要么给负号,即有两种情况:S = S+num(给正号)、S = S - num(给负号)那么对于一个元素来说有两种走法,这里可以生成一颗二叉树,关键就看所有元素都走完(即到达叶子结点)时,S是不是等于0;

public int findTargetSumWays(int[] nums, int S) {
    return findTargetSumWays(nums,0,S);
}

private int findTargetSumWays(int[] nums,int start,int S){
    if(start == nums.length){
        if(S==0)return 1;
        else return 0;
    }else {
        return findTargetSumWays(nums,start+1,S+nums[start])
                +findTargetSumWays(nums,start+1,S-nums[start]);
    }
}

小结:对于以上两个问题,改变数组符号的问题稍加转换可以使用0-1背包的解法,但是根据dp含义的不同状态转移方程不同,dp初始化也不同;两个问题的动态规划都可以压缩空间使用一维数组求解。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值