图论学的破防了,于是先来恶补搜索。
一、深度优先搜索(DFS)
是一种利用函数递归枚举的方式,先找到一个初始点然后枚举可行的方向,如果满足条件,那么就继续搜索,如果不满足条件就返回去寻找一个上一个可行的节点,继续枚举。
这种方式称为回溯算法,也称为深度优先搜索。是一种不撞南墙不回头的算法。
模板是这样的:
void dfs(int k){//k代表递归层数,或者说要填几个空
if(所有空已经填完了){
判断最优解/记录答案;
return;
}
for(枚举这个空能填的选项){
if(这个选项合法){
记录下这个空;
dfs(k+1);
取消这个空;
}
}
}
dfs思考的几个点:要传几个参,为什么要传参。而这个参数一定是需要保存的信息,也和结束条件是息息相关的。还有什么时候return,关系到程序的出口。
举几个例子:
P1219 [USACO1.5] 八皇后 Checker Challenge - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include <iostream>
using namespace std;
long long int ans=0;
int a[100];//存当前的排列
int b[100];//纵行
int c[100];//x+y;
int d[10000];//x-y+n*n;
int n;
void dfs(int x)
{
if(x>n)
{
ans++;
if(ans<=3)
{
for(int i=1;i<=n;i++)
{
cout<<a[i]<<" ";
}
cout<<endl;
}
return;
}
for(int i=1;i<=n;i++)//i如果是横行的话,那么x/n就是纵行, b[i]代表x=i这一列已经有人来过
{
if(b[i]==0&&c[i+x]==0&&d[i-x+n*n]==0)
{
a[x]=i;
b[i]=1;c[i+x]=1;d[i-x+n*n]=1;
dfs(x+1);
b[i]=0;c[i+x]=0;d[i-x+n*n]=0;
}
}
}
int main()
{
cin>>n;
dfs(1);
cout<<ans<<endl;
return 0;
}
这个问题就是能填多少个互相不去攻击的皇后。
那我们怎么传参呢?由于问题是求放置皇后,那就设x是皇后的数量,接下来看是否需要其他条件判断一个放置是否满足条件。由于每一行只能放一个皇后,可以把x当作当前正在处理行,枚举就可以了。
结束条件就是放置皇后的个数大于需要的个数。
判断条件是否合适是这道的小难点,就是用三个数组vis看所占据位置,一个存纵坐标,两个存斜的坐标,难点在于建立位置x和两条斜的坐标线。只要建立每个斜着的方格到斜坐标的一一映射就可以了。
P2392 kkksc03考前临时抱佛脚 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include <iostream>
#include <algorithm>
using namespace std;
int a[4];
int s[21];
int sum=0;
int sum1=0;
int maxdeep,nowtime,maxtime;
void dfs(int x)
{
if(x>=maxdeep)
{
maxtime=max(maxtime,nowtime);
return;
}
if(s[x]+nowtime<=sum/2)
{
nowtime+=s[x];
dfs(x+1);
nowtime-=s[x];
}
dfs(x+1);
}
int main()
{
cin>>a[0]>>a[1]>>a[2]>>a[3];
for(int i=0;i<4;i++)
{
maxdeep=a[i];
for(int j=0;j<a[i];j++)
{
cin>>s[j];
sum+=s[j];
}
dfs(0);
sum1+=sum-maxtime;
sum=0;nowtime=0;maxtime=0;
}
cout<<sum1<<endl;
return 0;
}
这个题找最短时间,先发现了四个集合是相互独立的,于是dfs四次。每个集合内寻找一组数字:即这一组时间的加起来的总和小于总时间的一半。在这些满足条件的组里面寻找最大值。
怎么传参?传入一个 x 用于表示当前枚举深度,但是怎么判断当前可不可以呢?这里使用了全局变量。
有亿点细节就是全局变量的使用,由于dfs内核是递归,用一个全局变量可以记录每一次递归的状态。然后就是这个深度优先和模板有点不一样,因为这个起点并不是给定的。可以任意寻找每一个作业作为起点,于是在判断条件下面又加了一个dfs;
P1036 [NOIP2002 普及组] 选数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
int n,k;
int a[30];
long long ans;
bool isPrime(int x){
if(x==1) return false;
for(int i=2;i*i<x;i++){
if(x%i==0) return false;
}
return true;
}
void dfs(int m,int sum,int start){
if(m==k){
if(isPrime(sum)) ans++;
return;
}
for(int i=start;i<=n;i++){
dfs(m+1,sum+a[i],i+1);
}
return;
}
int main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
}
sort(a+1,a+1+n);
dfs(0,0,1);
cout<<ans<<endl;
return 0;
}
题意是n个数字,随机选取k个数字相加,问结果有多少个是质数。
首先考虑怎么传参,需要保存什么,肯定要保存当前存了几个数字,也要保存当前的和。但是为了不重复,在对数组排序后,还要保存起点,保证在用过 i 之后,下一个枚举的数字一定是i+1 。
注意枚举之后要return,否则到后面一直凑不够k个数会导致程序没有出口。
Square - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
int n,m,cnt,sum;
bool vis[101];
int a[101];
bool dfs(int stick,int len,int last){
if(stick>3) return true;
if(len==cnt) return dfs(stick+1,0,1);
for(int i=last;i<=m;i++){
if(!vis[i]&&a[i]+len<=cnt){
vis[i]=1;
if(dfs(stick,len+a[i],i)) return true;
vis[i]=0;
}
}
return false;
}
int main(){
cin>>n;
while(n--){
sum=0;
cin>>m;
for(int i=1;i<=m;i++){
cin>>a[i];
sum+=a[i];
}
sort(a+1,a+1+m);
reverse(a+1,a+1+m);
if(sum%4!=0){
cout<<"no"<<endl;
continue;
}
cnt=sum/4;
memset(vis,0,sizeof(vis));
if(dfs(1,0,1)) cout<<"yes"<<endl;
else cout<<"no"<<endl;
}
}
题意是给一堆木棒,问可不可以拼成正方形。
考虑参数,首先需要考虑当前拼了多长,也需要考虑当前拼到了第几根,为了先用大的再用小的,保证大的拼完不会再使用,需要记录一个当前起点,使得选取这个木棒之后所有的木棒都比它短*(如果比它长的话可能没意义)。
这题的特点是dfs的返回值是bool值,这个函数每次return都只会return一个值,不能实现递归的效果,如果得出的结果是false,是需要继续运行的。所以bool值的bfs函数需要用if来控制出口。
二、广度优先搜索(BFS)
给定一个起点,广度优先搜索会优先考虑与当前阶段最相近的状态,如果是迷宫,就会优先走上下左右四个方向。每个时刻要做的事情就是从上个时刻每个状态扩展出新的状态。借用队列实现。有的时候要存一个vis数组;把走过的路全标记上vis。
模板如下:
Q.push(初始状态);
while(!Q.empty()){
state u=Q.front();//取出队首
Q.pop();//出队
for(枚举所有可扩展状态)//找到u的可达状态v
if(是合法的)
Q.push(v);
}
给几个例题:
P1443 马的遍历 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include <iostream>
#include <queue>
#include <cstring>
#include <iomanip>
using namespace std;
struct crood
{
int x;
int y;
};
int ans[410][410];
queue<crood> q1;
int main()
{
int dis[8][2]={{1,2},{2,1},{-1,-2},{-2,-1},{1,-2},{-2,1},{-1,2},{2,-1}};
crood pos;
int m,n;cin>>m>>n;
cin>>pos.x>>pos.y;
memset(ans,-1,sizeof(ans));
ans[pos.x][pos.y]=0;
q1.push(pos);
while(!q1.empty())
{
crood t=q1.front();
q1.pop();
for(int i=0;i<8;i++)
{
int ux=t.x+dis[i][0];
int uy=t.y+dis[i][1];
int d=ans[t.x][t.y];
if(ans[ux][uy]!=-1||ux<1||ux>m||uy<1||uy>n)
continue;
ans[ux][uy]=d+1;
// cout<<ux<<" "<<uy<<" "<<d+1<<endl;
crood tmp={ux,uy};
q1.push(tmp);
}
}
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
cout<<setw(4)<<ans[i][j];
}
cout<<endl;
}
return 0;
}
可以用dis数组描述它的路径;走过的就vis一下,可以保证是最短。
P1135 奇怪的电梯 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include <iostream>
#include <queue>
using namespace std;
struct node{
int pos;
int count;
};
int vis[300];
int num[300];
int pos1,count1;
int main()
{
queue<node> q1;
int n,A,B;
cin>>n>>A>>B;
for(int i=1;i<=n;i++)
{
cin>>num[i];
}
node a;
a.pos=A;vis[A]=1;
q1.push(a);
while(!q1.empty())
{
node t=q1.front();
pos1=t.pos;
count1=t.count;
q1.pop();
if(pos1==B) break;
for(int i=-1;i<=1;i+=2)
{
int pos2=pos1+i*num[pos1];
if(vis[pos2]==1||pos2<=0||pos2>n)
continue;
vis[pos2]=1;
node tmp={pos2,count1+1};
q1.push(tmp);
}
}
if(pos1==B) cout<<count1<<endl;
else cout<<-1<<endl;
return 0;
}
一样,走过的vis一下,可以保证最短路径。
最重量级的来了:
#include <iostream>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;
int n,m;
int a[300][300];
bool vis[300][300];
int kfc[300][300];
#define inf 0x3f3f3f3f
struct crood
{
int x;
int y;
int step;
};
queue<crood> q1;
int dis[4][2]={{1,0},{-1,0},{0,1},{0,-1}};
crood Yi,Me;
void bfs(crood u)
{
memset(vis,0,sizeof(vis));
int x1,y1;
q1.push(u);
while(!q1.empty())
{
crood u1=q1.front();
q1.pop();
if(a[u1.x][u1.y]==-1)
{
kfc[u1.x][u1.y]+=u1.step;
}
for(int i=0;i<4;i++)
{
x1=u1.x+dis[i][0];
y1=u1.y+dis[i][1];
if(vis[x1][y1]==0&&x1>=1&&x1<=n&&y1>=1&&y1<=m&&a[x1][y1]!=-2)
{
crood tmp={x1,y1,u1.step+1};
q1.push(tmp);
vis[x1][y1]=1;
}
}
}
}
int main()
{
//构建图:
char x;
while(cin>>n>>m)
{
memset(kfc,0,sizeof(kfc));
memset(a,0,sizeof(a));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>x;
if(x=='@') a[i][j]=-1,vis[i][j]=1;
if(x=='#') a[i][j]=-2;
if(x=='Y') Yi={i,j,0};
if(x=='M') Me={i,j,0};
}
}
bfs(Yi);
bfs(Me);
int min1=inf;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(kfc[i][j]!=0)
min1=min(min1,kfc[i][j]);
cout<<11*min1<<endl;
}
return 0;
}
在这里学到了一点,存图的技巧,可以直接存字符的图,障碍和判断走没走过的图是独立的。然后KFC也可以从字符的图里找,最后a+b就是每走到KFC就加当前的step,这样思路很清晰