例1:下图是一个3X3的数字拼图。
1 |
3 |
2 |
6 |
|
5 |
4 |
7 |
8 |
图1
它要还原成图2
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
|
图2
将问题一般化,在M*N的方格里有M*N-1个不同元素和一个空元素,只有空元素可以与上下左右相邻的元素交换位置。M*N方格中M*N-1个元素和一个空元素的位置确定一个图形。拼图游戏的问题是:一个图形经过一连串的交换能否得到另一个图形,如何得到。从交换方式的可逆性看出这种关系满足等价三性质,如果图形A通过交换变成图形B我们则称它们是等价的。把M*N-1个元素用1至M*N-1编号,空元素编号0。然后展成一个排列。每个图形对应一个排列。确定了展开方式,图形和排列是一一对应的。这里用到的展开方式是行优先的顺序(其他方式展开也能到相应的结果)。将例1的两个图形展开有:图1对应1 3 2 6 0 5 4 7 8,图2对应1 2 3 4 5 6 7 8 0。
定理1:图形A与图形B等价的充要条件图形A的排列的逆序数加上0元素行号和列号的奇偶性等于图形B的排列的逆序数加上0元素行号和列号的奇偶性。为方便表述,把图形排列的逆序数加上0元素行号和列号的奇偶性称为图形的奇偶性。
先看定理1如何起作用,图1:展开的排列1 3 2 6 0 5 4 7 8,它的逆序数为8,0元素行号为2,列号为2。逆序数加行号,列号的奇偶性为偶。图2:展开的排列1 2 3 4 5 6 7 8 0,它的逆序数为8,0元素行号为3,列号为3。逆序数加行号,列号的奇偶性为偶。两个图形的奇偶性相同,根据定理1判断它们等价。
首先证明必要性,即如果图形A与图形B等价,则图形A的奇偶性等于图形B奇偶性。
0元素和某个元素交换位置,则排列的逆序数的奇偶性就改变一次。交换后0元素的行号或者列号会加1或减1,即行号,列号之和的奇偶性也改变一次。这说明拼图的交换方式不改变图形的奇偶性,也说明拼图中至少有两组等价类,奇偶性不同的图形不等价。
下面证明充分性,如果图形A的奇偶性等于图形B的奇偶性,则图形A,B等价。
如果证明了拼图只有两组等价类,从必要性的证明过程可知,奇性图形是一组等价类,偶性是一组。从而证明了充分性。
先考虑一般的排列1 2 3 ... N。某个元素连续与后面M相邻的元素交换位置,称为向后M步移动。如排列:1 2 3 4 5 6。元素2的向后3步移动,排列变成1 3 4 5 2 6。同样的方式定义向前M步移动。如果排列A能够通过有限向前M步移动和向后M步移动变成排列B,称排列A与排列B M步等价。容易看出这也是等价关系。
引理1:任何一个1至N的排列M步等价于1 2 ... N-M(...)。括号里是N-M+1至N的某个排列。
证明:如果N=M,这显然成立。
假设N=k时成立,下面证明k+1的情况。
1元素的位置记为i
情况1:假设i=1,显然,余下的元素减1,就变成N=k的境况,得证。
情况2:如果1<i<=M,则元素1前面的元素向后M步移动,变为情况1。
情况3:如果i>M,则元素1有限次向前M步移动,使i有1<=i<=M,可变成情况1或2。
从而得证。
当M=2时,只有两组等价类。由于移动不改变排列的奇偶性,从而奇排列是一组等价类,偶排列是一组等价类。
考虑N*M的拼图。
当N=M=2,穷举法可证明只有两组等价类。
当N,M不同时为2时,设N不等于2(如果N等于2,M不等于2可颠倒行列讨论)。
只考虑第二行最后一个元素是空元素的情形,因为空元素在其他位置总可以等价某个空元素在第二行最后一个元素的图形。不考虑空元素以之字形方式展开图形,即第一行最后一个数字和第二行倒数第二个数字相连。如:
1 |
2 |
4 |
3 |
5 |
|
图3
展开成1,2,4,5,3。
下面证明两行拼图的交换方式可以实现排列的向前2步和向后2步移动。
要实现元素a的向前2步移动,则可顺着展开的方式循环移动拼图,使a在第一行第二列的位置,使空元素在第二行第二列的位置,此时可把元素i可与空元素对换。然后再沿着展开的顺序还原拼图。
例如:图3的元素4向前2步移动。可以如下操作,
2 |
4 |
5 |
1 |
|
3 |
图4
2 |
|
5 |
1 |
4 |
3 |
图5
4 |
1 |
2 |
3 |
5 |
|
图6
展开成4,1,2,5,3。实现了向前2步移动。
使i在第二行第二列的位置,使空元素在第一行第二列的位置可以实现向后2步移动。根据引理1及,两行拼图可以分成两组等价类。
假设M=k图形可以分成两组等价类,下面证明M=k+1,
只需要证明任何M=k+1图形总等价于第一行元素为1 2 ... N的某图形即可。
如果这N个元素都在第一行,把空元素移到第二行,从上面的证明可知,交换两个不同的非空元素,图形的奇偶性改变,属于不同的等价类。N大于2,第二行就有两个非空元素可供交换。所以两行图形可以等价与第一行为1 2 ... N的某个图形。
如果1至N的某个a元素不在第一行,设它在第i行。把空元素移动到i行,这样第i行和第i-1行可以看成M=2的图形。可以把a移动到第i-1行,并保证第i行和i-1行中1至N的元素的行号不增加。有限步移动可以使1至N元素全部在第一行。
显然M=k+1图形的等价类数目为2。
充分性得证。
拼图游戏的随机离散中加入定理1的判断可以保证游戏有意义,不会出现无解的情况。
原文链接
http://www.cppblog.com/lemene/archive/2007/10/04/33405.aspx
原作者的实现如下
// puzzle game for window console // gcc puzzle_console.c -o puzzle.exe #include <windows.h> #include <stdio.h> #include <conio.h> #include <stdlib.h> int g_row = 4; // 列数目 int g_line = 4; // 行数目 int g_puzzle[5*7]; // 拼图数据 // clear screen // 清屏 void Clrscr() { HANDLE hConsole; COORD coordScreen = {0, 0}; DWORD cCharsWritten; DWORD dwConSize; CONSOLE_SCREEN_BUFFER_INFO csbi; hConsole=GetStdHandle(STD_OUTPUT_HANDLE); GetConsoleScreenBufferInfo(hConsole, &csbi); dwConSize = csbi.dwSize.X * csbi.dwSize.Y; FillConsoleOutputCharacter(hConsole, (TCHAR)' ', dwConSize, coordScreen, &cCharsWritten); } // void GotoXY(int x, int y) { HANDLE hConsole; COORD pt; pt.X = x; pt.Y = y; hConsole = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(hConsole, pt); } // 计算拼图的奇偶性 int CalcParity(int *puzzle, int size, int row) { int ret = 0; int i, j; // 计算排列的奇偶性 for (i=0; i<size; i++) { for (j=i+1; j<size; j++) { if (puzzle[i] > puzzle[j]) ret++; } } // 计算0元素位置 for (i=0; i<size; i++) { if (puzzle[i] == 0) break; } ret += i%row + i/row; return ret; } // 拼图是否还原 int IsPuzzleFinished() { int i; for (i=0; i<g_line*g_row-1; i++) { if (g_puzzle[i] != i+1) return 0; } return 1; } // 随机离散拼图 void RandomPuzzle() { srand(GetTickCount()); int size = g_line * g_row; int i = 0; while ( i<size ) { g_puzzle[i] = rand() % size; int j; for (j=0; j<i; j++) { if (g_puzzle[i] == g_puzzle[j]) goto USELESS; } i++; USELESS: ; } // 判断拼图奇偶 if (CalcParity(g_puzzle, size, g_row)%2 != (size-1+g_line-1+g_row-1)%2) { // 如果不相等,交换两个非空元素。 int t, t1, t2; if (g_puzzle[0] == 0) { t1 = 1; t2 = 2; } else if (g_puzzle[1] == 0) { t1 = 0; t2 = 2; } else { t1 = 0; t2 = 1; } t = g_puzzle[t1]; g_puzzle[t1] = g_puzzle[t2]; g_puzzle[t2] = t; } } // 显示主菜单 void PrintMainMenu() { Clrscr(); GotoXY(0, 0); printf("1.Begin game!\n"); printf("2.Adjust line(%d) and row(%d)\n", g_line, g_row); printf("3.Quit\n"); } // 显示拼图 void PrintPuzzle() { HANDLE hConsole; COORD coordScreen = {0, 0}; DWORD cCharsWritten; DWORD dwConSize; CONSOLE_SCREEN_BUFFER_INFO csbi; hConsole=GetStdHandle(STD_OUTPUT_HANDLE); GetConsoleScreenBufferInfo(hConsole, &csbi); dwConSize = csbi.dwSize.X * csbi.dwSize.Y; int i, j; for (i=0; i<=g_line; i++) { for (j=0; j<g_row; j++) { coordScreen.Y = i*2; coordScreen.X = j*4 + 1; FillConsoleOutputCharacter(hConsole, '-', 3, coordScreen, &cCharsWritten); } } for (i=0; i<g_line; i++) { for (j=0; j<=g_row; j++) { coordScreen.Y = i*2+1; coordScreen.X = j*4 ; FillConsoleOutputCharacter(hConsole, '|', 1, coordScreen, &cCharsWritten); } } char str[4]; for (i=0; i<g_line*g_row; i++) { coordScreen.Y = i/g_row*2 + 1; coordScreen.X = i%g_row*4 + 1; if (g_puzzle[i] == 0) { sprintf(str, " "); } else sprintf(str, "%3d", g_puzzle[i]); WriteConsoleOutputCharacter(hConsole, str, 3, coordScreen, &cCharsWritten); } GotoXY(0, g_line*2+2); printf("use %c, %c, %c, %c to move item\n", 0x18, 0x19, 0x1a, 0x1b); printf("'Q' back to menu\n"); } // 调整拼图大小 void AdjustLineAndRow() { int line, row; Clrscr(); GotoXY(0, 0); printf("Please input line(2-5):"); scanf("%d", &line); printf("Please input row(2-7):"); scanf("%d", &row); if (line >=2 && line <=5) g_line = line; if (row >=2 && row <=7) g_row = row; } // 移动拼图 void MovePuzzle(int direct) { int i=0; for (; i<g_line*g_row; i++) if (g_puzzle[i] == 0) break; switch (direct) { case 75: // left; if (i%g_row != g_row-1) { g_puzzle[i] = g_puzzle[i+1]; g_puzzle[i+1] = 0; } break; case 77: // right if (i%g_row != 0) { g_puzzle[i] = g_puzzle[i-1]; g_puzzle[i-1] = 0; } break; case 72: // up if (i < g_line*g_row-g_row) { g_puzzle[i] = g_puzzle[i+g_row]; g_puzzle[i+g_row] = 0; } break; case 80: // down if (i >= g_row) { g_puzzle[i] = g_puzzle[i-g_row]; g_puzzle[i-g_row] = 0; } break; } } // 开始拼图游戏 void BeginGame() { Clrscr(); RandomPuzzle(); PrintPuzzle(); while (1) { int c = getch(); switch (c) { case 'q': case 'Q': return; case 0xe0: MovePuzzle(getch()); break; } PrintPuzzle(); if (IsPuzzleFinished()) { printf("Congratulation,you pass the game!\nAny key back to menu"); getch(); return; } } } int main() { char cInput; while (1) { PrintMainMenu(); cInput = getch(); switch (cInput) { case 'Q': case 'q': case '3': Clrscr(); GotoXY(0,0); goto End; case 'B': case 'b': case '1': BeginGame(); break; case 'A': case 'a': case '2': AdjustLineAndRow(); break; } } End: return 0; }