1. 程式人生 > 實用技巧 >廣度優先搜尋複習筆記

廣度優先搜尋複習筆記

前言

BFS 全稱是 \(Breadth First Search\),中文名是寬度優先搜尋,也叫廣度優先搜尋。

是圖上最基礎、最重要的搜尋演算法之一。

所謂寬度優先。就是每次都嘗試訪問同一層的節點。 如果同一層都訪問完了,再訪問下一層。

這樣做的結果是,BFS 演算法找到的路徑是從起點開始的 最短 合法路徑。換言之,這條路所包含的邊數最小。

在 BFS 結束時,每個節點都是通過從起點到該點的最短路徑訪問的。

演算法過程可以看做是圖上火苗傳播的過程:最開始只有起點著火了,在每一時刻,有火的節點都向它相鄰的所有節點傳播火苗。

當然也可以應用於其他的一些結構中進行搜尋

框架

void BFS()
{
	memset(vis,0,sizeof(vis));
	queue<資料型別> q;
	q.push(初始值);
	vis[初始值]=1;
	while(!q.empty())
	{
		int k=q.front();
		q.pop();
		//....操作,處理 
		if(符合條件)
		{
			q.push(當前節點);
			vis[當前節點]=1; 
		} 
	}
}

例題講解

P2730 [USACO3.2]魔板 Magic Squares

思路

根據題目要求先用函式處理好每一個狀態,運用\(set\)判重,因為每一層迴圈的操作都會是所有的運算元\(+1\),每一次彈出時判斷一下是不是最終狀態即可

程式碼
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
#include<map>
using namespace std;
const int N=10;
struct node{
	string xu;
	int num;
};
string mb;
string yuan="a12345678";
queue<string> q;
map<string,string> vis;//判斷函式,判斷這一種情況有沒有走過 
string A(string x) 
{
	string pd=x;
	swap(x[1],x[8]);
	swap(x[2],x[7]);
	swap(x[3],x[6]);
	swap(x[4],x[5]);
	if(!vis.count(x))//沒有這種情況沒有走過,就放進去繼續搜尋 
	{
		vis[x]=vis[pd]+'A';
		q.push(x);
	}
	return x;
}
string B(string x)
{
	string pd=x;
	char up=x[4];
	char down=x[5];
	x[4]=x[3];
	x[5]=x[6];
	x[3]=x[2];
	x[6]=x[7];
	x[2]=x[1];
	x[7]=x[8];
	x[1]=up;
	x[8]=down;
	if(!vis.count(x))
	{
		vis[x]=vis[pd]+'B';
		q.push(x);
	}
	return x;
}
string C(string x)
{
	string pd=x;
	char three=x[3];
	char six=x[6];
	char seven=x[7];
	char two=x[2];
	x[6]=three;
	x[7]=six;
	x[2]=seven;
	x[3]=two;
	if(!vis.count(x))
	{
		vis[x]=vis[pd]+'C';
		q.push(x);
	}
	return x;
}
void search()
{
	q.push(yuan);
	vis[yuan]="";
	while(!q.empty())
	{
		string k=q.front();
		q.pop();
		A(k);
		B(k);
		C(k);
		if(vis.count(mb)!=0)//當出現了最終情況的時候
		//這個時候因為每一次佇列的頭跳出可以看做可行的序列長度+1
		//因為每次走的ABC三種情況是一定不同的,所以如果出現了最終情況一定是
		//第一次出現的且是操作次數最短的,所以直接輸出就行。 
		{
			cout<<vis[mb].size()<<endl;
			cout<<vis[mb]<<endl;
			return;
		}
	}
}
int main()
{
	mb+="a";
	for(int i=1;i<=8;i++)
	{
		char a;
		cin>>a;
		mb+=a; 
	}//將字串的位置調到1-8便於操作 
	search(); 
	return 0;
} 

LOJ10028.Knight Moves

思路

存一下每一次走的步數,然後直接廣搜就完事了,遇到最終情況就返回後輸出,注意判重和打標記,因為是多次詢問,別忘了清空堆裡面存的值和 \(vis\) 的值

