leetcode_78. 子集

78. 子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]



思路一,全集中的元素是否在子集中,全集中的每个元素选与不选,组成所有子集:

叶子节点做记录

递归终止条件是显示的:

if(idnex == len) {
  res.add(new ArrayList<>(stack));
  return;
}
  • 1.求子集是全集的一部分:讨论全集的每一个元素是否在子集中,求所有子集。

  • 转换为代码:每一个数选与不选。也可以理解为全集的每一个元素在不在子集中。

  • 每一步:选不选全集中的数

  • 叶子节点就是答案,为了得到叶子节点,可以使用深度优先搜索或广度优先搜索,在遍历到叶子节点时,将叶子节点的数字添加到结果集中。

    • 1.1.深度优先遍历:
    • 不断的向下深入,直到到达底部,不撞南墙不回头,再向上弹,再去不断向下深入,直到到达底部,遍历的路径像一根伸缩的绳子。
    • 只用一个状态变量path记录叶子节点数的状态,向下深入一层:添加一个元素到状态变量path, 返回上一层,从状态变量中弹出一个元素。
    • 1.2.广度优先遍历:
    • 不同层之间的差异很大,遍历时,节点状态差异很大,比如:第三层尾到第四层头。所以需要每一个节点都创建一个状态节点,因为遍历时,一个状态变量不方便记录所有状态。
    • 对比深度优先遍历,遍历时,不同节点状态差异很小。
  • 2.为什么回溯算法效率高:

    • 深度优先搜索有回退的过程,所以又叫回溯算法。可以用较少的状态变量去搜索所有可能的状态值。在状态空间较大的情况下,使用较少的变量完成搜索,强大搜索算法之一。
    • 为什么能用较少状态变量?
      • 因为深度搜索,一条道走到黑,然后在向上一层弹回,继续向下层搜索。状态节点之间的差异较小。方便用一个状态变量记录节点状态。
        在这里插入图片描述
  • 3.体会回溯算法状态重置。

  • 3.1先考虑不选择,后考虑选择

private void dfs(int[] nums, int index, int len,
                     Stack<Integer> stack, List<List<Integer>> res) {
        if(idnex == len) {
            res.add(new ArrayList<>(stack));
            return;
        }

        // 不选,直接进入下一层
        dfs(nums, idnex + 1, len, stack, res);

        // 选了,进入下一层
        stack.add(nums[index]);
        dfs(nums, index + 1, len, stack, res);
        stack.pop();
    }

不选,直接进入下一层:
状态变量不变,不用改变和重置

 dfs(nums, index + 1, len, stack, res);

选了nums[index],stack.add(nums[index]),;
进入下一层,dfs(nums, index + 1, len, stack, res);
返回上一层后:stack.pop(); 重置状态变量

stack.add(nums[index]);
dfs(nums, index + 1, len, stack, res);
stack.pop();
  • 3.2 在考虑选择
private void dfs(int[] nums, int index, int len,
                     Stack<Integer> stack, List<List<Integer>> res) {
        if(idnex == len) {
            res.add(new ArrayList<>(stack));
            return;
        }

        // 选了,进入下一层
        stack.add(nums[index]);
        dfs(nums, index + 1, len, stack, res);
        stack.pop();
        
		 // 不选,直接进入下一层
        dfs(nums, idnex + 1, len, stack, res);
    }

在这里插入图片描述
在这里插入图片描述

  • 代码
public class Solution {

    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        int len = nums.length;
        if (len == 0) {
            return res;
        }
        Stack<Integer> stack = new Stack<>();
        dfs(nums, 0, len, stack, res);
        return res;
    }

    private void dfs(int[] nums, int index, int len,
                     Stack<Integer> stack, List<List<Integer>> res) {
        if (index == len) {
            res.add(new ArrayList<>(stack));
            return;
        }
        // 当前数可选,也可以不选

        // 不选,直接进入下一层
        dfs(nums, index + 1, len, stack, res);

        // 选了有,进入下一层
        stack.add(nums[index]);
        dfs(nums, index + 1, len, stack, res);
        stack.pop();
    }
public class Solution {

    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        int len = nums.length;
        if (len == 0) {
            return res;
        }
        Stack<Integer> stack = new Stack<>();
        dfs(nums, 0, len, stack, res);
        return res;
    }

    private void dfs(int[] nums, int index, int len,
                     Stack<Integer> stack, List<List<Integer>> res) {
        if (index == len) {
            res.add(new ArrayList<>(stack));
            return;
        }
        // 当前数可选,也可以不选
        // 选了有,进入下一层
        stack.add(nums[index]);
        dfs(nums, index + 1, len, stack, res);
        stack.pop();

        // 不选,直接进入下一层
        dfs(nums, index + 1, len, stack, res);
    }


