探索组合总和问题(力扣39,40,216)

news2024/11/26 13:44:10

文章目录

  • 题目
  • 前知
    • LinkedList和ArryayList
  • 组合总和I
    • 一、思路
    • 二、解题方法
    • 三、Code
  • 组合总和II
    • 一、思路
    • 二、解题方法
    • 三、Code
  • 组合总和III
    • 一、思路
    • 二、解题方法
    • 三、Code
  • 总结


先看完上一期组合问题再看这一期更加容易理解喔🤯

在算法和编程的世界中,组合总和问题是一个经典且常见的挑战。LeetCode 平台上的39号、40号和216号题目都涉及到这一问题的不同变体。在本文中,我们将深入研究这些问题,并提供相应的解决方案。

题目

Problem: 39. 组合总和
Problem: 40. 组合总和 II
Problem: 216. 组合总和 III

  1. 组合总和(Combination Sum):
    给定一个无重复元素的数组 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。

  2. 组合总和 II(Combination Sum II):
    给定一个数组 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。

  3. 组合总和 III(Combination Sum III):
    找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,且每种组合中不存在重复的数字。

前知

LinkedList和ArryayList

ArrayList 和 LinkedList 是 Java 中常用的集合类,用于存储一组元素。它们分别基于动态数组和双向链表实现,具有不同的特点和适用场景。

在 ArrayList 中,add 函数用于在列表的末尾添加元素,remove 函数用于删除指定位置的元素。

在 LinkedList 中,add 函数可以根据需要在列表的任意位置添加元素,remove 函数用于删除指定位置的元素,removeLast 函数用于删除链表的最后一个元素。

ArrayList 是基于数组实现的动态数组。它的特点包括:

  • 随机访问:

由于底层是数组,所以 ArrayList 支持通过索引快速访问元素,时间复杂度为 O(1)。

  • 尾部操作效率高:

在列表的末尾进行插入和删除操作效率较高,时间复杂度为 O(1)。

  • 内存空间连续:

所有的元素在内存中是连续存储的,这样可以利用 CPU 缓存,提高访问速度。

LinkedList 是基于双向链表实现的。它的特点包括:

  • 插入和删除效率高:

在任意位置进行插入和删除操作的效率较高,时间复杂度为 O(1)。

  • 不支持随机访问:

由于是链表结构,不支持通过索引直接访问元素,只能通过遍历或者迭代器来访问。

  • 内存空间不连续:

每个元素在内存中是通过指针相连的,不连续存储,因此对 CPU 缓存的利用率较低。

选择使用哪种集合
1.如果需要频繁地进行随机访问,应该使用 ArrayList。
2.如果需要频繁地进行插入和删除操作,尤其是在列表的中间位置,应该使用 LinkedList。


组合总和I

一、思路

此题的特点是每次递归可以重复选择相同的数字,for循环时startIndex可以不用加1,startIndex就是每次遍历时的位置,详细startIndex含义可以查看上期文章,并且终止条件也没有长度限制,只要元素总和达到结果即可

二、解题方法

回溯三部曲

  1. 递归函数的返回值以及参数:
    确定参数为整数数组,目标值,求和sum,开始遍历的位置startIndex
public void backTracking(int[] candidates,int target,int sum,int startIndex)
  1. 回溯函数终止条件:
    终止条件为求和大于目标值时,退出递归;或者sum等于目标值时,保存path结果到result集合里,退出递归
        if(sum > target){
            return;
        }
        if(sum == target) {
            result.add(new ArrayList<>(path));
            return;
        }
  1. 单层搜索的过程:
    for循环横向遍历,保存遍历的元素到path里,求和元素的大小,递归调用,回溯撤销结点,减去sum值
        for(int i = startIndex;i < candidates.length;i++){
            path.add(candidates[i]);
            sum += candidates[i];

            backTracking(candidates,target,sum,i);
            path.removeLast();
            sum -= candidates[i];
        }

剪枝优化:
如果提前知道sum求和之后会大于目标值的话,就不需要在进入递归之后再退出递归,此时可以先对数组进行排序,排序之后数组里的值是越来越大的,在保存结点前就判断如果求和之后就已经大于目标值的话,则后面的值递增求和肯定会越来越大,就直接退出本次for循环

