天花板编程手把手计划-第1期-第5天

今天我们来讲解一下上一篇的课后习题。

1. 题目

编程实现把1~9九个数字填入九宫格中,满足每行、每列和对角线上的三个数字和为15。如图所示。

注意:由于结果不唯一,只要打印出一种满足条件的结果即可。

2. 分析

这道题目很多同学拿到后觉得无从着手,完全和学过的知识没有一点联系。其实,这道题用到的就是学过的知识。

2.1 建模

首先,这道题是一个3 * 3的二维数组,要把9个数填进去。在众多的可能性中找到一个满足条件的即可。

接下来,我们想想之前讲到的二维数组和一维数组的关系,是不是可以把这道题转化成一个9个元素的一维数组,把9个数字排列组合放进这个数组中。这就和之前抛硬币的那道题有几分相像了。

如图所示,可能出现的情况应该共有9!种。我们要做的就是编程穷举出所有的可能性,在这个过程中判断每种可能性是否满足题目要求。

注意:这个解法并不是最优解,但却是最适合初学者学习的。我在如何评价一段代码中已经阐明了我的观点。

2.2 功能分解

有了一个基本的模型,我们可以把问题转化为几个子功能:

  • 计算排列组合

列举出所有的可能,拿到9!个二维数组。

  • 判断

判断得到的每个结果是否符合要求。

3. 判断功能

判断的条件比较简单,就是要每行数字、每列数字和两个对角线的数字之和均为15。大部分人很容易想到这种写法:

#define MAX_SIZE 3

int g_arr[MAX_SIZE][MAX_SIZE];

int Calculate()
{
    int i, j, sum;
    // 计算3行
    for (i = 0; i < MAX_SIZE; i++)
    {
        sum = 0;
        for (j = 0; j < MAX_SIZE; j++)
        {
            sum += g_arr[i][j];
        }
        if (sum != 15)
        {
            return 0;
        }
    }
    
    // 计算3列
    for (i = 0; i < MAX_SIZE; i++)
    {
        sum = 0;
        for (j = 0; j < MAX_SIZE; j++)
        {
            sum += g_arr[j][i];
        }
        if (sum != 15)
        {
            return 0;
        }
    }
    
    // 计算对角线
    sum = 0;
    for (i = 0; i < MAX_SIZE; i++)
    {
        sum += g_arr[i][i];
    }
    if (sum != 15)
    {
        return 0;
    }
    
    sum = 0;
    for (j = 0; j < MAX_SIZE; j++)
    {
        sum += g_arr[i][MAX_SIZE- 1 - j];
    }
    
    if (sum != 15)
    {
        return 0;
    }
    
    return 1;
}

Calculate()函数的功能是判断二维数组g_arr是否满足要求,如果满足返回1,如果不满足返回0。

这个函数功能上没有问题,但使用了太多次循环,执行效率太低。不过细心的同学肯定已经发现有些统计其实可以合并在一起做。我对这段代码做了优化,修改如下:

int Calculate()
{
    int i, j;
    int sumLine, sumCol, sumDL1, sumDL2;
    sumDL1 = 0;
    sumDL2 = 0;
    for (i = 0; i < MAX_SIZE; i++)
    {
        sumLine = 0;
        sumCol = 0;

        for (j = 0; j < MAX_SIZE; j++)
        {
            sumLine += g_arr[i][j];
            sumCol += g_arr[j][i];
        }

        if (sumLine != 15 || sumCol != 15)
        {
            return 0;
        }

        sumDL1 += g_arr[i][i];
        sumDL2 += g_arr[i][MAX_SIZE- 1 - i];
    }

    if (sumDL1 != 15 || sumDL2 != 15)
    {
        return 0;
    }

    return 1;
}

这段代码只用了一组嵌套的for循环,通过四个变量统计出了所有数据。变量sumLinesumCol分别用来计算每一行和每一列的数字之和。这里只是在循环中修改了一下i和j的下标顺序就完成了两个维度的统计。变量sumDL1sumDL2分别计算了两条对角线上数字之和。

sumDL1 += g_arr[i][i];
sumDL2 += g_arr[i][MAX_SIZE- 1 - i];

这两行代码用了在天花板编程手把手计划-第1期-第2天中介绍的方法,忘记的同学可以回去复习。

4. 打印

这里依然需要一个二维数组打印的函数,这和前面几个题目相似,不用多说了。

void PrintMap()
{
    int i, j;
    for (i = 0; i < MAX_SIZE; i++)
    {
        for (j = 0; j < MAX_SIZE; j++)
        {
            printf("%3d ", g_arr[i][j]);
        }

        printf("\n");
    }

    printf("\n");
}

5. 计算排列组合

这里依然需要用到一个递归的方法。每一次递归都给一个对应的位置填写一个数字。这里和抛硬币问题有一个唯一的不同,就是每个数字只能填一次。

5.1 排除法

Aha_斌同学是第一个完成这道题目的,他用的办法是这样。

首先,用抛硬币的方法找出允许重复情况下的可能矩阵。之后逐一判断,如果发现有重复数字就舍弃那种情况。

这个解法很聪明,一下就解决了问题,而且不需要太复杂的算法。代码大家可以去参考他发的作业帖。

5.2 用数字作递归条件

我要介绍的算法是另外一个思路,不是按照每个空格去递归,而是按照数字去递归。通过递归的方式执行九次动作,每次负责把一个数填在一个空格中。这样永远不会出现重复数字的问题。

