生成一個掃雷矩陣

DonnaDon發表於2020-08-20

警鐘

當被考到輸出一個掃雷矩陣時,才發現自己的程式碼寫得多麼差:忽略邊界條件、死迴圈,考慮不周到,健壯性極差!寫下這篇文章給自己敲個警鐘吧,也對等概率隨機抽取問題做一個總結

掃雷矩陣要求

m * n 的矩陣中有k個雷,輸出一個矩陣滿足:

  1. 等概率隨機生成地雷
  2. 非地雷的格子的值是包圍它的8個格子中含有地雷的總數

一開始的程式碼

被要求用C語言寫生成矩陣的函式,但我竟然忘記了C語言中的函式呼叫和陣列初始化,還把判斷條件寫在了for迴圈中,導致迴圈都進不去,太糟糕了,不好意思貼上來,下面的程式碼是改動後的。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int Count(int i, int j, int **a, int m, int n) {
    int cnt = 0;
    for (int p=i-1;p<=i+1;p++) {
        for (int q=j-1;q<=j+1;q++) {
            if (p < 0 || p >= n || q < 0 || q >= m) {
                continue;
            }
            if (a[p][q] == -1) {
                cnt++;
            }
        }
    }
    return cnt;
}

int** GenerateMatrix(int m, int n, int k, int **a) {
    for (int i=0;i<m;i++) {
        for (int j=0;j<n;j++) {
            a[i][j] = 0;
        }
    }
    srand((unsigned)time(NULL)); 
    while(k > 0) {
        int i, j;
        i = rand() % m;
        j = rand() % n;
        if (a[i][j] == -1) {
            continue;
        }
        a[i][j] = -1;
        k--;
    }
    for (int i=0;i<m;i++) {
        for (int j=0;j<n;j++) {
            if (a[i][j] != -1) {
                a[i][j] = Count(i, j, a, m, n);
            }
        }
    }
    return a;
}

int main(){
    int m, n ,k;
    m = 10;
    n = 9;
    k = 5;
    int **a = (int **)malloc(m*sizeof(int *));
     for(int i=0;i<m;i++){
         a[i] = (int *)malloc(n*sizeof(int));
    }
    a = GenerateMatrix(m,n,k,a);
    for (int i=0;i<m;i++) {
        for (int j=0;j<n;j++) {
            printf("%5d ", a[i][j]);
        }
        printf("\n");
    }
    for (int i=0;i<n;i++){
        free(a[i]);
    }
    free(a);
    return 0;
}

程式碼中的問題:

  1. 當 k>m*n 時出現死迴圈
  2. 生成地雷位置的過程用下面總結,覺得自己是在搞笑

    閉著眼睛亂猜整數,直到偶然碰上正確的那個為止”(《程式設計珠璣(續)》,13.1節)

改善思路

  • 為了避免重複取到已變成雷的位置,需要把已生成的地雷位置移除,可借用洗牌演算法,將二維陣列當成一維陣列看待,每個位置儲存一個矩陣格子的座標,但只抽取k個座標即可,每個座標抽取的概率為1/(m*n)
  • 限制 k 的最大值,當 k > m*n 時返回空陣列

改進後用go寫的

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    m, n, k := 9, 10, 5
    matrix := GenerateMatrix(m, n, k)

    for i, _ := range matrix {
        for j, _ := range matrix[i] {
            fmt.Printf("%5d ", matrix[i][j])
        }
        fmt.Println()
    }
}

