记忆化递归:

(1)参数个数为n,申请一个n维数组(哈希表),确保能够根据参数直接访问数组(哈希表)的值

(2)函数体:

            如果数组(哈希表)已经记录过参数对应的函数值,直接返回该值

            在所有出现return前,return的值记录到数组(哈希表)中

隐形回溯:

如果写成dfs(现在状态+())

//括号里为下一个状态的参数,这里隐含了回溯

 记忆化递归的必要性:

普通的递归可能会重复求解某一值,类似斐波那契数列。同样的子问题可能会被求解多次,这样就会很慢很慢很慢

解决方法:我们把历史求解(子问题)记录下来,如果下次需要求解子问题,那么直接取出就好。其时间复杂度为O(1)

根据n-1的状态推出第n个状态。

Leetcode 22. 括号生成(递归+去重)

算法题:dfs总结计蒜客:族谱_i++

 当n=1时,返回()

递归求n-1时的结果,由左右括号配对可知每一个结果字符串的长度都为2*(n-1)。在每一个结果的每一个位置+“()”,再经map去重就是n时的结果。

算法题:dfs总结计蒜客:族谱_i++_02


vector<string> generateParenthesis(int n):
  unordered_map<string,int>map;
  vector<string>res;
  if(n==1) return {"()"}
  vector<string>vec = generateParenthesis(n-1);
  string temp;
  for(auto&s:vec)
    for(int i=0;i<2*(n-1);i++)
    //字符串的连接
        temp=s.substr(0,i)+"()"+s.substr(i,2*(n-1));
  if(map.find(temp)==map.end())
      map[temp]=1;
      res.push_back(temp);
  return res;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.


做选择

算法题:dfs总结计蒜客:族谱_数组_03

算法题:dfs总结计蒜客:族谱_算法_04

算法题:dfs总结计蒜客:族谱_算法_05

     在一个n×m的网格状地宫中,每个格子内有一件具有特定价值的宝贝。小明从地宫的左上角出发,只能向右或向下移动,直至到达右下角的出口。在移动过程中,当他经过一个格子时,若该格子中的宝贝价值大于他当前手中任意一件宝贝的价值,他可以选择拿取这件宝贝(也可以选择不拿)。目标是让小明在走出地宫时,手中恰好拥有k件宝贝。请你计算出小明在满足上述条件下,走到终点能够成功获取k件宝贝的不同行动方案数量。

dfs(1,1,-1,0)表示从起点到终点的结果。

由于只能向右或向下移动,所以移动一直是单向的,不需要有vis标志。

算法题:dfs总结计蒜客:族谱_i++_06


#include<bits/stdc++.h>
using namespace std;
int mat[60][60];
int n,m,k;
bool check(int x,int y)
{
	return (x>=1&&x<=n&&y>=1&&y<=m);
} 
long long dp[60][60][60][15];
long long dfs(int x,int y,int maxv,int cnt)
{
	if(dp[x][y][maxv+1][cnt]!=-1)//注意1
		return dp[x][y][maxv+1][cnt];
	if(cnt>k)//如果手里有k+1件宝贝,越界,返回 
		return dp[x][y][maxv+1][cnt]=0;
	if(!check(x,y))//越界,修正
		return dp[x][y][maxv+1][cnt]=0;
	if(x==n&&y==m)//到达终点 
	{
		if(mat[x][y]>maxv)//能拿
		{
		   if(cnt==k)//如果手里已经有看件宝贝 
		    return dp[x][y][maxv+1][cnt]=1;//在终点处选择不拿有一种方案 
		   else if(cnt==k-1)return dp[x][y][maxv+1][cnt]=1;//在终点处选择拿,有一种方案 
		   else return dp[x][y][maxv+1][cnt]=0;
		}
		else//不能拿 
		{
			if(cnt==k)return dp[x][y][maxv+1][cnt]=1;//如果手里已经有k件宝贝,选择不拿,有一种方案
			else return dp[x][y][maxv+1][cnt]=0;
		} 
	}
	
	if(mat[x][y]>maxv)
		return dp[x][y][maxv+1][cnt]=dfs(x+1,y,mat[x][y],cnt+1)+dfs(x,y+1,mat[x][y],cnt+1)+dfs(x+1,y,maxv,cnt)+dfs(x,y+1,maxv,cnt);
	else
		return dp[x][y][maxv+1][cnt]=dfs(x+1,y,maxv,cnt)+dfs(x,y+1,maxv,cnt);
}
int main()
{
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			cin>>mat[i][j];
		}
	}
	memset(dp,-1,sizeof(dp));//memset的用法
	cout<<dfs(1,1,-1,0)%1000000007 ;//注意2,要取模
	
 
 }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.