//排序
Arrays.sort(candidates); 

//大于目标值,退出循环结构
if (sum + candidates[i] > target) break;

三、Code

未进行判断剪枝

class Solution {
	List<List<Integer>> result = new ArrayList<>();
	LinkedList<Integer> path = new LinkedList<>();
    
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        backTracking(candidates,target,0,0);
        return result;
    }

    public void backTracking(int[] candidates,int target,int sum,int startIndex) {
        if(sum > target){
            return;
        }
        if(sum == target) {
            result.add(new ArrayList<>(path));
            return;
        }

        for(int i = startIndex;i < candidates.length;i++){
            path.add(candidates[i]);
            sum += candidates[i];

            backTracking(candidates,target,sum,i);
            path.removeLast();
            sum -= candidates[i];
        }
    }
}

剪枝优化

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(candidates); // 先进行排序
        backtracking(res, new ArrayList<>(), candidates, target, 0, 0);
        return res;
    }

    public void backtracking(List<List<Integer>> res, List<Integer> path, int[] candidates, int target, int sum, int idx) {
        // 找到了数字和为 target 的组合
        if (sum == target) {
            res.add(new ArrayList<>(path));
            return;
        }

        for (int i = idx; i < candidates.length; i++) {
            // 如果 sum + candidates[i] > target 就终止遍历
            if (sum + candidates[i] > target) break;
            path.add(candidates[i]);
            backtracking(res, path, candidates, target, sum + candidates[i], i);
            path.remove(path.size() - 1); // 回溯,移除路径 path 最后一个元素
        }
    }
}

组合总和II

一、思路

此题与 组合总和I 唯一的区别就在于剪枝优化后的组合总和基础上多个条件:题目给的整数数组是可重复的。

对什么进行去重:在数组可以重复的情况下,并且要求结果中不能出现重复的组合,例如:组合1[1,2,2],组合2[1,2,2],这就是两个重复的组合。由于candidates可以存在重复元素,就要对结果进行去重,否则会出现重复的组合,去重是针对当前同一层不能出现重复,而一个组合内不同深度之间是可以出现重复元素的,例如组合[1,1,2]中尽管出现了两个1,但是是candidates不同位置上的1,并没有与其它组合重复。所以要判断所在的层前面相同的元素是否使用过,如果使用过,那么存在的所有情况都可以被包含在前面使用过的元素的组合情况里面,相当于是前面元素的组合情况的一个子集,必定会造成重复,所以直接对出现过的元素进行剪枝操作
在这里插入图片描述

二、解题方法

回溯三部曲

  1. 递归函数的返回值以及参数:
    先对candidates数组进行排序,创建一个used布尔类型的数组,比上面的 组合总和I 多传入一个use数组
public void backTracking(int[] candidates, int target, int sum,int startIndex,boolean[] used)
  1. 回溯函数终止条件:
    终止条件和 组合总和I 相同,略过

  2. 单层搜索的过程:
    在for循环横向遍历时,判断同层是否出现过相同的元素,如果出现过,则跳过该循环,继续横向遍历其它元素,判断逻辑为判断当前元素与前一个元素相同,并且不是纵向的元素重复,如果前一个元素used数组为true的话,说明是在相同元素的下一层重复了,而不是横向同层的重复。每当访问过的元素就让该元素对应的used数组置为true,回溯的话就重新为false。

//去重:同层出现重复结点,直接跳过
if(i>0 && candidates[i] == candidates[i-1] && used[i-1] == false) {
   continue;
}
//遍历完之后让used数组为true
used[i] = true;
//回溯为false
used[i] = false;

used数组.png

补充:也可以不使用used标记数组记录是否访问过该元素,startIndex也可以直接进行去重

排序之后的数组,当 i>startIndex 时说明已经不是遍历的同层的第一个结点了,至少从第二个结点开始了,而当此时出现元素和前面一个元素相同的情况的话,也就可以直接去重

三、Code

