随机数独局面的生成算法

数独,在9x9的格子填入1到9的数字,识得每行、每列和每个3x3的格子里面,都包含完整不重复的1到9的九个数字。

用于解题的数独,会在随机空出一些格子,解题人需要根据数字相关性,推断空格处应该填写的数字。

一般而言,空的越多难度相对变大,不过空格设置不合理,就可能出现多个解都可行的情况。

 

针对怎么生成一个数独游戏,试验了几种思路:

1)从空白局面开始,每次随机选取一个空格位置,在该位置可行的数字中随机选取一个,填入该空格。

每次填入数字后,排除当前位置所在行、列、3x3格的其它格子,关于这个数字的可能性。

一般生成了二十几个数字后,看起来挺像一个数独求解局面了,不过这样生成的数独,基本上不可解,总会出现冲突。

 

2)从空白局面开始,先填充左上的3x3格,然后填充中上的3x3格,然后右上的3x3格,如此依次填入9个3x3格。

每次随机选择一个当前位置可能的数字进行填充,同时同时排除当前位置所在行、列、3x3格的其它格子,关于这个数字的可能性。

这样看起来可行的办法,在生成第二个到第三个3x3格时候,几乎总是会出现冲突,使得有的位置不能填入任何数字。

 

看起来从空白局面开始,简单的随机填入数字的做法,基本不可行。

 

那么就反过来,从完整局面开始,进行挖孔,这样就肯定存在解,只是不能确定是否是唯一解。

 

3)从空白局面开始,将某个数字依次填入第一行到第九行的随机可行位置,然后选择下一个数字,做同样的操作,直至所有数字都填完。

如果数字填到某一行没有可行的列位置,则回溯到上一行,将上一行的数字移入下一可行的列位置;

如果上一行的数字,没有下一可行的列位置,则继续向上回溯。

 


针对3)的思路,生成完整局面成功,下面就针对这个思路进行程序分析。

以下是示意图,其中标记为X的格子,表示前面的数字已经占据了该位置。

 

-1
   +-------+-------+-------+
 0 |       |     1 |       |
 1 |     1 |       |       |
 2 |       |       |     1 |
   +-------+-------+-------+
 3 |   1   |       |       |
 4 |       | 1     |       |
 5 |       |       | 1     |
   +-------+-------+-------+
 6 |       |       |   1   |
 7 |       |   1   |       |
 8 | 1     |       |       |
   +-------+-------+-------+
   +-------+-------+-------+
 9 |       | 2   X |       |
10 |   2 X |       |       |
11 |       |       | 2   X |
   +-------+-------+-------+
12 |   X   |       |     2 |
13 |       | X 2   |       |
14 |     2 |       | X     |
   +-------+-------+-------+
...
   +-------+-------+-------+
78 | X X X | X X X | 9 X X |
79 | X X X | X X 9 | X X X |
80 | X 9 X | X X X | X X X |
   +-------+-------+-------+
81

 

 

一般的空白局面是9x9的局面,为方便程序的行逻辑,这里把空白局面抽象为81行,9列。

这样,第0~8行填入1,第9~17行填入2,依次类推。行数和要填的数字有唯一对应关系。

注:对于第0~8行,可以类比为8皇后问题,在9行9列中,放入9个1,使得每行每列都有一个1。

 

从数据结构上面来说,分成两个层面来考虑:

1)对于每个数字行(第0~80行)而言,需要存储当前行数字可能放的列;

为了回溯的需要,需要存放当前从哪列开始放置,以及当前放置在哪个列。

2)对于数独局面而言,需要存储当前搜索到了哪个数字的哪一行;以及每一个数字行的信息。

 

根据BIT位定义每一列,是否填充了数字;方便对不同行进行位或操作,计算列是否已经填充。

