本文将会详细的介绍最简单的搜索算法——BFS(广度搜索),(DFS)深度搜索及其背后的原理以及实际应用。同时,还会有一些具有图论性质的搜索问题,也就是图论问题,如弗洛伊德算法(和迪杰斯特拉同理,更加结构清晰)。
1.BFS——不用递归的简单搜索
听起来很困难,学起来很简单。即使小白,亦能学的轻轻松松。所谓广度优先搜索,就是一层一层进行搜索,不搜索完一层不开始下一层。想象一下顺藤摸瓜,抓到一个人,一个人叛变就会交代出一伙人;搜索这一伙人,这一伙人能各自供出一伙人...具体实现上,以一个点为基准,搜索与之相关联的所有点。全部搜索完成后,分别对每个点的相关联点进行搜索,一次类推,搜索整个图。整体是递推的思想,但不是递归的表现形式。以下的代码,将详细的展现其实现过程:
先给个模板,就是下面代码的过程:
1.构造map数组,存储能走的路和不能走的路,构建book数组存储走过的路
2.bfs函数里:构建队列queue
3.装入第一个点
4.循环:取出第一个点,装入关联的且未讨论过的点
5.条件判断
6.当queue空,结束
题目描述:一个有n个节点的连通图,这些节点以编号:1、2、……n进行编号,现给出节点间的连接关系。请以节点1为起点,按bfs的顺序遍历并输出该图。
输入:第一行为两整数,n和e,表示n个顶点,e条边
以下e行每行两个数,表示两个节点是联通的输出:只有一行,为节点的bfs顺序
#include<iostream>
#include<cstring>//cstring 不是高端头文件,只是给数组赋值时图个方便用的
#include<queue>//使用广度搜索,几乎不能避免队列
using namespace std;
bool book[1000];//book数组用来存储某一个点是不是走过,我不想重复劳动~
int map[1000][1000];//map数组存储搜索范围的图像(和处能走,何处不能,不要撞墙~)
int e, n;
void bfs() {//典型bfs广度搜索模板
queue<int>q;//构建队列,队列就是将被遍历的点
q.push(1);//先将第一个点装进来
while (!q.empty()){//队列最后空了,就是每个点都遍历了,看到最后会懂的
int a = q.front();
cout << a<<" ";
q.pop();//既然已经研究过这个点,把他弹出就好了
for (int i = 1; i <= e; i++) {
if (book[i])continue;//不行的条件:已经走过了
if (!map[a][i])continue;//不行的条件:地图上显示此路不通
q.push(i);//装入
book[i] = true;//标记
}
}
return;
}
int main(){
cin >> e >> n;
for (int i = 0; i < n; i++) {
int x, y;
cin >> x >> y;
map[x][y] = 1;
map[y][x] = 1;
}
memset(book, false, sizeof(book));//memset:全体赋值罢了,for(起始,结束++)也行
book[1] = true;//别忘了1一上来就用了,容易重复使用
bfs();
return 0;
}
实现细节值十分得一提:1.善于使用全局变量,bfs的传入参数就会少好多,也就会简单好多。
2.擅长使用continue是一个好习惯,把通过一个点找到的所有关联点,不符合的continue送走,比在一个if里写上很多“与”漂亮多了
总结一下,bfs写法:1.构建两数组 2.传进第一个 3.弹出加寻找 4.判断并会塞
2.略加难度——DFS深度搜索——初级递归
不是所有情况广度搜索都能解决。比如,人家就让你写深度搜索。或是,某些情况下对内存占用需求较大,深度搜索内存占用会小一些。包括,各种情况。递归是一件让人头疼的事情。不过别怕,因为我也怕。先仔细看下面的代码,后面有总结归纳。DFS就是模板,递归就是一种看问题的角度。在模板下,用正确的角度看问题,一切都很简单。注意,常常要将递归到哪一步了传入(不好描述,实在不懂可以直接看4的题)
题目描述:一个有n个节点的连通图,这些节点以编号:1、2、……n进行编号,现给出节点间的连接关系。请以节点1为起点,按dfs的顺序遍历并输出该图。
输入:第一行为两整数,n和e,表示n个顶点,e条边
以下e行每行两个数,表示两个节点是联通的输出:只有一行,为节点的dfs顺序
#include<iostream>
#include<cstring>
using namespace std;
int map[1000][1000];
bool book[1000];
int m, n;
void dfs(int step)//典型的dfs模板:
{
if (step > n)return;//先进行结束条件判断,达到了就结束这个循环(递归)
for (int i = 1; i <= n; i++) {/*正因为循环了每一个节点,第一保证
了寻找严密不落情况,第二保证return后不会整个递归过程结束*/
if (map[step][i] == 0)continue;//所有不符合条件的情况,一一列出,一一否决
if (book[i])continue;//所有不符合条件的情况,一一列出,一一否决
book[i] = true;//进行执行
cout << i << " ";//本题中:输出
dfs(i);//递归执行
}
return;
}
int main(){
int x, y;
cin >> n >> m;
for (int i = 0; i <= m; i++) {
cin >> x >> y;
map[x][y] = 1;
map[y][x] = 1;
}
memset(book, false, sizeof(book));
book[1] = 1;
cout << 1 << " ";
dfs(1);
return 0;
}
如果看不懂代码,没关系,先看一看文字解说。深度搜索是一个逐级溯流的过程,有一找二,有二找三。万一找到头没有找到,就返回上一个有节点的地方。在每一个节点,其实是“广度搜索”了每一个关联节点,并以此为基准深度搜索。
模板:1.构建map 和 book(意义见前文广度搜索)
2.每个节点挨个取出
3.符合条件进行输出
4.不和条件直接扔走
5.进行递归dfs( )
(6.恢复原状,因为下一次新节点的深度搜索与当前节点已经被选中情况不同)
3.牛刀小试:广度搜索
题目描述
虽然草儿是个路痴(就是在杭电待了一年多,居然还会在校园里迷路的人,汗~),但是草儿仍然很喜欢旅行,因为在旅途中 会遇见很多人(白马王子,^0^),很多事,还能丰富自己的阅历,还可以看美丽的风景……草儿想去很多地方,她想要去东京铁塔看夜景,去威尼斯看电影,去阳明山上看海芋,去纽约纯粹看雪景,去巴黎喝咖啡写信,去北京探望孟姜女……眼看寒假就快到了,这么一大段时间,可不能浪费啊,一定要给自己好好的放个假,可是也不能荒废了训练啊,所以草儿决定在要在最短的时间去一个自己想去的地方!因为草儿的家在一个小镇上,没有火车经过,所以她只能去邻近的城市坐火车(好可怜啊~)。
输入数据有多组,每组的第一行是三个整数T,S和D,表示有T条路,和草儿家相邻的城市的有S个,草儿想去的地方有D个;
接着有T行,每行有三个整数a,b,time,表示a,b城市之间的车程是time小时;(1=<(a,b)<=1000;a,b 之间可能有多条路)
接着的第T+1行有S个数,表示和草儿家相连的城市;
接着的第T+2行有D个数,表示草儿想去地方。
输出
输出草儿能去某个喜欢的城市的最短时间
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
int max;
int map[1001][1001];
int arrive[100];
int T, S, D;
bool find(int temp) {//是不是一个终点
for (int i = 0; i < D; i++)
if (arrive[i] == temp)
return 1;
return 0;
}
struct node {//节点类型
int name;//名字
int time;//用了多少时间
};
int bfs(int begin) {//广度搜索,进行多次即可
int ans = 9999;
node leave;
leave.name = begin; leave.time = 0;
queue<node>q;
q.push(leave);
while (!q.empty()) {
node top = q.front();
q.pop();
if (find(top.name)) {
if (top.time < ans) {
ans = top.time;
}
continue;
}
if (top.time >= ans)continue;//如果时间超时,一定要终止这种败类
for (int i = 1; i <= ::max; i++) {
if (!map[top.name][i])continue;//如果不存在这条路
node n0;
n0.name = i;
n0.time = top.time + map[top.name][i];
q.push(n0);
}
}
return ans;
}
int main() {
while (cin >> T >> S >> D) {
::max = 0;//全局变量不标注不一定能对
for (int i = 0; i < T; i++) {
int a, b, c;
cin >> a >> b >> c;
map[a][b] = c;
if (a > ::max)::max = a;
if (b > ::max)::max = b;//max代表点的数量,在光度搜索中有用
}
int begin[100];
for (int i = 0; i < S; i++)
cin >> begin[i];
for (int i = 0; i < D; i++) {
cin >> arrive[i];
}
for (int i = 0; i < S; i++)//多次进行循环
begin[i] = bfs(begin[i]);
int temp = begin[0];
for (int i = 1; i < S; i++)//寻找最小
if (begin[i] < temp)
temp = begin[i];
cout << temp << endl;
}
return 0;
}
本题中,为了使用广度搜索,不得不多次进行搜索,并且一次搜索多点。同时也意在说明,搜索只是一个模板,一个思路,一种方法,活学活用才是制胜根本。
4.牛刀小试:深度搜素
题目描述
在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子,棋子没有区别。要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,请编程求解对于给定形状和大小的棋盘,摆放k个棋子的所有可行的摆放方案C。
输入
输入含有多组测试数据。
每组数据的第一行是两个正整数,n k,用一个空格隔开,表示了将在一个n*n的矩阵内描述棋盘,以及摆放棋子的数目。 n <= 8 , k <= n
当为-1 -1时表示输入结束。
随后的n行描述了棋盘的形状:每行有n个字符,其中 # 表示棋盘区域, . 表示空白区域(数据保证不出现多余的空白行或者空白列)。输出
对于每一组数据,给出一行输出,输出摆放的方案数目C (数据保证C<2^31)。
#include<iostream>
#include<string>
using namespace std;
int n,k,result;
char arr[10][10];//储存棋盘
int line[10];//储存列的遍历情况。反正一行或一列只有一个子,不妨取一列一个,一行一行向下挪
void dfs(int hang, int temp)//行数 棋子数 输出结果
{
if (temp == k)如果棋子够多了,这种情况就应该结束了
{
result++;//显然,多了一个结果
return;//递归,脑子里不是一个函数,而是一组向下扩展的“函数树”。
//这种情况结束,不是整个函数结束
}
else if (hang >= n)//行超了,当然要结束这个“函数树”的分叉
return;
else{
for (int i = 0; i < n; i++) {//固定一行,每一个位置都可以插入一个元素,但只能插一个
if (arr[hang][i] == '#' && line[i] == 0)
{
line[i] = 1;//当前情况下,用到了这个行
dfs(hang + 1, temp + 1);
line[i] = 0;//为什么要归零?for的下个情况下,这个line还没用到
}
}
dfs(hang + 1, temp);//当然,空一行/从下一行开始也可以
}
return;
}
int main()
{
while (cin >> n >> k)
{
if (n == -1 || k == -1)
return 0;
else
{
result = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n;j++){
cin >> arr[i][j];
}
}
int a = 0;
dfs(0, 0);//dfs大多规定当前遍历位置,以及值
cout << result << endl;
}
}
return 0;
}
什么是做book的(用于储存用没用过,保证唯一性),什么是map,都因题而定。我们掌握的,只是基础和模板。切不可在刷题中迷失自我,不是到真正干的是什么一件事情。搜索只是一个模板,一个思路,一种方法,活学活用才是制胜根本。
接下来,谈一谈图论中的搜索——弗洛伊德算法
——只看名字,算法总是高不可攀。切实的研究,可以将古人的发明拉下神坛。——
弗洛伊德算法,应用在邻接矩阵上,用于计算两点间最小距离。看似,这是一件完全由深度广度搜索就能完成的事情。但如果仅仅这样,就不会有弗洛伊德算法了。弗洛伊德算法适合一次计算多对点之间的最小距离,注意,多对点,一次性,且只要三重for循环嵌套。
方法上,周全的讨论:对所有的c,如果(a->b距离) > (a->c距离)+(c->b距离),更新a->b距离,在一个邻接矩阵上。如果所有a、b、c考虑一遍,得到的就是任意两点之间最短距离。标准的证明十分复杂,这里就不加以展开了。你想到的a->c->d->e->f->b,多中间点问题,其实也可以被处理。相信算法是正确的。同时,他对有向图、无向图都成立。
算法核心:3层for循环嵌套:
·第一层是中间点
·第二层是出发点
·第三层是结束点
题目描述
在带权有向图G中,求G中的任意一对顶点间的最短路径问题,也是十分常见的一种问题。 解决这个问题的一个方法是执行n次迪杰斯特拉算法,这样就可以求出每一对顶点间的最短路径,执行的时间复杂度为O(n3)。 而另一种算法是由弗洛伊德提出的,时间复杂度同样是O(n3),但算法的形式简单很多。
在本题中,读入一个有向图的带权邻接矩阵(即数组表示),建立有向图并按照以上描述中的算法求出每一对顶点间的最短路径长度。
输入
输入的第一行包含1个正整数n,表示图中共有n个顶点。其中n不超过50。 以后的n行中每行有n个用空格隔开的整数。对于第i行的第j个整数,如果大于0,则表示第i个顶点有指向第j个顶点的有向边,且权值为对应的整数值;如果这个整数为0,则表示没有i指向j的有向边。当i和j相等的时候,保证对应的整数为0。输出
共有n行,每行有n个整数,表示源点至每一个顶点的最短路径长度。如果不存在从源点至相应顶点的路径,输出-1。对于某个顶点到其本身的最短路径长度,输出0。 请在每个整数后输出一个空格,并请注意行尾输出换行。
#include <iostream>
#include<cstring>
using namespace std;
int dis[1000][1000];//邻接矩阵,dis[a][b]=c相当于a->b有一条长为c的边
int main(){
int n;
cin >> n;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
int x;
cin >> x;
if (x == 0) {
if (i == j)
dis[i][j] == 0;
else
dis[i][j] = 999999;//极大距离,相当于两个点不相连,距离标成极大
}
else
dis[i][j] = x;
}
}
//弗洛伊德算法
for (int k = 0; k < n; k++) {//第一层循环:k是中间点
for (int v = 0; v < n; v++) {//第二层循环:v是出发点
for (int w = 0; w < n; w++) {//第三层循环:w是结束点
if(dis[v][w]>(dis[v][k]+dis[k][w]))//如果走a->c->b路线更快,就更新
dis[v][w] = (dis[v][k] + dis[k][w]);
}
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (dis[i][j] == 999999)//如果没有连接,输出-1
cout << -1 << " ";
else
cout << dis[i][j] << " ";
}
cout << endl;
}
return 0;
}
小试牛刀:如果出发点、节点固定,可以减少一层for循环,但是至少两层,不信自己画画试试
题目描述
P市有n个公交站,之间连接着m条道路。P市计划新开设一条公交线路,该线路从城市的东站(s点)修建到西站(t点),请为P市设计一条满足上述条件并且最短的公交线路图。
输入
第一行有5个正整数n,m,s,t。
接下来m行,每行3个数a,b,v描述一条无向道路a——b,长度为v。输出
如果有解,输出一行,表示满足条件的最短公交线路的长度c。 否则,输出“-1”样例输入
3 3 1 2 1 2 3 2 3 4 1 3 5
#include<iostream> using namespace std; int n; int m; int s; int t; int map[1002][1002];//地图,就是之前的关联矩阵 const int inf = 0x3f3f3f3f;//就是极其远距离的一种表示(大数) int main() { cin >> ::n >> ::m >> ::s >> ::t; for (int i = 1; i <= m; i++) { int a, b, c; cin >> a >> b >> c; ::map[a][b] = c; ::map[b][a] = c;//对称的图 } for(int i=1;i<=n;i++) for (int j = 1; j <= n; j++){ if (i == j)continue; if (::map[i][j])continue; ::map[i][j] = inf;//万一是0,还不在对角线上,当然是不相关联(无穷远) } //fluid for(int i=1;i<=n;i++) for (int j = 1; j <= n; j++) { if (::map[j][t] > ::map[j][i] + ::map[i][t])//设成了双层循环 ::map[j][t] = ::map[j][i] + ::map[i][t]; } if (::map[s][t] == inf) ::map[s][t] = -1; cout << ::map[s][t] << endl; }
自己去小试牛刀:弗洛伊德
题目描述
有一位使者要游历各国,他每到一个国家,都能学到一种文化,但他不愿意学习任何一种文化超过一次(即如果他学习了某种文化,则他就不能到达其他有这种文化的国家)。不同的国家可能有相同的文化。不同文化的国家对其他文化的看法不同,有些文化会排斥外来文化(即如果他学习了某种文化,则他不能到达排斥这种文化的其他国家)。
现给定各个国家间的地理关系,各个国家的文化,每种文化对其他文化的看法,以及这位使者游历的起点和终点(在起点和终点也会学习当地的文化),国家间的道路距离,试求从起点到终点最少需走多少路。
输入
每组输入数据的第一行为五个整数N,K,M,S,T,每两个整数之间用一个空格隔开,依次代表国家个数(国家编号为1到N),文化种数(文化编号为1到K),道路的条数,以及起点和终点的编号(保证S不等于T);
第二行为N个整数,每两个整数之间用一个空格隔开,其中第i个数Ci,表示国家i的文化为Ci。
接下来的K行,每行K个整数,每两个整数之间用一个空格隔开,记第i行的第j个数为aij,aij=1表示文化i排斥外来文化j(i等于j时表示排斥相同文化的外来人),aij=0表示不排斥(注意i排斥j并不保证j 一定也排斥i)。
接下来的M行,每行三个整数u,v,d,每两个整数之间用一个空格隔开,表示国家u与国家v有一条距离为d的可双向通行的道路(保证u不等于v,两个国家之间可能有多条道路)。
数据规模:
对于20%的数据,有2≤N≤8,K≤5;
对于30%的数据,有2≤N≤10,K≤5;
对于50%的数据,有2≤N≤20,K≤8;
对于100%的数据,有2≤N≤100,1≤K≤10,1≤M≤N2,1≤ki≤K,1≤u, v≤N,1≤d≤1000,S≠T,1≤S, T≤N。
输出
每组输出只有一行,一个整数,表示使者从起点国家到达终点国家最少需要走的距离数(如果无解则输出-1)。
下面是对样例数据的解释:
样例一:
由于到国家2必须要经过国家1,而国家2的文明却排斥国家1的文明,所以不可能到达国家2。
样例二:
路线为1->2。
样例输入 复制
2 2 1 1 2 1 2 0 1 1 0 1 2 10
样例输出 复制
-1
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
#define INF 0x3f3f3f3f
const int maxn=150;
int n,k,m,s,t;
int culture[maxn];
int dd[maxn][maxn];
int w[maxn][maxn];
void floyed()
{
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(!dd[culture[i]][culture[k]]&&!dd[culture[k]][culture[j]])//能否到达i--k--j
w[i][j]=min(w[i][j],w[i][k]+w[k][j]);
}
int main()
{
memset(w,0x3f,sizeof(w));
cin>>n>>k>>m>>s>>t;
for(int i=1;i<=n;i++)
cin>>culture[i];
for(int i=1;i<=k;i++)
for(int j=1;j<=k;j++)
cin>>dd[i][j];
for(int i=1;i<=m;i++)
{
int x,y,z;
cin>>x>>y>>z;
if(!dd[culture[y]][culture[x]])
w[x][y]=z;//若y不排斥x,建立x--y的边
if(!dd[culture[x]][culture[y]])
w[y][x]=z;
}
floyed();
if(w[s][t]==INF) cout<<"-1"<<endl;
else cout<<w[s][t]<<endl;
return 0;
}