gao-ji-sou-suo
高级搜索
posted on 2023-01-16 13:14:22 | under 总结 | source
一,前言
所谓高级搜索,就是对于普通的搜索(dfs,bfs,bdfs)进行优化后得到的在空间或时间上更优的算法,比如 A*,迭代加深,IDA*,双向搜索,折半搜索之类的。
二、算法
1.迭代加深
对于一个搜索的问题,如果他需要求的答案是深度最小的,那么通常会采用 bfs 处理,但是在搜索树上 bfs 所储存的是一层节点,很容易就 MLE 了,所以我们想到 dfs——它只会储存一条链,空间复杂度小得多。
举个例子:
对于上面这棵搜索树,如果我们采用 bfs,那么到最后将存储红色圆圈内的这一整层;如果用 dfs 则每次只用存储绿色圆圈里的一条链,空间要小得多。另外,以为 dfs 每搜索新的一层花费的时间都会接近甚至超过前面多次一共的时间。相比之下,时间复杂度大抵是一样的,最多会多一个常数(谁叫我们拿时间换空间)。
2.双向搜索
在 bfs 时,我们每次回扩展一层状态,直到有一个与结果一样的状态被发现。
就像这样,红色点一层一层地向外延申,最后碰到绿色点。
但是,我们可以发现,就上图而言,只有向绿色节点延申的部分是我们真正需要的,左侧浪费了许多时间进行无意义的搜索。所以我们需要改进算法。
可以发现,如果绿色节点也像红色这边延申,当它们相遇时就找到了一个最优解,而且时间复杂度会小许多(通常越往外搜索的节点会越多)。
为了向上图一样红色扩展玩一层绿色再扩展,我们需要将起始状态都放入搜索队列,并标记是正向搜索还是反向搜索,当一个状态既被正向搜到,又被反向搜到,则表明这是解。
3.折半搜索
对于一个 dfs,如果爆搜复杂度会炸掉,我们可以考虑折半,及先搜索前一半,在搜索后一半,最后将两从搜索的结果进行组合,这样复杂度也许会小很多。
4. A*
对于一个状态,我们取一个乐观的估价(即小于等于真实的最优距离),这样来进行有策略地选下一个打开的节点,这样我们第一个次搜到目标状态是就是最优情况(其他情况的乐观花费都比他大,那么真实花费一定会更大)。
5. IDA*
这是迭代加深与 A* 的结合,对于一次迭代,如果现在的层数加上乐观估价已经大于了我们设定的上限,那么真实值也会大于上限(这时就体现了乐观估价小于等于真实值的重要性),所以就可以跳过了。
三、例题
1.埃及分数
本题需要求最优解,很容易想到 BFS。但是,如果用 BFS 的话,你会得到 MLE 的好成绩。这时,我们就需要用到迭代加深了。
回到题目,本题还有一个难点是减枝,题目中除了保证所有答案大于
我们设现在的分数是
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int a,b,ans,res[55],las[55];
bool F=0;
bool dfs(long long a,long long b,int x){
if(a==1&&b>las[x-1]){
if(!F||b<res[x]){
las[x]=b;
for(int i=1;i<=x;i++)res[i]=las[i];
}
F=1;
return 1;
}
if(x>ans)return F;
long long s=ceil(1.0*b/a);
s=max(s,las[x-1]+1LL);
long long t=(ans-x+1)*b/a;
for(long long i=s;i<=t;i++){
int m=__gcd(i*a-b,b*i);
las[x]=i;
dfs((i*a-b)/m,b*i/m,x+1);
}
return F;
}
signed main()
{
scanf("%lld%lld",&a,&b);
while(!dfs(a,b,1))ans++;
for(int i=1;i<=ans;i++)cout<<res[i]<<' ';
return 0;
}
2. 埃及分数加强版
基本思路与前一题相同,额外对分母进行判断即可。
但是这个分母的范围有点大,所以用 map
来标记。
其余基本一致。
#include<bits/stdc++.h>
using namespace std;
#define int long long
int t,a,b,k,ans,tot;
unordered_map<int,bool>M;
deque<int>last,Ans;
void dfs(int cnt,int x,int y,int las){
if(cnt>ans){
if(x==0){
if(last<Ans)Ans=last;
}
return;
}
for(int j=max(las+1,(int)(ceil(1.0*y/x)));;j++){
if(y*(ans-cnt+1)<x*j)break;
if(M.count(j)==1)continue;
last.push_front(j);
dfs(cnt+1,x*j-y,y*j,j);
last.pop_front();
}
}
signed main()
{
scanf("%lld",&t);
while(t--){
scanf("%lld%lld%lld",&a,&b,&k);
for(int i=1,l;i<=k;i++)scanf("%lld",&l),M[l]=1;
ans=1;
Ans.clear();
while(1){
Ans.push_front(INT_MAX);
dfs(1,a,b,1);
if(Ans[0]!=INT_MAX){
printf("Case %lld: %lld/%lld=",++tot,a,b);
for(int i=ans-1;i>0;i--)printf("1/%lld+",Ans[i]);
printf("1/%lld\n",Ans[0]);
break;
}
ans++;
}
if(k)M.clear();
}
return 0;
}
3. 笨笨的跳棋
题目描述
一个
这是一道近似于模拟的玩意儿,我们可以用字符串标记:先将每个点排序后将坐标化为字符串,用 map
标记,再在构造时处理每颗棋子是否可以移动和跳跃。然后就是简单的双向搜索。
#include<bits/stdc++.h>
using namespace std;
const int dx[4]={0,1,0,-1};
const int dy[4]={-1,0,1,0};//四个方向
int x[5],y[5],c[5],r[5];
unordered_map<string,int>M[2];
struct state{
struct node{
int x,y;
inline bool operator<(const node &t){return x<t.x||(x==t.x&&y<t.y);}
}a[5];//存储所有棋子的坐标
bool tiao[4][4];//可不可以跳
int near[4][4],k;//四个方向挨着的棋子(边界为-2,没有为-1,否则为棋子编号0~3),k是现在的操作数
bool op;//标记正向或反向搜索
string hash;//字符串
inline state(){}
inline state(int _x[],int _y[],int _k,bool _op){//构造
for(int i=0;i<4;i++)a[i].x=_x[i],a[i].y=_y[i];//坐标
k=_k,op=_op;//状态
memset(tiao,0,sizeof(tiao));
memset(near,-1,sizeof(near));//初始化
sort(a,a+4);//排序
for(int i=0;i<4;i++){
for(int j=0;j<4;j++){
if(a[i].x+dx[j]>8||a[i].x+dx[j]<1||a[i].y+dy[j]<1||a[i].y+dy[j]>8)near[i][j]=-2;
}
}//标记边界
for(int i=0;i<4;i++){
for(int j=i+1;j<4;j++){
if(a[i].x==a[j].x&&a[i].y+1==a[j].y){
near[i][2]=j;
near[j][0]=i;
}
if(a[i].x+1==a[j].x&&a[i].y==a[j].y){
near[i][1]=j;
near[j][3]=i;
}
}
}//标记相邻
for(int i=0;i<4;i++){
for(int j=0;j<4;j++){
if(near[i][j]>=0&&near[near[i][j]][j]!=-2)tiao[i][j]=1;
}
}//判断跳跃(旁边有子且再走一步不是边界)
hash="";
for(int i=0;i<4;i++)hash+=(char)(a[i].x+'0'),hash+=(char)(a[i].y+'0');//转换成字符串
}
}S,T,tmp;
queue<state>q;
inline bool bfs(){
while(!q.empty())q.pop();
q.push(S);q.push(T);
M[1].clear();
M[0].clear();//清空
int X[5],Y[5];
while(!q.empty()){
tmp=q.front();
q.pop();
if(tmp.k>4)continue;
if(M[tmp.op^1].count(tmp.hash))return 1;//找到答案
if(M[tmp.op].count(tmp.hash))continue;//重复状态
M[tmp.op][tmp.hash]=1;
for(int i=0;i<4;i++)X[i]=tmp.a[i].x,Y[i]=tmp.a[i].y;//取出坐标
for(int i=0;i<4;i++){
for(int j=0;j<4;j++){
if(tmp.near[i][j]!=-2){//移动
X[i]+=dx[j];Y[i]+=dy[j];
q.push(state(X,Y,tmp.k+1,tmp.op));
X[i]-=dx[j];Y[i]-=dy[j];
}
if(tmp.tiao[i][j]){//跳跃
X[i]+=dx[j]*2;Y[i]+=(dy[j]*2);
q.push(state(X,Y,tmp.k+1,tmp.op));
X[i]-=(dx[j]*2);Y[i]-=(dy[j]*2);
}
}
}
}
return 0;
}
signed main()
{
while(scanf("%d%d%d%d%d%d%d%d",&x[0],&y[0],&x[1],&y[1],&x[2],&y[2],&x[3],&y[3])!=EOF){//读入
scanf("%d%d%d%d%d%d%d%d",&c[0],&r[0],&c[1],&r[1],&c[2],&r[2],&c[3],&r[3]);
S=state(x,y,0,1);
T=state(c,r,0,0);
if(bfs())puts("YES");
else puts("NO");
}
return 0;
}
/*
1 1 1 1 1 1 1 1
1 4 1 4 1 4 1 4
他们都说不可以重叠棋子,但是我在数据(第五组第11次棋盘)里找到了这个。。。
*/
4. 送礼物
这是典型的折半搜索。题目中的
#include<bits/stdc++.h>
using namespace std;
int n,w,K,ans,tot,g[50],M[20000000];
int las;
void dfs(int x){
if(x>K){
M[++tot]=las;
return;
}
dfs(x+1);
if(1LL*las+g[x]<=w){
las+=g[x];
dfs(x+1);
las-=g[x];
}
}//第一次搜索,存储结果
void dfs1(int x){
if(x>n){
ans=max(ans,M[upper_bound(M+1,M+tot+1,w-las)-M-1]+las);//更新答案(第一个大于他的数的前一个就是最大的小于等于他的数)
return;
}
dfs1(x+1);
if(1LL*las+g[x]<=w){
las+=g[x];
dfs1(x+1);
las-=g[x];
}
}
signed main()
{
scanf("%d%d",&w,&n);
for(int i=1;i<=n;i++)scanf("%d",&g[i]);
sort(g+1,g+n+1);
K=n/2;//折半
dfs(1);
sort(M+1,M+tot+1);
dfs1(K+1);//搜索
printf("%d",ans);
return 0;
}
5. 第k短路
一个正权图,求
在本题里,我们设
#include<bits/stdc++.h>
using namespace std;
int n,m,s,t,tot,ans[105],h[1005],cnt,H[1005],ecnt;//tot是目前ans的长度;h、cnt、H、ecnt是链式前向星的辅助变量
int k;
struct edge{
int v,nxt;
double w;
}e[10005],E[10005];//存储边
void adde(int u,int v,int w){
e[++cnt].nxt=h[u];
h[u]=cnt;
e[cnt].v=v;
e[cnt].w=w;
}//建边
void Adde(int u,int v,int w){
E[++ecnt].nxt=H[u];
H[u]=ecnt;
E[ecnt].v=v;
E[ecnt].w=w;
}//建边
int dis[1005];//到终点的最短距离
bool vis[1005];//标记是否到达过
struct node{
int x;
int k;
node(){}
node(int a,int b){x=a,k=b;}
bool operator<(const node &t)const{
return k>t.k;//重载运算符
}
};
void D(){//Dijkstra
priority_queue<node>q;
q.push(node(t,0));
for(int i=1;i<=n;i++)dis[i]=1e9;
dis[t]=0;//初始化
while(!q.empty()){
node tmp=q.top();
q.pop();
if(vis[tmp.x])continue;
vis[tmp.x]=1;
for(int i=H[tmp.x];i;i=E[i].nxt){
if(tmp.k+E[i].w<dis[E[i].v]){
dis[E[i].v]=tmp.k+E[i].w;//松弛
q.push(node(E[i].v,dis[E[i].v]));
}
}
}
}
struct Node{//准备A*
int x;
int k;
Node(){}
Node(int a,int b){x=a,k=b;}
bool operator<(const Node &t)const{
return k+dis[x]>t.k+dis[t.x];
}
};
void st(){
priority_queue<Node>q;
q.push(Node(s,0));
while(!q.empty()){
Node tmp=q.top();
q.pop();
if(tmp.x==t){//到了终点
ans[++tot]=tmp.k;//存储
if(tot==k)return;//k条都有了
continue;
}
for(int i=h[tmp.x];i;i=e[i].nxt){
q.push(Node(e[i].v,tmp.k+e[i].w));//扩展
}
}
}
int main()
{
scanf("%d%d%d",&n,&m,&k);s=n,t=1;//从n号点到1号点
int u,v,w;
for(int i=1;i<=m;i++){
scanf("%d%d%d",&u,&v,&w);
adde(u,v,w);//正向边
Adde(v,u,w);//反向边(求到终点最短路)
}
D();
st();
for(int i=1;i<=tot;i++)printf("%d\n",ans[i]);//输出答案
for(int i=tot+1;i<=k;i++)printf("-1\n");//不够的补全
return 0;
}
6. 引蛇出洞
我们设蛇头到洞口的曼哈顿距离为估价(显然这是估计的最优情况,有时因为蛇身和石头的原因会绕路)。然后使用双端队列来存储蛇的身体(方便移动),用字符串标记状态加上 map 标记状态,进行一个优秀的 A* 搜索即可。
#include<bits/stdc++.h>
using namespace std;
int read(){//快读
int x=0;
char ch=getchar();
while(ch>'9'||ch<'0')ch=getchar();
while(ch<='9'&&ch>='0')x=(x<<3)+(x<<1)+(ch-'0'),ch=getchar();
return x;
}
int n,m,l,k,X,Y,tot;
const int dx[10]={0,1,-1,0,0};
const int dy[10]={0,0,0,1,-1};//移动
bool vis[25][25];
int V[25][25];
struct make_node{
int first,second;
make_node(){}
make_node(int a,int b){first=a,second=b;}
}T;
unordered_map<string,bool>M;//标记
deque<make_node>L;//存储蛇,在移动的时候从前面加入新的头,删除旧的尾就可以简单维护。
string K;
int h(deque<make_node>L){
return L[0].first+L[0].second-2;
}//曼哈顿(化简后就是上面的式子)
struct node{//存储状态
int x,H;//步数和估价
deque<make_node>cnt;//蛇
bool vis[25][25];//标记该点是否有蛇的身体
node(){}
node(int X,deque<make_node>Cnt,bool f[25][25]){x=X,cnt=Cnt;memcpy(vis,f,sizeof(vis));H=h(cnt);}
bool operator<(const node &t)const{//比较
return x+H>t.x+t.H;
}
}tmp;
string To(deque<make_node>d){//转化状态为字符串
string t="";
while(!d.empty()){
t+=to_string(d[0].first)+'-'+to_string(d[0].second)+'-';
d.pop_front();
}
return t;
}
int bfs(){
priority_queue<node>q;
q.push(node(0,L,vis));
while(!q.empty()){
tmp=q.top();
q.pop();
if(!h(tmp.cnt))return tmp.x;//到达!
K=To(tmp.cnt);
if(M.count(K))continue;//重复,跳过
M[K]=1;//标记
X=tmp.cnt[0].first,Y=tmp.cnt[0].second;
T=tmp.cnt[l-1];
tmp.cnt.pop_back();
for(int i=1;i<=4;i++){
if(1<=X+dx[i]&&X+dx[i]<=n&&1<=Y+dy[i]&&Y+dy[i]<=m){//未出界
X+=dx[i],Y+=dy[i];
if(!tmp.vis[X][Y]){//没有撞到身体或石头
tmp.vis[T.first][T.second]=0;
tmp.vis[X][Y]=1;
tmp.cnt.push_front(make_node(X,Y));
q.push(node(tmp.x+1,tmp.cnt,tmp.vis));//加入队列
tmp.vis[X][Y]=0;
tmp.vis[T.first][T.second]=1;
tmp.cnt.pop_front();
}
X-=dx[i],Y-=dy[i];
}
}
}
return -1;//无解
}
int main()
{
while(1){
n=read(),m=read(),l=read();
if(!n&&!m&&!l)break;
memset(vis,0,sizeof(vis));
L.clear();//初始化
for(int i=1,x,y;i<=l;i++)x=read(),y=read(),L.push_back(make_node(x,y)),vis[x][y]=1;//读入蛇
k=read();
for(int i=1,x,y;i<=k;i++)x=read(),y=read(),vis[x][y]=1;//读入石头
M.clear();
printf("Case %d: %d\n",++tot,bfs());
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效