1. 程式人生 > >一維陣列建模表示二維的棋盤狀態

一維陣列建模表示二維的棋盤狀態


當我們想寫一個棋類遊戲的時候,不難發現,很多棋類遊戲的棋盤都可以用一個二維陣列表示,比如:

井字棋(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五子棋