随机生长算法没有按预期工作

编程语言 2026-07-10

在我正在做的一个游戏里,我先用一个8×8的整型数组,初始值全部为0。然后我在每一行/每一列随机放置一个非零值,要求每行只有一个非零值、每列也只有一个非零值。就像这样:

0 1 0 0 0 0 0 0  
0 0 0 0 2 0 0 0  
0 0 0 0 0 0 3 0  
4 0 0 0 0 0 0 0  
0 0 5 0 0 0 0 0  
0 0 0 0 0 0 0 6  
0 0 0 0 0 7 0 0  
0 0 0 8 0 0 0 0

这一部分工作得很好。每次生成都会得到一个不一样的网格。

但接下来我想把每个数字随机地向周围的0 扩展,这样就会得到8 个区域,每个区域由一个数字填充。下面是我得到的一个例子:

4 1 1 5 2 2 2 3  
4 1 1 5 2 2 3 3  
4 1 5 5 2 2 3 3  
4 4 5 5 5 2 3 6  
4 4 5 5 5 2 2 6  
4 4 5 5 5 2 2 6  
4 4 5 8 5 7 2 6  
4 4 4 8 8 7 2 6

请注意,虽然数字1 可以向下扩展,数字8 可以向上扩展,但位于最右边最远列的数字6 从未离开那一列。以及位于左上第一列的数字4,从来不让其他数字进入那一列。这是我用来进行扩展的函数。我会记录有多少个0,然后重复调用这个函数直到它们全部被填满:

在这种情况下,size 是8,而 color 数组在每个维度上从0 增长到9,尽管我实际只使用1 到8。之所以这样做,是为了在查看要检查的相邻单元格时,不需要使用条件判断。

private void ExpandColors()
{
    //1 is up, 2 is right, 3 is down, 4 is left
    int cell = rand.Next(1, size * size);

    string coord = CellToCoords(cell);
    string[] c = coord.Split('ÿ');
    int row = Convert.ToInt16(c[0]);
    int col = Convert.ToInt16(c[1]);

    if (color[row, col] > 0)
    {
        switch (rand.Next(1, 4))
        {
            case 1:
                if (row == 1) return; //can't look up
                if (color[row - 1, col] == 0)
                {
                    color[row - 1, col] = color[row, col];
                    uncolored--;
                    return;
                }
                break;
            case 2:
                if (col == size) return; //can't look right
                if (color[row, col + 1] == 0)
                {
                    color[row, col + 1] = color[row, col];
                    uncolored--;
                    return;
                }
                break;
            case 3:
                if (row == size) return; //can't look down
                if (color[row + 1, col] == 0)
                {
                    color[row + 1, col] = color[row, col];
                    uncolored--;
                    return;
                }
                break;
            case 4:
                if (col == 1) return; //can't look left
                if (color[row, col - 1] == 0)
                {
                    color[row, col - 1] = color[row, col];
                    uncolored--;
                    return;
                }
                break;
        }
    }
}

private string CellToCoords(int cell)
{
    int col = cell % size;
    if (col == 0) col = size;
    int row = (cell - col) / size + 1;
    return row.ToString() + "ÿ" + col.ToString();
    // so 2, 5 would be 2ÿ5 and can easily be split
}

我已经仔细检查过 CellToCoords 能正确工作。并且在 ExtendColors 上似乎没有任何功能差异会让水平与垂直之间产生差别。然而我仍然没有遇到顶部和底部行的数值正常扩展的问题,反而在最左列和最右列的数值扩展上确实存在问题。我已经运行了几十次,左列和右列同样的异常行为每次都出现。

难道我漏掉了什么明显的地方?

解决方案

这个问题已经在评论中得到解答。rand.Next 使用的是开区间上界。也就是说 rand.Next(1, 4) 只会返回1、2、3,永远不会返回4。请使用 rand.Next(1,5)rand.Next(0,4) 来生成4 个随机值。

但我也想提出一种对更大棋盘更实用的替代算法。你当前的方法在更大棋盘上会很慢,因为随着坐标被填充,随机得到未填充坐标的概率会下降。

其核心思路是维护一个与某个数值相邻的坐标列表。然后你可以从这个列表中迭代地选取值来填充。对于大型棋盘来说,即使需要额外花点时间来维护这个列表,这样会快得多。大致可以这样写:

public readonly record struct CoordinateAndValue(Vec2i Pos, int Value);

public readonly record struct Vec2i(int X, int Y)
{
    public static Vec2i operator +(in Vec2i l, in Vec2i r) => new(l.X + r.X, l.Y + r.Y);
};
public static void ExpandColors(int[,] board)
{
    var offsets = new Vec2i[]
    {
        new(-1, 0),
        new(1, 0),
        new(0, 1),
        new(0, -1),
    };
    var width = board.GetLength(0);
    var height = board.GetLength(1);

    var candidateSet = new HashSet<CoordinateAndValue>();

    // local function that adds all adjacent coordinates that are currently zero to the candidate set
    void AddAdjacent(Vec2i pos, int value)
    {
        foreach (var o in offsets)
        {
            var p = pos + o;
            if (p.X >= 0 && p.Y >= 0 && p.X < width && p.Y < height &&
                board[p.X, p.Y] == 0)
            {
                candidateSet.Add(new CoordinateAndValue(p, value));
            }
        }
    }
    // Fill the candidate set from the initial board state
    for (int x = 0; x < width; x++)
    {
        for (int y = 0; y < height; y++)
        {
            var pos = new Vec2i(x, y);
            var boardValue = board[x, y];
            if (boardValue != 0)
            {
                AddAdjacent(pos, boardValue);
            }
        }
    }

    // Start filling random coordinates from the candidate set
    var rand = new Random(42);
    while (candidateSet.Count > 0)
    {
        var index = rand.Next(0, candidateSet.Count);
        var c = candidateSet.ElementAt(index);
        candidateSet.Remove(c);
        var boardValue = board[c.Pos.X, c.Pos.Y];
        // We need to check if the value is zero again,
        // since the coordinate could have been filled by another value
        if (boardValue == 0)
        {
            board[c.Pos.X, c.Pos.Y] = c.Value;
            AddAdjacent(c.Pos, c.Value);
        }
    }
}

请注意 HashSet 可能并不理想,因为 candidateSet.ElementAt 有点慢。理想情况下你需要一个既具集合语义,又有快速索引的集合。但我会省略关于数据结构选择的深入讨论。

我也对这个思路做了一些快速测试,但问题仍可能存在。代码仅作为表达一般思路的示例。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章