程式碼
#include<iostream>
#include<cstring>
#include<cmath>
#include<queue>
#include<map>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<vector>
using namespace std;
const int N=409;
struct node{
	int x;
	int y;
}; 
queue<node> q;
int dx[8]={-1,-2,-2,-1,1,2,2,1};
int dy[8]={-2,-1,1,2,2,1,-1,-2};
int vis[N][N];
int step[N][N];
int n,L;
int strx,stry,endx,endy;
void search()
{
	q.push((node){strx,stry});
	vis[strx][stry]=1;
	step[strx][stry]=0;
	while(!q.empty())
	{
		node cun=q.front();
		q.pop();
		int x=cun.x;
		int y=cun.y;
		for(int i=0;i<8;i++)
		{
			int kx=x+dx[i];
			int ky=y+dy[i];
			if(kx>=0&&kx<L&&ky>=0&&ky<L&&!vis[kx][ky])
			{//如果沒有找過,判斷是否為最終狀態 
				if(kx==endx&&ky==endy)
				{
					cout<<step[x][y]+1<<endl;
					return;
				}
				else 
				{
					q.push((node){kx,ky});
					step[kx][ky]=step[x][y]+1;
					vis[kx][ky]=1;
				}
			}
		}
	}
}
int main()
{
	cin>>n;
	while(n--)
	{
		while(!q.empty()) q.pop();//十年OI一場空,佇列不空見祖宗 
		memset(vis,0,sizeof(vis)); 
	    //memset(step,0,sizeof(step));
		cin>>L;
		cin>>strx>>stry;
		cin>>endx>>endy;
		if(strx==endx&&stry==endy)
		{
			cout<<0<<endl;
			continue;
		}
		else search();
	} 
	return 0;
}

P4289 [HAOI2008]移動玩具

思路

對於已經匹配好的點,直接賦值為 \(0\) 不用再去管他,對於沒有匹配的點,就繼續是原先的位置,再找一個數組存一下沒有匹配的位置,以及與所有可以進行匹配的點之間的曼哈頓距離,然後用常規的深搜(亂入)比較一下最小值即可

程式碼
#include<iostream>
#include<cmath>
#include<cstdio>
#include<algorithm>
#include<queue>
#include<stack>
#include<map>
#include<vector> 
using namespace std;
struct node {
	int x,y;
} c[26];//未匹配點的座標 
int a[5][5];
int b[5][5];
int pipe[5][5][5][5];
//前兩個下標表示未匹配點座標
//後兩個下標為待匹配點座標
//儲存未匹配點到待匹配點的最短距離
bool used[5][5];
int sum=0;
int minn=0x7fffffff;
void search(int x,int now) 
{ //深搜找答案。
	if(x>sum) 
	{
		minn=min(minn,now);
		return ;
	}
	for(int i=1; i<=4; i++)
		for(int j=1; j<=4; j++)
			if(pipe[c[x].x][c[x].y][i][j]!=0&&!used[i][j])//列舉所有沒有匹配的點 
			{
				used[i][j]=1;
				search(x+1,now+pipe[c[x].x][c[x].y][i][j]);
				used[i][j]=0;
			}
}
int main() {
	char pu;
	for(int i=1; i<=4; i++)
		for(int j=1; j<=4; j++) 
		{
			cin>>pu;
			a[i][j]=pu-'0';
		}
	for(int i=1; i<=4; i++)
		for(int j=1; j<=4; j++) 
		{
			cin>>pu;
			b[i][j]=pu-'0';
			if(a[i][j]==b[i][j]) a[i][j]=b[i][j]=0;
		}//如果已經匹配好了,那麼就直接賦值為0,不用管了 
	for(int i=1; i<=4; i++)
		for(int j=1; j<=4; j++)
			if(a[i][j]==1) 
			{
				c[++sum].x=i;
				c[sum].y=j;
				for(int k=1; k<=4; k++)
					for(int l=1; l<=4; l++)
						if(b[k][l]==1) 
						{
							pipe[i][j][k][l]=abs(i-k)+abs(j-l);
							//記錄一下從不匹配點到另外的不匹配點的最短距離 
						}
			}
	search(1,0);
	printf("%d",minn);
	return 0;
}

P6909 [ICPC2015 WF]Keyboarding

坑點提醒

1、最後需要你手動新增一個\(“*”\)

2、注意在其實位置按鍵的情況

思路

首先我們可以把所有相關涉及到的字元都對映為數字,方便處理,包括\((A…Z),(1…9),("*""-")\)這些東東,處理的時候別對映 \(0\) 就行

當我們輸入的時候直接對映到一個二維陣列 \(a\) 來裝載鍵盤的每個鍵

將目標串對映到一個一維陣列 \(b\) 中來需要按得每一個鍵,最後別忘了手動新增一位來存 \("*"\)

