写给妹妹的编程札记 1 - 穷举: 从循环到递归


        编程(Programming)的含义可能很大, 在此我们局限于使用计算机编写程序解决问题。 要说计算机, 最重要的优点就是“非常听话”, 只要你能够通过她能够理解的语言(无歧义的编程语言)给出你的指示, 她就会任劳任怨地执行, 不管多么的机械枯燥。换句话说, 计算机相比人,尤其适合干机械的事情。


        比如, 求 1 + 2 + 3 + ... + 100 

        假如没有任何知识, 我们可以指示计算机直接一步步来

	int sum = 0;
	for(int i = 0; i <= 100; i++) {
		sum += i;
	}
	printf("1 + 2 + ... + 100 = %d", sum);

        当然,更好的办法是告诉计算机,按求和公式来

	int sum = (1 + 100) * 100 / 2;
	printf("1 + 2 + ... + 100 = %d", sum);


        上例中, 我们告诉计算机让她按照我们的指示进行按部就班的工作。很多实际使用的系统,都是类似地,让计算机在某种情况下执行某个任务。 在这篇文章中,我们想讨论穷举。 穷举, 顾名思义为遍历一个集合。这个集合的意义很广, 在解决不同的问题过程中, 这个需要穷举的集合也不尽相同。 比如,我们可以穷举一遍一个给定的整数数组,寻找其中最大的一个; 我们可以穷举问题域,找出满足特定条件的项;等等

        下面, 我们来看看一个经典的问题 - N皇后问题:

                在N X N格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。


        我们先考虑N = 4的情况,也就是在4*4的棋盘上布局。 由于任意两个皇后不能在同一行出现, 所以每行一定有且仅有一个皇后。我们假设r0, r1, r2, r3分别表示第一,二,三,四行的皇后所在的位置。r0, r1, r2, r3都有可能在第一,二,三,四列,也就是取值可能为0,1,2,3

	int ValidCount = 0;
	for(int r0 = 0; r0 < 4; r0++) {
		for(int r1 = 0; r1 < 4; r1++) {
			for(int r2 = 0; r2 < 4; r2++) {
				for(int r3 = 0; r3 < 4; r3++) {
					bool isValid = true;
					// 1. 验证 任意两个皇后没有处于同一行。 由于我们穷举的时候已经把ri表示为第i行的皇后位置, 保证了每行有且仅有一个皇后
					// 2. 验证 任意两个皇后没有处于同一列。
					if (r0 == r1 || r0 == r2 || r0 == r3 || r1 == r2 || r1 == r3 || r2 == r3) isValid = false;
					// 3. 验证 任意两个皇后没有处于同一条斜线上
					if ((r0 + 0 == r1 + 1) || (r0 + 0 == r2 + 2) || (r0 + 0 == r3 + 3) || (r1 + 1 == r2 + 2) || (r1 + 1 == r3 + 3)  || (r2 + 2 == r3 + 3)) isValid = false;
					if ((r0 - 0 == r1 - 1) || (r0 - 0 == r2 - 2) || (r0 - 0 == r3 - 3) || (r1 - 1 == r2 - 2) || (r1 - 1 == r3 - 3)  || (r2 - 2 == r3 - 3)) isValid = false;
					// 4. 找到合法的布局
					if (isValid) {
						// 更新合法布局总数
						ValidCount++;
                        // 输出布局
						for(int i = 0; i < 4; i++) if(r0 == i) printf("Q"); else printf(".");
						printf("\n");
						for(int i = 0; i < 4; i++) if(r1 == i) printf("Q"); else printf(".");
						printf("\n");
						for(int i = 0; i < 4; i++) if(r2 == i) printf("Q"); else printf(".");
						printf("\n");
						for(int i = 0; i < 4; i++) if(r3 == i) printf("Q"); else printf(".");
						printf("\n\n");
					}
				}
			}
		}
	}
	printf("TotalValidCount=%d", ValidCount);

        很快,我们的“老黄牛”输出下列结果



        如果N = 8呢?r1, r2, r3, r4, r5, r6, r7, r8? 写起来有点累了。申请一个数组r[8]吧, 用r[i]表示原来的ri。类似地, 我们很容易写出新的代码:

    int ValidCount = 0;

    const int N = 8;
    int r[N];

    for(r[0] = 0; r[0] < N; r[0]++) {
        for(r[1] = 0; r[1] < N; r[1]++) {
            for(r[2] = 0; r[2] < N; r[2]++) {
                for(r[3] = 0; r[3] < N; r[3]++) {
                    for(r[4] = 0; r[4] < N; r[4]++) {
                        for(r[5] = 0; r[5] < N; r[5]++) {
                            for(r[6] = 0; r[6] < N; r[6]++) {
                                for(r[7] = 0; r[7] < N; r[7]++) {
                                    bool isValid = true;
                                    // 1. 验证 任意两个皇后没有处于同一行。 由于我们穷举的时候已经把ri表示为第i行的皇后位置, 保证了每行有且仅有一个皇后
                                    // 2. 验证 任意两个皇后没有处于同一列。
                                    for (int i = 0; i < N; i++)
                                        for(int j = i + 1; j < N; j++)
                                            if (r[i] == r[j]) isValid = false;
                                    // 3. 验证 任意两个皇后没有处于同一条斜线上
                                    for (int i = 0; i < N; i++)
                                        for(int j = i + 1; j < N; j++)
                                            if (r[i] + i == r[j] + j) isValid = false;
                                    for (int i = 0; i < N; i++)
                                        for(int j = i + 1; j < N; j++)
                                            if (r[i] - i == r[j] - j) isValid = false;
                                    // 4. 找到合法的布局
                                    if (isValid) {
                                        // 更新合法布局总数
                                        ValidCount++;
                                        // 输出布局
                                        for (int i = 0; i < N; i++) {
                                            for(int j = 0; j < N; j++) if(r[i] == j) printf("Q"); else printf(".");
                                            printf("\n");
                                        }
                                        printf("\n");
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    printf("TotalValidCount=%d", ValidCount);


        8皇后问题有92个解:


        看到这里, 相信大家都觉得这代码实在太难看了。 实在太累了。N才等于8呢,再大不敢想象。 并且,在使用穷举解决有些问题的时候, 循环的重数并不确定, 怎么办呢? 我们先来观察上述代码的结构:


        不难看出, 每个循环(红色的矩形框)都有很强的相似性, 除了最内层循环之外。我们是否把重复逻辑使用一个函数来实现呢? 答案是肯定的,让我们来尝试使用递归来修改下, 把相似的代码都使用Search函数来实现吧:


void search(int* r, int N, int step) {
    if (step == N) {
        bool isValid = true;
        // 1. 验证 任意两个皇后没有处于同一行。 由于我们穷举的时候已经把ri表示为第i行的皇后位置, 保证了每行有且仅有一个皇后
        // 2. 验证 任意两个皇后没有处于同一列。
        for (int i = 0; i < N; i++)
            for(int j = i + 1; j < N; j++)
                if (r[i] == r[j]) isValid = false;
        // 3. 验证 任意两个皇后没有处于同一条斜线上
        for (int i = 0; i < N; i++)
            for(int j = i + 1; j < N; j++)
                if (r[i] + i == r[j] + j) isValid = false;
        for (int i = 0; i < N; i++)
            for(int j = i + 1; j < N; j++)
                if (r[i] - i == r[j] - j) isValid = false;
        // 4. 找到合法的布局
        if (isValid) {
            // 输出布局
            for (int i = 0; i < N; i++) {
                for(int j = 0; j < N; j++) if(r[i] == j) printf("Q"); else printf(".");
                printf("\n");
            }
            printf("\n");
        }
        return;
    }
    for(r[step] = 0; r[step] < N; r[step]++) {
        search(r, N, step + 1);
    }
}

int main() {

    const int N = 8;
    int r[N];

    search(r, N, 0);
}



        简单修改过后, 我们使用递归来实现了一个穷举逻辑, 代码变得清爽很多。 递归也将是很多搜索使用的框架。 从这个角度看, 递归不是什么神秘的东西, 她也只不过是折叠起来的多重循环而已。在编写递归程序的时候, 需要重点注意三件事情:

        1. 终止条件。 我们不能无限制地递归下来, 需要一个终止条件。 如上例, 当循环层数(step)到达N时, 我们不再递归, 转而验证解是否合法

        2. 上下文。 递归过程需要维和一个上下文,供不同层递归之间通讯。 如上例中的 r[] 数组。

        3. 转移逻辑。 递归两层之间的处理逻辑。在上例中, 我们只是简单的枚举,这块涉及相对较少。更复杂的例子中,转移逻辑可能极大改变后续递归。


        到此为止, 我们从一个暴力的多重循环枚举,到使用相对简洁的递归框架,解决了经典的八皇后问题。 这中间还有很多可以改进的地方, 后续再讨论。


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值