给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。返回可以使最终数组和为目标数 S 的所有添加符号的方法数。来自leetcode494,题目链接点击这里 。
首先给的数组里面的数一定要使用,因此自然而然想到了:dfs,也就是枚举每一种可能性,一直枚举到最后一层。这种想法的示意图如下:
其实类似于一个二叉树,我们要求的其实就是这个二叉树里面的从根节点root到叶子节点的所有路径里面,路径和符合要求的有几条。代码如下:
public int findTargetSumWays(int[] nums, int S) {
if(nums==null||nums.length<1)
return -1;
return process(nums,0,S,S);
}
public int process(int[] nums,int index,int target,int sum){
if(index==nums.length){//到了最后一层
return sum==target?1:0;
}
return process(nums,index+1,target,sum+nums[index])+process(nums,index+1,target,sum-nums[index]);
}
可以计算一下时间复杂度,因为数组里面每个元素都有两种情况,因此总的时间复杂度为O(2^N),N为数组元素个数。
但是这种方法比较慢,OJ里面需要600ms,太慢了。
对暴力递归进行第一步优化,首先思考一下为什么暴力递归的速度这么慢?很简单,因为重复计算了很多子问题,只要process方法里面的index确定,sum确定,那么返回的方法数就是一定的;而原来的递归里面,即使浅层递归的index和sum确定了,依然往深层继续递归计算,但是这是不必要的计算,因此我们可以把这个结果保存下来。
也就是说只要遇到相同的index和sum,那么直接就可以返回之前保存的方法数。实现的方法就是保存在一个hashmap里面,key为index+sum字符串,value就是对应的方法数。这种优化方式一般叫做:“记忆化搜索”,优化代码如下:
HashMap memo = new HashMap<>();
public int findTargetSumWays2(int[] nums, int S) {
if(nums==null||nums.length<1)
return -1;
return process2(nums,0,S,0);
}
public int process2(int[] nums,int index,int target,int sum){
if(index==nums.length){//到了最后一层
return sum==target?1:0;
}
String key=index+","+sum;
if(memo.containsKey(key))
return memo.get(key);//查询之前有没有保存过,如果有直接返回之前保存的方法数
else{
int res=process2(nums,index+1,target,sum+nums[index])+process2(nums,index+1,target,sum-nums[index]);
memo.put(key,res);
return res;
}
}
使用记忆化搜索可以显著提升速度,但是还可以进一步优化,那就是动态规划。
首先,我们可以根据上面的暴力递归把状态传递方程给写出来:
dp[i][j]=dp[i+1][j-nums[i]]+dp[i+1][j+nums[i]]
这里的i就是index,这里的j就是sum,其实就是根据上面的process函数写的。
然后,我们可以先举个例子,来尝试画个表,看看动态规划的结果对不对,这里我们设置输入的数组为:[1,1,1,1],也就是index=4,累加和为-4~4(如-1-1-1-1=-4)。因此我们可以画出下面的二维表:
注意,这里index一共为5行,0~3表示数组有4个数,最后一行是因为之前的递归process方法里面,index必须要到nums.length才能结束:
index==nums.length;
所以表格一共的层数为:数组元素个数+1个。
可以发现,最后,(0,0)位置就是我们要求的,他的值为4,是对的。代码如下:
//二维dp
public int findTargetSumWays(int[] nums, int S) {
if(nums==null||nums.length<1)
return -1;
int row=nums.length+1;
int sum=0;
for(int num:nums)
sum+=num;
if(sum=0;i--){
for(int j=0;j<=col-1;j++){
//注意边界问题,超过了边界就取第一列
int l=j-nums[i]>=0?j-nums[i]:0;
int r=j+nums[i]
时间复杂度为:O(M*N),M表示多少个数,N表示2倍的数组累加和。相比之前的暴力递归下降了很多。至于这里的边界问题,我的意思就是如果数组下标超了,那么数组元素就是用0来代替,刚好第0列都是0,那就直接换成第0列吧。
通过上面的表格可以发现,每一行的新元素只和下面一行的旧元素有关,因此我们可以只用一维数组解决。但是因为旧元素的列一个在左,一个在右,因此需要两个一维dp数组才行(因为不管从哪个方向更新,左右两个旧元素都会被有一个被“刷新”)。因此,用一个dp数组保存旧的元素,另外一个dp数组保存新的元素。代码如下:
//一维dp
public int findTargetSumWays(int[] nums, int S) {
if(nums==null||nums.length<1)
return -1;
int row=nums.length+1;
int sum=0;
for(int num:nums)
sum+=num;
if(sum=0;i--){
int[] next=new int[col];//新数组
for(int j=0;j<=col-1;j++){
//注意边界问题
int l=j-nums[i]>=0?j-nums[i]:0;
int r=j+nums[i]
时间复杂度和二维dp一样,但是空间复杂度又下降了。
这个题是一个很典型的动态规划方向的题目,优化的套路就是,先暴力递归,然后发现有重复子问题,然后使用记忆化搜索进行优化,然后再利用动态规划降低时间复杂度,最后再使用一维动态规划降低空间复杂度。