算法:数独专题(2)

靶形数独

靶形数独与普通数独的区别在于,它具有权重,我们需要求出某种意义上的权重最大值。

直接上例题:

小城和小华都是热爱数学的好学生,最近,他们不约而同地迷上了数独游戏,好胜的他们想用数独来一比高低。

但普通的数独对他们来说都过于简单了,于是他们向 Z 博士请教,Z 博士拿出了他最近发明的“靶形数独”,作为这两个孩子比试的题目。

靶形数独的方格同普通数独一样,在 9×9 的大九宫格中有 9 个 3×3 的小九宫格(用粗黑色线隔开的)。

在这个大九宫格中,有一些数字是已知的,根据这些数字,利用逻辑推理,在其他的空格上填入 1 到 9 的数字。

每个数字在每个小九宫格内不能重复出现,每个数字在每行、每列也不能重复出现。

但靶形数独有一点和普通数独不同,即每一个方格都有一个分值,而且如同一个靶子一样,离中心越近则分值越高(如下图所示)。

2bb7af889051e277d91cf5fbefd85055.png

上图具体的分值分布是:最里面一格(黄色区域)为 10 分,黄色区域外面的一圈(红色区域)每个格子为 9 分,再外面一圈(蓝色区域)每个格子为 8 分,蓝色区域外面一圈(棕色区域)每个格子为 7 分,最外面一圈(白色区域)每个格子为 6 分,如上图所示。

比赛的要求是:每个人必须完成一个给定的数独(每个给定数独可能有不同的填法),而且要争取更高的总分数。

而这个总分数即每个方格上的分值和完成这个数独时填在相应格上的数字的乘积的总和。

如图,在以下的这个已经填完数字的靶形数独游戏中,总分数为 2829。

游戏规定,将以总分数的高低决出胜负。

3b673f222ba9d61f7856e940f326a8d3.png

由于求胜心切,小城找到了善于编程的你,让你帮他求出,对于给定的靶形数独,能够得到的最高分数。

输入格式 输入一共包含 9 行。

每行 9 个整数(每个数都在 0—9 的范围内),表示一个尚未填满的数独方格,未填的空格用 0 表示。

每两个数字之间用一个空格隔开。

输出格式 输出可以得到的靶形数独的最高分数。

如果这个数独无解,则输出整数 −1。

数据范围 40% 的数据,数独中非 0 数的个数不少于 30。

80% 的数据,数独中非 0 数的个数不少于 26。

100% 的数据,数独中非 0 数的个数不少于 24。

输入样例:

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

输出样例:

2829

解决这一题的关键在于,不要被复杂的外表所迷惑,本题和上一节中发表过的数独问题其实是同父异母的亲兄弟。

我们在上一题中,能找到数独的一组解,在这个问题中,我们就可以通过减少一个递归出口

(if(dfs(cnt - 1)) return true;
-->
dfs(cnt - 1)

使得dfs遍历可以完全进行,从而找到数独的所有解,并比较最大值。

如何计算分数值?

为了方便书写代码,我在这里,将整个数独划分为5个有重叠的区域:

af27f8fbd72b2db74eed02e7b16c805d.png

总分值就等于:

绿框内所有元素 * 6+ 粉框内所有元素 + 紫框内所有元素 + 黄框内所有元素 +橙框元素

于是代码表现为:

int compute()
{
    int sum = 0;
    for(int i = 0; i < N; i++)
    for(int j = 0; j < N; j++)
    sum += 6 * sudu[i][j];


    for(int k = 1; k < 5; k++)
    for(int i = k; i < N - k; i++)
    for(int j = k; j < N - k; j++)
    sum += sudu[i][j];


    return sum;
}

其余代码与上一题就是大同小异啦~

#include <iostream>
#include <algorithm>


using namespace std;


const int N = 9;
int ones[1 << N], map[1 << N];
int row[N], col[N], cell[3][3];
int sudu[N][N];
int res;


int compute()
{
    int sum = 0;
    for(int i = 0; i < N; i++)
    for(int j = 0; j < N; j++)
    sum += 6 * sudu[i][j];


    for(int k = 1; k < 5; k++)
    for(int i = k; i < N - k; i++)
    for(int j = k; j < N - k; j++)
    sum += sudu[i][j];


    return sum;
}


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;
}


