基于深度优先搜索的算法,核心是 "回溯",在有限状态集中搜索解,不满足条件时返回上一状态尝试其他方案
- 设计思路:遍历解空间,对每个状态做选择;若状态不满足条件或到达边界,则回溯至上一状态,直至找到有效解或遍历完所有解空间。
- 适用场合:需遍历所有可能情况的问题,如组合、优化类问题。
- 注意事项
- 明确状态表示及合规判断规则
- 通常用递归实现遍历
- 合理剪枝,剔除无效路径以提升效率
- 借助栈或递归记录当前路径状态,实现回溯
【例题1】全排列问题
有一个含n个整数的数组a,所有元素均不相同,求其所有元素的全排列
例如,a[]={1,2,3},得到结果是:(1,2,3) (1,3,2) (2,3,1) (2,1,3) (3,1,2) (3,2,1)
要生成数组 a[0..n-1] 的全排列:
固定位置 0,让 a[0] 依次与 a[0]..a[n-1] 交换
对剩下的 a[1..n-1] 递归执行相同操作
每次递归完成后,恢复原数组(回溯)
cpp
#include<iostream>
using namespace std;
void swap(int &a,int &b){ // 标准的交换函数
int temp=a;
a=b;
b=temp;
}
void pailie(int a[],int i,int n){ //i当前位置
//递归出口:已经到叶子结点
if(i==n){ // 已经到达最后一个位置之后
//输出数组
cout<<'(';
for(int j=0;j<n;j++){
cout<<a[j];
}
cout<<')';
} else{
for(int j=i;j<n;j++){ // j从i到n-1
swap(a[i],a[j]); // 将a[i]与a[j]交换
pailie(a,i+1,n); // 递归处理下一个位置
//还原交换
swap(a[i],a[j]);
}
}
}
int main(){
int a[]={1,2,3};
int n=sizeof(a)/sizeof(a[0]);
//回溯拥有递归的思想
pailie(a,0,n);
return 0;
}
【例题2】求解0/1背包问题
0/1背包问题:
有 n 个物品,每个物品有重量 wᵢ 和价值 vᵢ
背包容量为 W
每个物品要么全拿(1),要么不拿(0)
目标:在不超过背包容量的情况下,最大化总价值
cpp
#include<iostream>
using namespace std;
int W=10,n=6; // 背包容量和物品数量
int w[6] = {2,2,6,5,4,3},v[6]={6,3,5,4,6,6};// 物品重量数组,物品价值数组
int ww,vv,maxv,op[6],bestop[6]; //当前背包的重量和价值 最大价值 当前选择方案(0/1数组) 最优选择
void fun(int i,int ww,int vv,int op[]){
//递归出口
if(i==n){ // 已考虑所有物品
if(ww<=W && vv>=maxv) { // 可行且更优
maxv = vv;
for(int j=0;j<n;j++){
bestop[j] = op[j]; // 保存最优方案
}
}
}else{
// 选择物品i
op[i] = 1;
fun(i+1, ww+w[i], vv+v[i], op);
// 不选择物品i
op[i] = 0;
fun(i+1, ww, vv, op);
}
}
int main(){
fun(0,0,0,op);
cout<<maxv<<endl;
for(int i=0;i<n;i++){
cout<<bestop[i];
}
return 0;
}
【例题3】求和固定值为100的问题
设计一个算法在1,2,....,9(顺序不能变)数字之间插入+或者-或者什么都不插入,使得计算结果总是100的程序,并输出所有的可能性。例如:1+2+34-5+67-8+9=100
i:当前处理到第几个数字(从1开始)
n:数字总数(9)
sum:当前累计和(已计算过的表达式的值)
preadd:前一个被处理的数值(用于处理空格连接),(可能由多个数字连接而成)
op:运算符数组
cpp
#include<iostream>
using namespace std;
void DFS(int a[],int i,int n,int sum,int preadd,char op[]){ //i=当前位置;n=数组长度 ;sum=和 ;
if(i==n){
if(sum==100){ // 如果结果等于100,输出表达式
cout<<a[0];
for(int j=1;j<n;j++){
if(op[j] != ' ') cout<<op[j]; // 空格运算符不输出(因为已经合并到数字中了)
cout<<a[j];
}
cout<<"=100"<<endl;
}
}else{
op[i]='+';
DFS(a,i+1,n,sum+a[i],a[i],op);// 直接加上当前数字,sum增加 a[i],preadd更新为当前数字 a[i]
op[i]='-';
DFS(a,i+1,n,sum-a[i],-a[i],op);// 减去当前数字,sum减少 a[i],preadd更新为 -a[i](注意负号)
op[i]=' '; //为空的情况(数字连接)
int temp=0;
if(preadd>=0) temp=10*preadd+a[i];
else temp=10*preadd-a[i];
// 更新sum:减去旧的preadd,加上新的temp
DFS(a,i+1,n,sum-preadd+temp,temp,op);
}
}
int main(){
int a[]={1,2,3,4,5,6,7,8,9}; // 固定数字序列
int n=sizeof(a)/sizeof(a[0]); //可以不求长度 n=9 已知
char op[9]; //定义字符合集,运算符数组,op[0]不使用,因为第一个数字前没有运算符
DFS(a,1,n,1,1,op); //1必然会加进去(不考虑空格的问题)
return 0;
}
【例题4】迷宫问题
char Maze[n][n]={
{'O','X','X','X','X','X','X','X'},
{'O','O','O','O','O','X','X','X'},
{'X','O','X','X','O','O','O','X'},
{'X','O','X','X','O','X','X','O'},
{'X','O','X','X','X','X','X','X'},
{'X','O','X','X','O','O','O','X'},
{'X','O','O','O','O','X','O','O'},
{'X','X','X','X','X','X','X','O'},
};
'O':可通行的路径
'X':障碍物/墙壁
起点:Maze[0][0] = 'O'
终点:Maze[7][7] = 'O'
cpp
#include<iostream>
using namespace std;
#define n 8
char Maze[n][n]={
{'O','X','X','X','X','X','X','X'},
{'O','O','O','O','O','X','X','X'},
{'X','O','X','X','O','O','O','X'},
{'X','O','X','X','O','X','X','O'},
{'X','O','X','X','X','X','X','X'},
{'X','O','X','X','O','O','O','X'},
{'X','O','O','O','O','X','O','O'},
{'X','X','X','X','X','X','X','O'},
};
//四个方向:右(0,1)、下(1,0)、左(0,-1)、上(-1,0)
int H[4]={0,1,0,-1}; // 行变化:右、下、左、上
int V[4]={1,0,-1,0}; // 列变化:右、下、左、上
void DFS(int i,int j){
if(i==n-1 && j==n-1){ // 到达右下角终点
Maze[i][j]=' '; // 标记终点为路径
for(int x=0;x<n;x++){
for(int y=0;y<n;y++){
cout<<Maze[x][y]; // 输出整个迷宫的一条完整路径
}
cout<<endl;
}
}else{
//选择方向
if(i>=0 && i<n && j>=0 && j<n && Maze[i][j]=='O'){
for(int k=0;k<4;k++){ // 尝试四个方向
Maze[i][j]=' '; // 标记当前位置为路径
DFS(i+H[k],j+V[k]); // 递归到下一个位置
Maze[i][j]='O'; // 回溯:恢复原状
}
}
}
}
int main(){
DFS(0,0);
return 0;
}
【例题5】求解活动安排问题
假设有一个需要使用某一资源的n个活动所组成的集合S,S={1,.....,n}。该资源任何时刻只能被一个活动所占用,活动i有一个开始时间bi和结束时间ei(bi<ei),其执行时间为ei-bi,假设最早活动执行为0.
一旦某个活动开始执行,中间不能被打断,直到其执行完毕。若活动j有bi>=ej,或bj>=ei,则称这两个活动兼容
设计算法求一种最优的活动安排方案,使得所有安排的活动个数最多
cpp
#include<iostream>
using namespace std;
int a[]={1,2,3,4,5,6,7,8};
int n=8,num=0,last=0,maxNum=0,best[8];//活动数量,当前兼容活动数,上一个选中活动的结束时间,最大兼容活动数,最优排列
struct Action{ //活动的结构体
int b; // 开始时间 begin
int e; // 结束时间 end
};
//活动数组
Action A[] = {
{0,0}, // 索引0占位
{1,3}, // 活动1
{2,5}, // 活动2
{4,8}, // 活动3
{5,7}, // 活动4
{8,11}, // 活动5
{9,10}, // 活动6
{6,10}, // 活动7
{10,12} // 活动8
};
void swap(int &a,int &b){
int temp=a;
a=b;
b=temp;
}
void DFS(int i){
if(i==n){ // 生成了一个完整排列
if(num > maxNum){ // 如果当前兼容数更大
maxNum = num; // 更新最大数
for(int j=0;j<n;j++){
best[j] = a[j]; // 保存最优排列
}
}
}else{
for(int j=i; j<n; j++){ // 从i到n-1
swap(a[i], a[j]); // 将a[j]放到位置i
// 保存当前状态(用于回溯)
int last_temp = last;
int num_temp = num;
// 检查a[i]是否与上一个活动兼容
if(A[a[i]].b >= last){ // 开始时间 ≥ 上一个结束时间
num += 1; // 兼容活动数+1
last = A[a[i]].e; // 更新最后结束时间
}
DFS(i+1); // 递归处理下一个位置
// 回溯:恢复状态
last=last_temp;
num=num_temp;
swap(a[i],a[j]); // 恢复交换
}
}
}
int main() {
DFS(0);
cout<<"最大兼容活动数量为:"<<maxNum<<endl;
cout<<"最大兼容活动分别是:";
last=0;
for(int j=0;j<n;j++){
if(A[best[j]].b>=last){
last=A[best[j]].e;
cout<<'('<<A[best[j]].b<<','<<A[best[j]].e<<')';
}
}
return 0;
}