算法案例之回溯法

基于深度优先搜索的算法,核心是 "回溯",在有限状态集中搜索解,不满足条件时返回上一状态尝试其他方案

  • 设计思路:遍历解空间,对每个状态做选择;若状态不满足条件或到达边界,则回溯至上一状态,直至找到有效解或遍历完所有解空间。
  • 适用场合:需遍历所有可能情况的问题,如组合、优化类问题。
  • 注意事项
    • 明确状态表示及合规判断规则
    • 通常用递归实现遍历
    • 合理剪枝,剔除无效路径以提升效率
    • 借助栈或递归记录当前路径状态,实现回溯

【例题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] 的全排列:

  1. 固定位置 0,让 a[0] 依次与 a[0]..a[n-1] 交换

  2. 对剩下的 a[1..n-1] 递归执行相同操作

  3. 每次递归完成后,恢复原数组(回溯)

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;
}
相关推荐
张祥6422889042 小时前
误差理论与测量平差基础笔记八
笔记·算法·机器学习
进击的小头2 小时前
传递函数与系统特性(核心数学工具)
python·算法·数学建模
程序员-King.2 小时前
day168—递归—二叉树的最大路径和(LeetCode-124)
算法·leetcode·深度优先·递归
小王努力学编程2 小时前
LangChain——AI应用开发框架(核心组件2)
linux·服务器·c++·人工智能·python·langchain·信号
源代码•宸2 小时前
Leetcode—513. 找树左下角的值【中等】
经验分享·算法·leetcode·面试·职场和发展·golang·dfs
_Soy_Milk2 小时前
【算法工程师】—— Pytorch
人工智能·pytorch·算法
wen__xvn2 小时前
模拟题刷题1
数据结构·算法
亲爱的非洲野猪2 小时前
1动态规划入门:从斐波那契到网格路径
算法·动态规划
CC.GG2 小时前
【C++】异常
java·jvm·c++