used标记数组记录

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new LinkedList<>();

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        boolean[] used = new boolean[candidates.length];
        Arrays.sort(candidates); // 先进行排序
        backTracking(candidates,target,0,0,used);
        return result;
    }

    public void backTracking(int[] candidates, int target, int sum,int startIndex,boolean[] used){
        if(sum == target){
            result.add(new ArrayList<>(path));
            return;
        }
        for(int i = startIndex;i<candidates.length;i++){
            // 剪枝:如果 sum + candidates[i] > target 就终止遍历
            if (sum + candidates[i] > target) {
                break;
            }
            //去重:同层出现重复结点,直接跳过
            if(i>0 && candidates[i] == candidates[i-1] && used[i-1] == false) {
                continue;
            }
            path.add(candidates[i]);
            sum += candidates[i];
            used[i] = true;
            backTracking(candidates,target,sum,i+1,used);
            //回溯
            path.removeLast();
            sum -= candidates[i];
            used[i] = false;
        }
    }
}

startIndex去重

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new LinkedList<>();

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates); // 先进行排序
        backTracking(candidates,target,0,0);
        return result;
    }

    public void backTracking(int[] candidates, int target, int sum,int startIndex){
        if(sum == target){
            result.add(new ArrayList<>(path));
            return;
        }
        for(int i = startIndex;i<candidates.length;i++){
            // 剪枝:如果 sum + candidates[i] > target 就终止遍历
            if (sum + candidates[i] > target) {
                break;
            }
            //去重:同层出现重复结点,直接跳过
            if(i>startIndex && candidates[i] == candidates[i-1]) {
                continue;
            }
            path.add(candidates[i]);
            sum += candidates[i];
            backTracking(candidates,target,sum,i+1);
            //回溯
            path.removeLast();
            sum -= candidates[i];
        }
    }
}

组合总和III

一、思路

此道题与上一期组合问题非常相似,建议先看一遍上期问题,只多加了判断当叶子结点时是否满足sum和为目标值,并且横向for循环直接定死为[1,2…9],n就可以直接写9。

二、解题方法

回溯三部曲

  1. 递归函数的返回值以及参数:
    输入的参数多了一个整数参数sum用于每遍历一个结点就把值给求和
private void backTracking(int targetSum, int k, int startIndex, int sum)
  1. 回溯函数终止条件:
    终止条件判断当到达叶子结点时sum是否等于目标值,等于就保存到result数组里退出本层递归,不等于就直接退出本层递归;还可以再进行一个剪枝操作,那就是当遍历之后的sum值如果还没到叶子结点之前就已经大于目标值的话,也直接退出本层递归。
if (path.size() == k) {
			if (sum == targetSum) result.add(new ArrayList<>(path));
			return;
		}
		
if (sum > targetSum) {
			return;
		}
  1. 单层搜索的过程:
    在把遍历到的数据添加到path里之后,就进行一次求和,递归完了之后,在撤销结点的同时,同样要记得把sum也给减去对应的值。
    9 - (k - path.size()) + 1可以查看上期组合问题剪枝优化的操作

三、Code

class Solution {
	List<List<Integer>> result = new ArrayList<>();
	LinkedList<Integer> path = new LinkedList<>();

	public List<List<Integer>> combinationSum3(int k, int n) {
		backTracking(n, k, 1, 0);
		return result;
	}

	private void backTracking(int targetSum, int k, int startIndex, int sum) {
		// 减枝
		if (sum > targetSum) {
			return;
		}

        //终止条件
		if (path.size() == k) {
			if (sum == targetSum) result.add(new ArrayList<>(path));
			return;
		}

		// 减枝 9 - (k - path.size()) + 1
		for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
			path.add(i);
			sum += i;
			backTracking(targetSum, k, i + 1, sum);
			//回溯
			path.removeLast();
			//回溯
			sum -= i;
		}
	}
}

总结

以上就是针对这道题的刷题笔记,通过使用回溯法,我们可以有效地解决组合总和问题。对于每个问题,我们都需要仔细考虑题目要求,并编写相应的回溯函数来搜索所有可能的解。在编写代码时,要注意避免重复计算和重复组合的情况,以提高算法的效率。希望本文对你理解和解决组合总和问题有所帮助!🌹

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1561173.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