void print()
{
    for(int i = 0; i < N; i++, cout << endl)
    for(int j = 0; j < N; j++)
    cout << sudu[i][j] << ' ';
}
void dfs(int cnt)
{
    if(!cnt)
    {
        int now = compute();
        print();
        if(now > res)res = now;
        return;
    }


    int minv = 10, x, y;
    for(int i = 0; i < N; i++)
    for(int j = 0; j < N; j++)
    {
        if(sudu[i][j]) continue;
        int t = ones[get(i, j)];
        if(t < minv)
        {
            minv = t;
            x = i, y = j;
        }
    }
    for(int state = get(x, y); state; state -= lowbit(state))
    {
        int put = map[lowbit(state)];
        row[x] -= 1 << put;
        col[y] -= 1 << put;
        cell[x / 3][y / 3] -= 1 << put;
        sudu[x][y] = put + 1;


        dfs(cnt - 1);


        row[x] += 1 << put;
        col[y] += 1 << put;
        cell[x / 3][y / 3] += 1 << put;
        sudu[x][y] = 0;


    }
}


int main()
{
    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;


    for(int i = 0; i < N; i++)map[1 << i] = i;
    for(int i = 0; i < 1 << N; i++)
    {
        int s = 0;
        for(int j = i; j; j -= lowbit(j))s ++;
        ones[i] = s;
    }
 
    for(int i = 0; i < N; i++)
    for(int j = 0; j < N; j++)
    cin >> sudu[i][j];


    int cnt = 0;
    for(int i = 0; i < N; i++)
    for(int j = 0; j < N; j++)
    if(sudu[i][j])
    {
        row[i] -= 1 << (sudu[i][j] - 1);
        col[j] -= 1 << (sudu[i][j] - 1);
        cell[i / 3][j / 3] -= 1 << (sudu[i][j] - 1);
    }
    else cnt ++;


    dfs(cnt);


    cout << res;
}

16*16 数独

9*9数独和16*16数独的复杂度远远不止一个数量级,这要求我们使用更优化的剪枝方式实现,对算法提出了更高的要求。

当你代码老是调试不出来的时候,尝试去想一想,是不是基本数据结构出了问题。 

对于高级数据结构,如string,其内部内存分配你可能是不知道的,对于此类数据结构,memcpy等内存操作会出问题,且问题非常隐蔽。

memset最好只用它来赋值0、0x3f3f3f3f等特殊值,一般值千万别用,否则会出问题,有时会统统变成-1。

——一个对这道题调试了一整天的笔者说到

例题:16*16数独

请你将一个 16×16 的数独填写完整,使得每行、每列、每个 4×4 十六宫格内字母 A∼P 均恰好出现一次。

保证每个输入只有唯一解决方案。

bcb510db08f226d5fe4f99ee997aceb0.png

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

每组测试用例包括 16 行,每行一组字符串,共 16 个字符串。

第 i 个字符串表示数独的第 i 行。

字符串包含字符可能为字母 A∼P 或 -(表示等待填充)。

测试用例之间用单个空行分隔,输入至文件结尾处终止。

输出格式 对于每个测试用例,均要求保持与输入相同的格式,将填充完成后的数独输出。

每个测试用例输出结束后,输出一个空行。

输入样例

--A----C-----O-I
-J--A-B-P-CGF-H-
--D--F-I-E----P-
-G-EL-H----M-J--
----E----C--G---
-I--K-GA-B---E-J
D-GP--J-F----A--
-E---C-B--DP--O-
E--F-M--D--L-K-A
-C--------O-I-L-
H-P-C--F-A--B---
---G-OD---J----H
K---J----H-A-P-L
--B--P--E--K--A-
-H--B--K--FI-C--
--F---C--D--H-N-

输出样例

FPAHMJECNLBDKOGI
OJMIANBDPKCGFLHE
LNDKGFOIJEAHMBPC
BGCELKHPOFIMAJDN
MFHBELPOACKJGNID
CILNKDGAHBMOPEFJ
DOGPIHJMFNLECAKB
JEKAFCNBGIDPLHOM
EBOFPMIJDGHLNKCA
NCJDHBAEKMOFIGLP
HMPLCGKFIAENBDJO
AKIGNODLBPJCEFMH
KDEMJIFNCHGAOPBL
GLBCDPMHEONKJIAF
PHNOBALKMJFIDCEG
IAFJOECGLDPBHMNK

这题将在专题(1)的数独的基础上,继续剪枝,使得运行效率达到题目要求。

思考:还能怎么剪?

在以前的数独中,我们是从每一个小格出发,以局部的视角,完成数独的深度优先搜索,在16*16的剪枝中,我们需要从全局的角度,在一次dfs内填写所有当前已经确定可以填写的格子,或遇到某种特殊情况已经不可能再继续填写下去时,立刻回溯。

