【回溯算法 8】目标和(medium)(每日一题)

 🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇

                                      ⭐回溯⭐

🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇🎇


前言

回溯算法是⼀种经典的递归算法,通常⽤于解决组合问题、排列问题和搜索问题等。

回溯算法的基本思想:从⼀个初始状态开始,按照⼀定的规则向前搜索,当搜索到某个状态⽆法前进 时,回退到前⼀个状态,再按照其他的规则搜索。

回溯算法在搜索过程中维护⼀个状态树,通过遍历 状态树来实现对所有可能解的搜索。 回溯算法的核⼼思想:“试错”,即在搜索过程中不断地做出选择,如果选择正确,则继续向前搜 索;否则,回退到上⼀个状态,重新做出选择。

回溯算法通常⽤于解决具有多个解,且每个解都需要 搜索才能找到的问题。


 回溯算法的模板

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Backtracking {
    public void backtracking(int[] candidates, int target) {
        List<List<Integer>> result = new ArrayList<>();
        Arrays.sort(candidates);
      // 如果需要对candidates进行排序

        backtrack(result, new ArrayList<>(), candidates, target, 0);
    }

    private void backtrack(List<List<Integer>> result, List<Integer> tempList, int[] candidates, int remain, int start) {
        if (remain < 0) { 
       // 如果 remain 小于 0,表示当前的组合不符合要求,直接返回
            return;
        } else if (remain == 0) { 
            // 如果 remain 等于 0,表示当前的组合符合要求,加入到结果中
            result.add(new ArrayList<>(tempList));
        } else {
            for (int i = start; i < candidates.length; i++) {
                if (i > start && candidates[i] == candidates[i - 1]) {
                    continue; 
                  // 避免重复计算,如果当前数字和上一个数字相同,则跳过
                }
                tempList.add(candidates[i]); // 将当前数字加入到临时列表中
                backtrack(result, tempList, candidates, remain - candidates[i], i + 1); 
                // 递归调用,继续向下搜索
                tempList.remove(tempList.size() - 1); 
                // 回溯,将最后加入的数字移除,尝试下一个数字
            }
        }
    }
}

 回溯算法是⼀种⾮常重要的算法,可以解决许多组合问题、排列问题和搜索问题等。回溯算法的核⼼ 思想是搜索状态树,通过遍历状态树来实现对所有可能解的搜索。回溯算法的模板⾮常简单,但是实 现起来需要注意⼀些细节,⽐如如何做出选择、如何撤销选择等。


2. 目标和(medium)

题目链接:494. 目标和 - 力扣(LeetCode)

问题描述

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

算法思路

算法思路

  1. 定义状态

    • index:当前处理到数组 nums 的索引。
    • path:当前运算路径上的累加结果。
  2. 递归终止条件

    • 当 index 达到数组 nums 的长度时,判断 path 是否等于 target。如果等于 target,则找到了一个有效的表达式,计数器 ret 加 1。
  3. 递归过程

    • 对于每一个 index,有两种选择:在当前数字前加上 '+' 或 '-'。
    • 对于每种选择,递归调用自身,处理下一个索引 index + 1
    • 在选择 '+' 时,path 增加当前的 nums[index]
    • 在选择 '-' 时,path 减去当前的 nums[index]
  4. 回溯

    • 在 dfs2 方法中,使用全局变量 path 来存储当前的累加结果。每次递归调用前后需要更新 path,并在返回时恢复原状,以便不影响其他分支的计算。

代码解释

dfs1 方法
  • dfs1 方法采用传参的方式传递当前路径的累加结果 path
  • 每次递归调用时,分别尝试加法和减法,并递归处理下一个索引。
  • 当到达数组末尾时,检查当前路径的结果是否等于目标值 target
dfs2 方法
  • dfs2 方法使用全局变量 path 来存储当前路径的累加结果。
  • 在递归调用前后,需要对 path 进行更新和恢复,以实现回溯。
  • 每次递归调用时,分别尝试加法和减法,并递归处理下一个索引。
  • 当到达数组末尾时,检查当前路径的结果是否等于目标值 target

 代码实现:

