1 算法思想
算法分类
搜索算法主要分为:
暴力搜索+剪枝,枚举,广度优先搜索,深度优先搜索,二分查找,哈希查找, A*算法,两边向中间逼近,从中间向两边扩散等
1.1枚举
枚举: 最直白的搜索方式,依次尝试搜索空间中的所有解。可以在搜索过程中通过加强条件约束来减少搜索范围图。
例如: 百鸡问题
1.2广度优先搜索(BFS)
含义:遍历解答树时使每次状态转移时扩展出尽可能多的状态,并按照各状态出现顺序依次扩展它们。
表现:在解答树上表现为树的层次遍历。
适用:可用于求解最优问题。因为其搜索到的状态总是按照某个关键字递增(例如时间,倒杯次数等)。一旦问题中出现最少,最短,最优等关键字,
就需要考虑是否使用广度优先搜索。
实现: 一般用队列实现,用结构体保存每个状态,用标记数组防止无效搜索。
实现过程:
1)定义结构体用于保存每个状态,定义标记数组防止无效搜索
2)初始化第一个元素,并将该元素塞入队列中,设置第一个元素为已经访问
3)只要队列非空,得到并弹出队头元素,扩展得到新的元素,
对每个新元素,判断其如果满足约束条件并且是未遍历过的,
则更新新元素的状态,并将新元素塞入队列,设置新元素为已经访问过,
如果新元素是所求解,则直接返回
剪枝: 剪去解答树上不可能存在答案的子树。
1.3深度优先搜索(DFS)
含义: 优先遍历层次更深的状态,直到遇到一个状态节点不再拥有子树,则返回上一层,
访问其未被访问过的子树,直到解答树中所有状态被遍历完成。
适用: 深度优先搜索缺少广度优先搜索按层次递增顺序遍历的特性,深度优先搜索到的状态不再具有最优特性,深度优先搜索更多求解的是有解或者无解的问题。
实现: 通常使用递归实现。
实现过程:
1)扩展得到新元素,如果新元素不符合约束条件,则过滤该新元素;
2)基于当前状态更新得到新元素状态,判断新元素状态是否等于所求状态,如果是,设置结果标记为成功并直接返回;
3)否则,设置新元素为已经访问,递归处理,设置新元素为未访问(因为后续状态全部遍历完成,需要退回上层状态),如果结果标记为成功,则停止搜索
1.2 特点
1.3适用
1.4通用解法
广度优先搜索(BFS)算法:
1)定义结构体用于保存每个状态,定义标记数组防止无效搜索 2)初始化第一个元素,并将该元素塞入队列中,设置第一个元素为已经访问 3)只要队列非空,得到并弹出队头元素,扩展得到新的元素, 对每个新元素,判断其如果满足约束条件并且是未遍历过的, 则更新新元素的状态,并将新元素塞入队列,设置新元素为已经访问过, 如果新元素是所求解,则直接返回
|
深度优先搜索(DFS)算法:
1)扩展得到新元素,如果新元素不符合约束条件,则过滤该新元素; 2)基于当前状态更新得到新元素状态,判断新元素状态是否等于所求状态,如果是,设置结果标记为成功并直接返回; 3)否则,设置新元素为已经访问,递归处理,设置新元素为未访问(因为后续状态全部遍历完成,需要退回上层状态),如果结果标记为成功,则停止搜索 |
1.5经典例题讲解
广度优先搜索(BFS):
胜利大逃亡
我被魔王抓走,城堡是A*B*C的立方体,即A个B*C的矩阵,我被关在(0,0,0)位置,出口在(A-1,B-1,C-1),魔王在T分钟内回到城堡,
我每分钟能移动1个坐标。若走到出口恰遇魔王,也算成功。请输出多少分钟可以离开,不能则输出-1
代码:
typedef struct Stat //定义结构体用于保存每个状态 { int x,y,z;//坐标 int t;//从(0,0,0)到达该坐标的时间 }Stat;//保存当前节点的状态
int maze[N][N][N];//用于标识每一个坐标是0:路,1:墙 bool mark[N][N][N];// 定义标记数组防止无效搜索,用于标识该坐标是否已经搜索过,false:未搜索过,true:搜索过,便于剪枝 int goNext[][3] = {1,0,0, -1,0,0, 0,1,0, 0,-1,0, 0,0,1, 0,0,-1};//用于进行下一次6个可达状态的遍历
queue<Stat> queueStat;
//进行深度遍历,无需判断超时 int BFS(int a,int b,int c) { //只要队列不空,说明仍有状态需要遍历 while(!queueStat.empty()) { //弹出当前状态,进行状态迁移 Stat stat = queueStat.front(); queueStat.pop();
//遍历6种状态,扩展得到新的元素 int x,y,z; for(int i = 0 ; i < 6 ; i++) { x = stat.x + goNext[i][0]; y = stat.y + goNext[i][1]; z = stat.z + goNext[i][2];
//对每个新元素,判断其如果满足约束条件并且是未遍历过 // 判断是否仍在围墙中 //易错,这里城堡的位置是不能为a,因为这是数组,这样做就超过了 //if(x < 0 || x > a || y < 0 || y > b || z < 0 || z > c) if(x <0 || x >= a || y < 0 || y >=b || z < 0 || z >= c ) { continue; }
//如果已经遍历过,则跳过 if(true==mark[x][y][z]) { continue; } //如果下一个是墙,则跳过 if(1==maze[x][y][z]) { continue; }
//更新新元素的状态,并将新元素塞入队列,设置新元素为已经访问过 Stat statTemp; statTemp.x = x; statTemp.y = y; statTemp.z = z; statTemp.t = stat.t + 1;//所耗时间进行累加
//易错,更新剪枝状态 mark[x][y][z] = true;
//将新状态放入队列 queueStat.push(statTemp);
//如果新元素是所求解,则直接返回 //判断是否已经到达终点,则返回其所耗时间 if(a-1==x && b-1==y && c-1==z) { return statTemp.t; } }//for }//while return -1;//如果一直没有找到,返回-1 } |
深度优先搜索(DFS)
Temple of the bone
有一个N*M的迷宫,起点S,终点D,墙X和地面,'.'表示路,0秒时,我从S出发,
每秒能走到4个与其相邻位置的任意一个,行走之后不能再次走入。
问:是否存在一条路径使主人公刚好在T秒走到D
代码
typedef struct Stat { int x,y;//横纵坐标 int t;//耗费时间 }Stat;
char maze[N][N];//保存迷宫元素 bool success;//设置是否找到的成功标记
//走下一个位置的数组 int goNext[][2] = {0,1, -1,0, 1,0, 0,-1 };
//深度优先搜索 void DFS(int x,int y,int t,int n,int m,int tLimit) { int i ; // 扩展得到新元素 for(i = 0 ; i < 4 ; i ++) { int iXNext = x + goNext[i][0]; int iYNext = y + goNext[i][1];
// 如果新元素不符合约束条件,则过滤该新元素 //判定有无超过迷宫位置 if(iXNext < 1 || iXNext > n || iYNext < 1 || iYNext > m) { continue; } //判定是否是墙 if('X'==maze[iXNext][iYNext]) { continue; } // 基于当前状态更新得到新元素状态,判断新元素状态是否等于所求状态,如果是,设置结果标记为成功并直接返回 //判定是否到达终点,并且时间要符合 if('D'==maze[iXNext][iYNext] && tLimit==(t + 1)) { //易错,需要设置成功标记 success = true; return; }
//设置新元素为已经访问,递归处理,设置新元素为未访问(因为后续状态全部遍历完成,需要退回上层状态),如果结果标记为成功,则停止搜索 maze[iXNext][iYNext] = 'X'; //递归调用 DFS(iXNext,iYNext,t+1,n,m,tLimit); //若其后续状态全部遍历完毕,返回上层状态,因为要搜索后续状态,因此再将墙改为普通状态 maze[iXNext][iYNext] = '.';
//易错,判断是否搜索成功 if(true==success) { return; } }//for //如果一直遍历不到,则返回-1 return; } |
2 搜索系列
类别-编号 |
题目 |
遁去的1 |
1 |
找x 输入一个数n,输入n个数值不同的数,输入给定值x,输出x在数组中的下标,从0开始,若不在数组中则输出-1
输入: 2 1 3 0 输出: -1 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/97750108 思路:用哨兵,从后向前搜索 代码:
int main(int argc,char* argv[]) { int iNum; while(EOF!=scanf("%d",&iNum) && iNum >= 1 && iNum <= 200) { //bool isBreak = false; int* iArr = (int*)malloc(iNum*sizeof(iNum)); int iValue; for(int i = 0;i < iNum ; i++) { scanf("%d",&iValue); *(iArr+i) = iValue; } int iSearch; scanf("%d",&iSearch); int iSoladir = -1;//设置哨兵 for(int j = iNum-1 ; j > iSoladir ; j--) { if(iArr[j]==iSearch) { printf("%d",j); //isBreak = true; break; } } if(j==iSoladir) { printf("%d",j); } free(iArr); } getchar(); return 0; } |
2 |
统计同成绩学生人数 读入N名学生的成绩,将获得某一给定分数的学生人数输出
输入 3 80 60 90 60 2 85 66 0 5 60 75 90 55 75 75 0 输出: 1 0 2 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/47159633 思路: 在给定的数中,找到最大的数,找到小于该最大数的最大质数,然后用每个数去除以该质数,得到的值就存放在A[1000]中相应的下标位置 在给定一个分数,计算出哈希值,
哈希的思路: Hash[x] 保存的是数字x出现的次数,建立了一种一一映射的关系
关键: 1 Hash[x] 保存的是数字x出现的次数,建立了一种一一映射的关系 2 判断输入的数字是不是0,不要放在循环体内,只需要放到循环条件内 while(EOF!=scanf("%d",&n) && n!=0)
代码: int main(int argc,char* argv[]) { int iNum; bool isExit = false; int iBuf[20][1000]; int iGiv[20]; int iRes[20]; int k = 0; while(EOF!=scanf("%d",&iNum)) { if(0 > iNum || iNum > 1000) { break; } else if(0==iNum) { //isExit = true; break; } int iValue,i; for(i = 0; i < iNum ; i++) { scanf("%d ",&iValue); if(iValue < 0 || iValue > 100) { break; } else { iBuf[k][i] = iValue; } } int iGivSco; scanf("%d",&iGivSco); iGiv[k] = iGivSco; int count = 0; for(i = 0; i < iNum ;i++) { if(iBuf[k][i]==iGivSco) { count++; } } iRes[k] = count; k++; } //这边k不需要减1,因为k是用来做数组循环的 for(int j = 0; j < k; j++) { printf("%d\n",iRes[j]); } system("pause"); getchar(); return 0; } |
3 |
百鸡问题 :<=n元买100只鸡,大鸡5元每只,小鸡3元每只,还有1/3元每只的小鸡,分别记为x只,y只,z只,求所有解
输入:输入n 输出:对于每组输入,输出x y z ,按照x,y,z依次增大顺序输出 输入: 40 输出: x=0,y=0,z=100 x=0,y=1,z=99 x=0,y=2,z=98 x=1,y=0,z=99 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/47186199 易错:n元不需要全部用掉,必须用浮点数,因为有1/3 思路:枚举x,y,而z=100-x-y
代码: int main(int argc,char* argv[]) { int n; int x,y; while(EOF!=scanf("%d",&n) || n>0) { for(x = 0; x <= 20 ; x ++) { for(y = 0 ; y <= 33 ; y++) { if( (5*x+3*y+1.0*(100-x-y)/3) <= n ) { printf("x=%d,y=%d,z=%d\n",x,y,100-x-y); } } } } system("pause"); getchar(); return 0; } |
4 |
胜利大逃亡 我被魔王抓走,城堡是A*B*C的立方体,即A个B*C的矩阵,我被关在(0,0,0)位置,出口在(A-1,B-1,C-1),魔王在T分钟内回到城堡,我每分钟能移动1个坐标。若走到出口恰遇魔王,也算成功。请输出多少分钟可以离开,不能则输出-1 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/47186231
参见经典例题解析 |
5 |
非常可乐 要用N,M,S三个杯子均匀分出2杯可乐,S=N+M,0<S<101,N>0,M>0。如果能够平分,请输出倒出可乐的最少次数,如果不能输出"NO" 输入:3个整数,S 可乐的体积,N和M是两个杯子的容量,以"0 0 0"结束 输出:若可以平分,输出最少要倒的次数,否则输出NO
输入:3个整数,S 可乐的体积,N和M是两个杯子的容量,以"0 0 0"结束 输出:若可以平分,输出最少要倒的次数,否则输出NO 输入: 7 4 3 4 1 3 0 0 0 输出: NO 3 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/47186245 思路:用四元组(x,y,z,t)表示一个状态,x,y,z表示3个瓶子中的体积,t表示从初始状态到该平分状态需要倾倒的次数的状态之间的相互扩展。平分状态第一次被搜索出的次数 即为所求。若t不是最少次数,则状态为无效状态,将其舍弃。 关键: 1 需要一个倾倒函数,来标记两个杯子之间的状态 2 平分的时候只要3个杯子中任意2个杯子各的总体积的一半就可以 3 3个杯子两两相互倾倒共有6次,而且需要注意方向,a倒向b与b倒向a是不同的 4 初始化将s,n,m之间的剪枝状态置为false,注意每次使用之前清空队列,每次设定起始节点,并设置剪枝状态 5 <stdio.h> int puts(char* str),将字符串写到标准输出上,成功返回非负值,失败返回EOF 6 注意更新后的状态必须要用更新后的东西,初始体积必须为偶数,否则不能平分。注意iVa -= (iConB - iVb)必须先求,放在后面值都改变了
代码: typedef struct Stat { int x,y,z;//三个瓶子的体积 int t;//从初始状态到平分状态的倾倒次数 }Stat;
bool mark[N][N][N];//剪枝状态 int maze[N][N][N];//
queue<Stat> queueStat;//保存状态的队列
//倾倒函数,iConA是容器A的体积,iVa是容器A中的原来的体积。将容器A中体积倒往容器B中,注意要用引用 void Dump(int iConA,int& iVa,int iConB,int& iVb) { //注意,由于容器没有刻度,要么将一个容器中的所有可乐全倒,或者将另一个容器加满 //如果B容器能够容纳A容器中所有可乐,则全部倒出 if(iConB >= iVa + iVb) { iVb += iVa; iVa = 0; } else { iVa -= (iConB - iVb); iVb = iConB; //iVa = iVa - (iConB - iVb);//这里错了,应该先计算iVa的值,因为iVb与iConB的值一样了,所以没有发生改变 } }
int BFS(int s,int n,int m) { int x,y,z; while(!queueStat.empty()) { Stat stat = queueStat.front(); queueStat.pop(); x = stat.x; y = stat.y; z = stat.z;
//从S倒向N Dump(s,x,n,y); if(mark[x][y][z]==false) { //进行剪枝 mark[x][y][z] = true; //生成新状态 Stat statNew; statNew.x = x; statNew.y = y; statNew.z = z; statNew.t = stat.t + 1; //判断是否达到平分状态 if( (x==s/2 && y==s/2) || (x==s/2 && z==s/2) || (y==s/2 && z==s/2)) { return statNew.t; } //没有达到平分状态,则放入队列中 queueStat.push(statNew); }
//重置初始状态,并进行状态更新 x = stat.x; y = stat.y; z = stat.z; //从N倒向S Dump(n,y,s,x); if(mark[x][y][z]==false) { //进行剪枝 mark[x][y][z] = true; //生成新状态 Stat statNew; statNew.x = x; statNew.y = y; statNew.z = z; statNew.t = stat.t + 1; //判断是否达到平分状态 if( (x==s/2 && y==s/2) || (x==s/2 && z==s/2) || (y==s/2 && z==s/2)) { return statNew.t; } //没有达到平分状态,则放入队列中 queueStat.push(statNew); }
//重置初始状态,并进行状态更新 x = stat.x; y = stat.y; z = stat.z; //从S倒向M Dump(s,x,m,z); if(mark[x][y][z]==false) { //进行剪枝 mark[x][y][z] = true; //生成新状态 Stat statNew; statNew.x = x; statNew.y = y; statNew.z = z; statNew.t = stat.t + 1; //判断是否达到平分状态 if( (x==s/2 && y==s/2) || (x==s/2 && z==s/2) || (y==s/2 && z==s/2)) { return statNew.t; } //没有达到平分状态,则放入队列中 queueStat.push(statNew); }
//重置初始状态,并进行状态更新 x = stat.x; y = stat.y; z = stat.z; //从M倒向S Dump(m,z,s,x); if(mark[x][y][z]==false) { //进行剪枝 mark[x][y][z] = true; //生成新状态 Stat statNew; statNew.x = x; statNew.y = y; statNew.z = z; statNew.t = stat.t + 1; //判断是否达到平分状态 if( (x==s/2 && y==s/2) || (x==s/2 && z==s/2) || (y==s/2 &a |