算法班笔记 第六章 基于组合的DFS

第六章 基于组合的DFS

在非二叉树上的深度优先搜索(Depth-first Search)中,90%的问题,不是求组合(Combination)就是求排列(Permutation)。特别是组合类的深度优先搜索的问题特别的多。

  • 通过全子集问题 Subsets 了解组合类搜索的两种形式
  • 通过全子集问题 II 了解如何在搜索中去重
  • 使用非递归的方法实现全子集问题

全子集问题 

题目的意思就是求出一个集合的所有子集。假设这个集合中是没有重复元素的。你可能已经会做这个问题,但是你知道么,这个问题存在 4 种解法么?

  1. 使用比较通用的深度优先搜索方法
  2. 使用组合类搜索的专用深度优先搜索算法。(一层一层的决策每个数要不要放到最后的集合里。)
  3. 使用宽度优先搜索算法的做法(BFS)(一层一层的找到所有的子集)
  4. 递归版本,利用二进制的方式逐个枚举 subsets。

我们将从下面的 3 个方面来讲解这个问题:

  1. 如何用最简单的递归方式来实现?
    1. 答案在leaf上
  2. 如何用可以推广到排列类搜索问题的递归方式来实现?
    1. 模板记牢
  3. 如果集合中有重复元素如何处理?
    1. 找完所有可能性再去重,复杂度高
    2. 当前数字和前一个数字一样而且没被放到集合中,忽略

 

全子集 Follow up II: 如何非递归? 

用非递归(Non-recursion / Iteration)的方式实现全子集问题,有两种方式:

  1. 进制转换(binary)
  2. 宽度优先搜索(Breadth-first Search)

基于 BFS 的方法

在 BFS 那节课的讲解中,我们很少提到用 BFS 来解决找所有的方案的问题。事实上 BFS 也是可以用来做这件事情的。
用 BFS 来解决该问题时,层级关系如下:

第一层: []
第二层: [1] [2] [3]
第三层: [1, 2] [1, 3], [2, 3]
第四层: [1, 2, 3]

每一层的节点都是上一层的节点拓展而来。

 

什么是 Deep Copy? 

在 Subsets 的实现中,我们用到了如下的代码记录每一个找到的集合

results.add(new ArrayList<Integer>(subset));

事实上,这句话是调用了 ArrayList 的一个构造函数(Constructor),这个构造函数可以接受另外一个 ArrayList 作为其初始化的状态。

这种方式,我们叫它深度拷贝(Deep Copy),又叫做硬拷贝(Hard Copy)或者克隆(Clone,名字多得老纸记不住啊)。与之对应的就有 软拷贝(Soft copy),又名引用拷贝(Reference Copy)。

不使用 Deep copy 会怎样呢? 

我们来看看不使用 Deep copy 会怎样:

List<Integer> subset = new ArrayList<>();
subset.add(1);  // 此时 subset 是 [1]

List<List<Integer>> results = new ArrayList<>();
results.add(subset);  // 此时 results 是 [[1]]

subset.add(2);  // 此时 subset 是 [1,2]
results.add(subset);  // 此时你以为 results 是 [[1], [1,2]] 而事实上他是[[1,2], [1,2]]

subset.add(3);  // 此时 results 里是 [[1,2,3], [1,2,3]]

我们看到由于每一次 results.add 都是加入了相同的变量 subset,因此如果 subset 有变化,那么 result 里的记录就会同步的发生变化。原因是 results.add(subset) 加入的是 subset 的 reference,也就是 subset 在内存中的地址。那么事实上,当 results 里有两个 subset 的时候,相当于存储的是两个内存地址,而这两个内存地址又是一样的,才会导致如果这个内存地址里存的东西发生了变化,results 看起来就每个元素都发生了变化。

参数中引用传递

来看这段代码

public void func(List<Integer> subset) {
    subset.add(1);
}
public void main() {
    List<Integer> subset = new ArrayList<>();
    // 此时 subset 是 []
    func(subset);
    // 此时 subset 就是 [1] 了
}

可能你会奇怪,不是说修改参数不会影响到函数之外的参数么?也就是:

public void func(int x) {
    x = x + 1;
}
public void main() {
    int x = 0;
    func(x);
    // 此时 x 仍然是 0
}

上面两者的区别在于,人们习惯性的认为 subset.addx = x + 1 都是对参数进行了修改。而事实上,x = x + 1 确实是对参数进行了修改,这个修改只在函数func的局部有效,出了func回到main就失效了。而 subset.add 并没有修改 subset 这个参数本身,而只是在 subset 所指向的内存空间中增加了一个新的元素,这个操作是永久性的,不是临时的,是全局有效的,不是局部有效的。那么怎么样才是对 subset 这个参数进行了修改呢?比如:

public void func(List<Integer> subset) {
    subset = new ArrayList<Integer>();
    subset.add(1);
}
public void main() {
    List<Integer> subset = new ArrayList<>();
    // 此时 subset 是 []
    func(subset);
    // 此时 subset 还是 []
}

我们可以看到如果你的修改操作是 参数x = ... 那么这才是对参数x的修改,而 参数x.call_method() 并不是对参数 x 本身的修改。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值