一維陣列建模表示二維的棋盤狀態
當我們想寫一個棋類遊戲的時候,不難發現,很多棋類遊戲的棋盤都可以用一個二維陣列表示,比如:
井字棋(3*3的二維陣列)、黑白棋(8*8的二維陣列)、五子棋(15*15的二維陣列)等等
使用二維陣列表示棋盤,陣列的下標就是棋子的座標,陣列中的值就是棋子的狀態。
好處就是資料訪問比較直觀,可直接根據下標快速找到某個位置的棋子的狀態。
但缺點也是很明顯的
比如:
首先是遍歷棋盤需要用雙重迴圈處理橫座標跟縱座標;
其次是判斷棋子狀態,比如以上所說的三種棋子,需要判斷行、列以及斜線8個方向上的棋子狀態,因為根據行、列和斜線的下標變化特點,加上判斷的演算法不統一,需要用多套不同的方法來處理。
針對這種情況,我來給大家介紹一種方法,正如標題所示,用一維陣列來表示棋盤狀態,那麼用一維陣列該怎樣才能表示一個二維陣列呢?
我們用井字棋做為例子來說:
首先來看看用二維陣列表示井字棋棋盤:
0 1 2
0 x x x
1 x x x
2 x x x
其中X就是代表棋盤的每個位置,0、1、2就是每個位置的座標,比如第二個棋子的座標是(0,1),第六個棋子的座標是(1,2)
假如起點在第一個棋子,即(x = 0,y = 0)這個位置,那向下走就需要執行一次x + 1, y + 0,才能向下走一格。其他方向同理,都需要處理兩個值。
好了,用二維陣列表示的就不多說了,相信大家都知道怎麼做的,下面來講講這次的主題:用一維陣列表示,用一維陣列表示的有兩種方法:
第一種:
0 1 2
3 4 5
6 7 8
直接有多少個棋子就開多大的陣列,例如:井字棋有3*3 = 9個,所以用長度為9的一維陣列表示棋盤,這樣在二維陣列中的第一行編號為0 1 2,第二行編號為3 4 5,第三行編號為6 7 8.。即一維陣列中的6個元素表示二維陣列中的第3行第1個。
這樣雖然損失了資料訪問的直觀性,但它處理資料更加簡潔,只需一層遍歷就能搜尋整個棋盤,不需要關注兩個下標。那它是怎麼向各個方向走的呢?
其實看上面那個圖就不難找到個規律,
向左步進量為1,
向下步進量為3,
向右下步進量為4,
向左下步進量為2,
跟以上各自相反方向的步進量則取負;
這樣我們就可以用一個一維陣列來表示步進量:
int dir[4] = {1,3,4,2};
但是用這種一維陣列儲存方法有一個缺點,就是比較難判斷是否越界了,比如:在3這個位置,減一變成2,在數組裡存在,所以程式會判斷出不越界,但是,減1的操作是向左走一步,即3這個位置再向左走一步就已經越界了。
所以給大家介紹第二種表示方法:
dddd
dxxx
dxxx
dxxx
ddddd
如上表所示,一個井字棋棋盤用長度為21的一維陣列表示,其中所有的d為標誌位,x為棋盤中棋子狀態。看到這個棋盤,有些人會覺得右邊沒有用標誌位d包圍整個棋盤,不是跟前面那種方法一樣不能判斷越界,其實並不會,這樣就已經能保證任意一個棋子位置向8個方向走都能遇到標誌位。我們把字母改為數字來表示就明白了。
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15
16 17 18 19 20
其中一維陣列中下標為0、1、2、3、4、8、12、16、17、18、19、20的都表示標誌位,我們可以用值-1表示,或任何一個不跟棋子狀態一樣的數值表示,剩下的位置都是表示棋盤。
這樣可以看出,
向左步進量為1,
向下步進量為4,
向右下步進量為5,
向左下步進量為3,
跟以上各自相反方向的步進量則取負;
現在我們再來看看越界這個問題,加入13這個位置,向左走一步,即減1操作,變成12,而12這個位置是標誌位,所以越界結束搜尋。
任意一個棋位向各個方向走都能遇到標誌位,大家可以自己算算,這裡就不多說了。
下面,來說說這些下標是怎麼從二維陣列下標中轉換過來的:
以下的x都表示縱向方向,y都表示橫向方向
先說第一種方法:
0 1 2
3 4 5
6 7 8
棋盤有r(3)行c(3)列
第一個位置便是0 * 3 + 0 = 0
第二個位置是0 * 3 + 1 = 1
…
第四個位置是1 * 3 + 0 = 3
…
最後一個是2 * 3 + 2 = 8
按照這個規律不難推出一條公式:p = c * x + y,其中p為一維陣列下標,c為棋盤有多少列,x為二維陣列的x座標,y為二維陣列的y座標。即一維陣列中的p可以表示二維陣列中的(x, y)這個位置。
3*3棋盤各個方向的步進量剛才已經寫出,為{1,3,4,2}
那如果是4 * 4、5*5甚至更多呢?別急,下面來算算就知道了:
4*4棋盤:
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15
5*5棋盤:
0 1 2 3 4
5 6 7 8 9
10 11 12 13 14
15 16 17 18 19
20 21 22 23 24
不難看出4*4的步進量為{1,4,5,3}
5*5的步進量為{1,5,6,4}
看出規律了把?,沒錯,向左右的步進量都不變為1,向下都為列數,向右下都為列數 + 1,向左下都為列數– 1;
所以可以推出一條公式:dir[4] = {1,c,c + 1, c - 1};
接下來是從一維下標推出二維下標,根據p = c * x + y,這條公式,
可以看出
y = p % c
x = (p – y) / c;
第二種方法:
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15
16 17 18 19 20
同樣棋盤有r(3)行c(3)列
棋盤為是5 6 7 9 10 11 13 14 15,那怎麼從二維中轉過來呢?可以看出:
5 = 3 + 2 + 0 + 0 * 4
6 = 3 + 2 + 1 + 0 * 4
…
9 = 3 + 2 + 0 + 1 * 4
…
15 = 3 + 2 + 2 + 2 * 4
同樣可以推出一條規律p = c + 2 + y + x * (c + 1) ,其中p為一維陣列下標,c為棋盤有多少列,x為二維陣列的x座標,y為二維陣列的y座標。即一維陣列中的p可以表示二維陣列中的(x,y)這個位置。
接下來看看步進量問題(大家可以自己畫出來算算):
3 * 3的為{1,4,5,3}
4 * 4的為{1,5,6,4}
5 * 5的為{1,6,7,5}
同樣不難看出其規律,dir[4] = {1, c + 1, c + 2, c},c為棋盤列數
接下來是從一維下標推出二維下標,根據p = c + 2 + y + x * (c + 1),這條公式,
同樣可以看出
y = (p – (c + 2)) % (c + 1),
x = (p – (c + 2) - y) / (c + 1)
好了,今天說的就這麼多,下面來看看我做的一個例子,是我在寫AI五子棋時用到的:
/**
* 設定先手落子的玩家ID
*
* @param playerid 玩家ID
*/
public void InitGameState(int playerid) {
this.m_playerId = playerid;
this.winner = playerid;
this.m_board = new int[15 * 15 + 34 + 15];
this.nullPos = new HashSet<>();
this.whitePos = new HashSet<>();
this.blackPos = new HashSet<>();
this.canPutPos = new ArrayList<>();
for (int i = 0; i < 15; ++i) {
for (int j = 0; j < 15; ++j) {
m_board[17 + i + j * 16] = -1;
}
}
this.len = 0;
this.is = false;
}
我在初始化中把標誌位都設為0,把空的棋盤都設為-1,遊戲中設定黑棋為1,白棋為2
下面以一個判斷是否五子連珠為例子:
//方向步進量
private static int f[] = {1, 16, 17, 15};
/**
* 檢查是否有形成五子連珠
*
* @param dis 下棋位置
* @param playid 玩家id,即白方還是黑方
*/
private void check(int dis, int playid) {
for (int i = 0; i < f.length; ++i) {
int c = 1;
int d = dis + f[i];
//向各個方向步進,直到遇到該位置是標誌位或已經有對方棋子結束
while (m_board[d] == playid) {
c++;
d += f[i];
}
d = dis - f[i];
Log.d("d", d + "");
//向相反方向步進,直到遇到該位置是標誌位或已經有對方棋子結束
while (m_board[d] == playid) {
c++;
d -= f[i];
}
if (c >= 5) {
winner = playid;
Log.d("over", winner + "贏");
is = true;
break;
}
}
}
下面是用一維陣列思路做出的二維矩陣旋轉的演算法(c++):
#include<iostream>
#include<string.h>
using namespace std;
int result[1000];
int main()
{
int n;
while (cin >> n)
{
int num = 1;
int i = 0, j = 0;
memset(result, 0, sizeof(result));
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < n; ++j)
{
result[n + 2 + i + j * (n + 1)] = -1;
}
}
int k = n + 1, f = 1;
int dis[4] = { 1, n + 1, -1, -(n + 1) };
while (num <= n * n)
{
for (int i = 0; i < 4; ++i)
{
while (result[k + dis[i]] == -1)
{
result[k += dis[i]] = num++;
}
}
}
int pos = 0;
for (int i = n + 2; i <= (n + 2) * n; ++i)
{
if (result[i])
{
printf("%5d", result[i]);
pos++;
if (pos % n == 0)
{
cout << endl;
}
}
}
}
return 0;
}
下面附上我寫的AI五子棋