VC++汉诺塔讲解、辅导汉诺塔VC++

- 首页 >> C/C++编程

基于 Windows 编程的汉诺塔游戏动画演示

1 概述

1.1 起源

汉诺塔是由法国数学家爱德华·卢卡斯在 1883 年发明的。汉诺塔 (又称河内塔) 问题是源于印

度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下

往上按照大小顺序摞着 64 片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新

摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一

个圆盘。

1.2 抽象为数学问题

如下图所示,从左到右有 A、B、C 三根柱子,其中 A 柱子上面有从小叠到大的 n 个圆盘,现

要求将 A 柱子上的圆盘移到 C 柱子上去,期间只有一个原则:一次只能移到一个盘子且大盘

子不能在小盘子上面,求移动的步骤和移动的次数。

图 1: 汉诺塔

1.3 递归解法

当 n=3 时,如果能把 1 和 2 号两个较小的盘子从 A 柱子通过 C 柱子移到 B 柱子,那么可以

直接将 3 号盘子从 A 柱子移到 C 柱子。剩下的问题就是将剩余的两个盘子从 B 柱子通过 A

1

2 动画演示的实现 2

柱子移动到 C 柱子,这时将问题转换为 n=2 时更小问题。

所以说一共就三步:

• 把 n-1 个盘子 A 柱子通过 C 柱子移动到 B 柱子;

• 把最后一个盘子从 A 柱子移动 C 柱子;

• 然后把 B 柱子的 n-1 个盘子通过 A 柱子移动 C 柱子。

C++ 代码如下:

Listing 1: 汉诺塔递归算法

void MovePlates ( i n t n , Tower& from , Tower& by , Tower& to )

{

i f (n == 1)

{

cout << ”move plate from source tower to d e s t i n a t i o n tower ” << endl ;

return ;

}

MovePlates (n − 1 , from , dst , by ) ;

MovePlates (1 , from , by , to ) ;

MovePlates (n − 1 , by , from , to ) ;

}

2 动画演示的实现

2.1 数据结构

1. 盘子类

如下图所示,容易定义盘子的数据结构如下:

图 2: 盘子示意图

2 动画演示的实现 3

Listing 2: 盘子类

c l a s s Plate {

public :

i n t left_ , right_ , top_ , bottom_ ;

i n t width_ , height_ ;

POINT center_point_ ;

Plate ( )

{

left_ = right_ = top_ = bottom_ = 0 ;

width_ = height_ = 0 ;

center_point_ = { 0 ,0 } ;

}

~ Plate ( ) {}

void set_position (POINT center_point )

{

center_point_ = center_point ;

left_ = center_point_ . x − width_ / 2 ;

right_ = center_point_ . x + width_ / 2 ;

top_ = center_point_ . y − height_ / 2 ;

bottom_ = center_point_ . y + height_ / 2 ;

}

void DrawPlate (HDC hdc )

{

HPEN plate_pen = CreatePen (PS_SOLID, 1 , color_ ) ;

HBRUSH plate_brush = CreateSolidBrush ( color_ ) ;

SelectObject ( hdc , plate_pen ) ;

SelectObject ( hdc , plate_brush ) ;

RoundRect ( hdc , left_ , top_ , right_ , bottom_ , width_ / 3 , height_ ) ;

}

} ;

成员函数 set_position 是通过给定盘子的中心点来构造盘子,而成员函数 DrawPlate 通过调

用设备环境来绘制盘子。

2. 塔类

每一个塔有 n 个盘子、底座和柱子三个部分组成。

Listing 3: 塔类

c l a s s Tower {

public :

std : : vector<Plate> plates_ ;

i n t pole_height_ ;

i n t base_length_ ;

POINT base_center_ ;

POINT pole_top_ ;

POINT base_left_ ;

POINT base_right_ ;

const Plate& operator [ ] ( i n t index ) const

{

return plates_ [ index ] ;

}

2 动画演示的实现 4

图 3: 塔示意图

} ;

3. 汉诺塔类

汉诺塔游戏中,总共有三个塔,由三个 Tower 类型的成员变量 src_tower_、by_tower_ 和

dst_tower_ 表示,HWND 类型变量 hwnd_ 是用于绘制图形的窗口句柄。

Listing 4: 汉诺塔类

