寫代碼的時候應該怎樣的思路將現實世界轉化為代碼, 魔方做一個例子?

昨晚轉了下魔方, 就在想我要是能在計算機上自己編程一個魔方就好了.

但想想這在我的極限以外, 就算給了我繪製 3D 圖形的 API, 我依然搞不定,

中間最重要的問題就是, 現實的物體, 怎樣將其轉化為代碼的結構?

代碼應該是怎樣一個模型去建構世界呢, 我們又應該怎樣思考?


魔方的狀態表示 。

基礎

魔方特指三階魔方(3*3*3),由 8 個角塊、12 個棱塊和 6 個中心塊組成。中心塊的位置固定在十字架上。每個角塊 3 面有顏色,每個棱塊 2 面有顏色,每個中心塊 1 面有顏色。

官方配色白黃相對、紅橘相對、藍綠相對,且藍、橘、黃三色以順時鐘排列。

F、B、L、R、U、D 分別代表前、後、左、右、上、下層。

若是順時針旋轉 90°,則直接寫上符號;若是逆時針旋轉,則在符號後加上「"」或是「i」;若是旋轉180°,則在符號後加上「2」或是「2」。

狀態表示

1.色片

一個還原好的魔方可以看做由 3*3*6 個色片組成,編號如下。

每一次轉動會將這些色片的位置重排。稱為置換。

做一次 F ,將得到:

僅觀察黃色面上色片。色片的置換表達有下面兩種方式:

F1 被移動到了 F3 (F1-?F3),

F2-?F6, F3-?F9, F4-?F2, F5-?F5, F6-?F8, F7-?F1, F8-?F4, F9-?F7.

F1 被替換為了 F7 (F1?-F7),

F2?-F4, F3?-F1, F4?-F8, F5?-F5, F6?-F2, F7?-F9, F8?-F6, F9?-F3.

這些表的第一行是確定的,忽略,記為:

(F3,F6,F9,F2,F5,F8,F1,F4,F7) 在「被移動到」表示方式下,或

(F7,F4,F1,F8,F5,F2,F9,F6,F3) 在「被替換為」表示方式下。

F面周圍一圈的 12 個色片同理。

兩個置換的積。

在第一次置換中,F1-?F2,接著在第二次置換中,F2-?F6。

在兩個置換的積置換中,F1-?F6。

通常情況下,兩個置換是不能相互交換先後順序的。

考慮轉動 F 以及 F",積為

也就是:F * F" = 1,這兩個互為逆置換。

並不只有轉動可以看作置換,任何打亂的魔方都可以寫成一個置換。

U1-?R3, U2-?L2, U3-?D3, U4-?U8,...

這個魔方可以表示為 (R3,L2,D3,U8,...).

色片表示的方法不適合快速計算。

2.色塊

在色塊層,置換的對象不是色片,而是 12 個棱塊和 8 個角塊。

這種表示方法跟現實中的魔方結構最接近。

角塊用 URF, UFL, ULB, UBR, DFR, DLF, DBL 和 DRB 來命名。

棱塊用 UR, UF, UL, UB, DR ,DF, DL, DB, FR, FL, BL 和 BR 來命名。

例如上圖中標出了:URF 角,DFR 角,FL 棱以及 UL 棱。

角塊可以在原地扭轉,棱塊也可以翻轉過來。

這些色塊在他們的原位,但是方向改變了。UFL 角被順時針扭轉了,UBR 角被逆時針扭轉了,DF 棱和 FR 棱被翻轉了。

對於二階段演算法,定義參考方向:還原的魔方上標出來的色片作為參考方向。

一次 F:

位於 URF 和 DLF 的角塊被順時針扭轉了,位於 UFL 和 DFR 的角塊被逆時針扭轉了,位於UF, DF, FL 和 FR 位置的棱塊被翻轉了。

使用 "被替換為" 的表示方式來記錄位置置換,對於上面例子:

URF 被替換為 UFL(URF?-UFL),

UFL?-DLF, ULB ?-ULB, UBR?-UBR, DFR?-URF, DLF?-DFR, DBL ?-DBL, DRB?-DRB.

這是角塊的位置置換。

再包括方向,

F =

角塊使用 "0" 表示方向沒有變化,用 "1" 表示順時針的原地扭轉,"2" 表示逆時針的原地扭轉。

棱塊使用 "1" 表示棱塊被翻轉,"0" 表示沒有被翻轉。

當然 F 也可以記成:

F(URF).c = UFL

F(URF).o = 1

F(UFL).c = DLF

F(UFL).o = 2

……

再做另一個轉動

R =

F*R 作用在UFL角上

(F*R)(UBR).c = F(R(UBR).c).c = UFL

(F*R)(UBR).o=F(R(UBR).c).o+R(UBR).o = 2

3.坐標

用自然數來表示角塊和棱塊的位置與方向。適合實現解魔方的快速演算法。

角塊方向坐標的定義。

一次R轉動,用「被代替為」表示

忽略 DRB 角塊的方向,因為這一方向是由其他 7 個角塊決定的:所有角塊方向的和必須被 3 整除。

8 個角塊的方向可以用 0 到 2186 (3^7 - 1) 的一個整數表示。

計算

2*3^6 + 0*3^5 + 0*3^4 + 1*3^3 +1*3^2 + 0*3^1 + 0*3^0 = 1494

其實就是三進位中的數字2001100。

棱塊方向也可以用類似的方式來定義。

12 個棱塊的方向由一個 0 到 2047 (2^11 - 1) 的數字表示。

這裡用二進位來代替三進位。同樣忽略了 BR 棱的方向,因為它是可以由其他 11 個棱決定的:所有棱塊的方向和必須為偶數。

