记忆化递归:
(1)参数个数为n,申请一个n维数组(哈希表),确保能够根据参数直接访问数组(哈希表)的值
(2)函数体:
如果数组(哈希表)已经记录过参数对应的函数值,直接返回该值
在所有出现return前,return的值记录到数组(哈希表)中
隐形回溯:
如果写成dfs(现在状态+())
//括号里为下一个状态的参数,这里隐含了回溯
记忆化递归的必要性:
普通的递归可能会重复求解某一值,类似斐波那契数列。同样的子问题可能会被求解多次,这样就会很慢很慢很慢
解决方法:我们把历史求解(子问题)记录下来,如果下次需要求解子问题,那么直接取出就好。其时间复杂度为O(1)
根据n-1的状态推出第n个状态。
Leetcode 22. 括号生成(递归+去重)
当n=1时,返回()
递归求n-1时的结果,由左右括号配对可知每一个结果字符串的长度都为2*(n-1)。在每一个结果的每一个位置+“()”,再经map去重就是n时的结果。
做选择
在一个n×m的网格状地宫中,每个格子内有一件具有特定价值的宝贝。小明从地宫的左上角出发,只能向右或向下移动,直至到达右下角的出口。在移动过程中,当他经过一个格子时,若该格子中的宝贝价值大于他当前手中任意一件宝贝的价值,他可以选择拿取这件宝贝(也可以选择不拿)。目标是让小明在走出地宫时,手中恰好拥有k件宝贝。请你计算出小明在满足上述条件下,走到终点能够成功获取k件宝贝的不同行动方案数量。
dfs(1,1,-1,0)表示从起点到终点的结果。
由于只能向右或向下移动,所以移动一直是单向的,不需要有vis标志。
K进制数
具体来说,给定整数N表示数的位数,以及基数K(K≥2),需要确定并求出所有满足以下条件的K进制数的数量:数字总共有N位;在这N位的K进制表示中,任何相邻的两位数字都不会同时是0。 给定两个数N和K, 要求计算包含N位数字的有效K-进制数的总数。
剪格子
方格分割
6x6的方格,沿着格子的边线剪开成两部分。 要求这两部分的形状完全相同。
试计算: 包括这 3种分法在内,一共有多少种不同的分割方法。 注意:旋转对称的属于同一种分割法。
蓝桥杯2013c++真题:振兴中华
深度优先搜索: 岛屿的周长
如果陆地有4个邻接陆地,那么这个陆地周长算为0;如果陆地有3个邻接陆地,那么这个陆地周长算1;如果陆地有2个邻接陆地,那么这个陆地周长算2;如果陆地有1个邻接陆地,那么这个陆地周长算3;如果陆地没有邻接陆地,那么这个路径周长算4。
求数组中的所有组合
{1,2,3}
//{1},{2},{3},{1,2},{1,3},{2,3},{1,2,3}
m行n列只能向右或者向下走输出所有路径
为什么需要回溯(撤销访问标记)?
在遍历所有可能的路径时,如果不撤销访问标记,马在回溯到上一个位置时,将无法再次经过刚刚访问过的 (nx, ny)
点,这会导致无法探索包含该点在内的其他可能路径。特别是在遍历整个棋盘寻找所有可能的途径时,我们必须确保每个位置在不同的搜索阶段都能有机会被再次访问,这样才能确保搜索的完整性。因此,每次递归调用结束后,都会恢复访问标志,使得马可以从其他方向再次到达此点,从而继续搜索其他可能的遍历方案。最终统计符合条件的方案数量 cnt。
DFS连通性 acwing
电话组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
方程的解数
用dfs解决:
对于每个xi,都有1~m种选择。dfs(index,s)表示考虑进行到下标为index的数和为s。
因为每个数都可以选1~m,互不影响,所以不用去重,直接选择即可。
for i from 1 to m:
dfs(index+1,s+k[index]*i^p[index]);
递归出口:
如果能到达下标为n,说明到了一个空节点(针对搜索树而言,xi的最大下标为n-1,对应搜索树最大高度为n-1,如果index==n,相当于到了一个空节点,这和二叉树的递归出口是空节点类似),
return
如果到达空节点时和为0,说明有1组解
if(index == n ) {if(s==0)ans++;return;}
不去重的全排列
计蒜客:族谱
vector<int>G[100000]表示树。G[i].push_back(j)表示i是j的父节点。
i是叶子节点:G[i].size()==0
计蒜客:王子救公主
思路:用两次深度优先搜索,数组vis1标记王子可能到达的点,数组vis2标记公主可能到达的点;
如果两个数组有重复的标记的坐标,那么王子可以救出公主
标记所有可能情况的深度优先搜索不需要回溯!如果回溯,那么会出错。
如果状态用参数传递的话,也可以直接改变参数,这一步相当于回溯(因为调用时没有改变原有参数的值)
【DFS笔记】什么时候需要回溯?什么时候不需要回溯?_迷宫问题什么时候用回溯什么时候不用回溯
一次性遍历: 该函数设计的目标是在满足边界条件的情况下,一次性遍历与给定坐标(x, y)相邻的所有可达格子。由于遍历是单向且不可逆的(一次遍历只会从一个未访问过的格子移动到另一个未访问过的格子),所以不需要撤销访问标记。
对于为什么遍历二叉树的时候好像没有回溯。
这里的“回溯”发生在每次递归调用返回时。对于给定的二叉树节点 root
:
- 首先访问根节点
root
。 - 然后递归地对左子树执行DFS,即调用
dfs(root.left)
。 - 当左子树的DFS完成并返回时,实际上就发生了“回溯”。因为DFS已经完成了对左子树的遍历,现在算法会回到上一层节点,即父节点
root
的上下文。 - 接着对右子树执行DFS,即调用
dfs(root.right)
。 - 同样,当右子树的DFS完成后返回时,又一次发生了“回溯”。
在整个过程中,每一步递归调用结束并返回时,都意味着回到了前一个节点的层次,继续处理下一个待访问的子节点或结束此次递归调用。这种从一个分支回退到其父节点并选择另一个分支的过程,就是DFS中的“回溯”行为。虽然代码中没有使用“回溯”这个词,但这是递归结构和深度优先搜索方法固有的逻辑。
当问题说到一共有n个数/n个选择,且按顺序依次选择时,一般都会用到这样的dfs模板
比如说8皇后问题:从8行依次做出选择,选择8次。状态参数为vis标记。
等等........
下面说说我对这个模板的理解:
首先,if放在dfs前面,就是说在选择之前就做了判断,这是什么意思呢?
(1)每一次dfs时,index总是增1,因为dfs调用的本质是做了一次选择,所以index参数可以理解为做选择的次数。当index传入0时,因为要做n次选择,所以index==n时才会return,而不是在n-1时return
(2)如果传入0,那么if判断时,选择的次数是已经选择的下标+1。因为下标0也算一次选择,所以下标0选择完后,在if判断时index为1,以此类推。
(3)状态参数就是做了index次选择后的状态。因为状态参数在做选择的时候更新,所以在if判断
时,对应的状态就是做了index次选择的状态(index初始传入0)
(4)因为在没有改变参数的值,所以这个模板自带回溯功能。
做了3次选择后,if判断时,index为3,下标为2的已经做完选择,状态参数是0~2状态。
可行性剪枝:
通过一些判断,砍掉搜索树上不必要的子树。
有时候,如果发现某个结点对应子树的状态不是我们想要的结果,那么没必要对这个分支进行搜索,砍掉这个子树,就是剪枝。
例:
n个数选k个数和为sum,如果发现当前和大于sum,那么之和不管怎么选和值都不可能是sum了
或者 如果选出数的个数大于k,那么之后不管怎么选数个数都大于k,
我们可以直接终止分支的搜索。
最优性剪枝:
在求解最优解的一类问题,通常可以用最优性剪枝
例1:
求解迷宫最短路时,如果发现当前的步数已经超过了当前最优解,那么从当前状态开始的搜索都是多余的,因为这样搜索下去永远得不到更优的解。通过这样的剪枝,可以省去大量冗余的计算
例2:
在搜索是否有可行解的过程中,一旦找到了一组可行解,后面所有的搜索都不必再进行了。
flag=false;
dfs:
if(flag)return;
重复性剪枝:
bool类型数组vis防止选重复的节点。
蓝桥:七段码
上图给出了七段码数码管的一个图示,数码管中一共有 7 段可以发光的二极管,分别标记为 a, b, c, d, e, f, g。选择一部分二极管(至少要有一个)发光来表达字符。在设计字符的表达时,要求所有发光的二极管是连成一片的。
请问,小蓝可以用七段码数码管表达多少种不同的字符?
二进制枚举+dfs连通块判断:
因为每个灯要么亮,要么不亮,可以看出1或0,一共7个灯,有2^7-1中情况(全不亮的情况舍弃),所以可以用二进制枚举。
1对应灯亮,0对应灯不亮
可以用一个大小为7数组ch表示灯是否亮(值1亮,值0亮)。
可以用一个7*7的二维数组G表示灯i与灯j是否直接相连,相连为1,否则为0。
acwing迷宫 DFS连通性问题
迷宫可以看成是由 n∗n 的格点组成,每个格点只有2种状态,.
和#
,前者表示可以通行后者表示不能通行。在某个格点时,只能移动到上下左右四个方向之一的相邻格点上,要从点A走到点B,问在不走出迷宫的情况下能不能办到。
疑问:为什么这个地方不需要回溯?
蓝桥杯:带分数
???
8皇后
搜索策略如下:从第0行开始,依次给每一行放置一个皇后,对于一个确定的行,我们只需要枚举放置的列即可,在确保不冲突的情况下,一直搜索下去。
如何判断是否发生了冲突?
因为逐行放置的,所以行内是不会冲突的。对于列也很好处理,如果某列被占用了,只需要一个数组标记即可。处理同一斜线:处在同一斜线上的横纵坐标之和或者差为定值
解释: 斜率是+-1,所以对应直线方程为x+y=k,或者x-y=k
可以用这一点来标记一条对角线是否被占用。
用col[i]记录下标为i的列是否被占用,用x1[i]记录横纵坐标和为i的位置是否被占用,用x2[i]记录横纵坐标差为i-8的位置是否被占用,因为差可能是负数,所以我们对差整体加8防止数组越界。
2n皇后
正方形
蒜头君手上有一些小木棍,它们长短不一,蒜头君想用这些木棍拼出一个正方形,并且每根木棍都要用到。 例如,蒜头君手上有长度为 1,2,3,3, 3 的 5 根木棍,他可以让长度为1,22 的木棍组成一条边,另外三根分别组成 3 条边,拼成一个边长为 3 的正方形。蒜头君希望你提前告诉他能不能拼出来,免得白费功夫。
假设正方形边分别为a ,b,c ,d, 每根木棍有4种选择 :可以加入abcd任一个,可用dfs
dfs(int index,int a,int b,int c,int d)//枚举到第index个数且当前边长分别为abcd
做选择
dfs(index+1,a+p[index],b,c,d;//加入a
dfs(index+1,a,b+p[index],c,d);//加入b
dfs(index+1,a,b,c+p[index],d);//加入c
dfs(index+1,a,b,c,d+p[index]);//加入d
出口:index==n:到达搜索树中的空节点,判断abcd边长是否相等
剪枝:当边长>总长/4时,不可能构成正方形,没有搜索下去的必要,剪掉
LeetCode1466. 重新规划路线
n座城市,从 0 到 n-1 编号,其间共有 n-1 条路线。因此,要想在两座不同城市之间旅行只有唯一条路线可供选择(路线网形成一颗树)。
决定重新规划路线,以改变交通拥堵的状况。路线用 connections 表示,其中 connections[i] = [a, b] 表示从城市 a 到 b 的一条有向路线。请重新规划路线方向,使每个城市都可以访问城市 0 。返回需要变更方向的最小路线数。(题目数据保证每个城市在重新规划路线方向后都能到达城市 0)
假设所有路是双向的,dfs(0)遍历这个图。图是一个连通图,建图时把图看成一个无向图,实际存在的边权值为1,不存在的边权值为0,可以 从 顶点 0 遍历到所有顶点。遇到权值为0的边,
如果遇到反向的边,结果加1。所以从0开始遍历,加上边的权值即可。
leetcode 93. 复原 IP 地址
有效 IP 地址 正好由四个整数(每个整数位于 0
到 255
之间组成,且不能含有前导 0
),整数之间用 '.'
分隔。
- 例如:
"0.1.2.201"
和"192.168.1.1"
是 有效 IP 地址,但是"0.011.255.245"
、"192.168.1.312"
和"192.168@1.1"
是 无效 IP 地址。
给定一个只包含数字的字符串 s
,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s
中插入 '.'
来形成。你 不能 重新排序或删除 s
中的任何数字。你可以按 任何 顺序返回答案。
计蒜客:置换的玩笑
给定一个字符串,输出分隔方案,把字符串分隔为不重复的1~100的数。思路类似复原IP地址。
一次可以选一个字符,或者两个字符。
LeetCode77:组合
题意: 求n个数中所有k个数的组合
带状态的dfs:第一次做选择的范围是1~n,第二次做选择的范围是第一次做选择范围的之后一个位置到n,....,依次类推。所以,需要用一个参数记录上一次做的选择。
dfs(int index,int cnt):当前已经选了cnt个数,并且上一次选择的下标是index
集合问题中去重问题的思考
解法参考:代码随想录
例题:
(1)给定一个数组,返回所有幂集(数组中有重复元素)
输入:nums = [1,2,2] 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
如:下标1的[2]和下标2的[2]是重复的,下标1的[1,2]和下标2的[1,2]是重复的.......
(2)给定一个数组,返回所有递增子序列(数组中有重复元素)
输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
(3)给定一个数组和一个目标数,找出数组中所有可以使数字和为 target 的组合。(数组中有重复元素)
【深度优先搜索】子集| &&子集||_暮色_年华的博客
上面的题目具有一个特征,就是在同一个集合中可以重复,但是集合与集合中间不可以重复。
因为dfs可以对应一棵树,同一个集合对应一棵树从根节点到叶子节点的一条路径。
而不同的集合就对应一颗树相同的高度的所有路径。如果在相同的高度有两条路径对应的集合是相同的,那么在进行下一步选的时候,这两个路径的选择是相同的,所以会有重复的情况。
所以要在树的同一层进行去重。for循环体内做的所有选择对应的就是在同一层上的操作。
两种方法:
如果数组可以进行排序,那么可以用num[i]==num[i-1]来去重。如果num[i-1]和num[i]相等,那么在同一层中,这两个就是重复元素,只算一个即可。
在for循环前加set数组去重,如果已经选过,就直接跳过。
int used[];
for():
if(used[i])continue;
used[i]=1;
注意:used不需要回溯, 如果回溯,那么相当于没有标记。在每一层的时候,used都需要进行一次初始化(memset),进行去重操作。
题解代码:
递增子序列:
子集||
活字印刷
(1) 注意每一次dfs之后,都要对结果加1
(2)在同一路径下,每个下标不能不能选择多次,所以要记录已经选择过的下标
找出所有相加之和为 n
的 k
个数的组合,且满足下列条件:只使用数字1到9
每个数字 最多使用一次 。返回所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。