深度优先搜索
深度优先搜索概述
假设从顶点 u 出发,深度优先搜索的基本思想是:访问顶点 u,然后从 u 的未被访问的邻接点中选取一个顶点 v,再从 v 出发进行深度优先搜索,直至图中所有和 u 有路径相通的顶点都被访问到。
算法:DFS
输入:起始顶点 u
输出:搜索过程中访问的顶点序列
1. 访问顶点 u; 标记顶点 u 已被访问;
2. v =顶点 u 的邻接点;
3. while ( v 存在)
3.1 如果顶点 v 未被访问,则递归执行DFS(v);
3.2 否则 v = 顶点 u 的下一个邻接点;
例题
山洞寻宝图
题目
在一座山上有 n 个山洞,其中有一个山洞藏有寻宝图,有个猎人知道山上有寻宝图但不知道藏在哪个山洞里,只要猎人到达寻宝图所在的山洞就一定能够得到藏宝图。假设猎人熟悉山路,但是有些山洞之间没有山路相通。给定 n(3 ≤ n ≤ 100)个山洞之间的连通关系、寻宝图所在山洞、以及猎人寻找的起始山洞,请问猎人是否能够得到寻宝图?
分析
这道题目可以用深度优先搜索来解决,首先我们需要建立一个邻接表,然后从起始山洞开始进行深度优先搜索,如果搜索到寻宝图所在的山洞,那么就说明猎人能够得到寻宝图,否则就说明猎人不能得到寻宝图。
实现
int Dfs(int u, int v)
{
int j;
visited[u] = 1;
if (u == v) return 1;
for (j = 0; j < n; j++)
{
if ((edge[u][j] == 1) && (visited[j] == 0)) {
flag = Dfs(j, v);
if (flag == 1) break;
}
}
return flag;
}
城堡问题
问题
某城堡被分割成 个方块,每个方块的四面可能有墙,“#”代表有墙,没有墙分割的方块连在一起组成一个房间,城堡外围一圈都是墙。如果 1、2、4 和 8 分别对应左墙、上墙、右墙和下墙,则可以用方块周围每个墙对应的数字之和来描述该方块四面墙的情况,请计算城堡一共有多少个房间,最大的房间有多少个方块。
分析
可以把方块看成顶点,相邻的方块之间如果没有墙,则在方块对应顶点之间连一条边,从而将城堡问题抽象为一个无向图。求城堡的房间个数,实际上就是求图中有多少个连通分量,求城堡的最大房间数,就是求最大连通分量包含的顶点数
实现
int roomArea,maxRoom;
void DFS(int i,int j){
if(visit[i][j]==1) return;
roomArea++;
visit[i][j]=1;
if((room[i][j]&1)==0) DFS(i,j-1);
if((room[i][j]&2)==0) DFS(i-1,j);
if((room[i][j]&4)==0) DFS(i,j+1);
if((room[i][j]&8)==0) DFS(i+1,j);
}
int castle(int m,int n){
int i,j;
int max=0;
int roomNum=0;
for(i=0;i<m;i++){
for(j=0;j<n;j++){
roomArea=0;
roomNum++;
DFS(i,j);
if(roomArea>max) max=roomArea;
}
}
return max;
}
回溯法
问题的解空间
所有可能的解向量构成了问题的解空间。将问题的可能解表示为满足某个约束条件的等长向量 ,其中分量 的取值范围是某个有限集合
对于 n 个物品的 0/1 背包问题,表示物品 i 装入背包, 表示物品 i 没有装入背包,当时,解空间为 ,即
问题的解空间树
问题的解空间一般用解空间树(也称状态空间树)的方式组织,树的根结点位于第 1 层,表示搜索的初始状态,第 2 层的结点表示对解向量的第一个分量做出选择后到达的状态,第 1 层到第 2 层的边上标出对第一个分量选择的结果,依此类推,从树的根结点到叶子结点的路径就构成了解空间的一个可能解。
回溯法
回溯法从根结点出发,按照深度优先搜索解空间树,对于解空间树的某个结点,如果该结点满足问题的约束条件,则进入该子树继续进行搜索,否则跳过以该结点为根的子树,也就是剪枝
如果再往下走不可能得到解,就及时回溯,退一步另找路径,从而避免无效搜索
问题的解空间树是虚拟的,并不需要在搜索过程中构建一棵真正的树结构
回溯法的解题思想
由于解向量 中每个分量 的值都取自集合 ,因此,可以依次试探集合 S 的元素,以确定当前分量 的值。一般情况下,如果 是问题的部分解,试探集合 的元素 作为分量 的值,有下面三种情况
- 如果 是问题的最终解,则输出 ,并返回
- 如果 $X=(x_1, x_2, …, x_i)满足问题的约束条件,是部分解,则继续试探分量 的值
- 如果 不满足问题的约束条件,有以下情况
- 是集合 的最后一个元素,且 不是问题的最终解,则返回
- 不是集合 的最后一个元素,且 不是问题的最终解,则回溯,
算法:回溯算法的一般框架
输入:集合 S={a1, a2, …, ak}
输出:解向量 X=(x1, x2, …, xn)
1. 初始化解向量xi(1 ≤ i ≤ n);
2. i = 1,表示搜索从根结点开始;
3. 当(k >= 1)时执行下述操作:
3.1 令xi = 当前值在集合 S 的下一个值;
3.2 如果 X=(x1, x2, …, xi)不是问题的解,转步骤 3.1 继续试探;
3.3 如果试探了集合 S 的所有元素,则 i = i - 1,转步骤 3.1 进行回溯;
3.4 如果 X=(x1, x2, …, xi)是问题的部分解,i = i + 1,转步骤 3.1 继续扩展;
3.5 如果 X=(x1, x2, …, xi)是问题的最终解,输出解向量 X,结束算法;
4. 退出循环,说明问题无解;
回溯法本质上属于蛮力穷举,搜索具有指数阶个结点的解空间树,在最坏情况下,时间代价肯定为指数阶。
回溯法的有效性体现在当问题规模 n 很大时,在搜索过程中对问题的解空间树实行大量剪枝
例题
素数环问题
问题
把整数填写到一个环中,要求每个整数只填写一次,并且相邻的两个整数之和是素数
分析
这个素数环有 20 个位置,每个位置可以填写的整数有 1~20 共 20 种可能,可以对每个位置从 1 开始进行试探,约束条件是正在试探的数满足如下条件:
- 与已经填写到素数环中的整数不重复;
- 与前面相邻的整数之和是素数;
- 最后一个填写到素数环中的整数与第一个填写的整数之和是素数
实现
int prime(int n){
if(n==1) return 0;
for(int i=2;i<=sqrt(n);i++)
if(n%i==0)
return 0;
return 1;
}
int check(int x[],int n,int i){
//检查x[i]是否与前面的数相同
for(int j=0;j<i;j++)
if(x[i]==x[j])
return 0;
//检查x[i]是否与相邻数相加是否为素数
if(i>0&&prime(x[i]+x[i-1])==0)
return 0;
//最后一个数与第一个数相加是否为素数
if(i==n-1&&prime(x[i]+x[0])==0)
return 0;
}
void primeCircle(int x[],int n){
for(int i=0;i<n;i++)
x[i]=0;
x[0]=1;
for(int i=1;i<n;){
x[i]++;
while (x[i]<=n){
if(check(x,i,x[i]))
break;
else
x[i]++;//试探下一个数
}
if(x[i]>n) x[i--]=0;//回溯
else if(i<n-1) i++;//下一个数
else{
for(int j=0;j<n;j++)
cout<<x[j]<<" ";
cout<<endl;
return;
}
}
cout<<"No answer"<<endl;
}
八皇后问题
问题
在 8×8 的国际象棋棋盘上放置 8 个皇后,使得任意两个皇后都不能相互攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上
分析
- 行:每行只能放置一个皇后,因此可以用一个长度为 8 的数组来表示,数组的下标表示行号,数组的值表示列号;
- 列:由于皇后不同列,有
- 斜线:由于皇后不同斜线,有
实现
int place(int x[],int i){
for(int j=0;j<i;j++){
if(x[i]==x[j]||abs(x[i]-x[j])==i-j)
return 0;
}
return 1;
}
int queenChess(int x[],int n){
//初始化棋盘
for(int i=0;i<n;i++)
x[i]=-1;
for(int i=0;i<n;){
x[i]++;
while(x[i]<n&&!place(x,i))
x[i]++;
if(x[i]==n){
x[i]=-1;
i--;
}
else if(i==n-1){
//摆放完毕
for(int j=0;j<n;j++)
cout<<x[j]<<" ";
return 1;
}
else
i++;
}
}
图着色问题
问题
给定一个无向图 G,要求对图中的每个顶点进行着色,使得任意两个相邻的顶点的颜色都不同
分析
- 顶点:每个顶点只能着一种颜色,因此可以用一个长度为 n 的数组来表示,数组的下标表示顶点,数组的值表示颜色;
- 首先把所有顶点的颜色初始化为 0,然后依次为每个顶点着色。如果当前顶点着色没有冲突,则继续为下一个顶点着色,否则,为当前顶点着下一个颜色,如果所有 m 种颜色都试探过并且都发生冲突,则回溯到当前顶点的上一个顶点
实现
int **arc;
int *color;
int isColorable(int i){
for(int j=0;j<i;j++){
if(arc[i][j]==1 && color[i]==color[j]){
return 0;//着色冲突返回0
}
}
return 1;
}
void graphColor(int m){
for(int i=0;i<m;i++){
color[i]=0;
}
for(int i=0;i<m;){
color[i]++;
while(color[i]<=m && !isColorable(i)){
color[i]++;
}
if(color[i]>m){
color[i]=0;
i--;
}
else if(i<m-1) i++;//着色成功,继续下一个节点
else{
for(int j=0;j<m;j++){
cout<<color[j]<<" ";
}
return;
}
}
}
批处理作业调度
问题
n 个作业${1, 2, …, n}要在两台机器上处理,每个作业必须先由机器 1 处理,再由机器 2 处理,机器 1 处理作业i所需时间为 ,机器 2 处理作业 i 所需时间为 ,批处理作业调度问题要求确定这 n 个作业的最优处理顺序,使得从第 1 个作业在机器 1 上处理开始,到最后一个作业在机器 2 上处理结束所需时间最少
分析
- 设a[n]为作业在机器1上的处理时间,b[n]为作业在机器2上的处理时间,x[n]为作业的处理顺序,sum1[n]为作业在机器1上的处理时间之和,sum2[n]为作业在机器2上的处理时间之和,有
实现
int batchJob(int a[],int b[],int n){
int x[n]={0};
int sum1[n]={0};
int sum2[n]={0};
int bestTime=INT_MAX;
for(int i=1;i<=n;){
x[i]++;
while (x[i]<=n){
int j;
for(j=1;j<i;j++)
if(x[i]==x[j])//判定作业是否重复处理过
break;
if(j==i){
sum1[i]=sum1[i-1]+a[x[i]];
sum2[i]=(sum1[i]>sum2[i-1])?sum1[i]+b[x[i]]:sum2[i-1]+b[x[i]];
if(sum2[i]<bestTime)
break;
else
x[i]++;//剪枝
}
else
x[i]++;//已经处理过,跳过
}
if(x[i]==n+1){//回溯
x[i]=0;
i--;
}
else if (i<n)
i++;
else{
bestTime=sum2[n];
cout<<"目前最短的时间为:"<<bestTime<<endl;
for(int k=1;k<=n;k++)
cout<<x[k]<<" ";
}
}
return bestTime;
}
哈密顿回路
问题
哈密顿回路问题要求从一个城市出发,经过每个城市恰好一次,然后回到出发城市
分析
- 对于回路中的,有以下约束
- 在解空间树中从根结点开始搜索,如果从根结点到当前结点对应一个部分解,则在当前结点处选择第一棵子树继续搜索,否则,对当前子树的兄弟结点进行搜索,如果当前结点的所有子树都已试探并且发生冲突,则回溯到当前结点的父结点
实现
int hamitton(int **arc,int n){
int v[n]={0};
//x表示经过的路径
int x[n]={-1};
x[0]=0;v[0]=1;//从第一个点开始
int i;
for(i=0;i>=1;){
x[i]++;
//试探下一个点
while(x[i]<n)
if(arc[x[i-1]][x[i]]==1&&v[x[i]]==0)
break;
else
x[i]++;
if(x[i]==n){
//回溯
v[x[i]]=0;
x[i--]=-1;
}
else if(i==n-1){
//判断是否是回路
if(arc[x[i]][0]==1){
for(int j=0;j<n;j++)
cout<<x[j]<<" ";
cout<<endl;
}
//回溯
else{
v[x[i]]=0;
x[i--]=-1;
}
}
else{
//进入下一层
v[x[i]]=1;
i++;
x[i]=-1;
}
}
}
0/1背包问题
问题
有一个背包,它的容量为 ,现在有 个物品,第 个物品的重量为 ,价值为 ,问应该如何选择装入背包的物品,使得装入背包中物品的总价值最大
分析
- 回溯法解决0/1背包问题,设变量bestP记录当前最优解,变量curP记录当前解,变量curW记录当前重量,w[n]表示是否装入背包
实现
int knapSack(int w[],int v[],int n,int W[],int C){
//初始化W数组
for(int i=0;i<=n;i++){
W[i]=-1;
}
int i=0;
int cw=0;
int cv=0;
int bestP=0;
for(i=0;i>=0;){
W[i]++;
cw+=w[i];
cv+=v[i];
if(cw>C){
W[i]=-1;
cw-=w[i];
cv-=v[i];
}
else{
if(i==n-1){
if(cv>bestP){
bestP=cv;
}
W[i]=-1;
cw-=w[i];
cv-=v[i];
}
else{
i++;
}
}
}
return bestP;
}
练习
-
- 题目:给定一个正整数集合和一个正整数y,设计回溯算法,使X中的一个子集Y,有
- 分析:限定条件为,若,则回溯到上一层,否则,若,则输出Y,否则,进入下一层
- 实现
int findSub(int X[], int n, int y, int Y[]) { int sum = 0; // 初始化Y for (int i = 0; i < n; i++) { Y[i] = -1; } int i = 0; while (i >= 0) { // 试图下一个 Y[i]++; //如果这个元素已经加入到Y中,那么就跳过 for (int j = 0; j < i; j++) { if (Y[i] == Y[j]) { Y[i]++; } } // 如果超出范围,回溯 if (Y[i] >= n || sum + X[Y[i]] > y) { i--; if (i >= 0) { sum -= X[Y[i]]; Y[i] = -1; } } else if(i<n-1){ sum += X[Y[i]]; i++; } else if(i==n-1){ sum += X[Y[i]]; if (sum == y) { for (int j = 0; j < n; j++) { cout << X[Y[j]] << " "; } cout << endl; } sum -= X[Y[i]]; Y[i] = -1; i--; if (i >= 0) { sum -= X[Y[i]]; Y[i] = -1; } } } return 0; }
-
- 问题:迷宫问题,找到一条从入口到出口的路径,前进的方向有8个,分别是上、下、左、右、左上、左下、右上、右下
- 分析:深度优先搜索,到找到出口为止
- 实现
int book[8][8] = {0}; int n = 8, m = 8; int flag = 0; int next[8][2] = { {0, 1}, {1, 0}, {0, -1}, {-1, 0}, {-1, -1}, {-1, 1}, {1, -1}, {1, 1} }; void dfs(int x, int y) { int tx, ty; if (x == n - 1 && y == m - 1) { flag = 1; return; } for (int i = 0; i < 8; i++) { tx = x + next[i][0]; ty = y + next[i][1]; if (tx < 0 || tx >= n || ty < 0 || ty >= m) { continue; } if (maze[tx][ty] == 0 && book[tx][ty] == 0) { book[tx][ty] = 1; dfs(tx, ty); book[tx][ty] = 0; } } return; }
-
- 问题:桥本分数,1—9的数字分别填入公式中,数字不然重复,使得等式成立,求所有可能的解
- 分析:深度优先搜索,限定条件为数字不重复,且等式成立(其实就是利用深度优先搜索实现全排列)
int n=9,vis[10],n1; int ans[10]; void dfs(int root,int level) { ans[level]=root; vis[root]=1; if(level==n) { if(n1==0) return; int num1=ans[2]*10+ans[3],num2=ans[5]*10+ans[6],num3=ans[8]*10+ans[9]; int sum=ans[1]*num2*num3+ans[4]*num1*num3-ans[7]*num1*num2; if(sum==0) { n1--; cout<<ans[1]<<"/"<<ans[2]<<ans[3]<<"+"<<ans[4]<<"/"<<ans[5]<<ans[6]<<"="<<ans[7]<<"/"<<ans[8]<<ans[9]<<endl; } } for(int i=1;i<=n;i++) { if(!vis[i]) { dfs(i,level+1); vis[i]=0; } } }
-
- 问题:错位问题,n个人拿n个帽子,所有人都拿错的概率,并展示所有带错帽子的情况
- 分析:回溯法,记录每个人拿的帽子,若有h[i]i,则回溯,否则,若in,则输出,否则,进入下一层
- 实现
int dislocation(int n){
int s = 0;//解的个数统计
int a[n+1]={0};
a[1]=2;
int i=1;
while(true){
bool flag = true;
if(a[i]!=i){
for(int j=1;j<i;j++){
if(a[j]==a[i]){
flag = false;
break;
}
}
}else{
flag = false;
}
if(flag&&i==n){
s++;
for(int j=1;j<=n;j++){
cout<<a[j]<<" ";
}
}
if(flag&&i<n){
i++;
a[i]=1;
continue;
}
while(a[i]==n&&i>0){
i--;//回溯,a[i]到末尾,则回溯
}
if(i>0){
a[i]++;//向后移
}else{
break;
}
}
}
本文作者:asdio
本文链接:https://www.cnblogs.com/agitm/p/17269498.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步