/**
*    m: 矩陣行數
*    n: 矩陣列數
*    k: 雷的個數
*    m, n為正整數, k為非負整數
**/
func GenerateMatrix(m, n, k int) [][]int {
    if k > m*n {
        return [][]int{}
    }
    last := m * n
    matrix := make([][]int, m)
    minePos := make([]int, k)
    for k, _ := range matrix {
        matrix[k] = make([]int, n)
    }
    // 初始化矩陣
    for i, _ := range matrix {
        for j, _ := range matrix[i] {
            matrix[i][j] = i*n + j // 把該二維陣列看成一維陣列,矩陣每格儲存一維陣列中的下標,該下標用以計算二維陣列中的橫縱座標,方便後續等概率隨機生成雷
        }
    }
    // 洗牌演算法,利用二維陣列等概率隨機生成雷的位置,減少記憶體開銷
    rand.Seed(time.Now().Unix())
    for k > 0 && last >= 1 {
        pos := rand.Intn(last) // 生成[0, last)的隨機值;注意:Intn函式的引數要大於0
        posi, posj := getPosiAndPosj(pos, m, n)
        lasti, lastj := getPosiAndPosj(last-1, m, n)
        matrix[posi][posj], matrix[lasti][lastj] = matrix[lasti][lastj], matrix[posi][posj]
        last--
        k--
    }
    // 將雷的位置轉移到另一陣列存放,避免將雷的位置標記為-1時覆蓋/丟失其它雷的位置。
    for i := last; i < m*n && k < m*n; i++ {
        posi, posj := getPosiAndPosj(i, m, n)
        minePos[k] = matrix[posi][posj]
        k++
    }
    // 標記矩陣中雷的位置,值置為-1
    for _, v := range minePos {
        posi, posj := getPosiAndPosj(v, m, n)
        matrix[posi][posj] = -1
    }
    // 遍歷矩陣,統計雷的個數
    for i, _ := range matrix {
        for j, _ := range matrix[i] {
            if matrix[i][j] != -1 {
                matrix[i][j] = Count(i, j, matrix)
            }
        }
    }
    return matrix
}

//////////////////////////
// 二維陣列a[i][j]和一維陣列a[p]的座標對應關係, 下標都從0開始
// i = p / 列數
// j = p - i * 列數
////////////////////////

func getPosiAndPosj(pos, m, n int) (posi, posj int) {
    if pos < 0 || pos > m*n {
        fmt.Println("座標轉換出現錯誤!")
        return
    }
    posi = pos / n
    posj = pos - posi*n
    return
}

// 周圍8個格子是雷的個數
func Count(i, j int, matrix [][]int) int {
    cnt := 0
    for p := i - 1; p <= i+1; p++ {
        for q := j - 1; q <= j+1; q++ {
            if p < 0 || p >= len(matrix) || q < 0 || q >= len(matrix[0]) {
                continue
            }
            if matrix[p][q] == -1 {
                cnt++
            }
        }
    }
    return cnt
}

若還有問題,請各位指出來!!謝謝了!!

等概率隨機選取擴充套件

上面矩陣大小由 m*n 固定,雷的個數也不太大,但如果矩陣大小不知或非常大,該如何等概率隨機生成雷的位置呢?
參考Reservoir Sampling - 蓄水池抽樣這篇博文,總結的很好。介紹了 如何從未知大小樣本中隨機抽取 1 個數,如何從未知大小樣本中隨機抽取 k 個數。
在這裡我解釋一下下面這段虛擬碼:從未知大小或非常大的樣本空間中,如何等概率隨機選取 k 個數

Init : a reservoir with the size: k

for i= k+1 to N
    M=random(1, i);
    // 從1到i中隨機選取的數在前k個數中的概率為k/i,即第i個數(大於k)選取概率為k/i
    if( M < k)
        SWAP the Mth value and ith value
end for 

在選取過程中,第 i 個數是否被選取,只和 i 之後的數有關聯,在選擇 i 之前的數時,i 還沒出現。

概率問題擴充套件

參考這篇文章概率問題總結,比較全面,面試可能會遇到的概率問題都有

參考連結

  1. 從未知大小的n個數中取m個數,使各數被取出的概率相等
  2. 等概率無重複的從n個數中選取m個數
  3. Reservoir Sampling - 蓄水池抽樣
  4. 概率問題總結
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章