【递归算法】目标和

文章摘要:

  • 本文解析了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]);
    }
}

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

相关推荐
KaMeidebaby7 小时前
卡梅德生物技术快报|PD1 单克隆抗体定制配套 N 糖全谱质控开发
前端·人工智能·算法·数据挖掘·数据分析
8Qi88 小时前
LeetCode 235. 二叉搜索树的最近公共祖先(LCA)
算法·leetcode·二叉树·递归·二叉搜索树·lca·迭代
bIo7lyA8v8 小时前
算法稳定性分析中的随机扰动建模的技术8
算法
科研online8 小时前
基于多源数据和XGBoost-SHAP分析中国大陆绿地碳汇空间变异影响因素的非线性相关性与尺度差异
算法·学习方法
Cthy_hy9 小时前
拓扑排序超详解:原理 + Kahn 贪心算法
python·算法·贪心算法
三品吉他手会点灯9 小时前
C语言学习笔记 - 43.运算符与表达式 - 运算符1 - 运算符的分类和简单介绍
c语言·笔记·学习·算法
心之伊始9 小时前
Java 后端接入大模型:从 Token、并发到推理成本的完整估算方法
java·spring boot·性能优化·大模型·llm
VkN2X2X4b9 小时前
算法复杂度的实验验证与误差分析的技术8
算法
其利天下技术10 小时前
风扇灯无刷电机自适应算法实战指南
算法·cocos2d·无刷电机自适应算法·bldc驱动自适应算法·其利无刷电机驱动算法
8Qi810 小时前
LeetCode 494:目标和(Target Sum)—— 题解 ✅
算法·leetcode·职场和发展·动态规划·01背包