Bootstrap

深度优先搜索(dfs)题目合集

全排列问题

P1706 全排列问题 - 洛谷 | 计算机科学教育新生态

深度优先搜索原理(纯个人理解)

和循环做对比:

两层循环嵌套的情况下,假设内层循环运行到一半时我不想运行了,我想把外层循环的状态往回拨(例如for循环一般用一个进度标记变量i表示循环进度,修改i的值用以控制循环)重新开始。

但是直接终止内层循环的话,即使修改外层循环的循环进度,也无法回到外层循环之前的状态,或者说需要付出巨大的条件。

我们知道,递归是函数调用自己本身。底层有专门的寄存器记录当前函数的进度,当函数递归时,在函数所在的栈空间会生成一个和自己一样的函数先执行新生成的函数执行完了会回到原来的函数原来的进度继续进行

利用这个特性,我们用递归代替外层的循环,内部再用一个循环,这样内层循环运行到一半不想运行了,直接退出当前函数,回到上一层函数发生递归的语句后,继续运行。这样做的最大好处是可以以非常低的成本回到之前的状态

这种利用递归和栈的特性实现的枚举方式被称作深度优先搜索depth first search简称dfs)算法。

例如这题枚举全排列,假设枚举到1,2,3时,输出123,然后3的空位让出来,3后面没有数字了,回到2,2的后面还有一个3,于是有了1,3;再到第三个空位,此时有1,2可以用,1之前用过了,就把2放上去,于是就有了132。

利用这个思路,即可完成全排列的枚举。而这个思路也是dfs的通用思路。

用这个算法时一定要想清楚状态,以及回来(书上和资料上叫回溯)的时候,哪些状态要还原,哪些不还原。例如这里1之前用过了,后面就不能用了,等之前的用完了才能用。

参考程序

#include<stdio.h>

int flag[10];
int ans[10],p;

//深度优先搜索
void dfs(int n,int depth){
	if(depth>n){
		int i=0;
		for(i=1;i<=n;i++){
			printf("%5d",ans[i]);
		}
		printf("\n");
		return;
	}
	int i=1;
	for(i=1;i<=n;i++){
		if(flag[i]==1)
			continue;
		flag[i]=1;//标记状态,下层枚举时跳过这层 
		ans[++p]=i;//记录答案 
		dfs(n,depth+1);//前往下一层 
		--p;//回溯 
		flag[i]=0;//回到之前的状态
	}
}

int main() {
    int n;
	scanf("%d",&n);
	dfs(n,1);
	return 0;
}

但这个算法的时间复杂度一般都是平方甚至是指数级,若递归太深还会导致栈溢出。所以dfs也有很大的局限性。为了降低复杂度和减少栈溢出,有很多技巧(有的大佬称之为剪枝)可以进行,后面有刷到相关的题就提一下。

dfs通用模版

模拟两层循环的dfs模版:

void dfs(int depth, 其他参数){//depth用于标记递归深度,可以没有
    if(depth太深不给往下走了或其他情况){
        整理状态;
        return;
    }
    if(其他情况(如果有的话,虽然可以放在一个if里处理)){
        整理状态;
        return;
    }
    //...
    for(初始情况;是否枚举完了所有情况;){
        if(情况1不符合条件)
            continue;
        if(情况2不符合条件)
            continue;
        //...
        标记状态;
        处理当前情况;
        dfs(depth+1,其他参数);
        将标记的状态回溯到原来的样子;
    }
}

个人更喜欢用这个,因为在比赛的环境下,自己并不能一下子想到所有不符合条件的情况或状态,所以用这个模版的话想到一个添加一个,也不用修改之前的判断。

当然,模版并不唯一,只要能理解整个思路,以及整理好实际问题的所有状态,就能用dfs解决问题。

;