力扣——dfs和回溯(17题 电话号码的字母组合)

一、前言

其实,自我感觉,回溯法和dfs算法没什么特别大的区别,dfs里渗透着回溯,回溯的前提是dfs(目前从所刷的题目上来看是这样的)。但是,我从别人的博客上了解到:

1.概述:
(1)回溯法:
是一种选优搜索法(试探法),被称为通用的解题方法,这种方法适用于解一些组合数相当大的问题。通过剪枝(约束+限界)可以大幅减少解决问题的计算量(搜索量)。
(2)DFS:
是一种用于遍历或搜索树或图的算法。沿着树的深度遍历树的节点,尽可能深的搜索树的分支。

2.区别:
(1)回溯是一种更通用的算法。可以用于任何类型的结构,其中可以消除域的部分,无论它是否是逻辑树。
(2)DFS是与搜索树或图结构相关的特定回溯形式。它使用回溯作为其使用树的方法的一部分,但仅限于树/图结构。

3.总结:
回溯和 DFS 之间的区别在于回溯处理隐式树而 DFS 处理显式树。这似乎微不足道,但它意味着很多。当通过回溯访问问题的搜索空间时,隐式树在其中间被遍历和修剪。然而对于 DFS 来说,它处理的树/图是明确构造的,并且在完成任何搜索之前已经抛出了不可接受的情况,即修剪掉了。
因此,回溯是隐式树的DFS,而DFS是回溯而不修剪。

4.原文链接:
https://blog.csdn.net/Herishwater/article/details/98989512

也许,目前没有接触到剪枝问题(约束+限界),对二者的理解并不是那么透彻,今后,刷到这类题目时,再总结二者的区别。

二、题目

下面来看一道力扣17题——电话号码的字母组合,这道题就用到了dfs和回溯。
题目是这样的:

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

电话

输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
说明:
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。

1.分析
这道题是组合问题,和排列组合类似,比排列组合的难度要低。一涉及到组合问题,我们首先要想到的方法就是遍历,解决遍历的算法,首当其冲应该想到dfs、bfs。
2.思路步骤
(1)用dfs怎么做呢,先不说,首先看看大体思想main()。
返回的是一个链表,该链表存储的是字符串型的数据,即List,因此先new一个链表,写上返回值:

 public List<String> letterCombinations(String digits) {
        List<String> res=new ArrayList<>();//List是接口,不能实例化,即不能new List();
        if (digits.length()==0) {//不要忽略digits为空字符串的特殊情况!
			return res;
		}
        //通过dfs方法获取res
        return res;
    }

(2)这一步是关键的一步,也就是dfs怎么来写?
dfs里要有几个参数:

a.遍历的对象——String str
可以是抽象的,这道题里是“23”,实际上是“23”对应的字母abc def。
b.目前遍历到的位置——int index
指遍历到2-abc,还是3-def。
c.用于存储每次遍历完的结果——StringBuilder sb
不可以是String型的,因为String是定长的,不能扩充。属于每次dfs遍历后的结果,也就是初始值中要new的原因。
d.存储最终结果——List<String> res
属于整个方法,初始值中不用再new了。
综上:
dfs(String str, int index, StringBuilder sb, List<String> res)
初始值为:
dfs(digits, 0, new StringBuilder(), res);

(3)整个题目代码的灵魂——dfs的实现
第一步:遍历的时候要想回溯以进行下一次遍历,就必须要有截止条件:

if (index == str.length()) {//"23"长度为2,角标是0 1 的时候不截止,+1变成2之后向res中添加sb,sb指的是每次遍历之后的结果,不是最终结果。
	res.add(sb.toString());
	return;
}

第二步:候选节点遍历过程:

for (char c : m[str.charAt(index) - '0']) {
	sb.append(c);
	dfs(str, index + 1, sb, res);
	sb.deleteCharAt(sb.length() - 1);
}

第二步叙述过程:
“外循环” for循环第一次:‘a’打头
index=0,即m[2],for循环遍历m[2],即abc,
sb=>a

dfs index=1,即m[3],for循环遍历m[3],即def,

“内循环” for循环第一次:ad打头
sb=>ad
dfs index=2截止,sb.deleteCharAt(sb.length() - 1),删掉‘d’, sb=>a