c l a s s HanoiTower {

public :

i n t plates_count_ ;

HanoiTower ( void ) : inited_ (FALSE) , plates_count_ (3) {}

~HanoiTower ( void ) {}

void set_inited (BOOL i n i t e d )

{

inited_ = i n i t e d ;

}

BOOL i s I n i t e d ( )

{

return inited_ ;

}

void set_hwnd (HWND hwnd)

{

hwnd_ = hwnd ;

}

2 动画演示的实现 5

void InitHanoi ( i n t plates_count , i n t width , i n t height ) ;

void DrawLine (HDC hdc , POINT start , POINT end ) ;

void DrawHanoi (HDC hdc ) ;

void ClearBackground (HDC hdc , RECT &r e c t ) ;

void MovePlates ( i n t plates_count , Tower& src_tower ,

Tower& by_tower , Tower& dst_tower ) ;

void StartMove ( ) ;

private :

Tower src_tower_ ;

Tower by_tower_ ;

Tower dst_tower_ ;

HWND hwnd_;

BOOL inited_ ;

} ;

2.2 汉诺塔成员函数的实现

1. 初始化汉诺塔

首先需要确定每一个盘子的高度和塔基的宽度。

如图 4 所示,窗口的高度和宽度分别为 height 和 width,假设有 n 个盘子,则盘子的高度显

然为:

plate_height = height/(n + 3)

设定每一个塔间距离为 Margin,则塔基的宽度为

base_width = (width − 4 ∗ M argin)/3

图 4: 汉诺塔示意图

第一个塔的塔基左边点的 X、Y 坐标分别是

base_lef t_x = M argin

base_lef t_y = plate_height ∗ (n + 2)

2 动画演示的实现 6

第一个塔的塔基右边点的 X、Y 坐标分别是

base_right_x = M argin + base_length

base_right_y = plate_height ∗ (n + 2)

第一个塔的塔基中点的 X、Y 坐标分别是

base_center_x = M argin + base_length/2

base_center_y = plate_height ∗ (n + 2)

第一个塔的柱子顶点的 X、Y 坐标分别是

base_top_x = M argin + base_length/2

base_top_y = plate_height

同理,可以计算出另外两个塔的相应参数。

因为初始状态,第二个和第三个塔都是没有盘子的,所以只需要计算第一个塔的盘子参数。

显然,自底向上,每一个盘子的宽度是相比于塔基宽度是逐渐减小的,对于第 i 个盘子,容易

求得缩小的比例为

ratio = 1 − i/(n + 2)

所以第 i 个盘子的宽度和中心点坐标分别是

plate_width = base_length ∗ ratio

plate_center_x = base_center_x

plate_center_y = base_center_y − (i − 1) ∗ plate_height + plate_height/2

有了盘子的中心点,可以通过成员函数 set_position 构建盘子。

相应的代码如下:

Listing 5: InitHanoi 函数

void HanoiTower : : InitHanoi ( i n t plates_count , i n t client_width , i n t client_height )

{

set_inited (TRUE) ;

plates_count_ = plates_count ;

i n t plate_height = client_height / ( plates_count + 3 ) ;

i n t base_length = ( client_width − 4 * MARGIN) / 3 ;

src_tower_ . base_left_={MARGIN, plate_height *( plates_count + 2 ) } ;

src_tower_ . base_right_={MARGIN+base_length , plate_height *( plates_count +2)};

src_tower_ . base_center_={MARGIN+base_length /2 , plate_height *( plates_count +2)};

src_tower_ . pole_top_ = {MARGIN+base_length /2 , plate_height } ; ;

src_tower_ . base_length_ = base_length ;

by_tower_ = src_tower_ ;

by_tower_ . base_left_ . x += MARGIN + base_length ;

by_tower_ . base_right_ . x += MARGIN + base_length ;

by_tower_ . base_center_ . x += MARGIN + base_length ;

by_tower_ . pole_top_ . x = by_tower_ . base_center_ . x ;

dst_tower_ = by_tower_ ;

dst_tower_ . base_left_ . x += MARGIN + base_length ;

dst_tower_ . base_right_ . x += MARGIN + base_length ;

dst_tower_ . base_center_ . x += MARGIN + base_length ;

2 动画演示的实现 7

dst_tower_ . pole_top_ . x = dst_tower_ . base_center_ . x ;

Plate plate ;

double r a t i o = 0 ;

src_tower_ . plates_ . c l e a r ( ) ;

by_tower_ . plates_ . c l e a r ( ) ;

dst_tower_ . plates_ . c l e a r ( ) ;

POINT center_point = { 0 ,0 } ;

std : : vector<int> rand_seq = s h u f f l e ( ) ;

f o r ( i n t i = 1 ; i <= plates_count ; i++)

{

r a t i o = 1 − ( double ) ( i ) / ( plates_count + 2 ) ;

plate . width_ = ( i n t ) ( base_length * r a t i o ) ;

plate . height_ = plate_height ;

center_point = src_tower_ . base_center_ ;

center_point . y −= ( i − 1)* plate_height + plate_height / 2 ;

plate . set_position ( center_point ) ;

src_tower_ . plates_ . push_back ( plate ) ;

}

}

