深度搜索与回溯算法

回溯法”也称“试探法”,是深度搜索的一种。它是从问题的某一状态出发,不断“试探”着往前走一步,当一条路走到“尽头”,不能再前进(拓展出新状态)的时候,再倒回一步或者若干步,从另一种可能的状态出发,继续搜索,直到所有的“路径(状态)”都一一试探过。这种不断前进、不断回溯,寻找解的方法,称为“回溯法”。

他的基本思想是:为了求得问题的解,先选择某一种可能情况向前搜索,在搜索过程中,一旦发现原来的选择是错误的,就退回一步重新选择,继续向前探索,如此反复进行,直到得到解或证明无解。

【组合】

例1. 给定两个整数n和k,返回1 ... n中所有可能的k个数的组合。
示例:
输入:n=4,k=2
输出:
1 2
1 3
1 4
2 3
2 4
3 4

思路

本题这是回溯法的经典题⽬。

直接的解法当然是使⽤for循环,例如示例中r为2,很容易想到⽤两个for循环,这样就可以输出和示例中⼀样的结果。

int n = 4;
for(int i=1; i<=n; i++){
	for(int j=i+1; j<=n; j++){
		cout<<i<<" "<<j<<endl;
	}
}

输⼊:n=100,k=3那么就三层for循环,代码如下:

int n = 100;
for(int i=1; i<=n; i++){
	for(int j=i+1; j<=n; j++){
		for(int u=j+1; u<=n; n++){
			cout<<i<<" "<<j<<" "<<u<<endl;
		}
	}
}

如果n为100,k为50呢,那就50层for循环...

此时就会发现虽然想暴力搜索,但是⽤for循环嵌套连暴力都写不出来!

回溯搜索法来了,虽然回溯法也是暴⼒,但⾄少能写出来,不像for循环嵌套k层让⼈绝望。那么回溯法怎么暴⼒搜呢?

上⾯我们说了要解决n为100,k为50的情况,暴力写法需要嵌套50层for循环,那么回溯就用递归来解决嵌套层数的问题。

递归来做层叠嵌套(可以理解是开k层for循环),每⼀次的递归中嵌套⼀个for循环,那么递归就可以⽤于解决多层嵌套循环的问题了


回溯法解决的问题都可以抽象为树形结构(N叉树),⽤树形结构来理解回溯就容易多了。

那么我把组合问题抽象为如下树形结构:

image


可以看出这个棵树,⼀开始集合是1,2,3,4,从左向右取数,取过的数,不在重复取。

第⼀次取1,集合变为2,3,4,因为k为2,我们只需要再取⼀个数就可以了,分别取2,3,4,得到集合[1,2] [1,3] [1,4],以此类推。

每次从集合中选取元素,可选择的范围随着选择的进⾏⽽收缩,调整可选择的范围。

图中可以发现n相当于树的宽度,k相当于树的深度。
 

==回溯法三部曲==

  • 递归函数的返回值以及参数
  • 回溯函数终止条件
  • 单层搜索的过程
     

==框架:==
 

image

本题程序如下:

//组合:不重复输出
#include<iostream>
using namespace std;
int a[50];  
int n,r;

void search(int t){   //t表示第几个数 
	if(t>r){  //满足2个数之后就输出并终止当前搜索,回溯到上一层 
		for(int i=1;i<=r;i++)
			cout<<a[i]<<" ";
		cout<<endl;
		return;
	}
	for(int i=1; i<=n; i++){
		if(i>a[t-1]){    //判断当前是否满足条件 :不重复输出 
			a[t]=i;    
			search(t+1); //查找下一个数 
		}
	}
}

int main()
{
    cin>>n>>r;
    search(1);
    return 0; 
}

练习1. 排列与组合是常用的数学方法,其中组合就是从n个元素中抽出r个元素(不分顺序且r<n)。 我们可以简单地将n个元素理解为自然数1,2,...,n从中任取 r 个数。
现要求你输出所有组合,例如 n=5,r=3 所有组合为:
123,124,125,134,135,145,234,235,245,345

输入格式
一行两个自然数n,r (1<n<21,0<r<n)。

输出格式
所有的组合,每一个组合占一行且其中的元素按由小到大的顺序排列,每个元素占三个字符的位置,所有的组合也按字典顺序。

程序如下:
//组合--从小到大输出 
 
   #include<iostream>
    using namespace std;
    int a[1005];  
    int n,r;
    
    void search(int t){
    	if(t>r){
    		for(int i=1;i<=r;i++)
    			cout<<a[i]<<" ";
    		cout<<endl;
    		return;
    	}
    	for(int i=1; i<=n; i++){
    		if(i > a[t-1]){  //判断当前是否满足条件: 从小到大,新数要比上一个数大 
    			a[t]=i;      //保存结果 
    			search(t+1);
    		}
    	}
    }
    
    int main()
    {
        cin>>n>>r;
        search(1);
        return 0; 
    }