总结下来,还能做以下4种剪枝:

  1. 对某一空格而言,如果该空格尚未填写,但该空格无法填写,则应当回溯;如果该空格只有一种填写方式,则应该马上将其填写完毕。

  2. 对某一行而言,如果该行有一个字母已经由于条件束缚不可能被填写,则应当回溯;如果该行有一个字母只有一个地方可以填写,则应该马上将其填写完毕。

  3. 对某一列而言,如果该列有一个字母已经由于条件束缚不可能被填写,则应当回溯;如果该行有一个字母只有一个地方可以填写,则应该马上将其填写完毕。

  4. 对某一四宫格而言,如果四宫格有一个字母已经由于条件束缚不可能被填写,则应当回溯;如果该行有一个字母只有一个地方可以填写,则应该马上将其填写完毕。

如何判断有字母已经无法填写或者某字母只有一种方式可以填写?

本题虽为数独题,但其中运用到了大量的位运算知识,是一道对位运算训练非常到位的题目,本题的思想在以前动态规划求解石板切割问题中大致也巧合地运用上了。

如何判断存在字母都无法填写 == 如何判断所有字母都可以填写,可以考虑使用按位或操作,求并集,将并集与满集做比较,倘若并集不等于满集,说明有元素确实无法被填写。则应该回溯。

sor |= state[i][j];

如何判断某字母只有一种方式可以填写?

可以考虑使用按位与操作,假如某一字母可以填写的次数出现了两次,则将其在全集中删除,最终如果全集有剩下的元素,说明它们都是只出现了一次的元素,则可直接填写。

sand &= ~(sor & state[i][j]);

在数独那一题的基础上,再加上以上四个剪枝,并维护一个state二维数组来保存每个位置的可以存放的字母的二进制表示,并注意恢复现场,这一题就大功告成了。

这一题是我写过最长的一道算法题,我debug也de了整整一天,主要原因在于这个bug十分隐蔽,我使用了string数组来保存数独中的字母,但是,string属于高级数据结构,它的内存分配并不是固定的,所以后来使用memcpy时就出现了问题。

数独四题对dfs、位运算等要求极高,推荐大家有空都挑战一下601d2656c7e500129057ccb8fc55c58d.png

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


using namespace std;


const int N = 16;
int ones[1 << N], map[1 << N];
char sudu[N][N + 1];
int state[N][N];//存放每一格能放什么
int bstate[N * N + 1][N][N];
int bstate2[N * N + 1][N][N];
char bsudu[N * N + 1][N][N + 1];


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


void draw(int x, int y, int c)//将c(所代表的字母)填入x,y位置并维护state
{
    for(int i = 0; i < N; i ++)
    {
        state[x][i] &= ~(1 << c);
        state[i][y] &= ~(1 << c);
    }
    int sx = x / 4 * 4, sy = y / 4 * 4; //(sx, sy)对应其子块开始坐标
    for(int i = 0; i < 4; i ++)
    for(int j = 0; j < 4; j ++)
    {
        state[sx + i][sy + j] &= ~(1 << c);
    }
    
    sudu[x][y] = c + 'A';
    state[x][y] = 1 << c;


    return;
}




