(本文用于帮助初学者入门dfs算法,碍于笔者本人实力,对更深的无涉及,请原谅。)
算法介绍
一种用于遍历或搜索树或图的算法。 沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所在边都己被探寻过或者在搜寻时结点不满足条件,搜索将回溯到发现节点v的那条边的起始节点。整个进程反复进行直到所有节点都被访问为止。属于盲目搜索,最糟糕的情况算法时间复杂度为O(!n)。
算法基本思想
回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
算法过程
这种算法在每一步中优先考虑深度较浅的分支,也就是说,它总是优先探索深度优先的路径。
深度优先搜索的过程可以大致分为几个步骤:
- 初始化:首先对问题进行初始化,确定初始状态。
- 选择当前节点:选择当前节点作为初始节点。
- 扩展当前节点:将当前节点的所有子节点展开,将其子节点添加到待探索列表中。
- 重复步骤3和4,直到所有节点都被展开。
模板
void dfs(int step)
{
判断边界
{
相应操作
}
尝试每一种可能
{
if(满足条件)
{
标记
继续下一步dfs(step+1)
恢复初始状态(回溯的时候要用到)
}
}
}
回溯与剪枝
在DFS算法中,回溯是一个关键的概念。当算法发现当前路径不能达到目标或者已经探索完所有可能的分支时,它会回溯到上一个节点,尝试其他的路径。这种方法确保了算法能够找到所有可能的解决方案。
剪枝是一种优化技术,它通过提前排除那些不可能达到目标的路径来减少搜索空间。在全排列问题中,如果一个数字已经被选择,那么在当前的排列中就不需要再考虑它,这实际上是一种剪枝操作,可以避免无效的搜索和重复的排列
问题
排列
和组合、切割、子集问题最大区别是,排列问题for循环不需要记录索引,每次递归从0开始,因为排列有序,[1,2]和[2,1]是两个集合。
一个元素在一个排列中只能使用一次,因此需要数组记录哪些元素使用过。
去重,一个排列元素不重复,首先对数组进行排序,使重复元素挨在一起
然后保存节点、递归、回溯,然后回溯时,如果同一层分支中存在重复元素,则跳过,注意递归时的起始位置,从0开始
P1706 全排列问题
思路:
利用深搜的思想:
- 对于给定的序列,刚开始时我们知道序列的第一个元素,剩余的序列元素此时又可以看成是一个全排列的例子;
- 我们可以对剩余的序列进行与开始相同的操作,直到中只一个元素为止。这样我们就获得了所有的可能性。因此不难看出这是一个递归的过程。
分析可知我们要知道目前为止已经选了几个数(范围是1 到 n);
所以我们的递归函数可以含有该参数,
即 dfs(int x) 其中 x表示的是其中一种情况的第几个数
还需要知道跳出递归的条件:当选到 n 个数的时候,也就是这一组数已经选完了,就 return。
核心代码
在函数内部,首先判断是否已经找到了n个数字,即是否满足题目要求的组合长度。如果满足条件,则输出当前组合的结果。
如果没有找到足够的数字,则进入循环,遍历数组a中的所有元素。对于每个元素,检查其是否已经被选中(标记为1)。如果没有被选中,则将其赋值给答案数组d的第x个位置,并将对应的标记设置为1。然后递归调用dfs函数,将x加1,继续寻找下一个数字。
在递归调用返回后,需要将标记c[i]重新设置为0,以清除该数字在该位置的结果。这是回溯的过程,确保每次递归调用都能恢复到原始状态,以便尝试其他可能的组合。
void dfs(int x)//x表示的是其中一种情况选好的第几个数
{
if(x==n)//说明该情况所需要的数都已经找到
{
for(int v=0;v<n;v++)
{
printf("%5d",d[v]);//题目要求一个数字5格
}
printf("\n"); //输出
return ;//结束本次情况
}
else
{
for(int i=0;i<n;i++)
{
if(c[i]==0)
{
d[x]=a[i];//对答案数组进行赋值
c[i]=1;//对选中的数进行标记,防止重复
dfs(x+1);//递归,开始下一个数字的寻找,如果该情况已经找完,则接着下面运行
c[i]=0;
//清除标记(回溯),这个数字在这个位置的结果已经查询完毕
}
}
}
}
ac代码
#include<stdio.h>
int a[10], c[10]={0}, n, d[10];
//c数组对出现过的数字进行标记,d数组为记录答案的数组
void dfs(int x)
{
if (x == n)
{//已选好x个数字
for (int i = 0; i < n; i++)
{
printf("%5d", d[i]);
}
printf("\n");
return;
}
else
{
for (int i = 0; i < n; i++)
{
if (c[i] == 0)
{
d[x] = a[i];
c[i] = 1;
dfs(x + 1);
c[i] = 0;
}
}
}
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
a[i] = i + 1;
}
dfs(0);
}
组合
排列和组合的不同点: 组合: 就是指从给定个数的元素中取出指定个数(取出来就得到一种组合,不需要考虑元素的先后顺序) 排列: 就是指从给定个数的元素中取出指定个数的元素进行排序(元素之间有先后顺序)----不仅考虑哪几个元素,还要考虑元素之间的顺序
转化到代码上来看的话,与全排列不同的只有一点:那就是搜索不同的数的时候,每次搜索的起始位置不是0,而是当前已经搜到的数的下一位!
P1157 组合的输出
思路:
与全排列不同的是,组合数选定的数是没有顺序的
换言之,后选定的数一定不会出现在已经选定的数的前面
核心代码
在函数内部,首先判断q
是否等于r
,如果等于r
,则表示已经选择了r
个数字,此时将ans
数组中的元素按照从小到大的顺序打印出来,并换行返回。
如果q
不等于r
,则表示还未选择完所有的数字。接下来,通过循环遍历从ans[q] + 1
到n
的数字,这些数字是当前位置可能的选择。在循环中,将当前数字i
存放到ans
数组的第q+1
个位置,然后递归调用dfs(q + 1)
,继续寻找下一个数字。
#include <stdio.h>
int n, r;
int ans[25] = { 0 };//存放选好的r个数字
void dfs(int q) {
//已选好q个数字
if (q == r) {
for (int i = 1; i <= r; i++) {
printf("%3d", ans[i]);
}
printf("\n");
return;
}
else {//还未挑选好
//挑选第q+1个数字
for (int i = ans[q] + 1; i <= n; i++) {
//第q+1 个数字可能的值
ans[q + 1] = i;
//已经选好了q+1个数字
dfs(q + 1); //递归调用 ,继续寻找下一个
}
}
}
int main() {
scanf("%d %d", &n, &r);
dfs(0);
return 0;
}
关于dfs其实还有 棋盘,子集,切割 等分类,将会在笔者之后学习后尝试。本篇仅仅只是入门导引。