思路2:全排列思想,一位一位的去考虑,这些位置可以填哪些元素。

不是在叶子节点做记录,而是所有节点都记录下来

递归终止条件是隐式的:for循环完后,递归结束

    private void find(int[] nums, int begin, List<Integer> pre) {
        res.add(new ArrayList<>(pre)); // 没有显式递归终止
        for (int i = begin; i< nums.length; i++) {
            pre.add(nums[i]);
            find(nums, i + 1, pre);
            pre.remove(pre.size() - 1); // 组合问题,状态递归完成后要重置
        }
    }
  • 1.遍历过程

  • 第一层:

    • 第一个位置可以填的元素是3个,1, 2, 3
  • 第二层:

    • 第一个位置填1的节点第二个位置可以填的元素是2个,2, 3 --> (1,2), (1,3)
    • 第一个位置填2的节点,第二个位置可以填的元素是1个, 2 (2,3), 因为(2,1)=(1,2);
    • 第一个位置填3的节点,第二个位置可以填的元素是0个。
  • 第三层:

    • 第一个位置填1,2的节点,可以填3。
  • 按顺序考虑,填完之后不能回头,就不会重复。

  • 和全排列问题的不同。

在这里插入图片描述

  • 递归终止条件是隐式的:for循环完后,递归结束
private void find(int[] nums, int begin, List<Integer> pre) {
        res.add(new ArrayList<>(pre)); // 没有显式递归终止
        for (int i = begin; i< nums.length; i++) {
            pre.add(nums[i]);
            find(nums, i + 1, pre); // 下一层是i+1这个变量
            pre.remove(pre.size() - 1); // 组合问题,状态递归完成后要重置
        }
    }
  • 对比每一层的选择,递归终止条件是显式的,到达叶子节点递归就结束,记录叶子节点状态:
if(idnex == len) {
  res.add(new ArrayList<>(stack));
  return;
}
  • 在回溯的过程中记录结点。
import java.util.ArrayList;
import java.util.List;


public class Solution {

    private List<List<Integer>> res;

    private void find(int[] nums, int begin, List<Integer> pre) {
        // 没有显式的递归终止
        res.add(new ArrayList<>(pre));// 注意:Java 的引用传递机制,这里要 new 一下
        for (int i = begin; i < nums.length; i++) {
            pre.add(nums[i]);
            find(nums, i + 1, pre);
            pre.remove(pre.size() - 1);// 组合问题,状态在递归完成后要重置
        }
    }

    public List<List<Integer>> subsets(int[] nums) {
        int len = nums.length;
        res = new ArrayList<>();
        if (len == 0) {
            return res;
        }
        List<Integer> pre = new ArrayList<>();
        find(nums, 0, pre);
        return res;
    }
}
  • 在回溯的过程中记录深度。
public class Solution {

    public List<List<Integer>> subsets(int[] nums) {
        int size = nums.length;
        List<List<Integer>> res = new ArrayList<>();
        if (size == 0) {
            return res;
        }
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < size + 1; i++) {
            dfs(nums, 0, i, stack, res);
        }
        return res;
    }

    private void dfs(int[] nums, int start, int depth, Stack<Integer> path, List<List<Integer>> res) {
        if (depth == path.size()) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = start; i < nums.length; i++) {
            path.add(nums[i]);
            dfs(nums, i + 1, depth, path, res);
            path.pop();
        }
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 3};
        Solution solution = new Solution();
        List<List<Integer>> subsets = solution.subsets(nums);
        System.out.println(subsets);
    }
}

作者:liweiwei1419
链接:https://leetcode-cn.com/problems/subsets/solution/hui-su-python-dai-ma-by-liweiwei1419/


状态变量,回溯函数的参数
结束条件
选择列表,与第一步紧密相关
判断是否需要剪枝
做出选择,递归调用,进入下一层
撤销选择

1.递归树
已选数,可选数,start标记当前选择列表的起始位置,也就是每一层的状态
eg: 第一层可选数为1,2,3, 从start=0都可以选。
第二层对于第一层选择了1的分支,可选数为2,3,从start=1都可以选。第二层分支选了2,从start=2都可以选。

2.结束条件
很特殊,所有路径都应该加入结果集,所以不存在结束条件。或者说start参数越过数组边界的时候,程序自己跳过下一层递归,因此不需要手写结束条件,直接加入结果集。

