算法:数独专题(1)

    数独是一种传统益智游戏,你需要把9*9的数独补充完整,使得每行、每列、每3*3九宫格内数字1~9均恰好出现一次,编写一个程序,将数独填写完整。

9661c34383a2dd27d353b88f6aaaaafb.png

    数独问题是dfs问题中的典型题,但是根据优化程度的不同,仍然具有许多有趣的地方可以探讨。

例题:数独

请编写一个程序填写数独。

输入格式 输入包含多组测试用例。

每个测试用例占一行,包含 81 个字符,代表数独的 81 个格内数据(顺序总体由上到下,同行由左到右)。

每个字符都是一个数字(1−9)或一个 .(表示尚未填充)。

您可以假设输入中的每个谜题都只有一个解决方案。

文件结尾处为包含单词 end 的单行,表示输入结束。

输出格式 每个测试用例,输出一行数据,代表填充完全后的数独。

输入样例:

4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......
......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3.
end

输出样例:

417369825632158947958724316825437169791586432346912758289643571573291684164875293
416837529982465371735129468571298643293746185864351297647913852359682714128574936

L1 :最简单的数独想法

人们对问题的思考总是由简单到深入,不是吗?

暴力搜索往往是最令人鄙视却又不得不第一个想到的方法。

我们随意地在数独上选择一个可以填写的位置,在数独中依序逐个尝试可以摆放的值,直到所有位置都被摆放完毕。

好,现在我们就来尝试完成这个暴力算法。

首先,需要研究如何才能快速地识别出某一位置可以摆放哪些数字是可以放置的。假如我们知道在这一排、这一列、这一3*3小方块内还剩下什么元素可以放置,然后再取他们的交集,就能得到这个位置的所有合法数字。

e2717b0ff915f5207a95a66c8fe16b04.png

这里,我们使用二进制的整数表示的方法,将一个状态集转换为一个整数:

eg. 5 = 000001001

然后我们就可以定义 row[]、col[]、cell[][]三个数组来记录某位置在某一判定条件上已经存放的情况。

C++中,求交集我们可以使用按位与完成 & ,于是可以定义一个get()函数,它返回一个当前所有可以放置位置的二进制整数表示。

int get(int x, int y)
{
    return row[x] & col[y] & cell[x / 3][y / 3];
}

既然可以随机选取,那么我们直接规定:

  1. 寻找第一个可以填数的位置作为本次填数的位置;

  2. 按照数位从低到高的顺序填数;

为实现数位从低到高填数,我们有一个方便的工具函数可以使用——lowbit();

lowbit算法巧妙地利用了计算机中进行补码运算的性质,作用是巧妙地求出某一数二进制表示中第一位1的位置。

0

1

0

1

0

0

5

4

3

2

1

0

如以上二进制数,其第一位出现1的位置对应为第2数位,经过lowbit()函数返回:

0

0

0

1

0

0

5

4

3

2

1

0

即,除了第一位1以外均为0。

该功能可利用补码的性质和C++中的按位与操作实现:

int lowbit(int x)
{
    return x & -x;
}

如何理解lowbit函数这里不再赘述,感兴趣的同学可以自己写出几个数尝试验证,它利用的是补码的性质。

有了lowbit后,我们就可以方便地求出最低一位可以放置的数,然后将lowbit函数返回值减去,再做lowbit,就可以求出下一位可以放置的数,直到全部放置。

于是整理出DFS的搜索递归结构:

291f6f3204f22c61868ac5c80dc1aa41.png

有了以上分析,就可以写出L1的数独填写代码:

#include <iostream>
#include <algorithm>


using namespace std;


const int N = 9;


int row[N], col[N], cell[3][3];//存放行可用、列可用、块可用
char str[100];


int lowbit(int x)
{
    return x & -x;
}


int get(int x, int y)
{
    return row[x] & col[y] & cell[x / 3][y / 3];
}
void init()
{
    for(int i = 0; i < N; i++) col[i] = row[i] = (1 << N) - 1;
    for(int i = 0; i < 3; i++)
    for(int j = 0; j < 3; j++)
    cell[i][j] = (1 << N) - 1;
}


bool dfs(int cnt)
{
    //cout << cnt;
    if(!cnt) return true;
    int i;
    for(i = 0; str[i] != '.'; i++); // 移动到第一个可以放置位置
    int x, y;
    x = i / 9;
    y = i - 9 * x;
    int state = get(x, y);
    int put;
    for(;state; state -= lowbit(state))
    {
        put = -1;
        int tmp = lowbit(state);
        while(tmp)
        {
            tmp = tmp >> 1;
            put ++;
        }
        row[x] -= 1 << put;
        col[y] -= 1 << put;
        cell[x / 3][y / 3] -= 1 << put;
        str[i] = put + '1';//试图放置,并更新可用性


        if(dfs(cnt - 1)) return true;//进入递归,放置下一个
        //恢复现场
        row[x] += 1 << put;
        col[y] += 1 << put;
        cell[x / 3][y / 3] += 1 << put;
        str[i] = '.';
    }
    return false;
}