// col mask
// +-------+-------+-------+
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// +-------+-------+-------+
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// +-------+-------+-------+
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// | 1 2 3 | 4 5 6 | 7 8 9 |
// +-------+-------+-------+
enum
{
    COL_MASK_NONE = 0x000, // 0 0000 0000
    COL_MASK_1 = 0x001,    // 0 0000 0001
    COL_MASK_2 = 0x002,    // 0 0000 0010
    COL_MASK_3 = 0x004,    // 0 0000 0100
    COL_MASK_4 = 0x008,    // 0 0000 1000
    COL_MASK_5 = 0x010,    // 0 0001 0000
    COL_MASK_6 = 0x020,    // 0 0010 0000
    COL_MASK_7 = 0x040,    // 0 0100 0000
    COL_MASK_8 = 0x080,    // 0 1000 0000
    COL_MASK_9 = 0x100,    // 1 0000 0000
    COL_MASK_123 = 0x007,  // 0 0000 0111
    COL_MASK_456 = 0x038,  // 0 0011 1000
    COL_MASK_789 = 0x1C0,  // 1 1100 0000
    COL_MASK_ALL = 0x1FF   // 1 1111 1111
};

定义某一行的数据结构,base_col和curr_col记录当前行的起始搜索列和实际存储列;

注意:col_mask记录了根据前面所有行的数据,推算出当前行实际可行的位置,方便对当前行的搜索。

typedef struct
{
    int base_col; // start col 0~8:valid, -1:invalid
    int curr_col; // current col 0~8:valid, -1:invalid
    unsigned int col_mask;
} col_index_t;

sodoku_t记录一个完整数独局面,long_row记录当前搜索到第几行;col_index[81]记录每一行实际搜索状态。

typedef struct{
    int long_row; // long_row 0~80:valid, -1:init, 81:finish
    col_index_t col_index[81]; // col_index[row]
} sodoku_t;

 

 

从算法而言,整体是一个回溯算法:

1)初始化数据结构。

2)设置long_row进入第一行,设置一个随机开始列base_col和cur_col。

3)如果没有到达最后一行,则继续搜索。

4)从当前搜索列cur_col开始,查找可放置的列位置,找到则放置在该位置(并更新cur_col),并转入下一行的搜索。

5)如果没有找到,则回溯到上一行。

6)全部数字放置完毕后,结束循环。

 

注:针对伪随机数的特征,我们可以设置随机数种子srand(0)、srand(1)、srand(2)等等,则可以得到伪随机序列数字所对应的局面。

int main(int argc, const char * argv[])
{
    sodoku_t sodoku;
    
    srand((unsigned int)time((time_t *)NULL));
    
    init_sodoku(&sodoku);
    goto_next_row(&sodoku);

    while(sodoku_not_finish(&sodoku))
    {
        if(goto_next_col(&sodoku))
        {
            goto_next_row(&sodoku);
        }
        else
        {
            rollback_row(&sodoku);
        }
    }
    
    print_sodoku(&sodoku);

    return 0;
}


下面依次解释循环中的代码:

 

1)初始化局面,设置当前行为起始状态。

 

void init_sodoku(sodoku_t * sodoku)
{
    do
    {
        if(sodoku == NULL)
            break;

        memset(sodoku, 0, sizeof(sodoku_t));
        
        sodoku->long_row = -1; // init invalid long_row
        
    } while(false);
}

 

2)判定有没有搜索完成

bool_t sodoku_not_finish(sodoku_t *sodoku)
{
    bool_t not_finish = false;
    do
    {
        if(sodoku == NULL)
            break;
        
        if(sodoku->long_row <= 80)
        {
            not_finish = true;
        }
        
    } while(false);
    
    return not_finish;
}

 

 

 

3)进入下一行和回退到上一行

 

void goto_next_row(sodoku_t *sodoku)
{
    do
    {
        if(sodoku == NULL)
            break;
        
        sodoku->long_row++;
        if(sodoku->long_row > 80)
            break;
        
        /* init col index */
        sodoku->col_index[sodoku->long_row].base_col = calc_rand_col(); // random col
        sodoku->col_index[sodoku->long_row].curr_col = -1; // invalid col
        
        /* init col mask */
        calc_col_mask_row(sodoku, sodoku->long_row);

    } while(false);
}

void rollback_row(sodoku_t *sodoku)
{
    do
    {
        if(sodoku == NULL)
            break;
        
        sodoku->long_row--;

        /* init col mask */
        calc_col_mask_row(sodoku, sodoku->long_row);
        
    } while(false);
}

 

 

4)辅助设置函数,对于col_mask是关于当前行可以放哪些列,根据规则,不能与与相同数字占据了相同行、列和3x3格。

 

int calc_rand_col(void)
{
    int col;
    col = rand() % 9; // col 0~8
    return col;
}

int calc_next_col(int col)
{
    return (col + 1) % 9; // col 012345678 => 123456780
}