“内循环” for循环第二次:ae打头
sb=>ae
dfs index=2截止,sb.deleteCharAt(sb.length() - 1),删掉‘e’, sb=>a

“内循环” for循环第三次:af打头
sb=>af
dfs index=2截止,sb.deleteCharAt(sb.length() - 1),删掉‘f’, sb=>a

内循环完成,回溯==》继续“外循环”
“外循环” for循环第二次:‘b’打头
sb=>b

“内循环” for循环第一次:bd打头
sb=>bd
dfs index=2截止,sb.deleteCharAt(sb.length() - 1),删掉‘d’, sb=>b

“内循环” for循环第二次:be打头
sb=>be
dfs index=2截止,sb.deleteCharAt(sb.length() - 1),删掉‘e’, sb=>b

“内循环” for循环第三次:bf打头
sb=>bf
dfs index=2截止,sb.deleteCharAt(sb.length() - 1),删掉‘f’, sb=>b

内循环完成,回溯==》继续“外循环”
“外循环” for循环第三次:‘c’打头
sb=>c

“内循环” for循环第一次:cd打头
sb=>cd
dfs index=2截止,sb.deleteCharAt(sb.length() - 1),删掉‘d’, sb=>c

“内循环” for循环第二次:ce打头
sb=>ce
dfs index=2截止,sb.deleteCharAt(sb.length() - 1),删掉‘e’, sb=>c

“内循环” for循环第三次:cf打头
sb=>cf
dfs index=2截止,sb.deleteCharAt(sb.length() - 1),删掉‘f’, sb=>c

注:每次截止的时候都要往res里添加每次遍历完得到的结果sb。
3.完整代码

//可以和全排列的代码比较学习
class Solution {
//char[][]最好定义成共享变量,局部变量的话每次都要访问,比较浪费时间
     char[][] m=new char[][]{
        {},{},{'a','b','c'},{'d','e','f'},
        {'g','h','i'},{'j','k','l'},{'m','n','o'},
        {'p','q','r','s'},{'t','u','v'},{'w','x','y','z'}
    };
    public List<String> letterCombinations(String digits) {
        List<String> res=new ArrayList<>();
        if (digits.length()==0) {
			return res;
		}
        dfs(digits,0,new StringBuilder(),res);
        return res;
    }

    void dfs(String str,int index,StringBuilder sb,List<String> res){
       
        if(index==str.length()){
            res.add(sb.toString());
            return;
        }

        for(char c:m[str.charAt(index)-'0']){
            sb.append(c);
            dfs(str,index+1,sb,res);
            sb.deleteCharAt(sb.length()-1);
        }
    }
}

三、全排列

//全排列 [A,B,C]
public class Permutations {
	public static void main(String[] args) {
		getPermutations("ABC");
	}
	public static void getPermutations(String A){
		char[] arr=A.toCharArray();
		Arrays.sort(arr);
		getPermutationCore(arr, 0);
	}
	public static void getPermutationCore(char[] arr,int k){
		ArrayList<String> res=new ArrayList<String>();
		if (k==arr.length) {
			res.add(new String(arr));
//			新掌握的字符串的方法:
//			res.add("asdf");
//			System.out.println(String.join(",,,", res));
			
//			String[] arrStr=new String[]{"LEGAL","MAIN","CREATE"};
//			System.out.println("数组拼接" + String.join(",", arrStr));
			
			System.out.println(String.join(",",res));//?
		}
		for (int i = k; i <arr.length; i++) {
			swap(arr,k,i);
			getPermutationCore(arr, k+1);
			swap(arr,k,i);
		}
	}
	public static void swap(char[] arr,int i,int j) {
		char t=arr[i];
		arr[i]=arr[j];
		arr[j]=t;
	}
}

四、总结

遇到排列或者是组合问题,要想到dfs+回溯,接下来要学习的连通块问题虽然不属于排列组合,但是解决这个问题的方法有两种:dfs遍历、并查集。所以,今后遇到题目首先分析用哪种方法合适,要善于把问题抽象出一种数据结构或者是一类算法思想。

如有错误之处,望广大网友批评指正!

发布了27 篇原创文章 · 获赞 2 · 访问量 928

猜你喜欢

转载自blog.csdn.net/wcy8733996wcy/article/details/105324904