【LeetCode每日一题】回溯:46. 全排列 78. 子集 39. 组合总和

每日一题

2026.11.1

46. 全排列

题目

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = 1,2,3

输出:\[1,2,3,1,3,2,2,1,3,2,3,1,3,1,2,3,2,1]

示例 2:

输入:nums = 0,1

输出:\[0,1,1,0]

示例 3:

输入:nums = 1

输出:\[1]

提示:

1 <= nums.length <= 6

-10 <= numsi <= 10

nums 中的所有整数 互不相同

总体思路

本题采用回溯算法(DFS)遍历所有可能的组合。维护一个当前路径 tmp,每次从数组中挑选一个数加入路径。

为了防止重复,每次挑选前遍历 tmp 查重:若该数字未在路径中,则将其加入 tmp 并递归进入下一层;若已存在,则跳过。

当 tmp 长度等于原数组长度时,说明生成了一个完整的全排列,将其深拷贝后存入结果集 res。

递归结束后,将 tmp 最后一个元素弹出(回溯),恢复现场,继续尝试其他分支。该方法逻辑直观,省去了额外的 visited 标记数组。

  • 时间复杂度:O(n * n!)
    一共有 n! 个结果。
    在这个特定版本的代码中,每次往深走一层,都要遍历 tmp 来查重(O(n)),所以总复杂度比使用 visited 数组的方法要高一点点。但由于全排列本身的复杂度极高(阶乘级别),常数项的差异在实际中影响不算太大。
  • 空间复杂度:O(n)
    递归调用栈的深度最大为 n。tmp 数组的长度最大也为 n。不计算最终返回结果 res 的占用空间。

代码

golang

go 复制代码
package main

import "fmt"

func permute(nums []int) [][]int {
    res := [][]int{}
    tmp := []int{}

    // 定义闭包 dfs
    var dfs func()
    dfs = func() {
        // 1. 终止条件:路径长度等于数组长度,说明找到了一个全排列
        if len(tmp) == len(nums) {
            // 深拷贝:防止后续 tmp 被修改影响已存入的结果
            tmpCopy := make([]int, len(tmp))
            copy(tmpCopy, tmp)
            res = append(res, tmpCopy)
            return
        }

        // 2. 遍历所有可用的数字
        for i := 0; i < len(nums); i++ {
            // 检查 nums[i] 是否已经在 tmp 中
            found := false
            for j := 0; j < len(tmp); j++ {
                if nums[i] == tmp[j] {
                    found = true
                    break
                }
            }

            // 如果没找到,说明可用
            if !found {
                tmp = append(tmp, nums[i]) // 做选择
                dfs()                      // 递归
                tmp = tmp[:len(tmp)-1]     // 撤销选择(回溯)
            }
        }
    }

    dfs()
    return res
}

func main() {
    nums := []int{1, 2, 3}
    fmt.Println(permute(nums))
}
///!!or!!
func main() {
	// 使用 bufio 提高读取速度
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Split(bufio.ScanWords) // 按空格分割输入

	// 处理多组输入直到 EOF
	for {
		if !scanner.Scan() {
			break
		}
		
		var n int
		fmt.Sscanf(scanner.Text(), "%d", &n)

		nums := make([]int, n)
		for i := 0; i < n; i++ {
			if scanner.Scan() {
				fmt.Sscanf(scanner.Text(), "%d", &nums[i])
			}
		}

		// 获取结果并打印
		res := permute(nums)
		for _, p := range res {
			for i, v := range p {
				if i > 0 {
					fmt.Print(" ")
				}
				fmt.Print(v)
			}
			fmt.Println()
		}
	}
}

78. 子集

题目

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = 1,2,3

输出:\[,1,2,1,2,3,1,3,2,3,1,2,3]

示例 2:

输入:nums = 0

输出:\[,0]

提示:

1 <= nums.length <= 10

-10 <= numsi <= 10

nums 中的所有元素 互不相同

总体思路

  • 时间复杂度:O(n * 2^n)。一共有 2^n 个子集,每个子集需要用 O(n) 的时间进行 copy 拷贝到结果集里。
  • 空间复杂度:O(n)。不计结果集的空间,递归栈深度和 tmp 的最大长度均为 n。如果算上结果集,总空间为 O(n * 2^n)。

代码

golang

go 复制代码
package main

import "fmt"

func subsets(nums []int) [][]int {
	res := [][]int{}
	tmp := []int{}

	// 定义闭包 dfs,pathLen 表示当前正在考虑 nums 的哪个数字
	var dfs func(pathLen int)
	dfs = func(pathLen int) {
		// 1. 终止条件:所有数字都已经做过决定了
		if pathLen == len(nums) {
			// 【大坑注意】必须深拷贝,否则最后 res 里全是空切片
			tmpCopy := make([]int, len(tmp))
			copy(tmpCopy, tmp)
			res = append(res, tmpCopy)
			return
		}

		// 2. 分支一:选择当前数字
		tmp = append(tmp, nums[pathLen]) // 把数字加进来
		dfs(pathLen + 1)                 // 递归,去决定下一个数字
		tmp = tmp[:len(tmp)-1]           // 回溯,把刚才加进来的数字弹出去(撤销选择)

		// 3. 分支二:不选择当前数字
		dfs(pathLen + 1)                 // 直接递归,去决定下一个数字
	}

	dfs(0)
	return res
}

func main() {
	fmt.Println(subsets([]int{1, 2, 3}))
}