2. 绘制汉诺塔

绘制汉诺塔包括绘制盘子、塔基和柱子,其中塔基和柱子可以用直线代替。DrawLine 成员函

数就是为了绘制线条,可以设置线条颜色和粗细,使得塔基和柱子看起来较为美观。

Listing 6: DrawHanoi 成员函数

void HanoiTower : : DrawLine (HDC hdc , POINT start_point , POINT end_point )

{

HPEN line_pen = CreatePen (PS_SOLID, 15 , RGB(0 , 0 , 0 ) ) ;

SelectObject ( hdc , line_pen ) ;

MoveToEx( hdc , start_point . x , start_point . y , NULL) ;

LineTo ( hdc , end_point . x , end_point . y ) ;

}

Listing 7: DrawHanoi 成员函数

void HanoiTower : : DrawHanoi (HDC hdc )

{

// base l i n e

DrawLine ( hdc , src_tower_ . base_left_ , src_tower_ . base_right_ ) ;

DrawLine ( hdc , by_tower_ . base_left_ , by_tower_ . base_right_ ) ;

DrawLine ( hdc , dst_tower_ . base_left_ , dst_tower_ . base_right_ ) ;

// pole l i n e

DrawLine ( hdc , src_tower_ . base_center_ , src_tower_ . pole_top_ ) ;

DrawLine ( hdc , by_tower_ . base_center_ , by_tower_ . pole_top_ ) ;

DrawLine ( hdc , dst_tower_ . base_center_ , dst_tower_ . pole_top_ ) ;

f o r ( std : : vector<Plate >:: i t e r a t o r i t e r = src_tower_ . plates_ . begin ( ) ;

i t e r != src_tower_ . plates_ . end ( ) ; i t e r ++)

{

i t e r −>DrawPlate ( hdc ) ;

}

2 动画演示的实现 8

f o r ( std : : vector<Plate >:: i t e r a t o r i t e r = by_tower_ . plates_ . begin ( ) ;

i t e r != by_tower_ . plates_ . end ( ) ; i t e r ++)

{

i t e r −>DrawPlate ( hdc ) ;

}

f o r ( std : : vector<Plate >:: i t e r a t o r i t e r = dst_tower_ . plates_ . begin ( ) ;

i t e r != dst_tower_ . plates_ . end ( ) ; i t e r ++)

{

i t e r −>DrawPlate ( hdc ) ;

}

}

3. 移动汉诺塔

移动汉诺塔的方法与 1.3 描述的递归算法一致。当 n=1 时,需要将第一个塔的盘子减少一个,

第三个塔的盘子增加一个。为了实现动画效果,每移动一次,需要发送一个消息给窗口函数,

使得重新绘制汉诺塔。所以需要在头文件中定义一个用户消息 WM_MOVE 为:

#define WM_MOVEON (WM_USER+0x0001)

详细代码如下:

Listing 8: MovePlates 成员函数

void HanoiTower : : MovePlates ( i n t plates_count , Tower& src_tower ,

Tower& by_tower , Tower& dst_tower )

{

i f ( plates_count == 1)

{

Plate plate = src_tower . plates_ . back ( ) ;

src_tower . plates_ . pop_back ( ) ;

POINT dst_point = dst_tower . base_center_ ;

dst_point . y −= dst_tower . plates_ . s i z e ()* plate . height_ ;

dst_point . y −= plate . height_ / 2 ;

plate . set_position ( dst_point ) ;

dst_tower . plates_ . push_back ( plate ) ;

SendMessage (hwnd_, WM_MOVEON, 0 , 0 ) ;

return ;

}

MovePlates ( plates_count − 1 , src_tower , dst_tower , by_tower ) ;

MovePlates (1 , src_tower , by_tower , dst_tower ) ;

MovePlates ( plates_count − 1 , by_tower , src_tower , dst_tower ) ;

}