int main()
{
    while(cin >> str, str[0] != 'e')
    {
        init();
        int cnt = 0;
        for(int i = 0, k = 0; i < N; i++)
        for(int j = 0; j < N; j++, k++)
        if(str[k] != '.')
        {
            row[i] -= 1 << (str[k] - '1');
            col[j] -= 1 << (str[k] - '1');
            cell[i / 3][j / 3] -= 1 << (str[k] - '1');
        }//将已经填写的部分在col row cell中可用性更新
        else cnt ++;
        dfs(cnt);
        cout << str << endl;
    }    
}

接下来,尝试对以上代码进行优化,首先,回顾常见的DFS优化方法:

① 优化搜索顺序

对于某些题目,不同的搜索顺序会带来完全不同的时间复杂度

② 排除等效冗余

对于某些题目,某些状态之间是等效的,举个不太好的例子,例如A+B=10,A=6和A=4是两者可能是等效的

③ 可行性剪枝

当目前已经不满足题意,应当及时回溯

④ 最优化剪枝

当目前搜索结果已经大于已经搜索出的最优结果,应当及时回溯

⑤ 记忆化搜索

将部分可能复用的搜索结果记录下来,起到不用重复搜索的效果

本题主要可以使用第一点。

L2 :优化后的数独想法

    Ⅰ 我们从优化搜索顺序上入手思考,对于整个数独而言,必然有的位置存在最少的放置方式,甚至某些位置的放置方式已经是确定的,我们将这些位置放在前面搜索,就能收敛搜索树,减少其他位置的状态量。因此,可以将原先的随机放置方式更改为贪心放置方式,从状态最少的地方开始放置。

    Ⅱ 原代码41~47行我们通过不断位移计数来获取放置的数是多少,这里也可以进行优化,优化方法是,将1~9的位移提前完成,打表保存,以后遇到时直接查表优化该部分时间复杂度至O(1)。

    Ⅲ 一些细节优化,对于lowbit和get函数,由于它们极其简单,我们可以使用C++的内联函数规则,将其改写为内联函数,加快调用速度。

#include <iostream>
#include <algorithm>
#include <cstring>


using namespace std;


const int N = 9;


int ones[1 << N], map[1 << N];
//ones数组作用是,给出一数记录其二进制中1的个数,map的作用是给出一lowbit返回值记录其在第k位


int row[N];//表示行可用
int col[N];//表示列可用
int cell[3][3];//表示3*3小九宫格可用
char str[100];//存放数独


inline int get(int x, int y)//内联函数返回可用数目,加快调用速度
{
    return row[x] & col[y] & cell[x / 3][y / 3];
}


inline int lowbit(int x)
{
    return x & -x;
}


bool dfs(int cnt)
{
    if(!cnt) return true;
    int x, y, minv = 10;
    for(int i = 0; i < N; i++)
    for(int j = 0; j < N; j++)
    {
        if(str[i * 9 + j] == '.' && ones[get(i, j)] < minv)
        {
            minv = ones[get(i, j)];
            x = i, y = j;
        }
    }
    int state = get(x, y);
    int put;
    while(state)
    {   
        put = map[lowbit(state)];//放置的值直接通过查表产生
        row[x] -= 1 << put;
        col[y] -= 1 << put;
        cell[x / 3][y / 3] -= 1 << put;
        str[x * 9 + y] = put + '1';
        
        state -= lowbit(state);
        
        if(dfs(cnt - 1))return true;
        //恢复现场
        row[x] += 1 << put;
        col[y] += 1 << put;
        cell[x / 3][y / 3] += 1 << put;
        str[x * 9 + y] = '.';
    }
    return false;
}


void init()
{
    for(int i = 0; i < N; i++)row[i] = col[i] = (1 << N) - 1;
    for(int i = 0; i < 3; i++)
    for(int j = 0; j < 3; j++)
    cell[i][j] = (1 << N) - 1;
}




int main()
{
    for(int i = 0; i < N; i++)map[1 << i] = i; //初始化map
    for(int i = 0; i < 1 << N; i++)
    {
        int cnt = 0, tmp = i;
        {
            while(tmp)
            {
                tmp -= lowbit(tmp);
                cnt ++;
            }
            ones[i] = cnt;
        }
    }// 初始化ones
    while(cin >> str, str[0] != 'e')
    {
        init();
        int cnt;
        cnt = 0;
        for(int i = 0, k = 0; i < N; i++)
        for(int j = 0; j < N; j++, k++)
        if(str[k] != '.')
        {
            row[i] -= 1 << str[k] - '1';
            col[j] -= 1 << str[k] - '1';
            cell[i / 3][j / 3] -= 1 << str[k] - '1';
        }
        else cnt ++;
        //初始化完成
        dfs(cnt);
        cout << str << endl;
    }
}

进行优化后,算法效率有了显著提升,这也是优化搜索顺序能使得dfs速度加快的典型例子。

但对于数独,仍然存在更多可以继续优化之处,数独除了9*9数独,还有16*16数独,各种异形数独等等,它们将在之后继续讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值