四塔问题:设有A,B,C,D四个柱子(有时称塔),在A柱上有由小到大堆放的n个盘子,如图所示。
今将A柱上的盘子移动到D柱上去。可以利用B,C柱作为工作栈用,移动的规则如下:
①每次只能移动一个盘子。
②在移动的过程中,小盘子只能放到大盘子的上面。
设计并实现一个求解四塔问题的动态规划算法,并分析时间和空间复杂性。
算法思想:
用如下算法移动盘子(记为FourPegsHanoi):
1)、将A柱上n个盘子划分为上下两部分,下方部分共有k(1≤k≤n)个盘子,上方部分共有n - k个盘子。
2)、将A柱上面部分n–k个盘子使用FourPegsHanoi算法经过C、D柱移至B柱。
3)、将A柱剩余的k个盘子使用ThreePegsHanoi算法经过C柱移至D柱。
4)、将B柱上的n–k个盘子使用FourPegsHanoi算法经过A、C柱移至D柱。
ThreePegsHanoi算法如下(设三个柱子分别为A、B、C,A柱上共有k个盘子):
1)、将A柱上方k-1个盘子使用ThreePegsHanoi算法经过B柱移至C柱。
2)、将C柱上最后一个盘子直接移至C盘。
3)、将B柱上k-1个盘子使用ThreePegsHanoi算法经过A柱移至C柱。
算法步骤:
根据动态规划的四个步骤,求解如下:
1)、最优子结构性质:
四柱汉诺塔问题的最优解是用最少的移动次数将A柱上的盘子全部移到D柱上。当盘子总数为i时,我们不妨设使用FourPegsHanoi的最少移动次数为f(i)。相应的ThreePegsHanoi 算法移动次数为g(k),由于g(k)=2g(k-1)+1=2^k -1,当k确定时,g(k)也是不变的。
f(i)为最优解时,其子问题f(i-k)也必为最优解。如果f(i-k)不是最优解,那么存在f’(i-k) < f(i-k)。用f’(i-k)替换f(i-k)将产生一个比f(i)更优的解。这与f(i)为最优解是矛盾的。所以本问题具有最优子结构性质。
2)、递归地定义问题的最优解:
根据上述FourPegsHanoi算法得到最少移动次数f(i):
通过这个表达式我们可以知道,k取那个值时f(i)的值,也就是说,不用具体操作,就可以知道移动的最少次数,并且知道k的值,所以在算法实现时,求出k的值是非常重要的。下面的代码就是用来求k的。
int k[100];
int moves[100][100];
void minMoves(int n) //求k
{
int min = -1;
moves[0][0] = 0;
k[0] = 0;
for(int i = 1;i<= n;i++)
{
min = 1000000000;
for(int j=1;j<=i;j++)
{
moves[i][j] = 2*moves[i-j][k[i-j]] -1 + (2<<(j-1)) ;
if(moves[i][j] < min)
{
min = moves[i][j];
k[i] = j;
}
}
}
}
完整代码:(动态规划)
#include
using namespace std;
int k[100];
int moves[100][100];
void minMoves(int n) //求k
{
int min = -1;
moves[0][0] = 0;
k[0] = 0;
for(int i = 1;i<= n;i++)
{
min = 1000000000;
for(int j=1;j<=i;j++)
{
moves[i][j] = 2*moves[i-j][k[i-j]] -1 + (2<<(j-1)) ;
if(moves[i][j] < min)
{
min = moves[i][j];
k[i] = j;
}
}
}
}
void move(int a,int b)
{
printf("%d --> %d\n",a,b);
}
void hanoi(int x,int a,int b,int c)
{
if(x == 1)
move(a,b);
else
{
hanoi(x-1,a,c,b);
move(a,b);
hanoi(x-1,c,b,a);
}
}
void fourhanoi(int x,int a,int b,int c,int d)
{
if(x == 1)
move(a,b);
else
{
int tk = k[x];
fourhanoi(x-tk,a,d,b,c);
hanoi(tk,a,b,c);
fourhanoi(x-tk,d,b,a,c);
}
}
int main()
{
minMoves(65);
// for(int i=1; i<=64; i++)
// {
// printf("k[%d] = %d\n",i,k[i]);
// }
int n;
cin >> n;
fourhanoi(n,1,2,3,4);
return 0;
}
下面我们来看看,1941年,美国的J. S. Frame,给出的四柱汉诺塔的算法思想,也叫Frame算法:
1、用4柱汉诺塔算法把A柱上部分的n- r个碟子通过C柱和D柱移到B柱上【F( n- r )步】。
2、用3柱汉诺塔经典算法把A柱上剩余的r个碟子通过C柱移到D柱上【2^r-1步】(参照上述三柱时的情况)。
3、用4柱汉诺塔算法把B柱上的n-r个碟子通过A柱和C柱移到D柱上【F(n-r)步】。
4、依据上边规则求出所有r(1≤r≤n)情况下步数f(n),取最小值得最终解。
因此Frame算法的递归方程如下:
F(n)=min(2*F(n-r)+2^r-1),(1≤r≤n)。
大家有没有发现,其实,这个算法思想跟我们之前认为合理的算法基本一致,差别只是在于他将我们的n-2个碟子缓存到B上,改为了将n- r个碟子转移到B柱上。
差别即使核心,这个算法的核心,就是计算n个盘子的情况下,r为何值时,能够使得算法最优。
找到了核心,我们现在的任务就明确了,就是对r值的计算。
这里给出了一个较笨的方法–枚举(不知道各位有没有其他方法)。就是将一定范围内的n与r的所有取值带入,得到满足F(n)为最小值的r的值,记为K[n] = r;
代码如下:
#define Max 100
#define maxx 1000000000
int k[Max+1] = {0};
void initk(void )
{
int i, j;
long long temp;
long long m[Max+1] = {0};
for( i = 1; i <= Max; i++ )
{
m[i] = maxx;
for( j = 1; j <= i; j++ )
{
temp = 2*m[i-j] + (long long)pow(2,j) - 1;
if( temp < m[i] )
{
m[i] = temp;
k[i] = j;
// printf("K[%d] = %d, m[i] = %d\n", i, k, temp );
}
}
}
}
完整代码:(非动态规划)
#include
using namespace std;
#define Max 100
#define maxx 1000000000
int k[Max+1] = {0};
void initk(void )
{
int i, j;
long long temp;
long long m[Max+1] = {0};
for( i = 1; i <= Max; i++ )
{
m[i] = maxx;
for( j = 1; j <= i; j++ )
{
temp = 2*m[i-j] + (long long)pow(2,j) - 1;
if( temp < m[i] )
{
m[i] = temp;
k[i] = j;
// printf("K[%d] = %d, m[i] = %d\n", i, k, temp );
}
}
}
}
void move(int a,int b)
{
printf("%d --> %d\n",a,b);
}
void hanoi(int x,int a,int b,int c)
{
if(x == 1)
move(a,b);
else
{
hanoi(x-1,a,c,b);
move(a,b);
hanoi(x-1,c,b,a);
}
}
void fourhanoi(int x,int a,int b,int c,int d)
{
if(x == 1)
move(a,b);
else
{
int tk = k[x];
fourhanoi(x-tk,a,d,b,c);
hanoi(tk,a,b,c);
fourhanoi(x-tk,d,b,a,c);
}
}
int main()
{
initk();
// for(int i=1; i<=64; i++)
// {
// printf("k[%d] = %d\n",i,k[i]);
// }
int n;
cin >> n;
fourhanoi(n,1,2,3,4);
return 0;
}