void HanoiTower : : StartMove ( )

{

MovePlates ( plates_count_ , src_tower_ , by_tower_ , dst_tower_ ) ;

set_inited (FALSE) ;

}

4. 清除背景

每一次绘制之前需要清除前一次绘制的汉诺塔,防止图像叠加。

2 动画演示的实现 9

Listing 9: 清除背景

void HanoiTower : : ClearBackground (HDC hdc , RECT &r e c t )

{

HBRUSH brush = (HBRUSH) GetStockObject (WHITE_BRUSH) ;

F i l l R e c t ( hdc , &rect , brush ) ;

}

2.3 窗口函数的消息处理

1. 设置基本变量

Listing 10: 窗口函数变量

LRESULT CALLBACK WndProc(HWND hWnd, UINT message , WPARAM wParam , LPARAM lParam )

{

s t a t i c i n t x c l i e n t = 0 , y c l i e n t = 0 ; // 绘 制 窗 口 尺 寸

s t a t i c i n t plates_count = 3 ; // 汉 诺 塔 盘 子 数 目

RECT bg_rect = { 0 ,0 ,0 ,0 } ; // 矩 形 区 域 变 量 , 用 于 设 置 清 除 窗 口 背 景

s t a t i c HanoiTower hanoi ; // 汉诺 塔变 量

hanoi . set_hwnd (hWnd) ; // 设 置 汉 诺 塔 的 窗 口 句 柄 , 用 于 绘 制 汉 诺 塔

}

2. 获取窗口尺寸。

窗口可能会缩放,可以通过 WM_SIZE 消息获取窗口尺寸,同时当窗口尺寸改变时,应该重

新初始化汉诺塔。

Listing 11: WM_SIZE 消息

case WM_SIZE:

{

x c l i e n t = LOWORD( lParam ) ;

y c l i e n t = HIWORD( lParam ) ;

hanoi . InitHanoi ( plates_count , x c l i e n t , y c l i e n t ) ; // 初 始 化 汉 诺 塔

InvalidateRect (hWnd, NULL, TRUE) ; // 发送WM_PAIT消息 , 绘制 汉 诺塔

}

break ;

3. 添加鼠标消息

设置左键初始化汉诺塔,右键开始移动汉诺塔。

Listing 12: 鼠标消息

case WM_LBUTTONDOWN:

{

hanoi . InitHanoi ( plates_count , x c l i e n t , y c l i e n t ) ;

InvalidateRect (hWnd, NULL, TRUE) ;

}

break ;

case WM_RBUTTONDOWN:

{

i f ( hanoi . i s I n i t e d ( ) )

2 动画演示的实现 10

{

hanoi . StartMove ( ) ;

}

}

break ;

4. WM_PAIT 消息 WM_PAIT 消息中主要实现清除背景和绘制汉诺塔工作。

Listing 13: WM_PAIT 消息

case WM_PAINT:

{

PAINTSTRUCT ps ;

HDC hdc = BeginPaint (hWnd, &ps ) ;

// TODO: Add any drawing code that uses hdc here . . .

GetClientRect (hWnd, &bg_rect ) ;

hanoi . ClearBackground ( hdc , bg_rect ) ;

hanoi . DrawHanoi ( hdc ) ;

EndPaint (hWnd, &ps ) ;

}

break ;

5. 用户自定义消息这一步是实现汉诺塔动画演示的关键。每当移动一次盘子时,都会发送

WM_MOVE,这时需要绘制当前状态下的汉诺塔。

Listing 14: 用户自定义消息

case WM_MOVEON:

{

HDC hdc = GetDC(hWnd) ;

GetClientRect (hWnd, &bg_rect ) ;

hanoi . ClearBackground ( hdc , bg_rect ) ; // 清除背景

hanoi . DrawHanoi ( hdc ) ; // 绘制 汉诺 塔

ReleaseDC (hWnd, hdc ) ;

Sleep ( 1 0 0 ) ; // 休眠 100ms

}

break ;

2.4 小结

• 通过简单实验掌握从实际问题抽象为数学问题,再通过编程实现的能力。

• 在现有基础之上,通过扩展美化界面效果。比如设置每一个盘子为不同颜色、添加消息

处理可以交互式增加盘子数量等等。通过设置变量来控制休眠时间,因为当盘子数目增

大时,需要移动的次数很多,可以适当减小休眠时间。


站长地图