美文网首页C/C++天花板编程手把手学习
天花板编程手把手计划-第1期-第5天

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

作者: 天花板 | 来源:发表于2017-05-03 10:25 被阅读974次

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

    1. 题目

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

    如图所示,可能出现的情况应该共有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天

    相关文章

      网友评论

        本文标题:天花板编程手把手计划-第1期-第5天

        本文链接:https://www.haomeiwen.com/subject/ggactxtx.html