3.选择列表
子集选择列表,是上一条选择路径之后的数?

4.判断是否需要剪枝
从递归树中看,路径没有重复,也没有不符合的条件,所以不需要剪枝

5.做出选择,for循环里面

5.撤销选择

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        // 递归树 
        // 结束条件:start参数越过数组,自动跳到下一层递归
        // 选择列表 nums
        // 判断是否需要剪枝 没有重复,也没有不符合的条件,
        // 做出选择,for循环里
        // 撤销选择

        List<List<Integer>> res = new ArrayList<>();
        if(nums == null || nums.length == 0) {
            return res;
        }

        Stack<Integer> path = new Stack<>();
        int start = 0;
        dfs(nums, start, path, res); // dfs 不断向下一层搜索,start记录这一层的状态,表示到达这一层,从哪个位置开始选择,
        // 深度方向,不断向下,start + 1; 每一层:都有一个循环,循环里面又有要走nums.length个dfs

        return res;
    }

    private void dfs(int[] nums, int start, Stack<Integer> path, List<List<Integer>> res) {
        res.add(new ArrayList(path)); // 每一个分支进入下一层时,将当前path添加到res中

        for(int i = start; i < nums.length; i++) { 
            path.add(nums[i]);
            dfs(nums, i + 1, path, res);
            path.pop();
        }
    }

    
}
  • 这个不是在每一次记递归结束后,将path添加到res中,而是
class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        // 递归树,每一层有两种选择,选或不选nums[i];
        // 递归结束条件:有nums.length层,到达nums.length层就向上弹回溯,而for那一种不是全部深入到nums.length层向上回溯,而是根据
        // 选择列表:不选,或nums[i], 二选
        // 判断是否需要剪枝:有重复,但是到达叶子节点记录,叶子节点是所有答案
        // 做出选择:
        // 撤销选择,回溯
        List<List<Integer>> res = new ArrayList<>();
        if(nums == null || nums.length == 0) return res;
        Stack<Integer> path = new Stack<>();

        dfs(nums, 0 , path, res);
        return res;
    }

    private void dfs(int[] nums, int index, Stack<Integer> path, List<List<Integer>> res) {
        if(index == nums.length) {
            res.add(new ArrayList(path));
            return;
        }

        dfs(nums, index+1, path, res);

        path.add(nums[index]);
        dfs(nums, index + 1, path, res);
        path.pop();
    }
}
    // 递归树: 层: 这一层的每个节点都有nums.length - start个选择;    递归树:层:这一层的每个节点都有选与不选nums[i]
    // 结束条件: start到达nums.length结束;     path.length到达nums.length
    // 剪枝:没有重复,不需剪枝;  叶子节点才统计,叶子节点没有重复,无需剪枝
    // 选择列表:从start后顺序选择;  选或不选nums[i]

参考:https://leetcode-cn.com/problems/subsets/solution/hui-su-python-dai-ma-by-liweiwei1419/

深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
题目描述: 给定一个只包含正整数的非空数组,是否可以将这个数组分成两个子集,使得两个子集的元素和相等。 示例: 输入:[1, 5, 11, 5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11]。 解题思路: 这是一道经典的 0-1 背包问题,可以使用动态规划或者回溯算法解决。 使用回溯算法,需要定义一个 backtrack 函数,该函数有三个参数: - 数组 nums; - 当前处理到的数组下标 index; - 当前已经选择的元素和 leftSum。 回溯过程中,如果 leftSum 等于数组元素和的一半,那么就可以直接返回 true。如果 leftSum 大于数组元素和的一半,那么就可以直接返回 false。如果 index 到达数组末尾,那么就可以直接返回 false。 否则,就对于当前元素,有选择和不选择两种情况。如果选择当前元素,那么 leftSum 就加上当前元素的值,index 就加 1。如果不选择当前元素,那么 leftSum 不变,index 也加 1。最终返回所有可能性的结果中是否有 true。 Java 代码实现: class Solution { public boolean canPartition(int[] nums) { int sum = 0; for (int num : nums) { sum += num; } if (sum % 2 != 0) { return false; } Arrays.sort(nums); return backtrack(nums, nums.length - 1, sum / 2); } private boolean backtrack(int[] nums, int index, int leftSum) { if (leftSum == 0) { return true; } if (leftSum < 0 || index < 0 || leftSum < nums[index]) { return false; } return backtrack(nums, index - 1, leftSum - nums[index]) || backtrack(nums, index - 1, leftSum); } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值