DFS

简介: -

深度优先搜索是递归实现的,是要搜索整个二叉树的,在这个搜索的基础上,再加点回溯/剪枝的操作就是这一类排列组合的题了,是这个关系


  • 空间复杂度为O(h)O(h),对空间复杂度高的考虑DFS
  • 不具备最短性

回溯

现实无法回到过去,就在程序回到过去吧!

与动态规划的区别

共同点

  • 用于求解多阶段决策问题。多阶段决策问题即:

  • 求解一个问题分为很多步骤(阶段);
  • 每一个步骤(阶段)可以有多种选择。

不同点

  • 动态规划只需要求我们评估最优解是多少,最优解对应的具体解是什么并不要求。因此很适合应用于评估一个方案的效果;
  • 回溯算法可以搜索得到所有的方案(当然包括最优解),但是本质上它是一种遍历算法,时间复杂度很高。

优化

由于回溯算法的时间复杂度很高,因此在遍历的时候,如果能够提前知道这一条分支不能搜索到满意的结果,就可以提前结束,这一步操作称为 剪枝

排序主要的作用,让程序在深搜的过程中尽量排除掉不能搜索到结果的分支,以节约时间。在回溯搜索这种时间复杂度很大的算法中,先排序再剪枝有些时候是有必要的。


练习

力扣

子集

基本思路

注意!!!

变量 t 所指向的列表 在深度优先遍历的过程中只有一份 ,深度优先遍历完成以后,回到了根结点,成为空列表。

在 Java 中,参数传递是 值传递,对象类型变量在传参的过程中,复制的是变量的地址。这些地址被添加到 res 变量,但实际上指向的是同一块内存地址,因此我们会看到 66 个空的列表对象。

解决的方法很简单,在 ans.add(new ArrayList<Integer>(t)); 这里做一次拷贝即可。

class Solution {

   List<Integer> t = new ArrayList<Integer>();

   List<List<Integer>> ans = new ArrayList<List<Integer>>();


   public List<List<Integer>> subsets(int[] nums) {

       dfs(0, nums);

       return ans;

   }


   public void dfs(int cur, int[] nums) {

       if (cur == nums.length) {

           ans.add(new ArrayList<Integer>(t));

           return;

       }

       t.add(nums[cur]);

       dfs(cur + 1, nums);

       t.remove(t.size() - 1);

       dfs(cur + 1, nums);

   }

}


全排列(无重复数据)

题目

分析

可能在这里回想为什么没有出现单体,例如[1]或者[1,2]这种,这里的判断条件和子集那道题不一样,

子集那题的计数,可能在nums.length-1时执行了一次,所以出现只有一个数的数组,执行几次便有几个

class Solution {

   public List<List<Integer>> permute(int[] nums) {

       List<Integer> List = new ArrayList<>();

       List<List<Integer>> res = new ArrayList<>();

       boolean[] st = new boolean[nums.length];

       if(nums.length==0)return res;

       DFS(nums,res,List,st);

       return res;

   }

   public void DFS(int[] nums, List<List<Integer>> res, List<Integer> List, boolean[] st){

       if(List.size() == nums.length){

           res.add(new ArrayList<>(List));

           return;

       }

       for(int i = 0; i < nums.length; i ++){

           if(!st[i]){

               List.add(nums[i]);

               st[i]=true;

               DFS(nums,res,List,st);

               List.remove(List.size()-1);

               st[i]=false;

           }

       }

   }

}

基础不牢,多练练这个,注意代码规范

总结:

全排列2(有重复数据)

分析

拿到题目,发现重复元素,便想到重复必有影响,例如两个相同元素交换可能又是一组数据,想到了剪枝

因为全排列,所以数据不够的不会被选中,我们只需要让重复的数据无法继续即可

  • 如果这个数和前一个数相同且前一个数还没有用过,continue
  • 比如11'2可以,1'12不行,因为1'在1前面使用,1还未使用

class Solution {

   public List<List<Integer>> permuteUnique(int[] nums) {

       List<Integer> list = new ArrayList<Integer>();

       List<List<Integer>> res = new ArrayList<List<Integer>>();

       boolean[] st = new boolean[nums.length];

       Arrays.sort(nums);//要去重,故先排序

       DFS(nums,list,res,st);

       return res;

   }

