回溯算法的定义:
在包含问题的所有解的解空间树中,按照深度优先的策略,从根结点出发搜索解空间树。算法搜索至解空间树的任一结点时,总是先判断该结点是否肯定不包含问题的解。如果肯定不包含,则跳过对以该结点为根的子树的系统搜索,逐层向其祖先结点回溯。否则,进入该子树,继续按深度优先的策略进行搜索。回溯法在用来求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。而回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可以结束。
解空间树:依据待解决问题的特性,用树结构表示问题的解结构、用叶子表示问题的解的一颗树。
最初接触回溯算法,应该是走迷宫问题用到DFS。对于一些直观的图论问题,回溯是很符合常识的思路,“此路不通,原路返回,另寻他路”,这是非常朴素的回溯思路,以致于当时根本没有意识到这也算是一种算法思想。
一个错误的认识是,回溯只能用于图论问题,其背后的根源是缺少将一般的问题抽象出解空间树的能力,以及将一般问题分解为若干状态的能力。
重新认识回溯思想是遇到全排列问题,这个问题里没有明显的图和树,当时甚至没有往DFS上想,事后看了题解,苦笑、默叹,以为妙绝。
回溯与剪枝:回溯的本质是枚举和暴力,这意味着他的效率不会高到哪里去,常常需要剪枝来优化。
回溯算法小归纳:
1.回溯就是从当前结点开始递归,如果递归成功则返回true,递归失败就把当前结点移出解空间 (pop_back()),这样就完成了回溯
2.剪枝:如果当前结点不满足条件,则剪枝(在循环体中表现为break,在函数中体现为return false)
DFS深度遍历函数模板
void dfs()//参数用来表示状态
{
if(到达终点状态)
{
...//根据题意添加
return;
}
if(越界或者是不合法状态) //剪枝
return;
if(特殊状态)//剪枝
return ;
for(扩展方式)
{
if(扩展方式所达到状态合法)
{
修改操作;//根据题意来添加
标记;
if(越界或者是不合法状态) //剪枝
return;
if(特殊状态)//剪枝
return ;
dfs();
(还原标记);
//是否还原标记根据题意
//如果加上(还原标记)就是 回溯法
}
}
}
例题:
主要思路:利用深度遍历,每次划分数字,从第三次开始判断数字是否合规
//可以利用本题涉及的一些规则进行剪枝并回溯:如 每次划分的数不能超过int的上限;
每次划分的数的首位不能为0,除非本身为0;
拆分出的数大于前两个数的和则此次拆分失败;
剩余拆分的数的位数,小于已经拆分的数的位数,则拆分失败(因为都是非负数,那么,后面的数肯定大于等于前面拆分的数,即后面数的位数肯定大于等于前面数的位数)
上面一行的思路其实不对,因为对于最后一个拆分的数来说,没有后续的数了,而且仔细 想想,其实这条规则已经包含在“拆分出的数大于前两个数的和则此次拆分失败”这条中了。
class Solution {
public:
bool dfs(vector<int> &re,string &S,int index,int pre,long long sum)//index表示当前的位置
{
//每次拆分字符串,从第三次开始进行判断;
//判断第三次拆分的数是不是前两个数的和,不是就说明此路不通进行剪枝
//
if(index>=S.length()) return re.size()>=3;//题目要求至少是3个
long long cur=0;
for(int i=index;i<S.length();i++)
{
if (i > index && S[index] == '0') //如果出现首位为0的情况就剪枝
return false;
// if(i>index&&S[i]=='0') return false;
cur=cur*10+S[i]-'0';
if(cur>INT_MAX) return false;//题目要求要小于int的最大值
if(re.size()>=2)//第三个数开始才需要进行判断
{
if(cur<sum) continue;//继续拆
else if(cur>sum) return false;//此路不通
}
re.push_back(cur);
if( dfs(re,S,i+1,cur,pre+cur)) return true;
//else//回溯
//{
re.pop_back();
//}
}
return false;
}
vector<int> splitIntoFibonacci(string S) {
vector <int> re;
dfs(re,S,0,0,0);
return re;
}
};