从零学算法

494.给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 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
+1 + 1 + 1 + 1 - 1 = 3
示例 2:
输入:nums = [1], target = 1
输出:1

  • 首先数组中每个数可以分为两种情况,即加或减,同样的,对于这个数之后的数也是加或减两种操作,很容易看出这就类似一颗二叉树,让根节点为 0,以便数组从首位开始分叉,树的第 x+1 层结点保存数组遍历到第 x 位时的和即可。比如[1,2,3] ,根节点 0,子节点为 0-1 和 0+1,-1 同样根据 2 分叉出 -1+2-1-2,1也同样分叉出 1+21-2…,代码可能更易看懂,每次累加分叉出的两种可能性即可
  •   int[] ns;
      int N,T;
      public int findTargetSumWays(int[] nums, int target) {
          ns=nums;
          N=ns.length;
          T=target;
          return find(0,0);
      }
      // cur:数组下标,count:到下标 cur 为止数组的和
      public int find(int cur,int count){
      	// 加到下标为 N 说明之前的 ns[0]~ns[N-1] 已经加完了
      	// 该看看是不是我们要的 target 了
          if(cur == N){
              if(count==T){
                  return 1;
              }else{
                  return 0;
              }
          }
          return find(cur+1, count+ns[cur]) + find(cur+1, count-ns[cur]); 
      }
    
  • 由于可变参数只有 cur 和 count 两个,并且在 dfs 某一层过程会重复计算很多次上一层的结果,所以可以记忆化该 dfs。
  •   int[] ns;
      int N,T;
      Map<String,Integer> cache = new HashMap<>();
      public int findTargetSumWays(int[] nums, int target) {
          ns=nums;
          N=ns.length;
          T=target;
          return find(0,0);
      }
      public int find(int cur,int count){
          String key = cur+"_"+count;
          if(cache.containsKey(key)){
              return cache.get(key);
          }
          if(cur == N){
              cache.put(key,count==T?1:0);
              return cache.get(key);
          }
          int result = find(cur+1, count+ns[cur]) + find(cur+1, count-ns[cur]);
          cache.put(key,result);
          return result; 
      }
    
  • 能以递归形式记忆化搜索,那么就能够动态规划,设 f(x,y) 为取数组前 x 位,和为 y 的情况有多少种。因为每个数只能为加或减,所以f(x,y) 要么由数组前 x-1 位的结果加上第 x 位而来,或者为第 x-1 位的结果减去第 x 位而来,即 dp[i][j] = dp[i-1][j-nums[i-1]] + dp[i-1][j+nums[i-1]]数组第 i 位为 nums[i-1]),比如 dp[x-1][j-nums[i-1]],我继续往后取一位,取到数组第 x 位时选择加它,就是成了 dp[x][j-nums[i-1]+nums[i-1]] 也就是 dp[x][j];我们的数组为 dp[][],他的长度根据 x 为数组前 x 位的定义,就定义为数组长度 n+1,最终结果为 dp[n][y],根据 y 的定义,y 的范围为 -sum(nums)~sum(nums),所以 dp 定义为 dp[n+1][sum(nums)+1];定义完状态转移方程和边界条件,看一下初始条件,不然这个动态规划没法启动,永远一堆 0 加来加去,f(0,0) 为只计算数组前 0 位和为 0 的方案数,很明显只有一种,即 dp[0][0] 为 1。大致代码如下
  •   public int findTargetSumWays(int[] nums, int target) {
          int sum = 0;
          for(int i:nums)sum+=i;
          itn n = nums.length;
          int[][] dp = new int[n+1][sum+1];
          dp[0][0] = 1;
          for(int i=1;i<=n;i++){
              int num = nums[i-1];
              for(int j=-sum;j<=sum;j++){
                  dp[i][j] = dp[i-1][j-num]+dp[i-1][j+num];
              }
          }
          return dp[n][target];
      }
    
  • 但是要知道,y 可以为负数,我数组下标可没有负数啊,也就是说为了代码正常运行,我们需要把 y 的范围整体右移一个 sum 。也就是把数组前 x 为和为 -1 有多少种可能存到 dp[x][-1+sum],和为 2 存到 dp[x][2+sum],同时排除掉之前没考虑的 y 不可能出现的值
  •   public int findTargetSumWays(int[] nums, int target) {
          int sum = 0;
          for(int i:nums)sum+=i;
          // 全加都没你大或全减都没你小,那肯定没可能了
          if(Math.abs(target)>sum){
              return 0;
          }
          int n = nums.length;
          int[][] dp = new int[n+1][sum*2+1];
          dp[0][0+sum] = 1;
          for(int i=1;i<=n;i++){
              int num = nums[i-1];
              for(int j=-sum;j<=sum;j++){
              	// 防止下标越界,实际含义就是:先去掉为了代码运行加上的偏移的 sum
              	// 比如 j-num+sum>=0 实际上就是 j-m>= -sum,也就是 -sum <= j-num,因为 y 最小也只能为 -sum
              	// 所以当 y 为 j-num 时,你也肯定大于等于 -sum,不可能比最小值还要小
              	// j+num+sum<=sum*2 去掉偏移得到 j+num <= sum,同理 y 为 j+num 时也不可能大于最大值
                  if(j-num+sum>=0)dp[i][j+sum] += dp[i-1][j-num+sum];
                  if(j+num+sum<=sum*2)dp[i][j+sum] += dp[i-1][j+num+sum];;
              }
          }
          return dp[n][target+sum];
      }
    
  • 但是以上考虑了很多无效的状态,我们知道动态规划就是下一步的状态值根据前一步得到(也就是状态转移),比如 sum 为 20, target 为 10,你在算到 y 为 20 时已经把 nums 里面的数都用完了,你这状态怎么再转移到 10,你已经没有下一步可以走了,也就是无法再进行状态转移。(个人理解,主要想说这个还能优化)首先我们知道,在我们组合后,这个数组就是一部分被我们加了起来,一部分被我们减了起来,比如这个数组的和为 20,我们要拿到 10,我们可能把一堆和为 15 的数加上加号,另一堆和为 5 的数加上了减号,最后 +15+(-5) = 10,也就是我们划分出了正值部分和负值部分,我们把负值部分记作 m(注意负值部分也是一堆数相加,也就是说只用了加号),数组和 sum 简写为 s,正值部分也就为 s-m,参照 +15+(-5) = 10 可以得到 +(s-m)+(-m) = target,也就是 m = (s - target) / 2,这时我们的问题可以转换为使用数组的一些数相加,能凑出和为 m 的方案数。重定义状态得到 f(x,y) 为取数组前 x 位,其中选几个数相加能凑出和为 y(y 就是负值部分) 的方案数,最终需求解 f(n,m);接下来状态转移方程也很容易得到,dp[i][j] = dp[i-1][j](之前就凑好了,现在这个 nums[i-1] 我就用不上了) + dp[i-1][j-nums[i-1]](比如我们要 dp[5][20],nums[4] 也就是数组第 5 个数为 10,dp[4][10] 再往后一步,选择为数组前五个数,相比之前多了一个选择为 nums[4],选择加上他正好能和我现在的 10 凑成 20),最终结果为 dp[i][j]=dp[i−1][j]+dp[i−1][j−nums[i−1]]
  •   public int findTargetSumWays(int[] nums, int t) {
          int s = 0;
          for(int i:nums)s+=i;
          // 因为 nums 里面都是非负整数,所以凑出来的 m 肯定也是非负整数,所以 s-t 一定为非负偶数
          if(Math.abs(t)>s || (s - t) % 2 != 0){
              return 0;
          }
          int m = (s-t)/2;
          int n = nums.length;
          int[][] dp = new int[n+1][m+1];
          dp[0][0] = 1;
          for(int i=1;i<=n;i++){
              int num = nums[i-1];
              for(int j=0;j<=m;j++){
                  dp[i][j] += dp[i-1][j];
                  // 一堆非负整数凑出来的数当然大于等于 0
                  if(j-num>=0){
                      dp[i][j] += dp[i-1][j-num];
                  }
              }
          }
          return dp[n][m];
      }
    

你可能感兴趣的:(算法学习,#,动态规划,算法)