前文我说动态规划/回溯算法就是二叉树算法两种不同思路的表现形式,相信能看到这里的读者应该也认可了我这个观点。但有细心的读者经常问我:东哥,你的理解思路让我豁然开朗,但你好像一直没讲过 DFS 算法?
这个细节上的差别是什么呢?其实就是「做选择」和「撤销选择」到底在 for 循环外面还是里面的区别,DFS 算法在外面,回溯算法在里面。
为什么有这个区别?还是要结合着二叉树理解。这一部分我就把回溯算法、DFS 算法、动态规划三种经典的算法思想,以及它们和二叉树算法的联系和区别,用一句话来说明:
动归/DFS/回溯算法都可以看做二叉树问题的扩展,只是它们的关注点不同:
- 动态规划算法属于分解问题的思路,它的关注点在整棵「子树」。
- 回溯算法属于遍历的思路,它的关注点在节点间的「树枝」。
- DFS 算法属于遍历的思路,它的关注点在单个「节点」。
怎么理解?我分别举三个例子你就懂了:
第一个例子,给你一棵二叉树,请你用分解问题的思路写一个 count
函数,计算这棵二叉树共有多少个节点。代码很简单,上文都写过了:
你看,这就是动态规划分解问题的思路,它的着眼点永远是结构相同的整个子问题,类比到二叉树上就是「子树」。
你再看看具体的动态规划问题,比如 [动态规划框架套路详解] 中举的斐波那契的例子,我们的关注点在一棵棵子树的返回值上:
第二个例子,给你一棵二叉树,请你用遍历的思路写一个 traverse
函数,打印出遍历这棵二叉树的过程,你看下代码:
不难理解吧,好的,我们现在从二叉树进阶成多叉树,代码也是类似的:
这个多叉树的遍历框架就可以延伸出 [回溯算法框架套路详解]中的回溯算法框架:
你看,这就是回溯算法遍历的思路,它的着眼点永远是在节点之间移动的过程,类比到二叉树上就是「树枝」。
第三个例子,我给你一棵二叉树,请你写一个 traverse
函数,把这棵二叉树上的每个节点的值都加一。很简单吧,代码如下:
你看,这就是 DFS 算法遍历的思路,它的着眼点永远是在单一的节点上,类比到二叉树上就是处理每个「节点」。
好,请你仔细品一下上面三个简单的例子,是不是像我说的:动态规划关注整棵「子树」,回溯算法关注节点间的「树枝」,DFS 算法关注单个「节点」。
有了这些铺垫,你就很容易理解为什么回溯算法和 DFS 算法代码中「做选择」和「撤销选择」的位置不同了,看下面两段代码:
看到了吧,你回溯算法必须把「做选择」和「撤销选择」的逻辑放在 for 循环里面,否则怎么拿到「树枝」的两个端点?