走向国际:区块链行业项目海外市场宣传与运营攻略

随着区块链技术的不断发展和应用&#xff0c;越来越多的区块链项目开始将目光投向海外市场。在全球范围内寻找用户和投资者&#xff0c;扩大品牌知名度&#xff0c;是许多区块链项目的共同目标。然而&#xff0c;要成功进军海外市场&#xff0c;并不是一件容易的事情。本文将深…

Vscode运行python

按住 xtrlshiftp&#xff0c;会出现下面的界面&#xff1a; 然后选择第一个选项&#xff0c;会出现如下的界面&#xff1a; 选择某个环境后就可以使用了。可以右键&#xff0c;如下所示&#xff1a; 就可以运行python程序了

如何在Ubuntu系统部署Z-blog博客结合cpolar实现无公网IP访问本地网站

文章目录 1. 前言2. Z-blog网站搭建2.1 XAMPP环境设置2.2 Z-blog安装2.3 Z-blog网页测试2.4 Cpolar安装和注册 3. 本地网页发布3.1. Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 1. 前言 想要成为一个合格的技术宅或程序员&#xff0c;自己搭建网站制作网页是绕…

基于WEB的花卉养殖知识平台

基于WEB的花卉养殖知识平台的设计与实现 摘要 随着人们生活水平及生活质量要求的日益提升&#xff0c;花卉也成为了人们日常生活的调味剂&#xff0c;同时对于花卉的养殖及养护不再是老年人的专利&#xff0c;很多年轻人也在通过花卉的养护来舒缓工作压力&#xff0c;同时通过…

Centos7.X服务器搭建VOS系统的REC录音转换MP3,并支持外呼系统wav转换MP3

由于有的公司客户需要自己下载录音或做话务质检等工作需要&#xff0c;需要从VOS系统中把录音下载到其它服务器使用&#xff0c;但是VOS录音格式是REC格式的&#xff0c;就算下载下来了也无法直接播放&#xff0c;因此我们需要搭建一台转换MP3的服务器来完成需求&#xff01; 外…

EfficientSAM 项目排坑

EfficientSAM 项目排坑 任务过程记录创建环境运行示例 任务 跑通这个项目代码 过程记录 创建环境 readme里没有说具体怎么配置环境&#xff0c;所以可能对我来说还挺困难的。 现把项目git下来&#xff1a; git clone https://github.com/yformer/EfficientSAM.git cd Effi…

插值字符串格式化代码中的感叹号(Python)

在csdn上读到&#xff0c;插值字符串格式化代码中有“!”&#xff0c;进行了一番探究&#xff0c;了解到其中的一点“隐秘”&#xff0c;在此共享。&#x1f92a; (笔记模板由python脚本于2024年03月31日 09:27:59创建&#xff0c;本篇笔记适合对Python字符串格式化有一定认知的…

【C语言】联合体、枚举: 联合体与结构体区别,枚举的优点

目录 1、联合体 1.1、什么是联合体 1.2、联合体的声明 1.3、联合体的特点 1.4、联合体与结构体区别 1.5、联合体的大小 2、枚举 2.1、枚举类型的声明 2.2、枚举类型的优点 3、三种自定义类型&#xff1a;结构体、联合体、枚举 正文 1、联合体 1.1、什么是联合体 联…

OpenHarmony实战:命令行工具hdc安装应用指南

一、工具概述 hdc&#xff08;OpenHarmony Device Connector&#xff09;是为开发人员提供的用于设备连接调试的命令行工具&#xff0c;该工具需支持部署在 Windows/Linux/Mac 等系统上与 OpenHarmony 设备&#xff08;或模拟器&#xff09;进行连接调试通信。 简言之&#xf…

开启 Sora 知识免费课,探索文生视频大模型

4 月 1 日&#xff0c;中国网游先锋&#xff0c;火石控股董事长、风险投资人吴渔夫开启了“跟我学 Sora 知识”的免费课程。他表示&#xff0c;讲述的知识涵盖了 Sora 的产品、技术及未来走向。自 2 月 16 日 Sora 文生视频模型问世以来&#xff0c;我已查阅众多的 Sora 中英文…