K进制数

具体来说,给定整数N表示数的位数,以及基数K(K≥2),需要确定并求出所有满足以下条件的K进制数的数量:数字总共有N位;在这N位的K进制表示中,任何相邻的两位数字都不会同时是0。  给定两个数N和K, 要求计算包含N位数字的有效K-进制数的总数。


int dfs(int idx,bool pre){
  if(dp[idx][pre]!=-1)return dp[idx][pre];
  if(idx==n){
      if(pre)return dp[idx][pre]=k-1;
      else return dp[idx][pre]=k;
  }
  if(pre)
     return dp[idx][pre] = (k-1)*dfs(idx+1,false); 
  else
     return dp[idx][pre] = (k-1)*dfs(idx+1,false)+dfs(idx+1,true)
}
dfs(1,0);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.


算法题:dfs总结计蒜客:族谱_算法_07

算法题:dfs总结计蒜客:族谱_递归_08

剪格子

算法题:dfs总结计蒜客:族谱_数组_09

方格分割

6x6的方格,沿着格子的边线剪开成两部分。 要求这两部分的形状完全相同。

试计算: 包括这 3种分法在内,一共有多少种不同的分割方法。 注意:旋转对称的属于同一种分割法。

算法题:dfs总结计蒜客:族谱_递归_10

蓝桥杯2013c++真题:振兴中华 

深度优先搜索: 岛屿的周长

如果陆地有4个邻接陆地,那么这个陆地周长算为0;如果陆地有3个邻接陆地,那么这个陆地周长算1;如果陆地有2个邻接陆地,那么这个陆地周长算2;如果陆地有1个邻接陆地,那么这个陆地周长算3;如果陆地没有邻接陆地,那么这个路径周长算4。

算法题:dfs总结计蒜客:族谱_递归_11


vector<vector<int>>G;
int n, m;
bool vis[105][105];
int res=0;
void dfs(int x, int y)
   if (!(x >= 0 && y >= 0 && x < n && y < m))return;
   if (G[x][y]==0)return
   if (vis[x][y])return
   int cnt = 0
   vis[x][y] = true
   if (x - 1 >= 0 && G[x - 1][y] == 1)cnt++
   if (x + 1 < n && G[x + 1][y] == 1)cnt++
   if (y - 1 >= 0 && G[x][y - 1] == 1)cnt++
   if (y + 1 < m && G[x][y + 1] == 1)cnt++
   res+=4-cnt 
   dfs(x + 1, y)
   dfs(x - 1, y)
   dfs(x, y + 1)
   dfs(x, y - 1)

int islandPerimeter(vector<vector<int>>& grid)
        G = grid;
        n = grid.size();
        m = grid[0].size();
        memset(vis, 0, sizeof(vis));
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
                if (G[i][j] == 1)
                    dfs(i,j);
                    break;        
        return res

//leetcode 463
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.


求数组中的所有组合 

{1,2,3}
//{1},{2},{3},{1,2},{1,3},{2,3},{1,2,3}


