参考文章:
1、Leetcode Subset I & II 作者:算法鱼 (里面是java版本的代码)
2、剪枝算法(算法优化) 作者:瞭望天空
3、Leetcode 上面的一些评论和题解
一、什么是 DFS?
图的深度优先搜索(Depth First Search),和树的先序遍历比较类似。
具体做法是:假设初始状态是图中所有顶点均未被访问,则从某个顶点V出发,首先访问该顶点,然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和V有路径相通的顶点都被访问到。若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
显然,深度优先搜索是一个 递归 的过程。
概念很简单,来看看例题的代码吧!
二、Leetcode 例题:Subsets
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
来源:力扣(LeetCode)
分析一下题目,其实有很多方法解这道题的,暴力法(会超时)、动态规划、DFS… 这里当然将DFS+递归(DFS里面本来就是递归)了。
这道题是要求生成所有子集,那么首先我们有一个能返回所有子集的二维数组results, 和一个临时变量temp(一维数组,初始为空,将他push_back,), 当temp满足一定条件的时候,往results里面添加结果。
好,开始我们的DFS重头戏了。一起来想象一下,输入的数组是一幅 有向图,方向是从大到小。
比如说,输入: nums = [1,2,3]
我们把他想象成有向图,进行 深度优先搜索 ,从1开始:
(我们先不管代码, 先看下面的图片和推导过程)
[] //刚开始temp为空集合,空集合影视[1,2,3]的一个子集
[1] //然后我们来到了1
[1,2] //接着,像DFS一样,到了2,也就是12
[1,2,3] //DFS,到了3, 123
[1,3] //像DFS一样循环结束退回去(不过和DFS算法不同,这里访问了再访问一次。不过没差了,这道例题易于理解DFS。)
[2] //然后继续返回,向上面一样。
[2,3]
[3]
好了看特别厉害的代码:
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>>results; //建立存储结果的数组
vector<int>temp; //建立当前一个结果的数组(第一个结果为空)
getResult(results, 0, temp, nums);
return results;
}
void getResult(vector<vector<int>>&results, int InitIndex, vector<int>temps, vector<int>nums)
{
results.push_back(temps); //存储结果
for(int i= InitIndex; i<nums.size(); i++)
{
temps.push_back(nums[i]);
getResult(results, i+1, temps, nums); //注意:这是i+1,不是i
temps.pop_back();
}
}
};
剪枝算法常用于DFS和BFS,下面的这道例题,我们将详细讲解剪枝算法。
三、剪枝算法(Leetcode例题:90. Subsets II)
1、什么是剪枝算法?
在搜索算法中优化中,剪枝算法,就是通过某种判断,避免一些不必要的遍历过程。也就是说剪去了搜索树中的某些“枝条”,故称剪枝。应用剪枝优化的核心问题是设计剪枝判断方法,即确定哪些枝条应当舍弃,哪些枝条应当保留的方法。
2、剪枝算法注意事项:
(1)如果随便剪枝,把带有最优解的那一分支也剪掉了的话,剪枝也就失去了意义,所以,剪枝的前提是一定要保证不丢失正确的结果;
(2)在保证了正确性的基础上,我们应该根据具体问题具体分析,采用合适的判断手段,使不包含最优解的枝条尽可能多的被剪去,以达到程序最优化的目的;
(3)设计优化程序的根本目的,是要减少搜索的次数,使程序运行的时间减少。但为了使搜索次数尽可能的减少,我们又必须花工夫设计出一个准确性较高的优化算法,而当算法的准确性升高,其判断的次数必定增多,从而又导致耗时的增多。如何在优化与效率之间寻找一个平衡点,使得程序的时间复杂度尽可能降低,同样是非常重要的。
3、啰啰嗦嗦了一大堆,我们用一道题来易于理解吧:
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
来源:力扣(LeetCode)
好,现在来分析一下这道题。
举个例子,nums[1,2,2]
当i=0的时候由于我们用于存储所有已知集合的retList只含有一个[]元素,那么不存在重复问题,我们经过这一步可以得到retList: [] [1] 来到2的时候我们在看 由于也不存在重复我们的2可以和之前的retlist中的元素全组合一遍得到retList:[] [1] [2] [1,2]
重点来了!
等i=2来到这个重复的2的时候,我们发现他和前面的元素重复了,那么如果我们先不考虑重复的问题重复会得到[] [1] [2] [1,2] [2] [1,2] [2,2] [1,2,2],因为我们发现[2] [1,2]这部分是重复的部分是需要被踢出的部分 那么我们现在的目标就转变成了如何鉴别出引起重复的这一部分,然后在组合的时候跳过他们。我们回忆一下重复的这个[2] [1,2]来源于 2 这个元素和 [] [1] 组合导致的,因为在这个重复的2之前,已经有一个2和[] [1]发生过组合,所以这里再去组合 必然发生重复现象。
那怎么解决这个问题呢?
很简单,以上面那个为例,当循环到i=1的时候,加一个判断,判断nums[i+1]和nums[i]是不是一样,如果是一样,就没必要比较了。
while(i<nums.size()-1 && nums[i]==nums[i+1]) i++;
这样子就可以了吗,交上去,然后:
解答错误。因为我自己的测试数据是按顺序的,然后提交时不是按顺序的,所以加一句sort就可以了。
sort(nums.begin(), nums.end());
好,我们看一下代码,和上面那个差不多,只是加了两句话:
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<vector<int>>results;
sort(nums.begin(), nums.end());
vector<int>temp;
getResult(results, 0, temp, nums);
return results;
}
void getResult(vector<vector<int>>&results, int InitIndex, vector<int>temps, vector<int>nums)
{
results.push_back(temps);
for(int i= InitIndex; i<nums.size(); i++)
{
temps.push_back(nums[i]);
getResult(results, i+1, temps, nums);
temps.pop_back();
while(i<nums.size()-1 && nums[i]==nums[i+1]) i++;
}
}
};
我在接下来一星期继续做DFS,加油!