void calc_col_mask_row(sodoku_t *sodoku, int long_row)
{
    int row;
    unsigned int mask = COL_MASK_NONE;
    
    // clear cell that already fill by other digit(in the same row)
    for(row = long_row % 9; row < long_row; row+=9)
    {
        mask |= sodoku->col_index[row].col_mask;
    }

    // clear cell that already fill by same digit(in the same col)
    for(row = long_row - long_row % 9; row < long_row; row++)
    {
        mask |= sodoku->col_index[row].col_mask;
    }
    
    // clear cell that already fill by same digit(in the same 3x3box)
    for(row = long_row - long_row % 3; row < long_row; row++)
    {
        if(sodoku->col_index[row].col_mask & COL_MASK_123)
            mask |= COL_MASK_123;
        else if(sodoku->col_index[row].col_mask & COL_MASK_456)
            mask |= COL_MASK_456;
        else // if(sodoku->col_index[row].col_mask & COL_MASK_789)
            mask |= COL_MASK_789;
    }
    
    sodoku->col_index[long_row].col_mask = ~mask; // store cols that can place
}


5)选择下一列,也就是从当前列循环向右搜索,直到起始列结束,找到可行的列,则

bool_t goto_next_col(sodoku_t *sodoku)
{
    bool_t success = false;
    int base_col;
    int curr_col;
    int next_col;
    bool_t found = false;

    do
    {
        if(sodoku == NULL)
            break;
        
        base_col = sodoku->col_index[sodoku->long_row].base_col;
        curr_col = sodoku->col_index[sodoku->long_row].curr_col;
        
        found = false;
        
        if(curr_col == -1)
        {
            // find first
            next_col = base_col;
            
            // base_col is valid
            if(sodoku->col_index[sodoku->long_row].col_mask & (1 << base_col))
            {
                found = true;

                next_col = base_col;
            }
            else
            {
                next_col = calc_next_col(base_col);
            }
        }
        else
        {
            next_col = calc_next_col(curr_col);
        }
 
        if(!found)
        {
            // find next
            while(next_col != base_col)
            {
                // next_col is valid
                if(sodoku->col_index[sodoku->long_row].col_mask & (1 << next_col))
                {
                    found = true;
                    break;
                }
                
                // find next
                next_col = calc_next_col(next_col);
            }
        }
        
        if(!found)
            break;

        // set next_col
        sodoku->col_index[sodoku->long_row].curr_col = next_col;
        sodoku->col_index[sodoku->long_row].col_mask = (1 << next_col); // store selected col

        success = true;
    } while(false);
    
    return success;
}


6)最后是打印出来当前局面:

void print_sodoku(sodoku_t *sodoku)
{
    int long_row;
    int row;
    int col;
    int digit;
    int cell[81] = { 0 };
    do
    {
        if(sodoku == NULL)
            break;
        
        for(long_row = 0; long_row < sodoku->long_row; long_row++)
        {
            row = long_row % 9;
            col = sodoku->col_index[long_row].curr_col;
            digit = long_row / 9 + 1;
            
            cell[row * 9 + col] = digit;
        }
        
        printf("Sodoku %d\n", sodoku->long_row);
        
        for(row = 0; row < 9; row++)
        {
            if(row % 3 == 0)
                printf("+-----+-----+-----+\n");
            printf("|");
            for(col = 0; col < 9; col++)
            {
                printf("%d", cell[row * 9 + col]);
                if(col % 3 == 2)
                    printf("|");
                else
                    printf(" ");
            }
            printf("\n");
        }
        printf("+-----+-----+-----+\n");
        
    } while(false);
}


随机生成的一个局面:

 

Sodoku 81
+-----+-----+-----+
|2 1 6|5 9 7|3 4 8|
|9 3 4|8 1 2|5 6 7|
|5 8 7|3 4 6|1 2 9|
+-----+-----+-----+
|3 7 1|4 5 8|6 9 2|
|6 9 5|7 2 3|8 1 4|
|4 2 8|1 6 9|7 3 5|
+-----+-----+-----+
|7 4 2|6 8 1|9 5 3|
|8 6 9|2 3 5|4 7 1|
|1 5 3|9 7 4|2 8 6|
+-----+-----+-----+

完整代码: https://github.com/baojian256/sodoku

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值