//可以看成每个元素选或者不选
//如果步数一定,可以用index表示进行到的步数 
//一共要进行nums.size()步,所以可以用index变量纪录当前步数
vector<vector<int>>res;
vector<int>temp;
//num下标为[0.....n-1] 
//index开始下标 ,从0开始
//如果下标到index==n-1,说明到最后一个元素 
void dfs(vector<int>nums,int index)
{
	if(index==nums.size()-1)
	{
		temp.push_back(nums[index]); 
	   	res.push_back(temp);
	   	temp.pop_back();
	   	
	   	res.push_back(temp);
	   	return;
	}
	temp.push_back(nums[index]);
	dfs(nums,index+1);
	temp.pop_back();
	
	dfs(nums,index+1);	
 }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.


m行n列只能向右或者向下走输出所有路径 


#include<iostream>
#include<vector>
using namespace std;
//m行n列只能向右或者向下走输出所有路径
//起始坐标为(1,1)
int m, n;
vector<vector<int>>path;
int cnt;
void dfs(int x, int y)
{
	if (x == m && y == n)
	{
		path.push_back({ x,y });
		for (int i = 0; i < path.size(); i++)
		{
			cout << "(" << path[i][0] << "," << path[i][1] << ")" << " ";
		}
		cout << endl;
		path.pop_back();
		return;
	}
	if (x > m || y > n)
	{
		return;
	}
	//不需要设置vis数组
	path.push_back({ x ,y });
	dfs(x + 1, y);
	path.pop_back();
	
	path.push_back({ x,y  });
	dfs(x, y + 1);
	path.pop_back();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.


为什么需要回溯(撤销访问标记)

在遍历所有可能的路径时,如果不撤销访问标记,马在回溯到上一个位置时,将无法再次经过刚刚访问过的 (nx, ny) 点,这会导致无法探索包含该点在内的其他可能路径。特别是在遍历整个棋盘寻找所有可能的途径时,我们必须确保每个位置在不同的搜索阶段都能有机会被再次访问,这样才能确保搜索的完整性。因此,每次递归调用结束后,都会恢复访问标志,使得马可以从其他方向再次到达此点,从而继续搜索其他可能的遍历方案。最终统计符合条件的方案数量 cnt。

DFS连通性 acwing 

算法题:dfs总结计蒜客:族谱_递归_12


bool dfs(int x, int y, int a, int b, int n) {
    if (x == a && y == b) return true;
    if (!(x >= 0 && x < n && y >= 0 && y < n)) return false;
    if (g[x][y] != '.') return false;
    g[x][y] = '!';
    return dfs(x + 1, y, a, b, n)||dfs(x - 1, y, a, b, n)||dfs(x, y - 1, a, b, n)||dfs(x, y + 1, a, b, n);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.


算法题:dfs总结计蒜客:族谱_算法_13


bool isValid(int x, int y) {
    return x >= 0 && y >= 0 && x < n && y < m;
}

void dfs(int x, int y) {
    if (!isValid(x, y)) return;
    if (g[x][y] != 'W') return;
    g[x][y] = '#';
    for (int i = 0; i < 8; i++) {
        dfs(x + dx[i], y + dy[i]);
    }
}

 for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            if (g[i][j] == 'W') {
                res++;
                dfs(i, j);
            }
        }
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.


电话组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

算法题:dfs总结计蒜客:族谱_算法_14


unordered_map<int,string>_map;
   int n;
   vector<string>s;
   void init(){
    _map[2]="abc";
    _map[3]="def";
    _map[4]="ghi";
    _map[5]="jkl";
    _map[6]="mno";
    _map[7]="pqrs";
    _map[8]="tuv";
    _map[9]="wxyz";
   }
    vector<string>res;
    void dfs(string path){
        if(path.size()>n)return;
        if(path.size()==n){
            res.push_back(path);
            return;
        }
        for(int i=0;i<s[path.size()].size();i++){
            dfs(path+s[path.size()][i]);
        }
    }
    vector<string> letterCombinations(string digits) {
              string path="";
              init();
              s.resize(10);
              n=digits.size();
              if(digits=="")return res;
              for(int i=0;i<digits.size();i++){
                  s[i]=_map[digits[i]-'0'];
              }
              dfs(path);
              return res;
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.


方程的解数 

算法题:dfs总结计蒜客:族谱_递归_15

用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;}


void dfs(int index,long long s){
	if(index==n){
		if(s==0){
			ans++;
		}
		return;
	}
	for(int i=1;i<=m;i++){
		long long mult=1;
	   for(int j=1;j<=p[index];j++)
		     mult*=i;
    	dfs(index+1,s+k[index]*mult);
   }
	
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.


不去重的全排列 


//n记录选到第几个集合
//str保存上一层递归的结果
string s;
int n=s.size();
void dfs(string str)
{
 //如果选到第size层,说明选择结束,输出str,清空str(节省空间)
   if(str.size()==n){
     cout<<str;
     return;
  }
  else{
    for(int i=0;i<s.size();i++)
      dfs(str+s[i]);//下一层做选择
  }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.


计蒜客:族谱

vector<int>G[100000]表示树。G[i].push_back(j)表示i是j的父节点。

i是叶子节点:G[i].size()==0


int dfs(int no)
{
	//如果是叶子节点 
	if(G[no].size()==0)return 0; 
	int sum=0;
	for(int i=0;i<G[no].size();i++)
	{
	  sum+=dfs(G[no][i]);	
	} 
	return sum+1;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.


计蒜客:王子救公主

思路:用两次深度优先搜索,数组vis1标记王子可能到达的点,数组vis2标记公主可能到达的点;

如果两个数组有重复的标记的坐标,那么王子可以救出公主

标记所有可能情况的深度优先搜索不需要回溯!如果回溯,那么会出错。

如果状态用参数传递的话,也可以直接改变参数,这一步相当于回溯(因为调用时没有改变原有参数的值)

【DFS笔记】什么时候需要回溯?什么时候不需要回溯?_迷宫问题什么时候用回溯什么时候不用回溯


void dfs1(int x,int y){
	  if(!(x>=0&&x<n&&y>=0&&y<m))return;
	  if(G[x][y]=='#')return;
	  if(vis1[x][y])return;
	  vis1[x][y]=true;
	  dfs1(x+2,y);
	  dfs1(x-2,y);
	  dfs1(x,y+2);
	  dfs1(x,y-2);
}
for(int i=0;i<n;i++){
		for(int j=0;j<m;j++){
			if(vis1[i][j]&&vis2[i][j]){
				cout<<"yes";
				return;
			}
		}
	}
cout<<"no";
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.


 一次性遍历: 该函数设计的目标是在满足边界条件的情况下,一次性遍历与给定坐标(x, y)相邻的所有可达格子。由于遍历是单向且不可逆的(一次遍历只会从一个未访问过的格子移动到另一个未访问过的格子),所以不需要撤销访问标记。

对于为什么遍历二叉树的时候好像没有回溯。

这里的“回溯”发生在每次递归调用返回时。对于给定的二叉树节点 root

  1. 首先访问根节点 root
  2. 然后递归地对左子树执行DFS,即调用 dfs(root.left)
  3. 当左子树的DFS完成并返回时,实际上就发生了“回溯”。因为DFS已经完成了对左子树的遍历,现在算法会回到上一层节点,即父节点 root 的上下文。
  4. 接着对右子树执行DFS,即调用 dfs(root.right)
  5. 同样,当右子树的DFS完成后返回时,又一次发生了“回溯”。

在整个过程中,每一步递归调用结束并返回时,都意味着回到了前一个节点的层次,继续处理下一个待访问的子节点或结束此次递归调用。这种从一个分支回退到其父节点并选择另一个分支的过程,就是DFS中的“回溯”行为。虽然代码中没有使用“回溯”这个词,但这是递归结构和深度优先搜索方法固有的逻辑。

当问题说到一共有n个数/n个选择,且按顺序依次选择时,一般都会用到这样的dfs模板

dfs(index,state){
    if(index==n)return;
    if(state not valid)cut; 
   for(choice):
    dfs(index+1,f(state,g(index)));
 }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

比如说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)因为在没有改变参数的值,所以这个模板自带回溯功能。

算法题:dfs总结计蒜客:族谱_数组_16

 做了3次选择后,if判断时,index为3,下标为2的已经做完选择,状态参数是0~2状态。

算法题:dfs总结计蒜客:族谱_递归_17

可行性剪枝:
通过一些判断,砍掉搜索树上不必要的子树。

有时候,如果发现某个结点对应子树的状态不是我们想要的结果,那么没必要对这个分支进行搜索,砍掉这个子树,就是剪枝。

例:

n个数选k个数和为sum,如果发现当前和大于sum,那么之和不管怎么选和值都不可能是sum了

或者 如果选出数的个数大于k,那么之后不管怎么选数个数都大于k,

我们可以直接终止分支的搜索。

最优性剪枝:
在求解最优解的一类问题,通常可以用最优性剪枝

例1:

求解迷宫最短路时,如果发现当前的步数已经超过了当前最优解,那么从当前状态开始的搜索都是多余的,因为这样搜索下去永远得不到更优的解。通过这样的剪枝,可以省去大量冗余的计算

例2:

在搜索是否有可行解的过程中,一旦找到了一组可行解,后面所有的搜索都不必再进行了。

flag=false;

dfs:

  if(flag)return;

重复性剪枝:
bool类型数组vis防止选重复的节点。

蓝桥:七段码

算法题:dfs总结计蒜客:族谱_i++_18

    上图给出了七段码数码管的一个图示,数码管中一共有 7 段可以发光的二极管,分别标记为 a, b, c, d, e, f, g。选择一部分二极管(至少要有一个)发光来表达字符。在设计字符的表达时,要求所有发光的二极管是连成一片的。

请问,小蓝可以用七段码数码管表达多少种不同的字符?

二进制枚举+dfs连通块判断:

因为每个灯要么亮,要么不亮,可以看出1或0,一共7个灯,有2^7-1中情况(全不亮的情况舍弃),所以可以用二进制枚举。


for(int i=1;i<(1<<7);i++){
    for(int j=0;j<7;j++){
        if(i&(1<<j)){ //判断第i位是不是1
        }
    } 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.


1对应灯亮,0对应灯不亮

可以用一个大小为7数组ch表示灯是否亮(值1亮,值0亮)。

可以用一个7*7的二维数组G表示灯i与灯j是否直接相连,相连为1,否则为0。


#include <iostream>
using namespace std;
int ch[7];
int G[7][7];
void init_G()
    G[0][1]=1;G[0][5]=1;G[1][0]=1;G[1][6]=1;G[1][2]=1;G[2][1]=1;
    G[2][3]=1;G[2][6]=1;G[3][2]=1;G[3][4]=1;G[4][3]=1;G[4][6]=1;
    G[4][5]=1;G[5][4]=1;G[5][6]=1;G[5][0]=1;G[6][1]=1;G[6][2]=1;
    G[6][4]=1;G[6][5]=1;
void init_ch()
  for(int i=0;i<7;i++)ch[i]=0;

void dfs(int n)
  if(ch[n]==0)return;
  ch[n]=0;
  for(int i=0;i<7;i++)
    if(G[n][i]==1)dfs(i);

bool check()
  int cnt=0;
  for(int i=0;i<7;i++)
    if(ch[i]==1)
      dfs(i);
      cnt++;
  return cnt==1;

int main()
{
  init_G();
  int res=0;
  for(int i=1;i<(1<<7);i++)
    init_ch();
    for(int j=0;j<7;j++)
      if(i&(1<<j))
        ch[j]=1;
    if(check())
      res++;
  cout<<res;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.


acwing迷宫 DFS连通性问题

迷宫可以看成是由 n∗n 的格点组成,每个格点只有2种状态,.#,前者表示可以通行后者表示不能通行。在某个格点时,只能移动到上下左右四个方向之一的相邻格点上,要从点A走到点B,问在不走出迷宫的情况下能不能办到。


bool dfs(int x, int y, int a, int b, int n)
    if (x == a && y == b) return true;
    if (!(x >= 0 && x < n && y >= 0 && y < n)) return false;
    if (g[x][y] != '.') return false;
    g[x][y] = '!';
    return dfs(x + 1, y, a, b, n) || dfs(x - 1, y, a, b, n) ||
           dfs(x, y - 1, a, b, n) || dfs(x, y + 1, a, b, n);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.


疑问:为什么这个地方不需要回溯?

 蓝桥杯:带分数

算法题:dfs总结计蒜客:族谱_递归_19

???

8皇后

搜索策略如下:从第0行开始,依次给每一行放置一个皇后,对于一个确定的行,我们只需要枚举放置的列即可,在确保不冲突的情况下,一直搜索下去。

如何判断是否发生了冲突?

因为逐行放置的,所以行内是不会冲突的。对于列也很好处理,如果某列被占用了,只需要一个数组标记即可。处理同一斜线:处在同一斜线上的横纵坐标之和或者差为定值

解释: 斜率是+-1,所以对应直线方程为x+y=k,或者x-y=k

算法题:dfs总结计蒜客:族谱_递归_20

 

算法题:dfs总结计蒜客:族谱_算法_21

 可以用这一点来标记一条对角线是否被占用。

用col[i]记录下标为i的列是否被占用,用x1[i]记录横纵坐标和为i的位置是否被占用,用x2[i]记录横纵坐标差为i-8的位置是否被占用,因为差可能是负数,所以我们对差整体加8防止数组越界。

算法题:dfs总结计蒜客:族谱_数组_22

 

算法题:dfs总结计蒜客:族谱_算法_23


#include<iostream>
using namespace std;
int ans=0;
bool col[10],x1[20],x2[20];
bool check(int r,int i){
	return !col[i]&&!x1[r+i]&&!x2[r-i+8];
}
void dfs(int r){
	if(r==8){ 
       ans++;
       return;
    }
	for(int i=0;i<8;i++)
		if(check(r,i)){
			col[i]=x1[r+i]=x2[r-i+8]=true;
			dfs(r+1);
			col[i]=x1[r+i]=x2[r-i+8]=true;
		}
	
}


int main(){
	dfs(0); 
	cout<<ans;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.


2n皇后

算法题:dfs总结计蒜客:族谱_数组_24


#include<iostream>
#include<cstring>
using namespace std;
int n;
int ans=0;
int G[10][10];
bool col1[10],col2[10],x11[20],x21[20],x12[20],x22[20],vis[10];
bool check1(int r,int i){
  return !col1[i]&&!x11[r+i]&&!x21[r-i+8]&&G[r][i]==1;
}
bool check2(int r,int i){
	return !col2[i]&&!x12[r+i]&&!x22[r-i+8]&&G[r][i]==1; 
}
void input(){
	cin>>n;
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
		   cin>>G[i][j];	
		}
	}
}
void dfs2(int r){
	if(r==n){
		ans++;
		return;
	}
	for(int i=0;i<n;i++){
		if(check2(r,i)){
			col2[i]=x12[r+i]=x22[r-i+8]=true;
			dfs2(r+1);
			col2[i]=x12[r+i]=x22[r-i+8]=false;
		}
	}
}
void dfs1(int r){
	if(r==n){
		dfs2(0);
		return;
	}
	for(int i=0;i<n;i++){
		if(check1(r,i)){
			G[r][i]=0;
			col1[i]=x11[r+i]=x21[r-i+8]=true;
			dfs1(r+1);
			col1[i]=x11[r+i]=x21[r-i+8]=false;
			G[r][i]=1;
		}
	}	
}
 
int main(){
	input(); 
	dfs1(0);
	cout<<ans;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.


正方形

蒜头君手上有一些小木棍,它们长短不一,蒜头君想用这些木棍拼出一个正方形,并且每根木棍都要用到。 例如,蒜头君手上有长度为 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时,不可能构成正方形,没有搜索下去的必要,剪掉


void dfs(int index,int a,int b,int c,int d){
	if(flag){
		return;
	}
	if(index==n){
		if(a==b&&b==c&&c==d&&d==sum/4){
			cout<<"YES";
			flag=true;
			return;
		}
	}
	if(a>sum/4||b>sum/4||c>sum/4||d>sum/4){
		return;
	}
	dfs(index+1,a+p[index],b,c,d);
	dfs(index+1,a,b+p[index],c,d);
	dfs(index+1,a,b,c+p[index],d);
	dfs(index+1,a,b,c,d+p[index]);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.


LeetCode1466. 重新规划路线

    n座城市,从 0 到 n-1 编号,其间共有 n-1 条路线。因此,要想在两座不同城市之间旅行只有唯一条路线可供选择(路线网形成一颗树)。

    决定重新规划路线,以改变交通拥堵的状况。路线用 connections 表示,其中 connections[i] = [a, b] 表示从城市 a 到 b 的一条有向路线。请重新规划路线方向,使每个城市都可以访问城市 0 。返回需要变更方向的最小路线数。(题目数据保证每个城市在重新规划路线方向后都能到达城市 0)

算法题:dfs总结计蒜客:族谱_i++_25

假设所有路是双向的,dfs(0)遍历这个图。图是一个连通图,建图时把图看成一个无向图,实际存在的边权值为1,不存在的边权值为0,可以 从 顶点 0 遍历到所有顶点。遇到权值为0的边,

如果遇到反向的边,结果加1。所以从0开始遍历,加上边的权值即可。


vector<vector<pair<int,int>>>g;
    int res=0;
    void dfs(int cur,int pre){
        for(auto ne:g[cur]){
            if(ne.first==cur||ne.first==pre)continue;
            res+=ne.second;
            dfs(ne.first,cur);
        }
    }
    int minReorder(int n, vector<vector<int>>& edge) {
        g.resize(n);
        for(int i=0;i<n-1;i++){
            g[edge[i][0]].push_back({edge[i][1],1});
            g[edge[i][1]].push_back({edge[i][0],0});
        }
        dfs(0,-1);
        return res;
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.


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 中的任何数字。你可以按 任何 顺序返回答案。


bool check(string s){
     if(s.size() > 3) return false;
     if(s.size()==0)return true;
     if(s.size()!=1&&s[0]=='0')return false;
     int x = stoi(s);
     return x >= 0 && x <= 255;
   }
void dfs(string s,int idx){
  if(tmp.size()==4 && idx >= n){
       string ipAddress;
       for(int i = 0; i < 4; ++i) {
           ipAddress += tmp[i] + (i != 3 ? "." : "");
       }
       res.push_back(ipAddress);  // 将找到的 IP 地址添加到结果列表 res 中
       return;
   }
   if(tmp.size()==4&&idx<n)return;
   if(idx<n&&check(s.substr(idx,1))){
       tmp.push_back(s.substr(idx,1));
       dfs(s,idx+1);
       tmp.pop_back();
   }
   if(idx+1<n&&check(s.substr(idx,2))){
       tmp.push_back(s.substr(idx,2));
       dfs(s,idx+2);
       tmp.pop_back();
   }
   if(idx+2<n&&check(s.substr(idx,3))){
       tmp.push_back(s.substr(idx,3));
       dfs(s,idx+3);
       tmp.pop_back();
   }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.


计蒜客:置换的玩笑

给定一个字符串,输出分隔方案,把字符串分隔为不重复的1~100的数。思路类似复原IP地址。

一次可以选一个字符,或者两个字符。


vector<string>tmp;
bool check(string s):
   if(s.size()==0&&s[0]=='0')return false;
   if(s.size()>2)return false;
   if(s.size()==2&&s[0]=='0')return false;
   return true

dfs(string s,int idx):
   if(idx>=n){cout<<tmp};
   if(idx+1<n&&check(s.substr(idx,1))){
     tmp.push_back(s.substr(idx,1));
     dfs(s,idx+1);
     tmp.pop_back();
   }
   if(idx+2<n&&check(s.substr(idx,2))){
     tmp.push_back(s.substr(idx,2));
     dfs(s,idx+2);
     tmp.pop_back();
   }  


dfs(s,0);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.


LeetCode77:组合

题意: 求n个数中所有k个数的组合

带状态的dfs:第一次做选择的范围是1~n,第二次做选择的范围是第一次做选择范围的之后一个位置到n,....,依次类推。所以,需要用一个参数记录上一次做的选择。

dfs(int index,int cnt):当前已经选了cnt个数,并且上一次选择的下标是index 


vector<vector<int>>res;
vector<int>path;
void dfs(int n,int k,int index,int cnt){
      if(cnt==k){
          res.push_back(path);
          return;
      }
      for(int i=index+1;i<=n;i++){
              path.push_back(i);
              dfs(n,k,i,cnt+1);
              path.pop_back();        
        }
    }
vector<vector<int>> combine(int n, int k) {
        dfs(n,k,0,0);
        return res;
  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.


 LCP 63. 弹珠游戏

集合问题中去重问题的思考

 解法参考:代码随想录

 代码随想录

例题:

(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循环体内做的所有选择对应的就是在同一层上的操作。

算法题:dfs总结计蒜客:族谱_递归_26


两种方法:

如果数组可以进行排序,那么可以用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),进行去重操作。

题解代码:

递增子序列:


class Solution {
public:
    vector<vector<int>>res;
    vector<int>path;
    int n;
    vector<int>a;
    void dfs(int index){
        if(path.size()>=2){
        res.push_back(path);
        int uset[201];
        memset(uset,0,sizeof(uset));
        for(int i=index;i<n;i++){
            if(uset[a[i]+100]==1)continue;
            if(path.size()>0&&a[i]<path.back())continue;
            uset[a[i]+100]=1;
            path.push_back(a[i]);
            dfs(i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        a=nums;
        n=nums.size();
        dfs(0);
        return res;
    }
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.


子集||


class Solution {
public:
    vector<vector<int>>res;
    vector<int>path;
    vector<int>a;
    int n;
    void dfs(int index){
        //每次做选择后都要计入res
        res.push_back(path);
        int uset[21];
        memset(uset,0,sizeof(uset));
    for(int i=index;i<n;i++){
       if(uset[a[i]+10]==1)continue;
       uset[a[i]+10]=1;
        path.push_back(a[i]);
        dfs(i+1);
        path.pop_back();
    }
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
          a=nums;
          sort(a.begin(),a.end());
          n=nums.size();
          dfs(0);
          return res;
    }
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.


活字印刷

算法题:dfs总结计蒜客:族谱_i++_27


(1) 注意每一次dfs之后,都要对结果加1

(2)在同一路径下,每个下标不能不能选择多次,所以要记录已经选择过的下标


class Solution {
public:
    int ans;
    string s;
    string path;
    void dfs(int index){
        ans++;
        if(index==s.size())return;
        int uset[128];
        memset(uset,0,sizeof(uset));
        for(int i=0;i<s.size();i++){
            //对同一层去重
            if(uset[s[i]])continue;
            //同一路径中选过的不能再选
            //用#标记字符已经在同一路径中选过
            if(s[i]=='#')continue;
            uset[s[i]]=1;
            char c=s[i];
            s[i]='#';
            dfs(index+1);
            s[i]=c;
        }
    }
    int numTilePossibilities(string tiles) {
         sort(tiles.begin(),tiles.end());
         s=tiles;
         dfs(0);
         return ans-1;
 
    }
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.


 216. 组合总和 III

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:只使用数字1到9

每个数字 最多使用一次 。返回所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

vector<vector<int>>res;
vector<int>tmp;
int N;   int K;
void dfs(int sum,int cnt,int pre):
   if(sum>N)return;
   if(cnt>K)return;
   if(sum==N&&cnt==K)
        res.push_back(tmp);
        return;
   for(int i=pre;i<=9;i++)
        tmp.push_back(i);
        dfs(sum+i,cnt+1,i+1);
        tmp.pop_back();
vector<vector<int>> combinationSum3(int k, int n):
      K=k;
      N=n;
      dfs(0,0,1);
      return res;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.