练习2. 素数环:现有10个位置,要求从1到10这十个数字摆成一个环,要求相邻的数字和为素数。输出所有排列方案和方案总数。
如:1 2 3 4 7 6 5 8 9 10
 

题目分析:
1.总共有10个位置放置数字,那么问题就分为10步,每步确定一个位置的数字。
2.每一步有10种可能。
参考程序:
#include<bits/stdc++.h>
using namespace std;
int a[50],v[50];  
int n,r,ans;
bool isPrime(int x){
	for(int i=2;i<=sqrt(x);i++){
		if(x%i==0) return false;
	}
	return true; 
}  
void search(int t){     
	if(t>10){    
		if(isPrime(a[1]+a[10])){ //首尾相邻也需判断 
			for(int i=1;i<=10;i++)
				cout<<a[i]<<" ";
			cout<<endl;
			ans++;
		}
		return;
	}
	for(int i=1; i<=10; i++){
		if(!v[i] && isPrime(i+a[t-1])){ 
			a[t]=i;  
			v[i]=1;  
			search(t+1); 
			v[i]=0;  
		}
	}
}

int main()
{
    search(1);
    cout<<ans;
    return 0; 
}



练习3. 输入n个从小到大的数,输出 r 个数的所有组合方案,无顺序区别。

输入样例
  4 3
  4 5 6 7

输出样例
  4 5 6
  4 5 7
  4 6 7
  5 6 7
 

【参考程序】
   
 #include<iostream>
    using namespace std;
    int a[1005],b[1005],c[1005];
    int n,r;
    
    void search(int t){  
    	if(t>r){  
    		for(int i=1;i<=r; i++)
    			cout<<c[i];
    		cout<<endl; 
    		return;
    	}
    	
    	for(int i=1; i<=n; i++){    
    		if(a[i]>c[t-1]){  
    			c[t]=a[i];  
    			search(t+1);
    			c[t]=0;   
    		}
    	}
    }
    
    int main()
    {
        cin>>n>>r;
        for(int i=1;i<=n;i++)
        	cin>>a[i];
        search(1);
        return 0; 
    }

完成练习
信息学奥赛一本通 题号1317-1318

 

【排列】

例1. 给定两个整数n和k,返回1 ... n中k个数的全排列。
输入:3 2
输出:
1 2
1 3
2 1
2 3
3 1
3 2
 

程序如下:
 #include<iostream>
    using namespace std;
    int a[1005],v[1005];
    int n,k;
    
    void search(int t){  
    	if(t>k){  
    		for(int i=1;i<=k; i++)
    			cout<<a[i]<<" ";
    		cout<<endl; 
    		return;
    	}
    	
    	for(int i=1; i<=n; i++){    
    		if(!v[i]){  //不能充分输出相同的数 
    			a[t]=i;       
    			v[i]=1;
    			search(t+1); //搜索下一个数 
    			v[i]=0;  //回溯,恢复可使用状态 
    		}
    	}
    }
    
    int main()
    {
        cin>>n>>k;
        search(1);
        return 0; 
    }



练习2. 输入n个从小到大的数,输出 r 个数的所有排列方案。

输入样例
  3 3
  7 8 9

输出样例
  7 8 9
  7 9 8
  8 7 9
  8 9 7
  9 7 8
  9 8 7

 #include<iostream>
    using namespace std;
    int b[1005],a[1005],v[1005];
    int n,k;
    
    void search(int t){  
    	if(t>k){  
    		for(int i=1;i<=k; i++)
    			cout<<a[i]<<" ";
    		cout<<endl; 
    		return;
    	}
    	
    	for(int i=1; i<=n; i++){    
    		if(!v[i]){  //不能充分输出相同的数 
    			a[t]=b[i];       
    			v[i]=1;
    			search(t+1); //搜索下一个数 
    			v[i]=0;  //回溯,恢复可使用状态 
    		}
    	}
    }
    
    int main()
    {
        cin>>n>>k;
        for(int i=1;i<=n;i++){
        	cin>>b[i];
		} 
        search(1);
        return 0; 
    }



例2. 八皇后问题:要在国际象棋棋盘中放八个皇后,皇后可以在横、竖、斜线上不限步数地吃掉其他棋子。如何将8个皇后放在棋盘上(有8 × 8个方格),使它们任意两个皇后不会被互相吃掉。
输出所有的方案。

输出样例
sum=1
1 0 0 0 0 0 0 0
0 0 0 0 0 0 1 0
0 0 0 0 1 0 0 0
0 0 0 0 0 0 0 1
0 1 0 0 0 0 0 0

