1. 递归的理解
当一个问题比较复杂时,我们设法将他转化为一个或多个形式一样但问题规模较小的问题来解决,并且当小问题解决后能够推导出大问题的解。若小问题仍然无法解决则继续将小问题转化为形式相同的更小问题,直到问题规模小到足以轻而易举地解决。这里将一个大问题转化或者说分解出的小问题称为该大问题的子问题。
对于每个子问题,我们需要思考三件事:
- 整个递归的终止条件。
- 一级递归需要做什么?
- 应该返回给上一级的返回值是什么?
因此,也就有了我们解递归题的三部曲:
- 找整个递归的终止条件:递归应该在什么时候结束?
- 找返回值:应该给上一级返回什么信息?
- 本级递归应该做什么:在这一级递归中,应该完成什么任务?
2. 递归中的循环理解
如果一个大问题分出来的子问题不一致,需要有多种形式的子问题来完成,就需要使用循环中嵌套递归的方式来完成了,一般多在回溯算法中出现,如果说情况一致的简单递归是一棵一直向下的单链表,那么情况多样的递归就是一棵n叉树,树的叶子节点就是最后的所有答案
2.1 从数组中取出n个元素的所有组合
数组为{1, 2, 3, 4, 5, 6},那么从它中取出4个元素的组合有哪些:
{1,2,3,4,5,6}中取4个数的所有组合 = 第一个数取1,后三个数为{2,3,4,5,6}中取3个数的所有组合 + 第一个数取2,后三个数为{3,4,5,6}中取3个数的所有组合 + 第一个数取3,后三个数为{4,5,6}中取3个数的所有组合
再把上面的第一个小问题拆分:
{2,3,4,5,6}中取3个数的所有组合 = 第一个数取2(全局来看是第二个数取2),后2个数为{3,4,5,6}中取2个数的所有组合 + 第一个数取3(全局来看是第二个数取3),后2个数为{4,5,6}中取2个数的所有组合 + 第一个数取4(全局来看是第二个数取4),后2个数为{5,6}中取2个数的所有组合
以此类推
现在思考,每个小问题的终止条件,我们设数组可供选取的元素总量是m,我们需要取出的元素数量为n,当 n > m 递归停止进行返回,
2.2 特殊的二进制序列
将题目中1 与 0 看做左右括号后,相当于就是最后不断递归特殊的子串,让最后得到的特殊序列中,左括号要尽可能排在前面,同时还要保证整个括号序列是正确可匹配的。
终止条件:当递归到的序列长度为0或者是2,此时无法有更好的字典序了或者压根不存在字典序。
if(s.size() <= 2)
return s;
返回给上一级:返回自己在这一层中优化排列了以后的子串
本级递归任务:下一层传递给本层特殊二进制序列,可能有一个或者多个,如果是一个,就直接往上一层传,如果是多个,那就按照字典序进行排序,再把排好序的几个子串进行合并,并返回给上一层
由于每层都需要合并当前层的几个子串,所以我们在每层都需要定义一个容器,来存放当前层的每个子串,并最终将这个容器里的内容排序合并再上传
由于每次我们都要遍历一遍我们当前层的这个子串,所以在每一层都需要有一个标记,来将当前层的子串分割为更小子串,从而来进行下一层的递归
鉴于容器和标记都是每一个层独有的,不可以共享,所以我们对于容器和标记的定义以及初始化应该是在递归函数体内构造,从而不让这两个变为全局共享的变量。
vector<string> cur;
int left = 0;
最后,如何确定遍历过的字符正好构成一个特殊子串呢,可以采用一个标记,当扫到的字符为“1”,就加一,扫到的字符为“0”就减一,当该标记为0时,代表扫过了一个子串,把该子串拿去递归即可得到该子串的字典序最大特殊序列。
在递归的时候有一个细节就是,由于我们的子串一定是这个特殊序列,所以其开头一定为“1”,结尾一定为“0”,所以它不能完整的拆分为两个特殊字串,拿括号比喻:( ( ) ( ( ) ) ),这个括号序列不可能完全拆分为两个正确的子序列,只有把头尾掐掉,再去划分才有可能拆为两个,所以在递归的时候,我们也需要把头尾掐了再去递归。
for(int i = 0;i < s.size();i++)
{
if(s[i] == '1')
cnt++;
else
{
cnt--;
if(cnt == 0)
{
string cur_s = makeLargestSpecial(s.substr(left+1, i-left-1));
cur.push_back("1"+cur_s+"0");
left = i+1;
}
}
}
sort(cur.begin(),cur.end(),[](const string& a, const string& b){return a>b;});
string result = accumulate(cur.begin(), cur.end(), ""s);