   public void DFS(int[] nums, List<Integer> list, List<List<Integer>> res, boolean[] st){

       if(list.size()==nums.length){

           //注意不要写成List<Integer>

           res.add(new ArrayList<>(list));

           return;

       }

       for(int i = 0; i < nums.length; i ++){

           if(i>0&&nums[i]==nums[i-1]&&!st[i-1])continue;

           if(!st[i]){

               list.add(nums[i]);

               st[i]=true;

               DFS(nums,list,res,st);

               list.remove(list.size()-1);

               st[i]=false;

           }

       }

   }

}

组合总和

分析

还是全排列,然后将符合的加入,能优化就是剪枝,关键点排序,主要的作用,让程序在深搜的过程中尽量排除掉不能搜索到结果的分支

DFS一般就那几个参数

如果不是全局变量,DFS(答案合集,单个答案,条件,目标,计数)

第一种成员

class Solution {

   private List<List<Integer>> res = new ArrayList<>();

   public List<List<Integer>> combinationSum(int[] candidates, int target) {

       List<Integer> path = new ArrayList<>();

       

       Arrays.sort(candidates);

       backtrack(path,candidates,target,0,0);

       return res;

   }

   public void backtrack(List<Integer> path,int[] candidates, int target, int sum, int begin){

       if(sum==target){

           res.add(new ArrayList<>(path));

           return;

       }

       for(int i = begin; i < candidates.length; i ++){

           int rs = sum + candidates[i];

           if(sum<=target){

               path.add(candidates[i]);

               backtrack(path,candidates,target,rs,i);

               path.remove(path.size() - 1);


           }else{

               break;

           }

       }

   }

}

第二种非成员(推荐)-方法本类就是适用于各种情况

class Solution {

   public List<List<Integer>> combinationSum(int[] candidates, int target) {

       List<List<Integer>> ans = new ArrayList<List<Integer>>();

       List<Integer> combine = new ArrayList<Integer>();

       dfs(candidates, target, ans, combine, 0);

       return ans;

   }


   public void dfs(int[] candidates, int target, List<List<Integer>> ans, List<Integer> combine, int idx) {

       if (idx == candidates.length) {

           return;

       }

       if (target == 0) {

           ans.add(new ArrayList<Integer>(combine));

           return;

       }

       // 直接跳过

       dfs(candidates, target, ans, combine, idx + 1);

       // 选择当前数

       if (target - candidates[idx] >= 0) {

           combine.add(candidates[idx]);

           dfs(candidates, target - candidates[idx], ans, combine, idx);

           combine.remove(combine.size() - 1);

       }

   }

}

括号生成

分析

还是全排列的思想,但是有个限制:左右符号都等于n时才算数据,可以采用剪枝(左边大于右边时结束,因为是有效的,必须保证左括号先有)

class Solution {

   public List<String> generateParenthesis(int n) {

       List<String> res = new ArrayList<>();

       DFS("",0,0,res,n);

       return res;

   }

   public void DFS(String curStr,int left, int right, List<String> res, int n){

       if(left==n && right==n){

           res.add(curStr);

           return;

       }

       if(left<right)return;

       if(left<n){

           DFS(curStr+"(",left+1,right,res,n);

       }

       if(right<n){

           DFS(curStr+")",left,right+1,res,n);

       }

   }

}

寻找重复的子树

/**

* Definition for a binary tree node.

* public class TreeNode {

*     int val;

*     TreeNode left;

*     TreeNode right;

*     TreeNode() {}

*     TreeNode(int val) { this.val = val; }

*     TreeNode(int val, TreeNode left, TreeNode right) {

*         this.val = val;

*         this.left = left;

*         this.right = right;

*     }

* }

*/

class Solution {

   Map<String,TreeNode> coll = new HashMap<String,TreeNode>();

   Set<TreeNode> res = new HashSet<TreeNode>();

   public List<TreeNode> findDuplicateSubtrees(TreeNode root) {

       dfs(root);

       return new ArrayList<>(res);

   }

   public String dfs(TreeNode root){

       if(root==null)return "";

       StringBuilder sb = new StringBuilder();

       sb.append(root.val).append("(");

       sb.append(dfs(root.left)).append(")(");

       sb.append(dfs(root.right)).append(")");

       String serial = sb.toString();

       if(coll.containsKey(serial)){

           res.add(coll.get(serial));

       }else{

           coll.put(serial,root);

       }

       return serial;

   }

}