0 0 0 1 0 0 0 0
0 0 0 0 0 1 0 0
0 0 1 0 0 0 0 0

sum=2
1 0 0 0 0 0 0 0
0 0 0 0 0 0 1 0
0 0 0 1 0 0 0 0
0 0 0 0 0 1 0 0
0 0 0 0 0 0 0 1
0 1 0 0 0 0 0 0

0 0 0 0 1 0 0 0
0 0 1 0 0 0 0 0
...以下省略

放置第i个(行)皇后的算法为:
    int search(i){ 
        if(i>8) 输出。
        for(第i个皇后的位置 j =1; j<= 8; j++){ //在本行的8列中去试
            if(本行本列允许放置皇后){
                放置第i个皇后;
                对放置皇后的位置进行标记;
                放置第i+1个皇后
                对放置皇后的位置释放标记,尝试下一个位置是否可行;
            }
         }
    }

算法分析

      
确定三个问题:

(1)递归函数的返回值和参数(参数是第几个皇后i)

(2)回溯搜索的终止条件(皇后数i>8即终止)

(3)单层搜索的过程

搜索过程中的关键问题在于如何判定行、列、斜线上是否有别的皇后;可以从矩阵的特点上找到规律,同行行号相同,同列列号相同,同斜线行列值之和相同(行列值之差相同)。
 

image

考虑每行有且仅有一个皇后,设一维数组a[1.. 8]表示皇后的放置:第i行皇后放在第j列,用a[i]=j来表示;b[1..8]数组标识哪一列被存放过,下一个皇后

必须不同列;c[1..16]和d[-7..7]数组标识对角线上是否有皇后,下一个皇后必须不同斜线,由算法思路转换成程序如下:

【参考程序】
   
 #include<iostream>
    #include<iomanip>
    using namespace std;
    int a[100];   //存放放置皇后列数
    bool b[100];   //标志第x列已被存放
    bool c[100], d[100];   //标志斜对角是否被存放 
    int sum;
        
    void search(int i){   //t表示第几个皇后 
        if(i>8){
            sum++;
        	cout<<"sum="<<sum<<endl;
        	for(int j=1;j<=8;j++){
        		for(int k=1;k<=8;k++){
        			if(j==a[k]) cout<<1<<" ";
        			else cout<<0<<" ";
        		} 
    		cout<<endl;
    		}
        	return;
       	}
       	for(int j=1; j<=8; j++){ 
       		if(!b[j] && !c[i+j] && !d[i-j+7]){        //不在同行 同列 对角线 
       			a[i]=j;       //摆放皇后 
       			b[j]=1;       //宣布占领第j列 
       			c[i+j]=1;     //占领两对角线 
       			d[i-j+7]=1;
       			search(i+1);   //继续递归放置下一个皇后 
    			b[j]=0;        //回溯 
       			c[i+j]=0;      
       			d[i-j+7]=0;
       			
       		}
       	}
    }
        
    int main()
    {
        search(1);
        return 0; 
    }

深度优先搜索回溯算法是一种在图结构中搜索特定路径的算法。它的基本思想是从根节点开始,按照深度优先搜索的策略,逐层向下探索解空间树,直到找到问题的解或者无法继续向下搜索时,回溯到上一层继续搜索其他路径。 深度优先搜索回溯算法的步骤如下: 1. 从根节点开始,将当前节点标记为已访问。 2. 判断当前节点是否包含问题的解,如果是,则记录解并继续向下搜索。 3. 如果当前节点不包含问题的解,则递归地访问当前节点的未访问邻居节点。 4. 重复步骤2和步骤3,直到找到问题的解或者无法继续向下搜索。 5. 如果无法继续向下搜索,则回溯到上一层节点,继续搜索其他路径。 以下是一个深度优先搜索回溯算法的示例代码: ```python def dfs_backtracking(graph, start, end, path, visited): # 将当前节点标记为已访问 visited[start] = True # 将当前节点添加到路径中 path.append(start) # 判断当前节点是否为目标节点 if start == end: # 打印路径 print(path) else: # 递归地访问当前节点的未访问邻居节点 for neighbor in graph[start]: if not visited[neighbor]: dfs_backtracking(graph, neighbor, end, path, visited) # 回溯到上一层节点 path.pop() visited[start] = False # 示例图结构 graph = { 'A': ['B', 'C'], 'B': ['A', 'D', 'E'], 'C': ['A', 'F'], 'D': ['B'], 'E': ['B', 'F'], 'F': ['C', 'E'] } # 初始化访问状态和路径 visited = {node: False for node in graph} path = [] # 调用深度优先搜索回溯算法 dfs_backtracking(graph, 'A', 'F', path, visited) ``` 运行以上代码,将会输出从节点A到节点F的所有路径。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值