bool dfs(int cnt)
{
    if(!cnt) return true;
    /*
    剪枝1:如果某一空格只能填1个或不能填
    剪枝2:对于每一行,如果某个字母都不能出现或只能出现一次
    剪枝3:对于每一列,如果某个字母都不能出现或只能出现一次
    剪枝4:对于每一块,如果某个字母都不能出现或只能出现一次
    递归出口
    优化:从可填数最少处开始填
    */
    //便于保护现场所做的处理
    int kcnt = cnt;
    memcpy(bstate[kcnt], state, sizeof state);
    memcpy(bsudu[kcnt], sudu, sizeof sudu);
    //剪枝1:如果某一空格只能填1个或不能填
    for(int i = 0; i < N; i ++)
    for(int j = 0; j < N; j ++)
    if(sudu[i][j] == '-')
    if(state[i][j] == 0)
    {
        memcpy(state, bstate[kcnt], sizeof state);
        memcpy(sudu, bsudu[kcnt], sizeof sudu);//恢复现场
        return false;
    }
    else if(ones[state[i][j]] == 1)
    {
        draw(i, j, map[state[i][j]]);
        cnt --;
    }
    //剪枝2:对于每一行,如果某个字母都不能出现或只能出现一次
    for(int i = 0; i < N; i ++)
    {
        int sor = 0, sand = (1 << N) - 1;
        int drawn = 0;
        for(int j = 0; j < N; j ++)
        {
            sand &= ~(state[i][j] & sor);
            sor |= state[i][j];


            if(sudu[i][j] != '-') drawn |= state[i][j];
        }


        if(sor != (1 << N) - 1)//存在有不能出现的字母
        {
            memcpy(sudu, bsudu[kcnt], sizeof sudu);
            memcpy(state, bstate[kcnt], sizeof state);
            return false;
        }


        for(int k = sand; k; k -= lowbit(k))
        {
            int put = lowbit(k);
            if(!(drawn & put))//未被填过,填
            {
                for(int j = 0; j < N; j ++)
                {
                    if(state[i][j] & put)
                    {
                        draw(i, j, map[put]);
                        cnt --;
                        break;
                    }
                }
            }
        }
    }
    //剪枝3:对于每一列,如果某个字母都不能出现或只能出现一次
    //只需将剪枝2中i,j互换
    for(int i = 0; i < N; i ++)
    {
        int sor = 0, sand = (1 << N) - 1;
        int drawn = 0;
        for(int j = 0; j < N; j ++)
        {
           sand &= ~(state[j][i] & sor);
            sor |= state[j][i];


            if(sudu[j][i] != '-') drawn |= state[j][i];
        }


        if(sor != (1 << N) - 1)//存在有不能出现的字母
        {
            memcpy(sudu, bsudu[kcnt], sizeof sudu);
            memcpy(state, bstate[kcnt], sizeof state);
            return false;
        }


        for(int k = sand; k; k -= lowbit(k))
        {
            int put = lowbit(k);
            if(!(drawn & put))//未被填过,填
            {
                for(int j = 0; j < N; j ++)
                {
                    if(state[j][i] & put)
                    {
                        draw(j, i, map[put]);
                        cnt --;
                        break;
                    }
                }
            }
        }
    }
    //剪枝4:对于每一块,如果某个字母都不能出现或只能出现一次
    for(int i = 0; i < N; i += 4)
    for(int j = 0; j < N; j += 4)
    {
        int sor = 0, sand = (1 << N) - 1;
        int drawn = 0;
        for(int m = 0; m < 4; m ++)
        for(int n = 0; n < 4; n ++)
        {
            sand &= ~(state[i + m][j + n] & sor);
            sor |= state[i + m][j + n];


            if(sudu[i + m][j + n] != '-') drawn |= state[i + m][j + n];
        }


        if(sor != (1 << N) - 1)//存在有不能出现的字母
        {
            memcpy(sudu, bsudu[kcnt], sizeof sudu);
            memcpy(state, bstate[kcnt], sizeof state);
            return false;
        }


        for(int k = sand; k; k -= lowbit(k))
        {
            int put = lowbit(k);
            if(!(drawn & put))//未被填过,填
            {
                int flag = 1;
                for(int m = 0; m < 4 && flag; m++)
                for(int n = 0; n < 4 && flag; n++)
                {
                    if(state[i + m][j + n] & put)
                    {
                        draw(i + m, j + n, map[put]);
                        cnt --;
                        flag = 0;
                    }
                }
            }
        }
    }
    //递归出口
    if(!cnt) return true;
    //优化:从可填数最少处开始填
    int minv = 100, x = -1, y = -1;
    for(int i = 0; i < N; i ++)
    for(int j = 0; j < N; j ++)
    {
        if(sudu[i][j] == '-' && ones[state[i][j]] < minv)
        {
            minv = ones[state[i][j]];
            x = i, y = j;
        }
    }
    memcpy(bstate2[kcnt], state, sizeof state);
    for(int i = state[x][y]; i; i -= lowbit(i))
    {
        int put = lowbit(i);
        draw(x, y, map[put]);


        if(dfs(cnt - 1)) return true;


        memcpy(state, bstate2[kcnt], sizeof state);
    }


    memcpy(sudu, bsudu[kcnt], sizeof sudu);
    memcpy(state, bstate[kcnt], sizeof state);
    return false;


}


void print()
{
    for(int i = 0; i < N; i ++)
    cout << sudu[i] << endl;
    cout << endl;
}


void init()
{
    for(int i = 0; i < N; i++)
    for(int j = 0; j < N; j++)
    state[i][j] = (1 << N) - 1;


    
}
int main()
{
    for(int i = 0; i < N; i++)map[1 << i] = i;


    for(int i = 0; i < 1 << N; i++)
    for(int j = i; j; j -= lowbit(j))ones[i] ++;


    while(cin >> sudu[0])
    {
        init();
        for(int i = 1; i < N; i++)
        cin >> sudu[i];


        int cnt = 0;
        for(int i = 0; i < N; i ++)
        for(int j = 0; j < N; j ++)
        if(sudu[i][j] != '-')
        {
            draw(i, j, sudu[i][j] - 'A');
        }
        else cnt++;


        dfs(cnt);


        print();
    }
    
}

成文 凌晨1:34分,为了那悲催的阅读量,还是放到明天10点发吧6f78f4f7100906f27fafb34f31fdbf04.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值