角塊位置可以由一個 0 到 40319 (8! - 1) 中的一個整數表示。

R轉動,忽略角塊的方向。

對角塊定義一個大小順序 URF&對於第二行的某個塊 XXX,第三行對應的數字表示在 XXX 左邊有多少塊比 XXX 更大。

比如上面的 UBR 塊的數字是 4 。

考慮所有在 UBR 左邊的 7 個角塊,其中有 4 個角塊比 UBR 更大: DFR, DLF, DBL, DRB。

同樣上面的 DLF 塊的數字是 1 。

在 DLF 左邊的 5 個角塊中總共有 1 個角塊比 DLF 更大:DRB。

根據第三行的數字來生成位置坐標。

1*1! + 1*2! + 3*3! + 0*4! + 1*5! + 1*6! + 4*7! = 21021

棱塊的位置可以類似的用一個 0 到 12! - 1 的數字表示。

綜上,可以用一組坐標 (x1,x2,x3,x4) 來表示一個魔方。

代碼表示

typedef enum {URF,UFL,ULB,UBR,DFR,DLF,DBL,DRB} Corner;
//8個角塊

typedef enum {UR,UF,UL,UB,DR,DF,DL,DB,FR,FL,BL,BR} Edge;
//12個棱塊

typedef enum
{ U1,U2,U3,U4,U5,U6,U7,U8,U9,R1,R2,R3,R4,R5,R6,R7,R8,R9,
F1,F2,F3,F4,F5,F6,F7,F8,F9,D1,D2,D3,D4,D5,D6,D7,D8,D9,
L1,L2,L3,L4,L5,L6,L7,L8,L9,B1,B2,B3,B4,B5,B6,B7,B8,B9} Facelet;
//54個面
typedef enum {mU1,mU3,mR1,mR3,mF1,mF3,mD1,mD3,mL1,mL3,mB1,mB3} Move;
//12種90度轉動

typedef enum {U,R,F,D,L,B} Axis;
//6種顏色或轉動

typedef struct faceletCube
{
Color f[54];
}FaceletCube;
//色片法表示魔方

struct corner_o
{ Corner c;
char o;
};
//帶方向的角塊位置

struct edge_o
{ Edge e;
char o;
};
//帶方向的棱塊位置

typedef struct cubieCube
{
struct corner_o co[8];
struct edge_o eo[12];
}CubieCube;
//色塊法表示魔方

typedef struct coordCube
{
int x1;
int x2;
int x3;
int x4;
//由一定方法計算得出
}CoordCube;
//坐標法表示魔方

CubieCube faceletCubeToCubieCube(FaceletCube fc);
FaceletCube cubieCubeToFaceletCube(CubieCube cc);
CoordCube cubieCubeToCoordCube(CubieCube cc);
//不同表示方法轉換

CubieCube cubeAxMove(CubieCube cc, Axis ax);
//對色塊法表示的魔方做一次(U,R,F,D,L,B)轉動

解魔方演算法

人工:層先,CFOP,橋式……

機器:降群,二階段演算法……

可以用計算機模擬人類還原步驟,也可以計算出步數更短的高效轉法。

演算法實現

交互界面

命令行

二維圖形(C++/Qt)

三維圖形(MFC/OpenGL)

參考:

Rubik"s Cube group

http://kociemba.org/cube.htm

optiqtmSrc(C)

CubeExplorer2.25(Delphi)

CubeExercise1.2.401(C#)


把現實世界中的物品轉化成代碼,主要是抽象行為。

考慮下用以下步驟實現抽象(建模):

1)確定你的問題,和與之相關的對象

題目很明確,創建一個魔方,可以生成謎題和進行解密(這裡假定並未任何顯示需求)。

2)確定你的對象的行為,你這個對象所需要承擔的職責,決定了它所必須的屬性

對這個題目而言,一個抽象的魔方,只需要能進行3軸的旋轉,各個面有不同的識別顏色就可以了。

可以用偽代碼確認一下介面,簡單如下:

rotateX(int idx) // 繞X軸旋轉,idx表示第幾行/列

rotateY(int idx)

rotateZ(int idx)

getColor(int faceIdx, int blockIdx) //獲取某面某塊的顏色

3)分析行為,得出必須的(必要的)屬性

可以判斷,rotate 行為本身只需要對 顏色的位置進行修改而已,而 getColor 也只是定義了返回顏色而已。可以得出結論:實現上述行為的最少屬性,應該是 6*9 個顏色值(數字0~5)而已。

注意:屬性的抽取永遠取決於你所需要實現的行為。比如說你需要3d顯示它,那麼可能還需要 大小, 三維角度,坐標 之類的屬性。如果你需要加入物理特徵,那麼它可能還有 質量,摩擦力 之類的屬性。但是一個僅僅可以顯示謎題和進行解密的魔方是不需要這些特徵的。

4)設計最合適需求的數據結構和演算法

我們可以用無數種方式來滿足 「6*9 個顏色值」 這個條件,比如一個 int[54]

但這個數據結構顯然會讓 rotate 行為的實現代碼顯得冗長並且低效,或者說,不優雅。

好一點的方式是用 二維或者三維數組來維護我們的數據。

更好的方式,則需要花更多的時間去思考了 : )


推薦閱讀:

用程序實現自動寫小說,難度有多大?有哪些需要克服的難點,大體思路是怎樣的?
獲得ACM ICPC Regional金牌是一種什麼樣的體驗?
如何看待杭電舉辦的acm女生專場?
如何理解 Tarjan 的 LCA 演算法?
二分查找有幾種寫法?它們的區別是什麼?

TAG:演算法 | 編程 | 面向對象編程 | 魔方 |