图中用A~I表示9个空格,整个递归的过程是一棵不规则的N叉树。根结点有9个子节点,第一层的每个节点有8个子节点,第二层的每个节点有7个子节点,以此类推。图中只画出了一个分支的前三次迭代。

现在剩下最后一个问题,每次递归时如何为不同的层执行不同次数的递归呢?

方法一:状态恢复法

另外创建一个二维数组纪录下当前被使用的格子和未使用的格子。每层递归函数返回时把填写过的格子更新成未使用的格子。

这个方法很容易想,但要多维护一个二维数组,而且有太多批量赋值的工作,不适合初学者。

方法二:利用数字特性

我使用了一种更巧妙的方法,我先把九个格子都初始化为0,再让递归按照从大到小的顺序填写数字,当空格中存在比当前数字小的数字时,说明这个格子没有在这个分支中被使用过。

代码如下:

int g_cnt = 0;
void FillBlank(int num)
{
    int i;
    int nBak;
    int* pArr = (int*)g_arr;

    if (num <= 0)
    {
        if (Calculate() == 1)
        {
            PrintMap();
        }

        printf("%d\r", g_cnt++);

        return;
    }

    for (i = 0; i < MAX_SIZE * MAX_SIZE; i++)
    {
        if (num >= pArr[i])
        {
            nBak = pArr[i];
            pArr[i] = num;
            FillBlank(num - 1);
            pArr[i] = nBak;
        }
    }
}

函数FillBlank()的功能是把数字num填在一个空格中。每一层递归函数调用下一层函数时,传递一个num - 1。当参数为0时,说明这个分支到达最低端的叶子节点。这时候二位数组中保存的是一个完成的可能。通过Caculate()函数判断它是否符合题意,如果符合就打印出来。这里打印的是所有符合题意的可能。

在FillBlank()中,我们通过一个循环遍历九个空格,当要填写的数字大于格子中的数字时,说明可以填写。这样就保证了每一层的填写次数正确。

最后,在填写数字之前,需要先用nBak变量保存空格中原有的数字,在这一轮递归结束后要恢复回来。原因大家好好思考一下。

注意:由于穷举的次数太多,等待的过程会比较漫长,我们在这里加入了一个显示当前寻找次数的功能。全局变量g_cnt记录了找到的可能性次数,通过printf("%d\r", g_cnt++);不断打印在屏幕上。

6. 功能调用

main()函数变得非常简单:

int main()
{
    FillBlank(9);

    return 0;
}

我们执行一下,效果如下:

7. 留下一种结果

对现有的程序稍作修改,我们只需要一个符合题意的结果即可。代码如下:

#define _CRT_SECURE_NO_WARNINGS
#include 
#include 

#define MAX_SIZE 3

int g_arr[MAX_SIZE][MAX_SIZE] = { 0 };

int Calculate()
{
    int i, j;
    int sumLine, sumCol, sumDL1, sumDL2;
    sumDL1 = 0;
    sumDL2 = 0;
    for (i = 0; i < MAX_SIZE; i++)
    {
        sumLine = 0;
        sumCol = 0;

        for (j = 0; j < MAX_SIZE; j++)
        {
            sumLine += g_arr[i][j];
            sumCol += g_arr[j][i];
        }

        if (sumLine != 15 || sumCol != 15)
        {
            return 0;
        }

        sumDL1 += g_arr[i][i];
        sumDL2 += g_arr[i][MAX_SIZE - 1 - i];
    }

    if (sumDL1 != 15 || sumDL2 != 15)
    {
        return 0;
    }

    return 1;
}

void PrintMap()
{
    int i, j;
    for (i = 0; i < MAX_SIZE; i++)
    {
        for (j = 0; j < MAX_SIZE; j++)
        {
            printf("%3d ", g_arr[i][j]);
        }

        printf("\n");
    }

    printf("\n");
}

int g_cnt = 0;
int g_return = 0;
void FillBlank(int num)
{
    int i;
    int nBak;
    int* pArr = (int*)g_arr;

    if (g_return == 1)
    {
        return;
    }

    if (num <= 0)
    {
        if (Calculate() == 1)
        {
            PrintMap();
            g_return = 1;
        }

        printf("%d\r", g_cnt++);

        return;
    }

    for (i = 0; i < MAX_SIZE * MAX_SIZE; i++)
    {
        if (num >= pArr[i])
        {
            nBak = pArr[i];
            pArr[i] = num;
            FillBlank(num - 1);
            pArr[i] = nBak;
        }
    }
}

int main()
{
    FillBlank(9);

    return 0;
}

执行效果如下:

由于这个递归方法比较抽象,不理解的同学可以在群里提问,我再仔细讲解一下。

8. 课后练习

给出一组代数表达式,请编程判断出他们的括号是否配对正确。

8.1 输入内容

5
[a+(b+c)]*(a+b]
(a-1+b)-(b+c)
x-[a*(b+c))]
a+(b+c+(d-(e+m))
a+b+[c-d*e-(a-b)-c]

第一行中的5表示共有5个表达式需要判断。下面的每一行有一个表达式。要求把这组数据原样保存在文件input.txt中,通过读文件的方式读入数据完成判断。

8.2 输出

括号匹配正确打印1,匹配错误打印0。正确的输出结果应该是:

0
1
0
0
1

我是天花板,让我们一起在软件开发中自我迭代。
如有任何问题,欢迎与我联系。


上一篇:天花板编程手把手计划-第1期-第4天

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容