【递归算法】目标和

文章摘要:

  • 本文解析了LeetCode上的目标和问题,要求在非负数组的每个数字前添加"+"或"-",使得总和等于目标值。通过决策树和深度优先搜索(DFS)实现组合枚举,分别讨论了全局变量和函数参数两种回溯方法。核心思路是递归遍历所有可能的符号组合,统计符合条件的路径数。代码示例展示了两种实现方式,并解释了回溯与剪枝的细节。最终,该问题转化为二叉树路径搜索,时间复杂度为指数级,但通过回溯优化避免了重复计算。

文章目录

题目链接

一、题目解析

题目给我们一个非负数组,要我们对数组中的每个数字添加 "加号+ " 或 "减号- ",使得数组中的数字之和等于 target。要我们返回能使得数组中数字之和等于 target 的组合个数。

例如,nums = [ 1, 1, 1, 1, 1 ],target = 3。我们需要对 nums 数组中的每一个数字添加符号 "+" 或 "-" ,使得 nums 中的数字之和等于 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。因此共有五种,返回 5。

二、算法原理 + 代码实现

本题同样是一道找组合的题目,按照暴搜的思考方式来做。

决策树

我们根据示例1,画出决策树。

遍历 nums 数组,然后依次选择给当前数字添加 "+" 或者 "-",看看总共有多少种组合。

我们在每一个节点直接写上数组中数字之和,则决策树(部分)如下:

  1. 从第一个数字开始,若选择给它添加 "+",则得到 1;若选择给它添加 "-",则得到 -1。
  2. 基于第一个数字为 "1" 的情况,再给第二个数字选择符号,若是 "+",得到 1,两数之和就是 2;若是 "-",得到 -1,两数之和就是 0。
  3. 基于第一个数字为 "1" 、第二个数字为 "1" 的情况,再给第三个数字选择...以此类推。
  4. 最终在叶子节点就可以得到结果,统计结果的数量为 5。

全局变量

这里需要两个全局变量:ret 记录结果,path 记录路径。本题可以多设置一个 aim,作为 target,就无需每一次递归都将 target 作为参数传来传去了。

Java 复制代码
int ret, path, aim;

dfs 函数

函数头

我们在每一次的递归过程中都需要对 nums 数组中的数字进行添加符号,因此需要将 nums 作为参数。由于我们是通过递归对 nums 数组进行遍历的,因此需要知道每一次递归的时候遍历的位置 pos,需要将 pos 下标作为参数。

Java 复制代码
dfs(int[] nums, int pos);

本题有另一种思路是将记录路径的变量 path 改为函数参数,具体原因是:本题的路径可以依靠程序自动完成恢复现场的操作(只是对整数类型进行加减),回到上一层就相当于完成了恢复现场的执行了(读者可以自行模拟一遍流程体会),因此可以将它作为参数(参考 二叉树的所有路径 这道题目)。而作为全局变量是因为依靠程序的回溯无法自动完成恢复现场,需要在 dfs 函数中手动处理来恢复现场(例如数组,没法通过函数参数的方式自动完成恢复现场的操作,只能手动处理),干脆改成全局变量(参考 子集 这道题目)。

Java 复制代码
dfs(int[] nums, int pos, int path);

函数体

我们每一次递归要做的事情就是选择 "+" 或者 "-",分别执行这两种情况的逻辑就行了。

Java 复制代码
// path 作为全局变量
// 添加加号的情况
path += 当前位置的数字;
// 递归
// 回溯恢复现场

// 添加减号的情况
path -= 当前位置的数字;
// 递归
// 回溯恢复现场

若是 path 作为函数参数,则代码会相对简洁,因为无需手动恢复现场

Java 复制代码
// 添加加号的情况
dfs(nums, 下一个数字的下标, path + 当前位置的数字);

// 添加减号的情况
dfs(nums, 下一个数字的下标, path - 当前位置的数字);

细节问题

回溯

path 为全局变量的时候,需要在回溯的时候恢复现场,将加上/减去的数字再减去/加上即可。

而当 path 为函数参数的时候,程序执行回溯会自动恢复现场,无需手动处理。

剪枝

我们这里是将所有组合都枚举到了,因此不涉及到剪枝操作。

递归出口

在给当前数字添加符号之前我们要判断一下 pos 是否到达数组末尾,如果到了数组末尾(即叶子节点),再判断一下当前的 path 是否等于 aim/target,如果等于就更新结果然后返回,否则不更新结果直接返回。

代码实现

path 作为全局变量的版本

Java 复制代码
class Solution {
    int ret, aim, path;

    public int findTargetSumWays(int[] nums, int target) {
        aim = target;
        
        dfs(nums, 0);
        return ret;
    }

    private void dfs(int[] nums, int pos) {
    	// 递归出口
        if (pos == nums.length) {
            if (path == aim) ret++;
            return;
        }

        // 添加加号的情况
        path += nums[pos];
        dfs(nums, pos + 1);
        path -= nums[pos]; // 回溯恢复现场

        // 添加减号的情况
        path -= nums[pos];
        dfs(nums, pos + 1);
        path += nums[pos]; // 回溯恢复现场
    }
}

path 作为函数参数的版本

Java 复制代码
class Solution {
    int ret, aim;

    public int findTargetSumWays(int[] nums, int target) {
        aim = target;
        
        dfs(nums, 0, 0);
        return ret;
    }

    private void dfs(int[] nums, int pos, int path) {
    	// 递归出口
        if (pos == nums.length) {
            if (path == aim) ret++;
            return;
        }

        // 添加加号的情况
        dfs(nums, pos + 1, path + nums[pos]);

        // 添加减号的情况
        dfs(nums, pos + 1, path - nums[pos]);
    }
}

文章到这里就告一段落啦,若有错误请尽管指出~

相关推荐
亦暖筑序3 小时前
手写 Spring AI Agent:让大模型自主规划任务,ReAct 模式全流程拆解
java·人工智能·spring
敖正炀3 小时前
ReentrantLock 与 synchronized对比
java
旖-旎3 小时前
哈希表(字母异位次分组)(5)
数据结构·c++·算法·leetcode·哈希算法·散列表
XiYang-DING3 小时前
【Java】二叉搜索树(BST)
java·开发语言·python
weixin_437957613 小时前
Mysql安装不成功
java
Lyyaoo.3 小时前
【JAVA基础面经】进程安全问题(synchronized and volatile)
java·开发语言·jvm
别或许3 小时前
4、高数----一元函数微分学的计算
人工智能·算法·机器学习
_深海凉_4 小时前
LeetCode热题100-最长连续序列
算法·leetcode·职场和发展
Andya_net4 小时前
Java | 基于 Feign 流式传输操作SFTP文件传输
java·开发语言·spring boot