目录
相关文章
|
4月前
|
机器学习/深度学习 存储 算法
动态规划算法深度解析:0-1背包问题
0-1背包问题是经典的组合优化问题,目标是在给定物品重量和价值及背包容量限制下,选取物品使得总价值最大化且每个物品仅能被选一次。该问题通常采用动态规划方法解决,通过构建二维状态表dp[i][j]记录前i个物品在容量j时的最大价值,利用状态转移方程避免重复计算子问题,从而高效求解最优解。
625 1
|
11月前
|
存储 Java
几种锁:偏向锁、轻量级锁、重量级锁、自旋锁
**锁机制简介:** Java中,锁分为偏向锁、轻量级锁和重量级锁。偏向锁适用于单一线程多次获取同一锁的情况,减少无竞争下的性能消耗;轻量级锁在多线程竞争时通过自旋避免阻塞,提升效率;重量级锁则是在自旋超时或多个线程竞争时,将其他线程阻塞以防止CPU空转,但性能较低。锁的升级路径为:偏向锁 → 轻量级锁 → 重量级锁,且不可降级。偏向锁默认开启,可通过JVM参数调整或关闭。
449 13
几种锁:偏向锁、轻量级锁、重量级锁、自旋锁
|
人工智能 算法 安全
详解贪心算法
详解贪心算法
|
10月前
|
算法 Java
算法系列之数据结构-Huffman树
Huffman树(哈夫曼树)又称最优二叉树,是一种带权路径长度最短的二叉树,常用于信息传输、数据压缩等方面。它的构造基于字符出现的频率,通过将频率较低的字符组合在一起,最终形成一棵树。在Huffman树中,每个叶节点代表一个字符,而每个字符的编码则是从根节点到叶节点的路径所对应的二进制序列。
329 3
 算法系列之数据结构-Huffman树
「Mac畅玩鸿蒙与硬件46」UI互动应用篇23 - 自定义天气预报组件
本篇将带你实现一个自定义天气预报组件。用户可以通过选择不同城市来获取相应的天气信息,页面会显示当前城市的天气图标、温度及天气描述。这一功能适合用于动态展示天气信息的小型应用。
604 38
「Mac畅玩鸿蒙与硬件46」UI互动应用篇23 - 自定义天气预报组件
|
消息中间件 NoSQL Java
Redis系列学习文章分享---第六篇(Redis实战篇--Redis分布式锁+实现思路+误删问题+原子性+lua脚本+Redisson功能介绍+可重入锁+WatchDog机制+multiLock)
Redis系列学习文章分享---第六篇(Redis实战篇--Redis分布式锁+实现思路+误删问题+原子性+lua脚本+Redisson功能介绍+可重入锁+WatchDog机制+multiLock)
650 0
|
存储 算法 C++
【C++数据结构——图】图的邻接矩阵和邻接表的存储(头歌实践教学平台习题)【合集】
本任务要求编写程序实现图的邻接矩阵和邻接表的存储。需掌握带权有向图、图的邻接矩阵及邻接表的概念。邻接矩阵用于表示顶点间的连接关系,邻接表则通过链表结构存储图信息。测试输入为图的顶点数、边数及邻接矩阵,预期输出为Prim算法求解结果。通关代码提供了完整的C++实现,包括输入、构建和打印邻接矩阵与邻接表的功能。
608 10
|
存储 运维 监控
自动化运维:使用Shell脚本简化日常任务
【9月更文挑战第35天】在IT运维的日常工作中,重复性的任务往往消耗大量的时间。本文将介绍如何通过编写简单的Shell脚本来自动化这些日常任务,从而提升效率。我们将一起探索Shell脚本的基础语法,并通过实际案例展示如何应用这些知识来创建有用的自动化工具。无论你是新手还是有一定经验的运维人员,这篇文章都会为你提供新的视角和技巧,让你的工作更加轻松。
383 2
|
C语言
数据结构基础详解(C语言):图的基本概念_无向图_有向图_子图_生成树_生成森林_完全图
本文介绍了图的基本概念,包括图的定义、无向图与有向图、简单图与多重图等,并解释了顶点度、路径、连通性等相关术语。此外还讨论了子图、生成树、带权图及几种特殊形态的图,如完全图和树等。通过这些概念,读者可以更好地理解图论的基础知识。
1285 8
|
存储
【基础知识整理】图的基本概念 & 邻接矩阵 & 邻接表
图(graph)是由一些点(vertex)和这些点之间的连线(edge)所组成的; 其中,点通常被成为&quot;顶点(vertex)&quot;,而点与点之间的连线则被成为&quot;边或弧&quot;(edege)。 通常记为,G=(V,E)。