来自yuzan1830的挑战(三)

社会我赞哥

说在前面

题目背后的故事

虽然最近有些自闭,但是该来的总是要来的。
yuzan1830给出了一个 n × n n\times n n×n的网格,每个格子内填了 [ 1 , n 2 ] [1,n^2] [1,n2]内的一个数字,且每个数字恰好被填一次。现在有两种类型的操作可以进行:

  • 任选一个数字,再从上下左右与它相邻的数字中选一个,两个数字进行交换。即两个数字所在的行数相差 1 1 1、列数相同,或列数相差 1 1 1、行数相同。
  • 任选一个数字,再从左上、左下、右上、右下与它相邻的数字中选一个,两个数字进行交换。即两个数字所在的行数和列数都相差 1 1 1

为了增加难度,yuzan1830规定每一次操作不能和上一次操作属于同一种类型,即两种操作必须交替进行。而第一次操作可以从两种类型中任选一种。
现在需要让网格中的数字从上到下、从左到右按 1 , 2 , … , n 2 1,2,\dots,n^2 1,2,,n2的顺序排列,求所需的最少步数,并输出任意一种方案。输出的第 i i i行应包括第 i i i次操作交换的两个数字。步数为 0 0 0输出"No Need",无解输出"No Solution"。

样例输入
1 2 9
4 6 3
7 8 5

样例输出
Step #1: 3 9
Step #2: 5 6
Step #3: 6 9

接受挑战

先考虑一下 n = 3 n=3 n=3的情况。把 n × n n\times n n×n的网格展开成一个长为 n 2 n^2 n2的序列,由于 9 ! = 362880 9!=362880 9!=362880,网格中数字的摆放顺序不会超过 362880 362880 362880种,宽搜 表示毫无压力。
每一次操作前网格中数字的摆放顺序定义成一个状态,作为图中的结点;能够相互转化的顺序之间连边以转移状态。图中每个结点第一次到达时遍历的层数一定是最少的,若遇到已经访问过的结点应该停止往下搜索。标记已经访问过的结点,康托展开 表示毫无压力。

状态数:

const int maxn=362880+5;

一个状态应该包括 9 9 9个格子内的数字,图方便的话可以这样写:

typedef int state[10];

但是这样写起来方便用起来不方便,我们最好还是装一下逼:

struct state{
  int st[10];
  state operator=(int arr[10]){
    memcpy(st,arr,10);return *this;
  }
  int &operator[](int x){return st[x];}
};

然后是康托展开部分。这样就有 encode({6,5,2,8,7,9,1,2,4}) = 223051 \text{encode(\{6,5,2,8,7,9,1,2,4\})}=223051 encode({6,5,2,8,7,9,1,2,4})=223051 decode(223051) = { 6 , 5 , 2 , 8 , 7 , 9 , 1 , 2 , 4 } \text{decode(223051)}=\{6,5,2,8,7,9,1,2,4\} decode(223051)={6,5,2,8,7,9,1,2,4}之类的映射关系了。

int fact[]={1,1,2,6,24,120,720,5040,40320,362880};
int encode(state st){
  int cnt=(st[1]-1)*fact[8];
  for(int i=2;i<=9;i++){
    int tmp=0;
    for(int j=1;j<i;j++)tmp+=st[j]<st[i];
    cnt+=(st[i]-tmp-1)*fact[9-i];
  }
  return cnt+1;
}
state decode(int c){
  state ans;int vis=0;c--;
  for(int i=1;i<=9;i++){
    int j,t=c/fact[9-i];
    for(j=1;j<=9;j++)if(!(vis>>j&1)&&!t--)break;
    ans[i]=j,vis|=1<<j,c%=fact[9-i];
  }
  return ans;
}

然后是核心部分:宽搜。由于第一步操作有两种选择,因此要搜两次,而在两次都搜完之前是不确定最优解的,因此要把两次搜索的结果都存下来,这里本人偷懒用了一个类。
宽搜前的声明:

  • v i s [ x ] vis[x] vis[x]标记状态 x x x x x x encode \text{encode} encode值)是否被访问过。
  • f a [ x ] fa[x] fa[x]记录状态 x x x上一步的状态。
  • s t e p [ x ] step[x] step[x]记录从初始状态到达状态 x x x所需的步数。
  • t y p e [ x ] type[x] type[x]记录从状态 f a [ x ] fa[x] fa[x]转移到状态 x x x选择的操作类型。
  • o p t [ x ] opt[x] opt[x]记录从状态 f a [ x ] fa[x] fa[x]转移到状态 x x x交换的数字对。

bfs( s t , t y p e s t ) \text{bfs(}st,type_{st}\text) bfs(st,typest):从状态 s t st st搜索,且第一步的操作类型为 t y p e s t type_{st} typest。两次搜索的 t y p e s t type_{st} typest值不同。
print_path( u ) \text{print\_path(}u\text) print_path(u):递归输出从初始状态到状态 u u u的解。

