11.6力扣刷题笔记 494.目标和

本文探讨了一种使用动态规划的方法来解决整数数组构造表达式的问题,将问题转化为背包子集划分,通过状态转移方程dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i-1]]求解不同表达式的数量。重点在于理解背包问题的模型和状态转移过程的优化。
摘要由CSDN通过智能技术生成

题目描述

给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

思路

这道题最简单的一个思路就是回溯穷举,实现很简单,这里不多赘述,缺点是时间复杂度非常高。下面我们会讲到另外一种思路。

动态规划求解

说实话这道题动态规划我是没啥思路,一方面是我目前对动态规划掌握的确实不深,这道题的状态转移方程想不出来。

我们看看用动态规划怎么求解吧:
首先这个问题可以转化为子集划分问题,而子集划分问题又是一个经典的背包问题。。。。动态规划就是这样玄学
首先,如果我们把nums划分为两个子集A和B的话,分别代表分配+的数和分配-的数,那么他们和target存在如下关系:

sum(A) - sum(B) = target
sum(A) = target + sum(B)
sum(A) + sum(A) = target + sum(B) + sum(A)
2 * sum(A) = target + sum(nums)

可以推出sum(A) = (target + sum(nums)) / 2,然后问题可以转化为:nums中存在几个子集,使得A中的元素和为 (target + sum(nums)) / 2。

变成背包问题的标准形式:
有一个背包,容量为 sum,现在给你 N 个物品,第 i 个物品的重量为 nums[i - 1](注意 1 <= i <= N),每个物品只有一个,请问你有几种不同的方法能够恰好装满这个背包?

下面开始解题:
第一步:明确【状态】和【选择】
对于背包问题的话,状态是【背包的容量】和【可选择的物品】,选择就是【装进背包】和【不装背包】
第二步:明确dp数组的定义
dp[i][j] = x表示:若只在前i个物品中选择,若当前背包的容量为j,则有最多x种方法可以恰好装满背包
翻译成我们这道题就是:只在前i个数中选择,若目标和为j,有x中办法划分子集
base case:dp[0][…]为0,因为没有物品的话,没有办法填充背包,dp[…][0]为1,因为若背包容量为0,什么都不装就是唯一方案
第三步:根据【选择】,思考状态转移的逻辑
如果不把nums[i]算入子集,那么恰好装满背包的背包的方案数就取决于上一个状态dp[i-1][j]。
如果nums[i]算入子集,那么只要看前i-1种物品有几种方法可以装满j-nums[i-1]的重量就行了,所以取决于状态dp[i-1][j-nums[i-1]]

PS:注意我们说的 i 是从 1 开始算的,而数组 nums 的索引时从 0 开始算的,所以 nums[i-1] 代表的是第 i 个物品的重量,j - nums[i-1] 就是背包装入物品 i 之后还剩下的容量。

状态转移方程为:dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]]
这里我们还可以做下优化,因为dp[i][j]只和前面dp[i-1][…]有关,所以可以优化为一维dp,代码如下:

/* 计算 nums 中有几个子集的和为 sum */
int subsets(int[] nums, int sum) {
    int n = nums.length;
    int[] dp = new int[sum + 1];
    // base case
    dp[0] = 1;
    
    for (int i = 1; i <= n; i++) {
        // j 要从后往前遍历
        for (int j = sum; j >= 0; j--) {
            // 状态转移方程
            if (j >= nums[i-1]) {
                dp[j] = dp[j] + dp[j-nums[i-1]];
            } else {
                dp[j] = dp[j];
            }
        }
    }
    return dp[sum];
}

二维dp代码如下:

/* 计算 nums 中有几个子集的和为 sum */
int subsets(int[] nums, int sum) {
    int n = nums.length;
    int[][] dp = new int[n + 1][sum + 1];
    // base case
    for (int i = 0; i <= n; i++) {
        dp[i][0] = 1;
    }
    
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= sum; j++) {
            if (j >= nums[i-1]) {
                // 两种选择的结果之和
                dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];
            } else {
                // 背包的空间不足,只能选择不装物品 i
                dp[i][j] = dp[i-1][j];
            }
        }
    }
    return dp[n][sum];
}

对照二维 dp,只要把 dp 数组的第一个维度全都去掉就行了,唯一的区别就是这里的 j 要从后往前遍历,原因如下:

因为二维压缩到一维的根本原理是,dp[j] 和 dp[j-nums[i-1]] 还没被新结果覆盖的时候,相当于二维 dp 中的 dp[i-1][j] 和 dp[i-1][j-nums[i-1]]。

那么,我们就要做到:在计算新的 dp[j] 的时候,dp[j] 和 dp[j-nums[i-1]] 还是上一轮外层 for 循环的结果。

如果你从前往后遍历一维 dp 数组,dp[j] 显然是没问题的,但是 dp[j-nums[i-1]] 已经不是上一轮外层 for 循环的结果了,这里就会使用错误的状态,当然得不到正确的答案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值