那麼如何選擇位置呢,如果在搜尋的過程中暴力美劇會非常耗時間,那麼這個時候就可以預處理每一個鍵第一個到達的位置即可

為了方便我們要設一個結構體型別的三維陣列

結構體的變數分別為 到達的位置的橫座標 \(x\) ,到達位置的縱座標 \(y\) ,到達這個位置的時候要匹配目標串的哪一位 \(step\) ,以及到這個點已經走了幾步 \(dis\)

三維陣列 \(f[k][i][j]\) 表示從 \((i,j)\)\(k\) 方向走到達的第一個位置的資訊(即結構體中的內容)

那麼預處理之後就開始廣搜了

在開始廣搜之前,因為我們是從第一位開始的,那麼我們最好先預處理一下在第一位一共可以連續按幾次,然後再把結構體重所表示的資料存進去,在程式碼中有就不詳細說了

那麼現在開始廣搜

考慮兩種情況,我們要把選擇和匹配分開找

一、

那麼先是匹配,要看一下堆頂的這個位置是否和當前要匹配的位置相同,如果是相同的時候,那就在進行判斷一下這個是不是最終狀態,如果是,那直接把答案賦值為當前的步數+1就是最有的答案

那如果不是最後的一個情況呢

可以設一個 \(vis\) 陣列,用來存當前堆頂的點接下來該匹配哪一位了,然後把相應的結構體中的資料都放進去繼續查詢

二、

最後再開始選擇

首先列舉該點四個位置的到達點,當然因為我在預處理位置的時候有一個位置得移動忘加了,所以還要加上,一次移動

首先判斷一下是否越界

然後再判斷一下當前要選擇的地方要比原座標的要處理的位置要更大,那麼就不需要更新 \(vis\) 陣列。

否則的話,就更新當前的 \(vis\) 陣列為現在要處理的位置(不用+1,因為是選擇,即跳過當前的匹配直接跳進),然後把當前要選擇的位置的資料放進去

最後直接輸出即可

程式碼

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<queue>
#include<stack>
#include<map>
using namespace std;
const int N=59;
const int M=10009;
struct node{
	int x;
	int y;
	int step;//指下一個到了第幾個位置, 
	int dis;//按了幾次 
}f[5][N][N];//後兩個表示座標,前一個表示方向,即i,j在k方向上到的最近的點 
int dx[4]={0,1,0,-1};
int dy[4]={1,0,-1,0};
char wor[M];
int n,m,len,a[N][N],b[M],vis[N][N];
map<char,int> mp;
void prepare()//預先將所有的字串處理成數字型別的,方便處理
{
	for(int i=0;i<=9;i++)
	mp[(char)('0'+i)]=i+1;
	for(int i=0;i<26;i++)
	mp[(char)('A'+i)]=i+11;//1-10被取過了 
	mp['-']=37;
	mp['*']=38; 
} 
void get()//處理一下四個方向可以到達的點 
{
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			for(int k=0;k<4;k++)
			{
				int x=i;
				int y=j;
				while(a[x][y]==a[x+dx[k]][y+dy[k]])//如果一致的話 
				{
					x+=dx[k];
					y+=dy[k];//一致加到不同為止 
				}
				f[k][i][j]=(node){x,y,0,0};
			}
}
int search()
{
	memset(vis,0,sizeof(vis));
	queue<node> q;
	int ans=0;
	int k=1;
	while(a[1][1]==b[k]&&k<=len) ++k;//在起點選取的情況
	q.push((node){1,1,k,k-1});
	vis[1][1]=k;
	while(!q.empty())
	{
		node cun=q.front();
		q.pop();
		int x=cun.x;
		int y=cun.y;
		int now=cun.step; 
		if(a[x][y]==b[now])//此時相等了
		{
			if(now==len) 
			{
				ans=cun.dis+1;
				break;//如果找完了?直接跳出 
			}
			vis[x][y]=now+1;//更新一下,因為按了一遍鍵盤 
			q.push((node){x,y,now+1,cun.dis+1});
		} 
		for(int i=0;i<4;i++)
		{
			node chose=f[i][x][y];
			chose.x+=dx[i];
			chose.y+=dy[i];//預處理上在加一次,保證完整
			if(chose.x<1||chose.x>n||chose.y<1||chose.y>m) continue;//越界了
			if(vis[chose.x][chose.y]>=now) continue;//如果選擇的地方的要處理的位置比現在的要大,不用改,防止變worse 
			vis[chose.x][chose.y]=now;//如果可行,那麼把接下來要弄得位置給存進去
			q.push((node){chose.x,chose.y,now,cun.dis+1});//跳到那一步 
		} 
	} 
	return ans;
}
int main()
{
	prepare();
	while(scanf("%d",&n)!=EOF)//UVA經典輸入方式
	{	
//		cin>>n;
		cin>>m;
		for(int i=1;i<=n;i++)//輸入每一行的字串
		{
			cin>>wor;		
			for(int j=0;j<m;j++)
				a[i][j+1]=mp[wor[j]];//將每個字元的代表的數字對映到a陣列中
				//從1開始方便 
		} 
		cin>>wor;
		len=strlen(wor);
		for(int i=0;i<len;i++) b[i+1]=mp[wor[i]];//依舊是對映,從1開始
		len++;
		b[len]=38;//注意最後有一個*
		get();
		cout<<search()<<endl; 
	} 
	return 0;
} 