class Solution {
    //path指的是运算到当前数字的结果,这里目标结果是整形,非数组,把 path 放到参数里代码更加简洁
    int ret;
    public int findTargetSumWays(int[] nums, int target) {
      ret =0;
      int path =0;
    //   dfs1(nums,target,path,0);
      dfs2(nums,target,0);
      return ret;
    }

    //dfs path做参数
    public void dfs1(int[] nums,int target,int path,int index){
       if(index==nums.length){
        if(path == target){
         ret++;
        }
         return;
       }
       //选加法
       dfs1(nums,target,path+nums[index],index+1);
       //选减法
       dfs1(nums,target,path-nums[index],index+1);
    }

    int path =0;
    //dfs2 path做全局变量  所以在运算的时候要回溯
    public void dfs2(int[] nums,int target,int index){
        if(index==nums.length){
        if(path == target){
         ret++;
        }
         return;
       }
       //选加法
       path+=nums[index];
       dfs2(nums,target,index+1);
       //回溯
       path-=nums[index];
        //选减法
       path-=nums[index];
       dfs2(nums,target,index+1);
       //回溯
       path+=nums[index];
    }
}

代码优化:

问题背景

给定一个非负整数数组 nums 和一个整数 target,向数组中的每个整数前添加 '+' 或 '-',然后串联起所有整数,构造一个表达式。返回可以通过上述方法构造的、运算结果等于 target 的不同表达式的数量。

解析

  1. 数学分析

    • 设最终选取的结果中,前面加 + 号的数字之和为 a,前面加 - 号的数字之和为 b
    • 整个数组的总和为 sum,于是我们有:
      • a + b = sum
      • a - b = target
    • 消去 b 可得 a = (sum + target) / 2
  2. 转化为背包问题

    • 我们只需要在 nums 数组中选择一些数,使得它们的和为 (sum + target) / 2
    • 这个问题可以转化为经典的背包问题:在给定的数组中选出一些数,使得它们的和等于某个目标值。

动态规划算法

  1. 状态表示

    • dp[i][j] 表示在前 i 个数中选,总和正好等于 j 的方案数。
  2. 状态转移方程

    • 不选择 nums[i]dp[i][j] = dp[i - 1][j]
    • 选择 nums[i]:如果 nums[i - 1] <= j,则 dp[i][j] += dp[i - 1][j - nums[i - 1]]
  3. 初始化

    • dp[0][0] = 1,表示不选择任何元素时,总和为 0 的方案数为 1。
  4. 返回值

    • 返回 dp[n][aim] 的值,其中 n 为数组大小,aim 为要凑的目标和。
  5. 空间优化

    • 由于 dp 表的第二维只依赖于前一行的数据,可以将二维数组简化为一维数组。
    • 修改第二层循环的遍历顺序,以避免覆盖所需的数据。
 public int findTargetSumWays(int[] nums, int target) {
      int sum=0;
      
      //计算sum 和
      for(int x:nums){
         sum+=x;
      }
      int aim = (sum +target);
      //这里是 2a = sum + target, 如果差的结果不为偶数就说明不存在这种表达式
      if(aim%2!=0 || aim<0){
         return 0;
      }
      aim/=2;
      //建表 dp[i][aim] 表示的是挑选前 i 个数中某些数的和为 aim 的挑选方式的个数
      int[][] dp = new int[nums.length+1][aim+1];
      
      //初始化  在前 0 个数中挑选的和只能为 0 这一种,其他和都不存在所以 
      dp[0][0] = 1;

      //填表
      for(int i = 1;i<nums.length+1;i++){
        for(int j = 0;j<aim+1;j++){
            dp[i][j] = dp[i-1][j];
            if(j>=nums[i-1]){    //这里 i-1 是因为当时建表的时候多建了一层
                dp[i][j]+=dp[i-1][j-nums[i-1]];
            }
        }
      }
      return dp[nums.length][aim];  
    }

总结

动动手点个赞会让作者更开心,感谢阅览,加油各位 !

  • 13
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值