//!!!!!!!
func main() {
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Split(bufio.ScanWords)

	for {
		if !scanner.Scan() {
			break
		}
		var n int
		fmt.Sscanf(scanner.Text(), "%d", &n)

		nums := make([]int, n)
		for i := 0; i < n; i++ {
			if scanner.Scan() {
				fmt.Sscanf(scanner.Text(), "%d", &nums[i])
			}
		}

		res := subsets(nums)
		for _, subset := range res {
			for i, v := range subset {
				if i > 0 {
					fmt.Print(" ")
				}
				fmt.Print(v)
			}
			fmt.Println() // 每个子集占一行
		}
	}
}

39. 组合总和

题目

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

输入:candidates = 2,3,6,7, target = 7

输出:\[2,2,3,7]

解释:

2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。

7 也是一个候选, 7 = 7 。

仅有这两种组合。

示例 2:

输入: candidates = 2,3,5, target = 8

输出: \[2,2,2,2,2,3,3,3,5]

示例 3:

输入: candidates = 2, target = 1

输出: \[\]

提示:

1 <= candidates.length <= 30

2 <= candidatesi <= 40

candidates 的所有元素 互不相同

1 <= target <= 40

总体思路

这道题的核心要求是:从给定的数组 candidates 中找出所有和为 target 的组合,每个数字可以被无限次重复选取,且组合不能重复(例如 2, 2, 3 和 2, 3, 2 视为同一个)。

🧠 总体思路

为了解决"组合去重"和"数字可重复选"两个问题,我们需要在递归时引入一个 start 参数。

  1. 组合去重:每次从下标 start 开始遍历,这保证了我们选出的数字组合是有序的(比如选了 2 之后,只能选 2 或 2 之后的数,不能回头选前面的数),从而避免出现 2, 3, 2 这种重复组合。
  2. 数字可重复:在递归进入下一层时,传入的 start 是当前下标 i,而不是 i + 1。这样下一层依然可以从当前数字开始选,实现无限次重复。
  3. 剪枝优化:如果当前累加和已经超过 target(即 target - candidatesi < 0),直接停止当前分支的搜索。
  • 时间复杂度:O(S),其中 S 是所有可行解的长度之和。由于是无限重复选取,解的数量会非常庞大(如果包含 1,可能是指数级甚至阶乘级,称为"组合爆炸"),最坏情况是指数级别。
  • 空间复杂度:O(target)。递归树的最大深度取决于剩余目标值 target 能够被最小的数字减多少次。比如最小是 1,那么深度就是 target。不计算结果集 res 所占的空间。

代码

golang

go 复制代码
package main

import "fmt"

func combinationSum(candidates []int, target int) [][]int {
	res := [][]int{}
	tmp := []int{}

	var dfs func(start, remain int)
	dfs = func(start, remain int) {
		// 1. 终止条件:如果剩余目标值为 0,说明找到了一个合法组合
		if remain == 0 {
			tmpCopy := make([]int, len(tmp))
			copy(tmpCopy, tmp) // 深拷贝
			res = append(res, tmpCopy)
			return
		}

		// 2. 遍历当前可选的数字
		for i := start; i < len(candidates); i++ {
			// 剪枝:如果当前数字比剩余目标值还大,直接跳过(因为数组全是正数,再加只会更大)
			if candidates[i] > remain {
				continue
			}

			// 做选择
			tmp = append(tmp, candidates[i])
			// 递归:注意这里传的是 i,而不是 i+1。因为数字可以重复使用
			dfs(i, remain - candidates[i])
			// 撤销选择(回溯)
			tmp = tmp[:len(tmp)-1]
		}
	}

	dfs(0, target)
	return res
}

func main() {
	candidates := []int{2, 3, 6, 7}
	target := 7
	fmt.Println(combinationSum(candidates, target))
	// 输出: [[2 2 3] [7]]
}
///or!!!!!!!
func main() {
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Split(bufio.ScanWords)

	for {
		if !scanner.Scan() {
			break
		}
		var n, target int
		fmt.Sscanf(scanner.Text(), "%d", &n)
		scanner.Scan()
		fmt.Sscanf(scanner.Text(), "%d", &target)

		candidates := make([]int, n)
		for i := 0; i < n; i++ {
			if scanner.Scan() {
				fmt.Sscanf(scanner.Text(), "%d", &candidates[i])
			}
		}

		res := combinationSum(candidates, target)
		for _, combination := range res {
			for i, v := range combination {
				if i > 0 {
					fmt.Print(" ")
				}
				fmt.Print(v)
			}
			fmt.Println()
		}
	}
}
相关推荐
Keep_Trying_Go4 小时前
Consistent-Point半监督人群计数算法详解
人工智能·深度学习·算法·计算机视觉·人群计数
在所不辞兄4 小时前
【零基础学智能仿真-69】综合项目——材料参数驱动的仿真研究
人工智能·深度学习·神经网络·算法·机器学习·1024程序员节
Tisfy5 小时前
LeetCode 3665.统计镜子反射路径数目:三个动态规划数组
leetcode·动态规划·代理模式·题解
怕浪猫5 小时前
Super-agent 在各大模型厂商的应用情况
算法·面试·github
海绵宝宝转agent6 小时前
LeetCode100 二叉树个人题解
算法·leetcode·深度优先
octopus_c6 小时前
C++ 模板详解:函数模板、类模板、特化与分离编译
开发语言·c++·算法
東隅已逝,桑榆非晚6 小时前
算法1. 双指针法-移动零
c++·算法
CQU_JIAKE7 小时前
11.1[A]
算法
文丑颜不良啊7 小时前
LeetCode1581~1600题目解析
java·leetcode