搜索,指通过对状态进行枚举,来计算最优解或统计解的个数。
一般的题目很少有纯粹搜索,但它是很多算法的基础,并且在考场上考虑拿暴力分时,它应该是最好的选择了。
dfs
深度优先搜索(dfs),指通过递归的方式,将状态分为几层,每层基于前面的层决策,直到达到目标状态。搜索一般要考虑几个问题:搜索边界、递归更新状态。通常在搜索完某一个状态的时候,还需要回溯到原来的状态。
八皇后
题目链接:https://www.luogu.com.cn/problem/P1219
点击查看
发现 \(n\) 的范围很小,考虑搜索。设 \(a_i\) 表示占领第 \(i\) 行的在第几列,\(b_i\) 表示第 \(i\) 列是否有棋子,\(c_{i+j}\) 与 $d_{i-j} $表示点 \((i,j)\) 占领了两条对角线。由于 \(i-j\) 可能为负,所以我们可以将其改为 \(i-j+n\)。
#include<bits/stdc++.h>
using namespace std;
int a[100],b[100],c[100],d[100],cnt,n;
void dfs(int i) {if(i>n) {if(cnt<=2) {for(int k=1;k<=n;k++) cout<<a[k]<<' ';cout<<'\n';} cnt++;return;}else {for(int j=1;j<=n;j++)if((!b[j])&&(!c[i+j])&&(!d[i-j+n])) {a[i]=j;b[j]=1;c[i+j]=1;d[i-j+n]=1;dfs(i+1);b[j]=0;c[i+j]=0;d[i-j+n]=0;}}
}
int main() { cin>>n;dfs(1);cout<<cnt;return 0;
}
奶酪
题目链接:https://www.luogu.com.cn/problem/P3958
点击查看
考虑把每个洞缩成一个点。对于两个可以互达的洞,向它们之间连一条边;我们再对于顶部新开一个点,如果某个洞可以到达顶部,我们就从该点向顶部连一条边,然后我们枚举每个洞,如果可以从底部到达该洞,就从这个洞开始 dfs 即可。
复杂度 \(O(n^2)\)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e3+10;
inline int read() {int s=0,x=1;char ch=getchar();while(!isdigit(ch)) {if(ch=='-') x=-1;ch=getchar();}while(isdigit(ch)) s=(s<<3)+(s<<1)+(ch^48),ch=getchar();return s*x;
}
int n,h,r;
bool vis[N];
struct node{int x,y,z;}a[N];
vector<int>e[N];
inline bool cmp(node x,node y) {return x.z<y.z;}
inline int dis(int i,int j) {return (a[i].x-a[j].x)*(a[i].x-a[j].x)+(a[i].y-a[j].y)*(a[i].y-a[j].y)+(a[i].z-a[j].z)*(a[i].z-a[j].z);}
inline bool dfs(int u) {if(u==n+1) return 1;vis[u]=1;int res=0;for(auto v:e[u])if(!vis[v]) res|=dfs(v);return res;
}
signed main() {int T=read();while(T--) {n=read(),h=read(),r=read();for(int i=1;i<=n;++i) a[i].x=read(),a[i].y=read(),a[i].z=read(),e[i].clear();for(int i=1;i<=n;++i) {if(abs(h-a[i].z)<=r) e[n+1].push_back(i),e[i].push_back(n+1);for(int j=1;j<i;++j)if(dis(i,j)<=4ll*r*r) e[i].push_back(j),e[j].push_back(i);}int ans=0;for(int i=1;i<=n;++i)if(abs(a[i].z)<=r) {memset(vis,0,sizeof(vis));ans|=dfs(i);}ans?puts("Yes"):puts("No");}return 0;
}
bfs
宽度优先搜索(bfs),指通过队列的方式,一层层扩展,直到达到目标状态。它与 dfs 略有不同。
迷宫寻路
题目链接:https://www.luogu.com.cn/problem/B3625
点击查看
bfs 板子。
#include<bits/stdc++.h>
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
queue<pii>q;
int n,m;
bool vis[110][110];
char ch[110][110];
int dx[4]={0,1,0,-1},dy[4]={1,0,-1,0};
inline bool bfs() {while(!q.empty()) {auto u=q.front();q.pop();if(u.fi==n&&u.se==m) return 1;for(int i=0;i<4;++i) {int x=dx[i]+u.fi,y=dy[i]+u.se;if(x<1||x>n||y<1||y>m||vis[x][y]||ch[x][y]=='#') continue;q.push({x,y}),vis[x][y]=1;}} return 0;
}
int main() {cin>>n>>m;q.push({1,1});vis[1][1]=1;for(int i=1;i<=n;++i) scanf("%s",(ch[i]+1));bfs()?puts("Yes"):puts("No");return 0;
}
刺杀大使
题目链接:https://www.luogu.com.cn/problem/P1902
点击查看
看到最大值最小,考虑二分答案。我们二分一个答案 \(k\),然后每次 bfs 时只走点权 \(\le k\) 的点,判断是否合法即可。
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int n,m,p[N][N];
bool vis[N][N];
int dx[4]={-1,0,1,0};
int dy[4]={0,-1,0,1};
struct node{int x,y;};
bool bfs(int f)
{memset(vis,0,sizeof(vis));queue<node>q;q.push((node){1,1});vis[1][1]=1;while(!q.empty()){node u=q.front();q.pop();for(int i=0;i<4;++i){int qx=u.x+dx[i],qy=u.y+dy[i];if(qx<1||qx>n||qy<1||qy>m)continue;if(vis[qx][qy])continue;if(p[qx][qy]>f)continue;if(qx==n)return true;vis[qx][qy]=1;q.push((node){qx,qy});}}return false;
}
int main()
{scanf("%d %d",&n,&m);for(int i=1;i<=n;++i)for(int j=1;j<=m;++j)scanf("%d",&p[i][j]);int l=0,r=1e6;while(l<=r){int mid=l+r>>1;if(bfs(mid))r=mid-1;else l=mid+1;}cout<<l;return 0;
}
双向搜索
双向搜索有两种,一种是“双向同时搜索”,指从起点和终点开始同时 bfs 或 dfs,如果两者相遇则说明有一组可行解。这个并不常用。另一种是“折半搜索”,指将搜索的过程分成两半,分别搜索前一半和后一半,最后将两半的结果合并的方法。
世界冰球锦标赛
题目链接:https://www.luogu.com.cn/problem/P4799
点击查看
发现如果直接搜索,\(2^{40}\) 肯定会 T,考虑折半搜索。我们将 \(n\) 场比赛分为两半,分别搜索两部分的结果,用数组存下, \(sort\) 一遍后用 upper_bound 计算答案就好了。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int n,tot1,tot2;
ll m,a[42],p1[2000010],p2[2000010],ans;
void dfs1(int nw,int s,ll cnt)
{if(nw==s) {p1[++tot1]=cnt;return ;}dfs1(nw+1,s,cnt+a[nw+1]),dfs1(nw+1,s,cnt);
}
void dfs2(int nw,ll cnt,int sum) {if(nw==n) {p2[++tot2]=cnt;return ;}dfs2(nw+1,cnt+a[nw+1],sum+1),dfs2(nw+1,cnt,sum);
}
int main() {scanf("%d %lld",&n,&m);for(int i=1;i<=n;++i)scanf("%lld",&a[i]);dfs1(0,n/2,0);sort(p1+1,p1+tot1+1);dfs2(n/2,0,0);sort(p2+1,p2+tot2+1);for(int i=1;i<=tot1;++i) ans+=upper_bound(p2+1,p2+tot2+1,m-p1[i])-p2-1;printf("%lld",ans);return 0;
}
迭代加深搜索 and 剪枝
这个可能有点难。
迭代加深搜索,相当于 dfs 和 bfs 的结合体。有时可能会遇到某些题,用 dfs 很好写,但 bfs 计算效率更高的情况,或者我们并不知道搜索需要的具体层数,此时我们就需要迭代加深搜索。每次进行 dfs 前,我们先限制搜索深度,在 dfs 时,如果搜到限制的深度边界就返回,然后逐步放宽限制,直到搜到可行解为止。
剪枝,指减掉一些多余的状态。在进行搜索时,可能会枚举到一些无用的状态,此时我们可以不去枚举这些无用的状态来达到优化复杂度的目的。剪枝分为三类:记忆化搜索、可行性剪枝和最优性剪枝。
记忆化搜索:有时某些状态可能会被枚举到多次,我们可以在第一次枚举到时记录这些状态的答案,再次枚举时直接累加记录过得答案即可。
可行性剪枝:在搜索时往往存在一些限制,如果我们当前搜索的状态已经不满足这些限制了,接下来的搜索肯定都没用了,可以直接停止。
最优性剪枝:如果当前搜索的解已经比之前搜索到的解更劣了,那么这个状态就无用了,可以直接停止。
滑雪
题目链接:https://www.luogu.com.cn/problem/P1434
点击查看
数据范围有些大,直接搜会 T,考虑剪枝。我们发现对于每个点,它接下来会滑到的最长长度是固定的,所以可以考虑记忆化搜索。对于每个点,我们用一个 f 数组记录在该点能滑到的最长长度,下一次搜索时直接查 f 数组即可。
#include<bits/stdc++.h>
using namespace std;
int R,C,a[505][505],f[501][501];
int dx[]={1,0,0,-1};
int dy[]={0,1,-1,0};
int dfs(int x,int y)
{if(f[x][y])return f[x][y];for(int i=0;i<4;++i){int cx=x+dx[i],cy=y+dy[i];if(a[cx][cy]<a[x][y]&&cx<=R&&cy<=C&&cx>=1&&cy>=1){f[x][y]=max(f[x][y],dfs(cx,cy)+1);}}return f[x][y];
}
int main()
{scanf("%d %d",&R,&C);int ans=0;for(int i=1;i<=R;++i)for(int j=1;j<=C;++j)scanf("%d",&a[i][j]);for(int i=1;i<=R;++i)for(int j=1;j<=C;++j)ans=max(ans,dfs(i,j)+1);printf("%d\n",ans);return 0;
}
Addition Chains
题目链接:https://loj.ac/p/10021
点击查看
发现题目并没有给出 \(m\) 的范围,考虑迭代加深搜索。暴搜是很好写的。但这样仍然会 T,考虑剪枝。
每次搜一个答案时,我们枚举它由哪两个数相加得到。如果 \(a_j+a_k>n\),那么由于序列单调递增,接下来不论怎么拼,最后的值都一定大于 \(n\),可以直接返回。但这样还不够。考虑继续剪枝。
我们有一个很好的性质:\(a_i \le a_{i-1} \times 2\)。那么此时我们又有一个剪枝:设剩余层数为 \(s\),当前值为 \(x\),那么如果 \(x \times 2^s \le n\),接下来无论怎么拼,最终答案都无法满足 \(a_m=n\),此时我们直接返回。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int q[(int)1e6];int n;
bool dfs(int nw,int ai,int dep) {if(ai>n)return 0;if(nw==dep) {if(ai!=n)return 0;else {for(int i=0;i<=nw;i++)cout<<q[i]<<" ";return 1;}}bool pn=0;for(int i=0;i<=nw;i++) {for(int j=0;j<=nw;j++) {if(q[i]+q[j]<=q[nw]) continue;if((q[i]+q[j])*pow(2,dep-nw)<n) continue;q[nw+1]=q[i]+q[j];pn=pn|dfs(nw+1,q[i]+q[j],dep);q[nw+1]=0;if(pn) break;}if(pn) break;}return pn;
}
signed main() {while(true) {cin>>n;if(n==0)return 0;if(n==1) {cout<<1<<endl;continue;}q[0]=1;for(int i=1;;i++) {if(dfs(0,1,i))break;}puts("");}return 0;
}
题单:https://www.luogu.com.cn/training/895980#problems