圖的遍歷是指從圖中某一頂點出發,按照某種搜索方法沿着圖中的邊對圖中的所有頂點訪問一次,且僅訪問一次。圖的遍歷常見算法有BFS和DFS。
文章目錄
(一)深度優先搜索DFS
1、基本思路
DFS 用於找所有解的問題,它的空間效率高,但是找到的不一定是最優解,必須記錄並完成整個搜索,故一般情況下,深搜需要非常高效的剪枝。DFS類似於樹的先序遍歷,搜索策略爲儘可能“深”的搜索一個圖。首先訪問圖中某一起始頂點v,然後由v出發,訪問與v鄰接且未被訪問的任一頂點w1,再訪問與w1鄰接且未被訪問的任意頂點w2,…重複上述過程。當不能再繼續向下訪問時,依次退回到最近被訪問的頂點,若它還有鄰接頂點未被訪問過,則從該點開始繼續上述搜索過程,知道圖中所有頂點均被訪問過爲止。程序僞代碼如下:
bool visited[MAX_VERTEX_NUM];//訪問標記數組
void DFSTraverse(Graph G){
//對圖G進行深度優先遍歷,訪問函數爲visit()
for(v=0;v<G.vexnum,++v){//本代碼中是從v=0開始遍歷
if(!visited[v])
DFS(G,v);
}
}
void DFS(Graph G,int v){
//從頂點v出發,採用遞歸思想,深度優先遍歷圖G
visit(v);
visited[v]=true;
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
if(!visited[w]){//w爲u的尚未訪問的鄰接頂點
DFS(G,w);
}
}
2、圖示
3、算法性能分析
DFS算法是一個遞歸算法,需要藉助一個遞歸工作棧,故它的空間複雜度爲O(|V|)。遍歷圖的過程實質上是對每個頂點查找其鄰接點的過程,其耗費的時間取決於所採用的存儲結構。當以鄰接矩陣表示時,查找每個頂點的鄰接點所需時間爲O(|V|),故總的時間複雜度爲O(|V*V|)。當以鄰接表表示時,查找所有頂點的鄰接點所需時間爲O(|E|),訪問頂點所需時間爲O(|V|),此時,總的時間複雜度爲O(|V|+|E|)。
4、深度優先遍歷的非遞歸寫法
在深度優先搜索的非遞歸算法中使用一個棧S,用來記憶下一步可能訪問的頂點,同時使用了一個訪問標記數組visited[i],在visited[i]中記憶第i個頂點是否在站內或者曾經在棧內。若是,以後他不能再進棧。圖採用鄰接表方式,僞代碼如下所示:
void DFS_Non_Rc(AGraph& G,int v){
//從頂點v開始進行深度優先搜索,一次遍歷一個連通分量的所有頂點
int w;//頂點序號
InitStack(S);//初始化棧S
for(i=0;i<G.vexnum;i++)
visited[i]=false;//初始化visited[]
Push(S,v);
visited[v]=true;//v入棧,並置visited[v]
while(!IsEmpty(S)){
k=Pop(S);//出棧
visit(k);//先訪問,再將其子結點入棧
for(w=FirstNeighbor(G,k);w>=0;w=NextNeighbor(G,k,w))
if(!visited[w]){//未進過棧的頂點進棧
Push(S,w);
visited[w]=true;//作標記,以免再次入棧
}//end if
}//end while
}
(二)廣度優先遍歷BFS
1、基本思想
BFS 常用於找單一的最短路線,它的特點是 “搜到就是最優解”,類似於二叉樹的層序遍歷算法,它的基本思想是:首先訪問起始頂點v,接着由v出發,依次訪問v的各個未訪問過的鄰接頂點w1,w2,w3,…wi,然後再依次訪問w1,w2,…,wi的所有未被訪問過的鄰接頂點…依次類推,直到圖中所有頂點都被訪問過爲止。類似的思想還將應用於Dijkstra單源最短路徑算法和Prim最小生成樹算法。其實現藉助於一個輔助隊列。僞代碼如下所示:
bool visited[MAX_BERTEX_NUM];//訪問標記數組
void BFSTraverse(Graph G){
//對圖G進行廣度優先遍歷,設訪問函數爲visit()
for(i=0;i<G.vexnum,++i)
visited[i]=FALSE;
InitQueue(Q);
for(i=0;i<G.vexnum;++i)//從0號頂點開始遍歷
if(!visited[i])
BFS(G,i);//vi未訪問過,從vi開始BFS
}
void BFS(Graph G,int v){
//從頂點v出發,廣度優先遍歷圖G,算法藉助一個輔助隊列Q
visit(v);//訪問初始頂點v
visited[v]=true;
Enqueue(Q,v);//頂點v入隊列
while(!isEmpty(Q)){
DeQueue(Q,v);//頂點v出隊列
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
//檢測v所有鄰接點
if(!visited[w]){//w爲v的尚未訪問的鄰接頂點
visit(w);//訪問頂點w
visited[w]=true;//對w做已訪問標記
EnQueue(Q,w);//頂點w入隊列
}
}
}
2、圖示
3、算法性能分析
無論是使用鄰接表還是鄰接矩陣的存儲方式,BFS算法都需要藉助一個輔助隊列Q,n個頂點均需入隊一次,在最壞的情況下,空間複雜度爲O(|V|)。
當採用鄰接表存儲方式時,每個頂點均需搜索一次(或入隊一次),故時間複雜度爲O(|V|),在搜索任一頂點的鄰接點時,每條邊至少訪問一次,故時間複雜度爲O(|E|),算法總時間複雜度爲O(|V|+|E|)。當採用鄰接矩陣存儲方式時,查找每個頂點的鄰接表所需時間爲O(|V|),故算法總時間複雜度爲O(|V|^2)。
4、應用—BFS算法求解非帶權圖單源最短路徑問題
void BFS_MIN_Distance(Graph G,int u){
//d[i]表示從u到i結點的最短路徑
for(i=0;i<G.vexunm;i++)
d[i]=∞;//初始化路徑長度
visited[u]=true;
d[u]=0;
EnQueue(Q,u);
while(!isEmpty(Q)){
DeQueue(Q,u);//隊頭元素u出隊
for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w))
if(!visited[w]){
//w爲u尚未訪問的鄰接頂點
visited[w]=true;
d[w]=d[u]+1;//路徑長度+1
EnQueue(Q,w);//頂點w入隊
}//if
}//while
}
(三)經典算法題目分析
1.Red and Black
There is a rectangular room, covered with square tiles. Each tile is colored either red or black. A man is standing on a black tile. From a tile, he can move to one of four adjacent tiles. But he can’t move on red tiles, he can move only on black tiles.
Write a program to count the number of black tiles which he can reach by repeating the moves described above.
- Input:
The input consists of multiple data sets. A data set starts with a line containing two positive integers W and H; W and H are the numbers of tiles in the x- and y- directions, respectively. W and H are not more than 20.
There are H more lines in the data set, each of which includes W characters. Each character represents the color of a tile as follows.
‘.’ - a black tile
‘#’ - a red tile
‘@’ - a man on a black tile(appears exactly once in a data set)
The end of the input is indicated by a line consisting of two zeros.
- Output:
For each data set, your program should output a line which contains the number of tiles he can reach from the initial tile (including itself).
- SampleInput:
6 9
…#.
…#
…
…
…
…
…
#@…#
.#…#.
11 9
.#…
.#.#######.
.#.#…#.
.#.#.###.#.
.#.#…@#.#.
.#.#####.#.
.#…#.
.#########.
…
11 6
…#…#…#…
…#…#…#…
…#…#…###
…#…#…#@.
…#…#…#…
…#…#…#…
7 7
…#.#…
…#.#…
###.###
…@…
###.###
…#.#…
…#.#…
0 0
- Sample Output
45
59
6
13
題目大意:題意:給你一張圖,圖上有黑色,紅色兩種方塊,人只能走黑色塊,問你
人最多能走多少個黑色塊
輸入:
w、h代表圖的寬度高度,再給出圖
@代表人的位置,#代表紅色,.代表黑色
該題主要是用bfs/dfs求最優路徑,屬於暴力搜索,上圖是分別使用dfs和bfs算法走的路徑示意圖,下面是兩種方法代碼
/**
使用DFS算法進行搜索最優
*/
#include <iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int vis[25][25];//標記是否走過
char map[25][25];//走的圖
int to[4][2]={0,1,0,-1,1,0,-1,0};//相鄰位置
int ans;//記錄結果
int w,h;
void dfs(int x,int y){
if(vis[x][y]) return ;//已經訪問過,則不訪問
if(map[x][y]=='#') return;//若爲#則不訪問
vis[x][y]=1;//標記已訪問
ans++;//步數+1
for(int i=0;i<4;i++){
int xx=x+to[i][0];
int yy=y+to[i][1];
if(!vis[xx][yy]&&map[xx][yy]!='#'){//未訪問或者不爲'#'
//在範圍內
if(xx>=1&&xx<=h&&yy>=1&&yy<=w){
dfs(xx,yy);
}
}
}
}
int main(int argc, char** argv) {
int x,y;
while(~scanf("%d%d",&w,&h)&&w+h){
ans=0;
memset(vis,0,sizeof(vis));
for(int i=1;i<=h;i++){
scanf("%s",map[i]+1);
for(int j=1;j<=w;j++){
if(map[i][j]=='@'){
x=i;
y=j;
}
}
}
dfs(x,y);
printf("%d\n",ans);
}
return 0;
}
/**
使用BFS算法進行搜索最優
*/
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
int vis[25][25];
char map[25][25];
int to[4][2]={0,1,0,-1,1,0,-1,0};//相鄰位置
int ans;
int w,h;
struct node{
int x,y;
};
//
queue<node> q;//需要用到隊列
void bfs(int sx,int sy){
//若隊列不爲空,則出隊 ,爲了保證隊此時爲空
while(!q.empty()){
q.pop();
}
node a;
a.x=sx;
a.y=sy;
//入隊
q.push(a);
//置爲已經訪問過
vis[sx][sy]=1;
ans++;//步數++
while(!q.empty()){
//取隊首元素
node cur=q.front();
q.pop();//出隊
//在四個方向逐層進行搜索
for(int i=0;i<4;i++){
int xx=cur.x+to[i][0];
int yy=cur.y+to[i][1];
//若超出了範圍,則跳過
if(xx<1||yy<1||xx>h||yy>w){
continue;
}
//若已經訪問過
if(vis[xx][yy]){
continue;
}
//若爲'#'代表此路不通
if(map[xx][yy]=='#'){
continue;
}
vis[xx][yy]=1;
ans++;
node b;
b.x=xx;
b.y=yy;
q.push(b);
}
}
}
int main(){
int x,y;
while(~scanf("%d%d",&w,&h)&&w+h){
ans=0;
memset(vis,0,sizeof(vis));
for(int i=1;i<=h;i++){
//+1跳過'\0'
scanf("%s",map[i]+1);
//printf("%s\n",map[i]+1);
for(int j=1;j<=w;j++){
//獲得初始位置
if(map[i][j]=='@'){
x=i;
y=j;
}
}
}
bfs(x,y);
printf("%d\n",ans);
}
return 0;
}
2、最優配餐問題
- 題目描述
棟棟最近開了一家餐飲連鎖店,提供外賣服務。隨着連鎖店越來越多,怎麼合理的給客戶送餐成爲了一個急需解決的問題。
棟棟的連鎖店所在的區域可以看成是一個n×n的方格圖(如下圖所示),方格的格點上的位置上可能包含棟棟的分店(綠色標註)或者客戶(藍色標註),有一些格點是不能經過的(紅色標註)。
方格圖中的線表示可以行走的道路,相鄰兩個格點的距離爲1。棟棟要送餐必須走可以行走的道路,而且不能經過紅色標註的點。
送餐的主要成本體現在路上所花的時間,每一份餐每走一個單位的距離需要花費1塊錢。每個客戶的需求都可以由棟棟的任意分店配送,每個分店沒有配送總量的限制。
現在你得到了棟棟的客戶的需求,請問在最優的送餐方式下,送這些餐需要花費多大的成本。
- 輸入格式
輸入的第一行包含四個整數n, m, k, d,分別表示方格圖的大小、棟棟的分店數量、客戶的數量,以及不能經過的點的數量。
接下來m行,每行兩個整數xi, yi,表示棟棟的一個分店在方格圖中的橫座標和縱座標。
接下來k行,每行三個整數xi, yi, ci,分別表示每個客戶在方格圖中的橫座標、縱座標和訂餐的量。(注意,可能有多個客戶在方格圖中的同一個位置)
接下來d行,每行兩個整數,分別表示每個不能經過的點的橫座標和縱座標。
- 輸出格式
輸出一個整數,表示最優送餐方式下所需要花費的成本。
- 樣例輸入
10 2 3 3
1 1
8 8
1 5 1
2 3 3
6 7 2
1 2
2 2
6 8
- 樣例輸出
29
該題與上一道題的不同之處在於:上一道爲單源求最優,本道題是多源(一至多個分店)求最優,使用BFS實現最佳
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=1000;
const int TRUE=1;
const int DIRECTSIZE=4;
//定義方向結構體
struct direct{
int drow,dcol;
}direct[DIRECTSIZE]={{-1,0},{1,0},{0,-1},{0,1}} ;
int buyer[N+1][N+1];//存儲顧客所在位置
int visited[N+1][N+1];//標記是否訪問過
struct node{
int row,col,step;
node(){
}
//自家分店構造函數
node(int r,int c,int s){
row=r;
col=c;
step=s;
}
};
queue<node> q;
int count=0;//訂餐點總數
long long ans=0;
//多源點進行遍歷
void bfs(int n){
node front,v;
while(!q.empty()){
//首先將隊首出隊,從第一家店開始搜索
front=q.front();
q.pop();
for(int i=0;i<DIRECTSIZE;i++){
//移動一格
v.row=front.row+direct[i].drow;
v.col=front.col+direct[i].dcol;
//步數加1
v.step=front.step+1;
//若行列越界,則跳過
if(v.row<1||v.row>n||v.col<1||v.col>n) continue;
if(visited[v.row][v.col]) continue;
//如果是訂餐點,則計算成本並且累加
if(buyer[v.row][v.col]>0){
visited[v.row][v.col]=1;
//點一個餐送一個人,有可能這些顧客在一個點
ans+=buyer[v.row][v.col]*v.step;
//若已經遍歷完所有買家,則return
if(--count==0){
return ;
}
}
//向前繼續搜索
visited[v.row][v.col]=1;
q.push(v);//將v加入隊尾,表示已經訪問過
}
}
}
/**
先將所有的餐廳信息(座標以及步數)入隊,
在遍歷一個店鋪之後就會將擴展的上右下左四個方向入隊,
直到最後一個餐廳結束,就完成了所有店鋪的擴展。
以此類推,將每一個點都要遍歷一下。每到達客戶的地點,就會計算相應的費用。
*/
int main(){
int m,k,d,x,y,c;
memset(buyer,0,sizeof(buyer));
memset(visited,0,sizeof(visited));
//輸入數據
cin>>n>>m>>k>>d;
for(int i=1;i<=m;i++){
cin>>x>>y;
//將各個分店加入隊列中
q.push(node(x,y,0)) ;
visited[x][y]=true;
}
for(int i=0;i<k;i++){
cin>>x>>y;
cin>>c;
//統計客戶所在地點數量(多個客戶可能在同一地點)
if(buyer[x][y]==0){
count++;//客戶所在地點數量
}
buyer[x][y]+=c;//統計某個地點的訂單數量
}
//將不能經過的座標置爲true
for(int i=0;i<d;i++){
cin>>x>>y;
visited[x][y]=true;
}
//廣度優先搜索
bfs(n);
cout<<ans<<endl;
return 0;
}
3、CCF201604-4 遊戲
- 問題描述
小明在玩一個電腦遊戲,遊戲在一個n×m的方格圖上進行,小明控制的角色開始的時候站在第一行第一列,目標是前往第n行第m列。
方格圖上有一些方格是始終安全的,有一些在一段時間是危險的,如果小明控制的角色到達一個方格的時候方格是危險的,則小明輸掉了遊戲,如果小明的角色到達了第n行第m列,則小明過關。第一行第一列和第n行第m列永遠都是安全的。
每個單位時間,小明的角色必須向上下左右四個方向相鄰的方格中的一個移動一格。
經過很多次嘗試,小明掌握了方格圖的安全和危險的規律:每一個方格出現危險的時間一定是連續的。並且,小明還掌握了每個方格在哪段時間是危險的。
現在,小明想知道,自己最快經過幾個時間單位可以達到第n行第m列過關。
- 輸入格式
輸入的第一行包含三個整數n, m, t,用一個空格分隔,表示方格圖的行數n、列數m,以及方格圖中有危險的方格數量。
接下來t行,每行4個整數r, c, a, b,表示第r行第c列的方格在第a個時刻到第b個時刻之間是危險的,包括a和b。遊戲開始時的時刻爲0。輸入數據保證r和c不同時爲1,而且當r爲n時c不爲m。一個方格只有一段時間是危險的(或者說不會出現兩行擁有相同的r和c)。
- 輸出格式
輸出一個整數,表示小明最快經過幾個時間單位可以過關。輸入數據保證小明一定可以過關。
- 樣例輸入
3 3 3
2 1 1 1
1 3 2 10
2 2 2 10
- 樣例輸出
6
- 樣例說明
第2行第1列時刻1是危險的,因此第一步必須走到第1行第2列。
第二步可以走到第1行第1列,第三步走到第2行第1列,後面經過第3行第1列、第3行第2列到達第3行第3列。
- 評測用例規模與約定
前30%的評測用例滿足:0 < n, m ≤ 10,0 ≤ t < 99。
所有評測用例滿足:0 < n, m ≤ 100,0 ≤ t < 9999,1 ≤ r ≤ n,1 ≤ c ≤ m,0 ≤ a ≤ b ≤ 100。
- 問題分析
本題需要一個三維的標誌來避免重複搜索。除了行列座標外,需要考慮時間問題,故將其設置爲三維數組。因爲一些格在某個時間範圍是危險的,不可進入,但是這個時間範圍之外,是可以隨意進入的。所以有時候需要在一些地方踱步,等過了這段時間再前行,就不能簡單地限制爲進入過的格不能再進入。
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=100;
const int DIRECTSIZE=4;
struct direct{
int drow,dcol;
}direct[DIRECTSIZE]={{-1,0},{1,0},{0,-1},{0,1}};
//需要定義個三維數組,第三維存儲在這個時間是否可以通過
int visited[N+1][N+1][300+1];
struct node{
int row,col;
int level;
};
int bfs(int n,int m){
node start,front,v;
//從第一行第一列開始走
start.row=1;
start.col=1;
start.level=0;
queue<node> q;
q.push(start);
while(!q.empty()){
front=q.front();
q.pop();
//設置出口
//到達終點則結束
if(front.row==n&&front.col==m) return front.level;
for(int i=0;i<DIRECTSIZE;i++){
//四個方向各向前走一步
v.row=front.row+direct[i].drow;
v.col=front.col+direct[i].dcol;
v.level=front.level+1;
//行界越界則跳過
if(v.row<1||v.row>n||v.col<1||v.col>m){
continue;
}
//已經訪問過的點無法再次訪問
if(visited[v.row][v.col][v.level]) continue;
//向前搜索:標記v點爲已經訪問過,v點加入隊列中
visited[v.row][v.col][v.level]=1;
q.push(v);
}
}
return 0;
}
int main(){
int n,m,t,r,c,a,b;
memset(visited,0,sizeof(visited));
cin>>n>>m>>t;
for(int i=1;i<=t;i++){
cin>>r>>c>>a>>b;
//設置方格危險時間,使之那些時間不可進入
for(int j=a;j<=b;j++){
visited[r][c][j]=1;
}
}
int ans=bfs(n,m);
cout<<ans<<endl;
return 0;
}
(四)參考文獻
【1】2018年數據結構考研複習指導. 王道論壇組編.
【2】https://www.cnblogs.com/kungfupanda/p/11248014.html
【3】https://vjudge.net/problem/POJ-1979
【4】參考視頻 https://www.bilibili.com/video/av78091226?from=search&seid=16502200292163627678
【5】https://blog.csdn.net/tigerisland45/article/details/54934916