动态规划典型题之“目标和”

题目

给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。返回可以使最终数组和为目标数 S 的所有添加符号的方法数。来自leetcode494,题目链接点击这里 。
  

例子


思路1、dfs(递归)

首先给的数组里面的数一定要使用,因此自然而然想到了: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,太慢了。


思路2、记忆化搜索

暴力递归进行第一步优化,首先思考一下为什么暴力递归的速度这么慢?很简单,因为重复计算了很多子问题,只要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;
        }
          
    }

使用记忆化搜索可以显著提升速度,但是还可以进一步优化,那就是动态规划


思路3、动态规划

首先,我们可以根据上面的暴力递归把状态传递方程给写出来:
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列吧。


思路4、继续优化成1维dp数组

通过上面的表格可以发现,每一行的新元素只和下面一行的旧元素有关,因此我们可以只用一维数组解决。但是因为旧元素的列一个在左,一个在右,因此需要两个一维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一样,但是空间复杂度又下降了。


总结

这个题是一个很典型的动态规划方向的题目,优化的套路就是,先暴力递归,然后发现有重复子问题,然后使用记忆化搜索进行优化,然后再利用动态规划降低时间复杂度,最后再使用一维动态规划降低空间复杂度。

你可能感兴趣的:(算法)