从经典情景来看,DFS算法很容易理解,虽然模拟起来很伤脑筋,但还是很容易明白其中的逻辑。DFS算法来遍历图时,需要一个vis[]数组用于记录结点是否已经被访问,然后顺着连接的点一直深度优先遍历过去即可。但是,我们应该思考一下,DFS算法的本质思想是什么,从而在解决其他具体问题时使用DFS算法的改编形式来更好地解决问题。本质就是借助递归手段完成深度优先的遍历。可以写出下面伪代码:
void DFS(){
if(到达递归边界){ //也可能没有这一句,而由下面的循环条件直接判断了递归边界
//可改编处
return;
}
//可改编处
while/for深度优先的条件{
//可改编处
操作;
}
//可改编处
return;
}
于是,现在从源点s到目的点t之间有多条路径时,已知每个点的前一个点(用vector<int> pre[MAXN]存储),我们也可以借助DFS的思想来将所有路径存储在一个二维数组里(可以使用vector<vector<int> >,再用一个tempPath变长数组临时存放单条路径)。
可以写出下面核心代码:
void DFS(int t, int s){
if(t == s){
tempPath.push_back(t);
path.push_back(tempPath);
tempPath.pop_back();
return;
}
tempPath.push_back(t); //*
for(int i=0; i<pre[t].size(); i++){
DFS(pre[t][i],s); //*
}
tempPath.pop_back(); //*
}
以上面的代码为例,我们不需要每次使用DFS算法时都在脑子里模拟一遍计算机 执行DFS的流程,我们只需要知道什么时候可以使用DFS、怎么使用DFS、以及DFS中代码块不同位置会产生什么效果即可。
不过,我们现在还在可以来模拟一个上面代码的执行流程,给大家一个DFS运行的直观感受。
可以发现,我们只需要确定下面代码中语句1和语句2的作用即可:
DFS(...){
递归边界
语句1;
DFS(...);
语句2;
}
它们分别在递归调用DFS之前和之后。
在递归调用DFS之前的语句,会随着递归不断入栈执行。在DFS之后的语句,会随着递归不断执行并退栈。也就是对于语句1与语句2之间循环的每一个DFS,都共用语句1和语句2,先语句1,然后把DFS看做一个已经封装好的解决好的相同子问题的答案,再结合语句2得到最终结果。说白了,DFS依然使用的是一个分治思想。
如上例,从目的点t开始找前点pre直到找到源点s,要得到的路径存在tempPath里,那么可以把问题分解为:
语句1:(把t存到tempPath里),
DFS:(再把某一个pre[t]到源点s的路径存到tempPath里),
语句2:(再把t从tempPath里弹出)。
总而言之,假设调用一次DFS()我们就完成了我们需要的DFS的功能即可,在调用DFS()前后,我们只需实现一次DFS的功能,就能将全局问题利用递归分解为子问题层层解决。
不过这里要注意全局变量和函数内定义的变量的不同,全局变量在递归入栈时改变了,退栈时不会自动还原,需要手动还原,如这里的语句2:tempPath.pop_back(); 而函数体内定义的变量在递归出栈时会自动释放,同变量名变为外一层函数的变量值。
上面的DFS实现可以形象地说:目的点t来过tempPath数组,然后目的点前的某一点pre[t]到源点s的路径来过tempPath数组,然后又走了,然后t点也走了。注意目的点前的某一点pre[t]到源点s的路径相较于t点先来也先走,因为是调用的DFS的完整功能。