USACO 4.4.1 Shuttle Puzzle棋盘游戏

题目描述

大小为3的棋盘游戏里有3个白色棋子,3个黑色棋子,和一个有7个格子一线排开的木盒子。3个白棋子被放在一头,3个黑棋子被放在另一头,中间的格子空着。

初始状态: WWW_BBB

目标状态: BBB_WWW

在这个游戏里有两种移动方法是允许的:

  1. 你可以把一个棋子移到与它相邻的空格;

  2. 你可以把一个棋子跳过一个(仅一个)与它不同色的棋子到达空格。

大小为N的棋盘游戏包括N个白棋子,N个黑棋子,还有有2N+1个格子的木盒子。

这里是3-棋盘游戏的解,包括初始状态,中间状态和目标状态:

WWW BBB

WW WBBB

WWBW BB

WWBWB B

WWB BWB

W BWBWB

WBWBWB

BW WBWB

BWBW WB

BWBWBW

BWBWB W

BWB BWW

B BWBWW

BB WBWW

BBBW WW

BBB WWW

请编一个程序解大小为N的棋盘游戏(1 <= N <= 12)。要求用最少的移动步数实现。

INPUT FORMAT

一个整数N。

SAMPLE INPUT (file shuttle.in)

3

OUTPUT FORMAT

用空格在棋盘的位置(位置从左到右依次为1, 2, …, 2N+1)表示棋盘的状态。输出棋盘的状态变换序列,每行20个数(除了最后一行)。

输出的解还应当有最小的字典顺序(即如果有多组移动步数最小的解,输出第一个数最小的解;如果还有多组,输出第二个数最小的解;…)。

SAMPLE OUTPUT (file shuttle.out)

3 5 6 4 2 1 3 5 7 6 4 2 3 5 4

分析:

看到只有n<=12,第一个反应就是暴搜加剪枝。

想到的第一个剪枝是状态压缩剪枝,然后算了一下,我们需要开2^25×25=838860800那么大。

毫无疑问,行不通。

有一个神奇剪枝就是A的只往右边换,B的只往左边换,效率超级高。

注意答案的个数一定是n×(n+2),所以层数超过n×(n+2)时就退,等于时就判断是否正确。

判断时可以用我们之前提到的状态压缩,二进制中把A看作1,其余的看作0。

代码:

#include<cstdio>
#define fo(i,x,y) for(int i=x;i<=y;i++)
using namespace std;
int n,a[25],b[169],p,max,ans;
void dg(int x,int w,int s)
{
     if(x>max)return; 
     b[x]=w; 
     if(x==max)
     {
               if (w!=n+1) return;
               fo(i,1,x) 
               {
                         printf("%d ",b[i]);
                         if(i%20==0) printf("\n");
               }
               p=1;
               return;
     }
     if(w>2&&a[w-2]==1)
     {
            int k=a[w];a[w]=a[w-2];a[w-2]=k;
            dg(x+1,w-2,s-(1<<(w-3))+(1<<(w-1))); 
            if(p)return;    
            k=a[w];a[w]=a[w-2];a[w-2]=k;      
     }
     if(w>1&&a[w-1]==1)
     {
            int k=a[w];a[w]=a[w-1];a[w-1]=k;
            dg(x+1,w-1,s-(1<<(w-2))+(1<<(w-1))); 
            if(p)return;  
            k=a[w];a[w]=a[w-1];a[w-1]=k;      
     }
     if(w<n+n+1&&a[w+1]==2)     
     {
            int k=a[w];a[w]=a[w+1];a[w+1]=k;
            dg(x+1,w+1,s);  
            if(p)return;    
            k=a[w];a[w]=a[w+1];a[w+1]=k;      
     }
     if(w<n+n&&a[w+2]==2)
     {
            int k=a[w];a[w]=a[w+2];a[w+2]=k;
            dg(x+1,w+2,s);   
            if(p)return;  
            k=a[w];a[w]=a[w+2];a[w+2]=k;      
     }
}
int main()
{
    scanf("%d",&n);
    fo(i,1,n) {a[i]=1; a[i+n+1]=2;}
    a[n+1]=0;
    max=(n+2)*n;
    ans=(1<<(n+n+1))-(1<<(n+1));
    dg(0,n+1,(1<<n)-1);
}

其实我一开始的做法并不是暴搜,因为我没有想到那个神奇的剪枝。

经LL推荐,我用较慢的程序打了个表,找规律。

下面是n=1..6的情况:


n=1
1 3 2

n=2
2 4 5 3 1 2 4 3

n=3
3 5 6 4 2 1 3 5 7 6 4 2 3 5 4

n=4
4 6 7 5 3 2 4 6 8 9 7 5 3 1 2 4 6 8 7 5 3 4 6 5

n=5
5 7 8 6 4 3 5 7 9 10 8 6 4 2 1 3 5 7 9 11 10 8 6 4 2 3 5 7 9 8 6 4 5 7 6

n=6
6 8 9 7 5 4 6 8 10 11 9 7 5 3 2 4 6 8 10 12 13 11 9 7 5 3 1 2 4 6 8 10 12 11 9 7 5 3 4 6 8 10 9 7 5 6 8 7


我们发现第一个答案一定是n。

然后我们把前后两个数相减(a[i+1]-a[i]),可以得到n×(n+2)-1个差。


n=1
2 -1

n=2
2 1 -2 -2 1 2 -1

n=3
2 1 -2 -2 -1 2 2 2 -1 -2 -2 1 2 -1

n=4
2 1 -2 -2 -1 2 2 2 1 -2 -2 -2 -2 1 2 2 2 -1 -2 -2 1 2 -1

n=5
2 1 -2 -2 -1 2 2 2 1 -2 -2 -2 -2 -1 2 2 2 2 2 -1 -2 -2 -2 -2 1 2 2 2 -1 -2 -2 1 2 -1

n=6
2 1 -2 -2 -1 2 2 2 1 -2 -2 -2 -2 -1 2 2 2 2 2 1 -2 -2 -2 -2 -2 -2 1 2 2 2 2 2 -1 -2 -2 -2 -2 1 2 2 2 -1 -2 -2 1 2 -1


最后一个数都是-1,把它独立出来。

前面是1个2,1个1,2个-2,1个-1,3个2,一个1,……,n-1个(-1)^n×2,1个(n-1)^(n)。

之后n个(-1)^(n-1)×2,不算它们,把前面出现过的数倒过来,就是接下来的数,最后一个是-1。

非常愉快的从n开始,加上这些差,输出就行了。

代码:

#include<cstdio>
#define fo(i,x,y) for(int i=x;i<=y;i++)
using namespace std;
int n,b[169],l,r;
int main()
{
    scanf("%d",&n);
    l=2;r=1;
    while(r<n)
    {
              fo(i,1,r)
              {
                       b[++b[0]]=l;
                       b[n*(n+2)-b[0]-1]=b[b[0]];
              }
              b[++b[0]]=l/2; 
              b[n*(n+2)-b[0]-1]=b[b[0]];
              l=-l; r++;
    }

    fo(i,1,r) 
       b[++b[0]]=l;
    b[n*(n+2)-1]=-1; 
    l=n;
    fo(i,1,n*(n+2))
    {
                    printf("%d ",l); 
                    l+=b[i];      
                    if (i%20==0) 
                       printf("\n");     
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值