int dx1[]={-1,1,0,0},dy1[]={0,0,-1,1};
int dx2[]={-1,-1,1,1},dy2[]={-1,1,-1,1};
class solver{
public:
  bool vis[maxn];
  int fa[maxn],step[maxn],type[maxn];
  pair<int,int> opt[maxn];
  void bfs(state st,int type_st){
    memset(vis,0,sizeof(vis));
    int u=encode(st),v;
    state cur,next;
    vis[u]=1,fa[u]=-1,step[u]=0;
    type[u]=type_st^1;
    queue<int> q;q.push(u);
    while(!q.empty()){
      u=q.front();q.pop();
      if(u==1)return;
      cur=decode(u);
      for(int x=1;x<=3;x++){
        for(int y=1;y<=3;y++){
          int pos=(x-1)*3+y;
          for(int i=0;i<4;i++){
            int tx,ty;
            if(type[u])tx=x+dx2[i],ty=y+dy2[i];
            else tx=x+dx1[i],ty=y+dy1[i];
            int newpos=(tx-1)*3+ty;
            if(tx>0&&tx<=3&&ty>0&&ty<=3){
              next=cur;swap(next[pos],next[newpos]);
              v=encode(next);
              if(!vis[v]){
                vis[v]=1,fa[v]=u,step[v]=step[u]+1;
                type[v]=type[u]^1;
                opt[v]=make_pair(next[pos],next[newpos]);
                q.push(v);
              }
            }
          }
        }
      }
    }
  }
  void print_path(int u){
    if(fa[u]<0)return;
    print_path(fa[u]);
    printf("Step #%d: %d %d\n",step[u],opt[u].first,opt[u].second);
  }
};
solver g1,g2;

主函数就比较显然了:

int main(){
  state st;
  for(int i=1;i<=9;i++)scanf("%d",&st[i]);
  g1.bfs(st,0);
  g2.bfs(st,1);
  if(!g1.vis[1]&&!g2.vis[1])printf("No Solution\n");
  else if(!g2.vis[1]||g1.step[1]<g2.step[1]){
    if(!g1.step[1])printf("No Need\n");
    else{g1.print_path(1);printf("\n");}
  }
  else{
    if(!g2.step[1])printf("No Need\n");
    else{g2.print_path(1);printf("\n");}
  }
  return 0;
}

可能有多解的情况。比如一开始的样例的另一个解为:

Step #1: 6 9
Step #2: 3 6
Step #3: 5 9

分析一下时间复杂度:对于边长为 n n n的网格,一共有不超过 ( n 2 ) ! (n^2)! (n2)!种状态,每种状态可以选择 n 2 n^2 n2个位置,每个位置又可以选择另外 4 4 4个位置的数来交换,而一次康托展开或逆康托展开的复杂度为 O ( n 4 ) O(n^4) O(n4),因此一次搜索的时间复杂度为 O ( ( n 2 ) ! ⋅ 4 n 6 ) O((n^2)!\cdot 4n^6) O((n2)!4n6)。看起来很可怕,但在实际情况下很多状态都是达不到的,因此这个上界特别松。

说在后面

这里只写了 n = 3 n=3 n=3的情况。因为一旦 n = 4 n=4 n=4,状态数就达到了 16 ! = 20922789888000 16!=20922789888000 16!=20922789888000种。这样的后果就是程序运行了几十秒后你不得不重启电脑。
但是即使是 n = 3 n=3 n=3的情况仍然有值得优化的地方。比如下面这组数据,上面的代码跑了8.2秒!

样例输入
9 8 7
6 5 4
3 2 1

样例输出
Step #1: 6 9
Step #2: 5 6
Step #3: 6 8
Step #4: 4 6
Step #5: 2 8
Step #6: 2 7
Step #7: 7 9
Step #8: 1 9
Step #9: 3 7
Step #10: 3 4
Step #11: 2 3
Step #12: 1 5

我们不能忘了 A*搜索 。本蒟蒻用A*代替BFS之后,上面这组数据只跑了0.4秒。不过它求出的是另一组解:

Step #1: 4 5
Step #2: 4 9
Step #3: 6 4
Step #4: 1 9
Step #5: 1 8
Step #6: 3 8
Step #7: 1 6
Step #8: 3 7
Step #9: 2 8
Step #10: 2 7
Step #11: 5 2
Step #12: 2 6

为什么A*搜索这么快?宽搜是从起点向各个方向搜索,而A*可以说是直接朝着目标搜索的。
A*搜索最关键的部分是估价函数 h ( x ) h(x) h(x),它表示从当前状态 x x x到目标状态所需步数的估计值。这个估计值应该小于实际所需步数,且要尽可能接近。下面是本蒟蒻针对yuzan1830的这个问题设计的估价函数(设目标状态为 y y y x i 1 j 1 = y i 2 j 2 = k x_{i_1j_1}=y_{i_2j_2}=k xi1j1=yi2j2=k),也许是个假的估价函数。
h ( x ) = ⌊ 1 3 ∑ k = 1 9 ( ∣ i 1 − i 2 ∣ + ∣ j 1 − j 2 ∣ ) ⌋ h(x)=\lfloor\frac 13\sum_{k=1}^9(\vert i_1-i_2\vert+\vert j_1-j_2\vert)\rfloor h(x)=31k=19(i1i2+j1j2)

  int h(state st){
    int hv=0;
    for(int x=1;x<=3;x++){
      for(int y=1;y<=3;y++){
        int pos=(x-1)*3+y;
        int tx=(st[pos]-1)/3+1,ty=(st[pos]-1)%3+1;
        hv+=abs(x-tx)+abs(y-ty);
      }
    }
    return int(hv/3.0);
  }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值