题目大意:像汉诺塔一样有三个柱子,不同的是并不一定像汉诺塔一样开始时和结束时只有一个柱子上有盘子。现在给出初始状态和终末状态,求最少的步数从初始状态到终末状态。
分析:
A.如果我们把1~k号盘子从1号柱子移动到2号柱子,那么1号柱子上大于k的盘子自然是不用动的,直接忽略即可。
B.移动最少的步数?我们知道,在旧汉诺塔中,我们是以空柱子作为中转站,进行移动的,在这个题目中,可能存在没有空柱子的情况,这个时候如何移动并且还要最小?
如果我们熟知汉诺塔的话,应该知道,在汉诺塔中,也可以到达三个盘子都有盘子的情况。
旧汉诺塔的过程:A柱子:1~4,B、C柱子空,现在要把1~4移动到B柱子上。
1->C:
2->B:
1->B:
3->C:
1->A:
2->C:
1->C:
4->B:
再利用A柱子把C柱子上1~3移动到B柱子上即可,不再赘述
因此我们可以知道,三个柱子都有盘子只是汉诺塔的一个中间态而已,而中间态往下继续就是我们所分析过的一般的汉诺塔状态(解释一下:因为上图中我们已经完成了4号盘子的移动,因此我们可以直接忽略掉这个盘子,问题就变成了把C柱子上的1~3号盘子放到B柱子上,1~3号盘子正好就在4号盘子对应的中转的柱子上)。因此我们可以知道,从初始状态到终末状态会经过我们熟知的汉诺塔常态。所以在这个题目中我们就让初始状态和终末状态共同到达一个经典汉诺塔状态,求步数和即可。
我们知道了初态和终态,就可以推论出中间态的盘子处于的位置了。设start[i]表示i号盘子的初态,over[i]表示i号盘子的终态,那么我们设柱子的编号为1、2、3,于是中间态的盘子位置有如下结论:
A.start[i]==over[i] ,这个盘子是不用移动的
B.start[i]!=over[i],这个盘子在中间态的时候处于编号为6-start[i]-over[i]的柱子上
我们用函数f(P,i,finial)表示第i号盘子移动到finial位置上所花费的最少步数,P[i]表示i号盘子所在位置。
算法流程为:
A.首先找到要移动的最大的盘子编号k
B.将1~k号盘子移动到他们的中间态的位置上
递归函数的实现:
if(P[i]==finial)return f(P,i-1,finial);//i号盘子已经在最终位置上了,无需移动。
解释:我们只是找到要移动的最大盘子,但并不代表1~k的所有盘子都在一个柱子上
else return f(P,i-1,6-start[k]-over[k])+(1LL<<(i-1)-1)+1;
详细解释:
1.因为对于c++编译器来说,直接写一个数字会被默认当成const int 类型,而这个题目盘子数量高达60个,int显然会溢出,1LL是告知编译器将这个常量当作是const long long类型。
2.当i号盘子不在其最终位置上的时候:
我们要先将1~i-1移动到要移动i号盘子时候的中转柱子上,代价为 f(P,i-1,6-start[k]-over[k])
然后将这个没有被其他盘子压住的i号盘子移动到finial柱子上,代价为 1
最后在把中转柱子上的1~i-1个盘子压在i上面, 代价为 (1LL<<(i-1)-1) 这是经典汉诺塔问题的代价
最终的答案就是f(start,k-1,6-start[k]-over[k])+f(finish,k-1,6-start[k]-over[k])+1;
我们的方法就是让两个状态都达到类似上面最终一个1->C的图的状态,1~k-1的盘子从初始盘子移动到k盘子的中转盘上,1~k-1的盘子从终末盘子移动到k盘子的中转盘上,再加上将k号盘子移动到目标盘子的步数1即可。
AC代码(无优化):
#include<cstdio>
using namespace std;
int start[65],over[65],n;
long long f(int p[],int i,int finial){
if(i==0)return 0;
if(p[i]==finial)return f(p,i-1,finial);//第i个盘子不用动
long long x=1;
x<<=(i-1);
return x+f(p,i-1,6-p[i]-finial);//要动第i号盘 则要先动压在上面的i-1个盘放在中转盘f(p,i-1,6-p[i]-finial) 将i放置在目标盘后1 再将中转盘上的盘子都移过去
}
int main(){
int kase=0,i,k,other;
long long answer;
while(scanf("%d",&n)==1&&n){
for(i=1;i<=n;i++){
scanf("%d",&start[i]);
}
for(i=1;i<=n;i++){
scanf("%d",&over[i]);
}
k=n;
while(k>=1&&start[k]==over[k])k--;//找到最大的需要移动的盘子
answer=0;
if(k>=1){
other=6-start[k]-over[k];//把1~k-1移动到中转盘other上
answer=f(start,k-1,other)+f(over,k-1,other)+1;
}
printf("Case %d: %lld\n",++kase,answer);
}
}
AC代码:
#include<cstdio>
using namespace std;
int start[65],over[65],n;
long long f(int p[],int i,int finial){
if(i==0)return 0;
if(p[i]==finial)return f(p,i-1,finial);//第i个盘子不用动
return (1LL<<(i-1))+f(p,i-1,6-p[i]-finial);//要动第i号盘 则要先动压在上面的i-1个盘放在中转盘f(p,i-1,6-p[i]-finial) 将i放置在目标盘后1 再将中转盘上的盘子都移过去
}
int main(){
int kase=0,i,k,other;
long long answer;
while(scanf("%d",&n)==1&&n){
for(i=1;i<=n;i++){
scanf("%d",&start[i]);
}
for(i=1;i<=n;i++){
scanf("%d",&over[i]);
}
k=n;
while(k>=1&&start[k]==over[k])k--;//找到最大的需要移动的盘子
answer=0;
if(k>=1){
other=6-start[k]-over[k];//把1~k-1移动到中转盘other上
answer=f(start,k-1,other)+f(over,k-1,other)+1;
}
printf("Case %d: %lld\n",++kase,answer);
}
}