警鐘
當被考到輸出一個掃雷矩陣時,才發現自己的程式碼寫得多麼差:忽略邊界條件、死迴圈,考慮不周到,健壯性極差!寫下這篇文章給自己敲個警鐘吧,也對等概率隨機抽取問題做一個總結
掃雷矩陣要求
m * n 的矩陣中有k個雷,輸出一個矩陣滿足:
- 等概率隨機生成地雷
- 非地雷的格子的值是包圍它的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;
}
程式碼中的問題:
- 當 k>m*n 時出現死迴圈
- 生成地雷位置的過程用下面總結,覺得自己是在搞笑
閉著眼睛亂猜整數,直到偶然碰上正確的那個為止”(《程式設計珠璣(續)》,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 還沒出現。
概率問題擴充套件
參考這篇文章概率問題總結,比較全面,面試可能會遇到的概率問題都有
參考連結
本作品採用《CC 協議》,轉載必須註明作者和本文連結