P3456 [POI2007]GRZ-Ridges and Valleys

思路

運用廣搜板子去做即可,每一次搜尋的時候都把找到的高度相同的點標記一下,避免進入死迴圈,一開始進入搜尋的時候先把山峰山谷的值設為 \(1\),然後根據後續的判斷,把不匹配的都設為 \(0\) ,最後加上即可

程式碼
//#include<iostream>
//#include<cstdio>
//#include<cstring>
//#include<algorithm>
//#include<cmath>
//#include<map>
//#include<queue>
//#include<stack>
//#include<set>
//#define int long long 
//using namespace std;
//const int N=1009;
//set<int> s;
//int dx[8]={-1,0,1,0,-1,-1,1,1};
//int dy[8]={0,1,0,-1,-1,1,1,-1};
//int n;
//int mount[N][N];
//bool vis[N][N];
//int feng;
//int gu;
//int big(int x,int y)//找比他小的 
//{
//	int ret=0; 
//	for(int i=0;i<8;i++)
//	{
//		int kx=x+dx[i];
//		int ky=y+dy[i];
//		if(kx>=1&&kx<=n&&ky>=1&&ky<=n&&mount[x][y]>=mount[kx][ky])
//		//如果大於周圍的,不用判斷周圍的是不是合法,方便判斷 
//		ret++;
//	}
//	return ret;
//}
//int small(int x,int y)//找比他大的 
//{
//	int ret=0; 
//	for(int i=0;i<8;i++)
//	{
//		int kx=x+dx[i];
//		int ky=y+dy[i];
//		if(kx>=1&&kx<=n&&ky>=1&&ky<=n&&mount[x][y]<=mount[kx][ky])
//		//如果小於周圍的,不用判斷周圍的是不是合法,方便判斷 
//		ret++;
//	}
//	return ret;
//} 
//int cha(int x,int y){
//	
//	int ret=0; 
//	for(int i=0;i<8;i++)
//	{
//		int kx=x+dx[i];
//		int ky=y+dy[i];
//		if(kx>=1&&kx<=n&&ky>=1&&ky<=n&&mount[x][y]==mount[kx][ky])
//		//如果小於周圍的,不用判斷周圍的是不是合法,方便判斷 
//		ret++;
//	}
//	return ret;
//
//} 
//bool check_big(int x,int y,int num,int lastx,int lasty)//檢查大的 
//{
//	//cout<<"zsf ak ioi"<<endl; 
//	vis[x][y]=1;
//	int pd=0;//用來判斷符合的有多少
//	int cnt=0;//用來判斷一共需要符合條件的是多少 
//	for(int i=0;i<8;i++)
//	{
//		int kx=x+dx[i];
//		int ky=y+dy[i];
//		if(kx<=0||kx>n||ky<=0||ky>n) continue;
//		if(vis[kx][ky]) continue;//別向回找 
//		if(mount[kx][ky]==num)//如果是相等的值
//		{
//			cnt++;
//			if(small(kx,ky)-cha(kx,ky)==0)//如果周圍沒有比他大的
//			{
//				if(check_big(kx,ky,num,x,y)) pd++;
//				else return false;
//			} 
//			else return false;
//		} 
//	}
//	if(pd==cnt) return true;
//}
//bool check_small(int x,int y,int num,int lastx,int lasty)//檢查小的 
//{
//	//cout<<"zsf ak ioi"<<endl;
//	vis[x][y]=1;
//	int pd=0;//用來判斷符合的有多少
//	int cnt=0;//用來判斷一共需要符合條件的是多少 
//	for(int i=0;i<8;i++)
//	{
//		int kx=x+dx[i];
//		int ky=y+dy[i];
//		if(kx<=0||kx>n||ky<=0||ky>n) continue;
//		if(vis[kx][ky]) continue;//別向回找 
//		if(mount[kx][ky]==num)//如果是相等的值
//		{
//			cnt++;
//			if(big(kx,ky)-cha(kx,ky)==0)//如果周圍沒有比他小的
//			{
//				if(check_small(kx,ky,num,x,y))pd++;
//				else return false;
//			} 
//			else return false;
//		} 
//	}
//	if(pd==cnt) return true;
//}
//void search()
//{
//	for(int i=1;i<=n;i++)
//		for(int j=1;j<=n;j++)
//		{
//			if(vis[i][j]) continue;
//			int jian=cha(i,j);//減掉相等的
//			int lar=big(i,j)-jian;
//			int simp=small(i,j)-jian;
//			//cout<<"i= "<<i<<" j= "<<j<<" 比他大的有"<<simp<<"個"<<" 比他小的有"<<lar<<"個"<<" 和他一樣的有"<<jian<<"個"<<endl; 
//			if(jian==8)//如果周圍都是相同的,那就直接忽略掉,留到以後找
//			continue;
//			else if(lar==0)//如果沒有比他小的 
//			{
//				if(check_small(i,j,mount[i][j],i,j))
//				gu++; 
//			} 
//			else if(simp==0)//如果沒有比他大的
//			{
//				if(check_big(i,j,mount[i][j],i,j))
//				feng++; 
//			} 
//		}
//}
//signed main()
//{
//	cin>>n;
//	for(int i=1;i<=n;i++)
//		for(int j=1;j<=n;j++)
//			scanf("%lld",&mount[i][j]),s.insert(mount[i][j]);
//	if(s.size()==1)
//	{
//		cout<<1<<" "<<1<<endl;
//		return 0; 
//	}
//	search();
//	cout<<feng<<" "<<gu<<endl;
//	return 0;
//} 
/* 暴力深搜,luogu 100pts,LOJ 62pts,好像是爆棧了=_=*/
/* 廣搜大法好!!!*/
#include<cstdio>
#include<iostream>
#include<queue>
#include<cstring>
#include<cmath>
#include<stack>
using namespace std;
const int N = 1000 + 10;
int dx[8] = {-1, -1, -1, 0, 0, 1, 1, 1};
int dy[8] = {-1, 0, 1, -1, 1, -1, 0, 1};
struct node 
{
    int x, y;
};
//結構體存點
int n, big, small;
int map[N][N], vis[N][N];
bool sg, sf;
queue<node> q;
void search(int x, int y) 
{
    node start;	//初始值
    start.x=x, start.y=y;
    vis[x][y]=1;
    q.push(start);
    while (!q.empty()) 
	{
        node cun=q.front(); 
		q.pop();	//隊首
        for (int i=0;i<=7;i++) 
		{//八方向
            int nx=cun.x+dx[i];
            int ny=cun.y+dy[i];
            if (nx<1||nx>n||ny<1||ny>n) continue;	//越界
            if (map[nx][ny] == map[cun.x][cun.y]&&!vis[nx][ny]) 
			{	// 高度相等打上標記接著搜
                vis[nx][ny] = 1;
                q.push((node){nx, ny});
            }
            else 
			{	// 山峰山谷是否成立
                if (map[nx][ny] > map[cun.x][cun.y]) sf = 0;
                if (map[nx][ny] < map[cun.x][cun.y]) sg = 0;
            }
        }
    }
}

int main() {
    scanf("%d",&n);
    bool flag=0;//判斷一下是不是所有的都相等 
    for (int i=1;i<=n;i++)
        for (int j=1;j<=n;j++) 
		{
            scanf("%d", &map[i][j]);
            if (map[i][j]!=map[1][1]) flag=1;
        }
    if (!flag) 
	{	// 判斷是否全部高度相等
        cout<<1<<" "<<1<<endl;
        return 0;
    }
    for (int i=1;i<=n;i++)
        for (int j=1;j<=n;j++) 
		{
            if (!vis[i][j]) 
			{ //如果是新的聯通塊
                sf=1,sg=1;
                search(i,j);
                big+=sf;
                small+=sg;
            }
        }
    printf("%d %d\n", big, small);
    return 0;
}

結語

當做一個題目的時候,儘量先有爆搜的思路穩住分數,把暴力分拿滿之後再去思考正解,穩重求進。