关于DFS的几个讨论

 

下面内容是我这几天求解DFS相关题目过程中困扰自己的几个点。

递归函数的参数与全局变量的转化

DFS是递归函数的一种。对于所有的递归函数,它们的函数体内会调用自身。既然函数体是完全一样的,那么这种自身的调用要想收敛就必须是参数变量有所推进。比如求解字符串相关问题时推进的是下标,从0开始,一直搜索到str.size(),再比如数独问题,推进的是行与列。

对于大多数递归函数,这些变量参数都是直接写成递归函数的参数的。但实际上,它们也能事先定义成全局变量的形式。这两者是等价的~  比如 dfs(int n), 它的函数体内必然有类似 dfs(n-1) 这样的递进形式,参数n会向前发展,这种发展要么达到有解的边界条件,要么碰壁然后回溯。 递归函数的所有推进、回溯的过程都是由它的参数实现的。

用斐波那契数求解为例,除了循环形式外,还有如下所示的递归形式:

int fib(int n) {
	if (n == 1|| n==2) return 1;
	return fib(n - 1) + fib(n - 2);
}

这里参数n是递归的核心,但整个函数都没有去改变n的值,所以用n-1能实现带有自动回溯功能的推进。如fib(n-1)求出了fib函数向后走一步的结果,但因为n本身没变,所以在求fib(n-2)之前已经相当于回溯完了。

这种参数的形式可以等价得转化成如下所示的全局变量形式。只不过这时候,我们需要手动去推进和回溯。并且在调用该函数时需要先初始化这个全局变量。

int n;
int fib() {
	if (n == 1 || n == 2) return 1;
	n-=1;
	int f1 = fib();
	n += 1;
	n-=2;
	int f2 = fib();
	n += 2;
	return f1 + f2;
}

那么第二种全局变量的形式这么麻烦有什么用呢?我总结了两个用处:首先,很多dfs问题所涉及的变量非常多,通常都是一大群变量共进退。这时候全写成参数的形式会非常冗长;其次,一些参数不是简单的int类型,而是一个STL容器、一个矩阵或者结构体数组等等,这些参数在进行推进和回溯的过程中,不是简单的n-1就能完成,通常都是“赋值-->推进-->回溯擦除”这样的流程,这时候能提前定义成全局变量,就能省去参数列表中非常长的一串类型定义等等。

一般化后就是下面的形式。

//函数参数
void dfs(int K) {
	if (...) {};//边界条件
	for (...) {
		if (...) {
			dfs(K - 1);
		}
	}
}
//全局变量
int K;
void dfs() {
	if (...) {};//边界条件
	for (...) {
		if (...) {
			K -= 1;
			dfs();
			K += 1;
		}
	}
}

DFS参数的两个类别:推进回溯类、结果采集类

DFS问题的参数有两类,第一类是很长的一串,这些变量是共同推进/回溯的,所以会有改写、擦除的过程。包括用于轨迹标记以防重复的一些数组和矩阵、记录搜索轨迹的path等。  第二类是结果收集类的,一般是个vector数组,这个变量不参与推进/回溯,只在搜索到正确结果后进行改写(结果采集)。

第二类变量一般仅仅出现在需要得到全部解的问题中,因为这类问题里,搜索到一个解后会继续回溯去找下一个解,第一类变量会迅速回溯变化,使得解消失,所以需要一个容器来采集这些解。而对于只要求一个解的问题,搜索到解后会停止,所以把第一类变量的最后形式保存并输出就可以了。

全部解和一个解

DFS问题有的要求输出全部解,有的找到一个解即可。对于要求全部解的DFS,它的代码比较好写,因为不管过程如何总归要把所有的方向都遍历一次,暴力遍历就行。但是对于第二种问题,有一个很棘手的难题:当得到一个解后,怎么让递归的过程停下来??

当得到一个解后,搜索的过程无法用"return;"来停止,因为return只会返回这次递归调用,回溯到上一层后,上一层函数的for循环会继续去取下一个可行的值然后继续向下递归。除非这个return 能让上一层也停止,并且一路向上把所有的函数都停止。怎么才能实现这一点?我们不能只是傻傻得返回,而是需要让这个返回携带信息给上一层,告诉它们找到解了!这时候有两种做法,第一种是让DFS函数的返回类型变为bool型,一旦找到解就返回1,上一层接到1后继续返回1.  第二种是设置一个全局变量flag,找到解后将flag设置为1,函数只有在flag为0时才继续搜索。

剪枝的方法

有时候全部遍历是徒劳的,用剪枝可以极大地加速深搜的过程。根据所要剔除的点的类别,剪枝可以分成两类:一是筛选,二是截断。假设我们要for循环遍历 1 到 n 这些点,筛选的意思是有的点k不符合要求,忽略它并继续下轮循环,截断的意思是 k 点是临界点,k点之后的所有点都不符合要求,这时候需要退出循环。

我们知道dfs的函数体由两部分组成:if边界条件 和 for循环遍历。

筛选的写法:在if区用一个if判断语句返回,或者在for区用一个if判断语句进入下轮循环。

//写法一:在if区新增if语句return
void dfs(int start) {
	if (...) {};//找到了解
	if (start不符合要求) { return; }
	for (int i = start; i < n; i++) {
		遍历;
	}
}
//写法二:在for区用if语句进入下轮循环
void dfs(int start) {
	if (...) {};//找到了解
	for (int i = start; i < n; i++) {
		if (i不符合要求) continue;
		遍历;
	}
}
//写法三:在for区用if语句进入下轮循环(同上)
void dfs(int start) {
	if (...) {};//找到了解
	for (int i = start; i < n; i++) {
		if (i符合要求) {
			遍历;
		}
	}
}

截断的写法:让for循环停下来即可,可以用for循环内部的if..break,也可以直接把剪枝语句放在for循环的终止条件里。

//写法一:for循环中的if...break
void dfs(int start) {
	if (...) {};//找到了解
	for (int i = start; i < n; i++) {
		if (i不符合要求) break;
		遍历;
	}
}
//写法二:for的终止语句
void dfs(int start) {
	if (...) {};//找到了解
	for (int i = start; i符合条件 && i < n; i++) {
		遍历;
	}
}

 

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值