记一次对Codis的无知引起的逻辑变更

先提前说明&#xff0c;对Codis的无知是因为Codis不支持一些Redis的命令&#xff0c;而这次的逻辑变更&#xff0c;就是因为使用了PUBLISH&#xff0c;而Codis又不支持PUBLISH导致的。 1. 前言 前段时间的一次需求中&#xff0c;因为设计到多个服务的注册问题&#xff0c;在项…

docker容器添加新端口映射的步骤及`wsl$`目录的作用

在Docker容器已经创建后&#xff0c;需要添加新的端口映射&#xff0c;即对已经存在的Docker容器添加新的端口映射&#xff0c;可以通过以下步骤来添加&#xff0c;即通过修改配置文件的方法。 如何新增端口映射&#xff1f; 查找容器的hash值 docker inspect [容器id或名称…

机器学习在智能音箱中的应用探索与实践:让声音更懂你

&#x1f9d1; 作者简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟,欢迎关注。提供嵌入式方向的学习指导…

椋鸟数据结构笔记#5:树、二叉树基础

文章目录 树树的相关概念树的表示 二叉树基础二叉树分类满二叉树完全二叉树 二叉树的性质二叉树的存储结构顺序存储链式存储 萌新的学习笔记&#xff0c;写错了恳请斧正。 树 树是一种非线性的数据结构&#xff0c;它是由 n 个节点组成的一个具有层次关系的数据集合。其大概结…

算法学习——LeetCode力扣补充篇3(143. 重排链表、141. 环形链表、205. 同构字符串、1002. 查找共用字符、925. 长按键入)

算法学习——LeetCode力扣补充篇3 143. 重排链表 143. 重排链表 - 力扣&#xff08;LeetCode&#xff09; 描述 给定一个单链表 L 的头节点 head &#xff0c;单链表 L 表示为&#xff1a; L0 → L1 → … → Ln - 1 → Ln 请将其重新排列后变为&#xff1a; L0 → Ln → …

题目:小蓝的神秘行囊(蓝桥OJ 3937)

问题描述&#xff1a; 解题思路&#xff1a; 二维优化01背包模板题。与一维优化01背包不同在于多增加一维。 代码&#xff1a; #include <bits/stdc.h> using namespace std;const int N 1e2 9; int dp[N][N]; //二维的01背包&#xff0c;dp[i][j]&#xff1a;i是体…

【SpringCloud】一文详谈Nacos

&#x1f3e1;浩泽学编程&#xff1a;个人主页 &#x1f525; 推荐专栏&#xff1a;《深入浅出SpringBoot》《java对AI的调用开发》 《RabbitMQ》《Spring》《SpringMVC》《项目实战》 &#x1f6f8;学无止境&#xff0c;不骄不躁&#xff0c;知行合一 文章目录 …

metasploit使用及内网笔记

1 基本操作 Metasploit就是一个漏洞框架。它的全称叫做The Metasploit Framework&#xff0c;简称叫做MSF。Metasploit作为全球最受欢迎的工具&#xff0c;不仅仅是因为它的方便性和强大性&#xff0c;更重要的是它的框架。它允许使用者开发自己的漏洞脚本&#xff0c;从而进行…

Dockerfile和Docker-compose

一、概述 Dockerfile和Docker Compose是用于构建和管理 Docker 容器的两个工具&#xff0c;但它们的作用和使用方式不同。 Dockerfile Dockerfile 是一个文本文件&#xff0c;用于定义 Docker 镜像的构建规则。它包含一系列指令&#xff0c;如 FROM&#xff08;指定基础镜像…

RAG:检索增强生成系统如何工作

随着大型语言模型&#xff08;LLM&#xff09;的发展&#xff0c;人工智能世界取得了巨大的飞跃。经过大量数据的训练&#xff0c;LLM可以发现语言模式和关系&#xff0c;使人工智能工具能够生成更准确、与上下文相关的响应。 但LLM也给人工智能工程师带来了新的挑战&#xff…