示例分析总结递归,回溯,DFS以及动态规划的各自特点与区别
一、递归,回溯,DFS以及动态规划的概念
1.1 递归的定义
我们将程序自身调用的编程方式称为递归;
递归在程序设计中有着广泛的应用,将复杂问题转化为原问题相似的小问题求解,以达到通过少量的代码实现解题过程所需的多次重复计算的目的;
比较经典递归有阶乘和斐波纳契数列
- 阶乘
int fac(int n) { if (n == 1) { //递归终止条件 return 1; } return n*f(n-1); }
- 斐波纳契数列
int fib(int n) { if (n == 1 || n == 2) { //递归终止条件 return 1; } return fib(n-1) + fib(n-2); }
在回溯和深度优先搜索算法中都能见到递归的身影,需要说明一点,实现回溯和DFS算法并非只有递归方式,也可以用非递归方式,如栈实现,毕竟递归的本质也是先进后出;
1.2 回溯的定义
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,
而满足回溯条件的某个状态的点称为“回溯点”。
回溯算法思想是在一个类似枚举的搜索尝试的过程同时增加剪枝(条件约束)可以大幅度降低计算量,其目的是在搜索尝试过程中寻找满足问题的解,当发现已不满足边界的约束条件时,就“回溯”返回,尝试别的路径。
- 回溯算法的实现代码框架
vector<vector<int> > res; //满足要求的所有组解的集合 vector<int> list;// 满足条件的一组解 void dfs(当前状态){ if (当前状态到达边界) res.push_back(list); //更新一组解 return ; //表示找到满足条件的一组解 if (当前状态不满足约束条件) //剪枝 return ; //回溯进入下个节点 for 遍历所有下一个可能的状态 产生下一个新的状态 dfs(下一个新的状态) 回到上一个状态 }
例如,一组简单有序数组a = [1,2,3,4,5],求出和为5的解,当我们累计1+2+3时,发现已经超过5,后面的元素4,5不再进行累计,这用到回溯的剪枝功能,是对搜索过程中的一种优化;
1.3 DFS的定义
深度优先搜索适用于遍历或图或搜索树的算法,DFS是一个不断探查和回溯的过程。在探查的每一步,算法都有一个当前的顶点。最初的当前顶点,作为起始顶点。每一步探查过程中,首先对当前顶点V进行访问,并将该点的访问标志visited[v] = true.接着在v的所有邻接顶点中找出未被标志的过一个点,将其作为下一步的探查的当前顶点,倘若当前顶点的所有邻接顶点都被标志过,则退回一步,将前一步所访问的顶点重新取出,作为探查的当前顶点,重复上述过程,直到最初指定起始顶点的所有邻接顶点都被访问为止。
从DFS定义中不难发现,深度优先搜索算法也用到回溯,DFS与回溯关键区别在于DFS是一般应用于树和图的结构,回溯算法应用范围更广不限于固定数据结构,DFS搜索过程记录图或树的搜索完整路径,而回溯有剪枝功能在求解过程中不保留树或图的完整路径。
DFS应用题例如,力扣上的岛屿问题求解。
1.4 动态规划定义
动态规划是将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法。
适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解。
经典背包问题,最长回文子串以及搜索算法中常见的编辑距离问题可以用动态规划解决
以上列举的问题除了利用动态规划算法解决外,也可以使用DFS算法解决,DP算法主要在于难以构造设计,DFS相对更易理解。
二、例题分析讲解(C++)
例题1 对二叉树进行先序、中序和后序三种遍历,应用深度优先搜索树的算法,采用递归和非递归两种实现方式:
#include <iostream.h> #include "stack.h" template <class T> struct BintreeNode { T data; BintreeNode<T> *leftChild, *rightChild; BintreeNode(): leftChild(NULL), rightChild(NULL) {} BintreeNode(T x, BintreeNode<T> *l = NULL, BintreeNode<T> *r = NULL):data(x), leftChild(l), rightChild(r) {} }; // 递归方式实现先序遍历 void PreOrder(BintreeNode<T> * subTree, void (* visit)(BintreeNode<T> *p)) { if (subTree != NULL) { visit(subTree); PreOrder(subTree->leftChild, visit); PreOrder(subTree->rightChild, visit); } }; // 递归方式实现中序遍历 void InOrder(BintreeNode<T> * subTree, void (* visit)(BintreeNode<T> *p)) { if (subTree != NULL) { InOrder(subTree->leftChild, visit); visit(subTree); InOrder(subTree->rightChild, visit); } }; // 递归方式实现后序遍历 void PostOrder(BintreeNode<T> *subTree, void (* visit)(BintreeNode<T> *p)) { if (subTree != NULL) { PostOrder(subTree->leftChild, visit); PostOrder(subTree->rightChild, visit); visit(subTree); } }; // 非递归算法利用栈实现先序遍历 void PreOrder(void (* visit)(BintreeNode<T> *p)) { stack<BintreeNode<T> * > S; BintreeNode<T> *p = root; S.Push(NULL); while (p != NULL) { visit(p); if (p->rightChild != NULL) S.Push(p->rightChild); if (p->leftChild != NULL) p = p->leftChild; else S.Pop(p); } }; // 非递归算法利用栈实现中序遍历 void InOrder(void (* visit)(BintreeNode<T> *p)) { stack<BintreeNode<T> *> S; BintreeNode<T> *p = root; while (p != NULL || !S.IsEmpty()) { while (p != NULL) { S.Push(p); p = p->leftChild; } if (!S.IsEmpty()) { S.Pop(p); visit(p); p = p->rightChild; } } }; struct stkNode { BintreeNode<T> *ptr; enum tag {L, R}; stkNode(BintreeNode<T> *N = NULL): ptr(N), tag(L) {} }; // 非递归算法利用栈实现后序遍历 void PostOrder(void (* visit)(BintreeNode<T> *P)) { stack<stkNode<T>> S; stkNode<T> w; BintreeNode<T> *p = root; while (!S.IsEmpty()) { while (p != NULL) { w.ptr = p; w.tag = L; S.Push(w); p = p->leftChild; } int continuel = 1; while (continuel && !S.IsEmpty()) { S.Pop(w); p = w.ptr; switch(w.tag) { case L: w.tag = R; S.Push(w); continuel = 0; p = p->rightChild; break; case R: visit(p); break; } } } cout << endl; };
例2 回溯算法应用场景排列组合,实例如下:
- 全排列 (DFS)
vector<vector<int>> permute(vector<int>& nums) { int len = nums.size(); vector<vector<int>> res; if (len == 0) { return res; } vector<int> path; vector<bool> used(len); dfs(nums, len, 0, path, used, res); return res; } void dfs(vector<int>& nums, int len, int deepth, vector<int> path, vector<bool> used, vector<vector<int>>& res) { if (deepth == len) { res.emplace_back(path); return; } for (int i = 0; i < len; i++) { if (used[i] == true) continue; path.emplace_back(nums[i]); used[i] = true; dfs(nums, len, deepth + 1, path, used, res); path.pop_back(); used[i] = false; } }
- 组合数之和(回溯)当target<0时,提前结束本次递归再回退
void dfs(vector<int>& candidates, int target, vector<vector<int>>& ans, vector<int> combine, int idx) { if (target < 0) { return; } if (target == 0) { ans.emplace_back(combine); return; } for (int idx = pos; idx < candidates.size(); idx++) { combine.emplace_back(candidates[idx]); dfs(candidates, target - candidates[idx], ans, combine, idx);//注意此时idx不变,允许重复数字 combine.pop_back();//对组合进行回溯 } } vector<vector<int>> combinationSum(vector<int>& candidates, int target) { vector<vector<int>> ans; vector<int> combine; dfs(candidates, target, ans, combine, 0); return ans; }
动态规划例题:
int minDistance(string word1, string word2) { int n = word1.length(); int m = word2.length(); if (m*n == 0) return m+n; vector<vector<int>>dp(n+1,vector<int>(m+1)); for (int i = 0; i < n+1; i++) { dp[i][0] = i; } for (int j = 0; j < m+1; j++) { dp[0][j] = j; } for (int i = 1; i < n+1; i++) { for (int j = 1; j < m+1; j++) { int left = dp[i-1][j] + 1; int right = dp[i][j-1] + 1; int left_down = dp[i-1][j-1]; if (word1[i-1] != word2[j-1]) left_down += 1; dp[i][j] = min(left, min(right, left_down)); } } return dp[n][m]; }
三、总结
深度优先遍历和回溯算法相似之处在于通过递归方式实现dfs过程中用到回溯思想,dfs适用于完成的图或树形结构,回溯应用场景更广,如排列组合上以及dfs递归场景下都有回溯算法的身影,动态规划算法适用于有重叠子问题和最优子结构性质的问题,并且记录所有子问题的结果,因此动态规划方法所耗时间往往远少于朴素解法,在数学,计算机科学,生物信息等领域有着广泛的应用,将复杂问题简化成相对简单子问题,同时每个子问题相互依赖,即下一个子问题的解依赖于上一个子问题的求解结果,动态规划难点在于方案设计的构思,有些场景下,如背包问题回溯算法也可作为动态规划的降级解决方法。