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 的好成绩。这时,我们就需要用到迭代加深了。

回到题目,本题还有一个难点是减枝,题目中除了保证所有答案大于 1107 之外,什么都没有,很明显,不能直接枚到 107

我们设现在的分数是 b,这次迭代的层数限制是 len,目标分数是 x,现在正在枚举第 i 层,上一层选的是 1last。这一次的枚举下限就是 last+1,这很明显,我们要先枚举大的,再枚举小的,还要不能有相等的。如果这次选了 1y,那么在后面的搜索中,最多得到 leni+1y,也就是如果 b+leni+1y<x 的话,就无解了,这样上限也出来了。

代码:

  #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. 笨笨的跳棋

题目描述

一个 8×8 的棋盘上有 4 个跳棋,可以移动,如果旁边有棋子可以跳一步(但是不可以连跳),允许一个格子里有多颗跳棋。给定初始状态和目标状态,问是否可以在 8 步以内达到。

这是一道近似于模拟的玩意儿,我们可以用字符串标记:先将每个点排序后将坐标化为字符串,用 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. 送礼物

这是典型的折半搜索。题目中的 N 只有 48,折半后搜两次是复杂度为 2×224,在接受范围内。在第二次搜索后,我们在前一次的搜索结果中去二分寻找最大的一个加上本次结果仍在 W 之内的值,并更新答案。总共复杂度为 O(2n2+2n2log),可以接受。

  #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短路

一个正权图,求 stk 短路,这道题是专门为 A* 算法准备的(在大数据下 A* 是错解)。

在本题里,我们设 h(i) 表示 i 点到终点的最短路,g(i) 表示目前到 i 点已走过的距离。类似于 Dijkstra 算法,唯一不同的是使用 A* 的 BFS 需要让节点重复入队于出队,当 i 号点第 k 次出队时,所走的路线就是从起点到第 i 号点的第 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;
  }
posted @   Grisses  阅读(3)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
Document
点击右上角即可分享
微信分享提示