【声明】
本文意在基于我的初始解法进行最浅显的解释及优化。
【题目信息】
算法竞赛入门经典(第 2 版)/ 刘汝佳 p35
习题 2-6 排列 (permutation)
用 1, 2, 3, ..., 9 组成 3 个三位数 abc, def 和 ghi,每个数字恰好使用一次,要求 abc: def: ghi = 1: 2: 3。后略。
【写作缘起】
一看这题目首先想到暴力解法,但 9 重循环实在是不想去用,故在网上搜索了一下大家的解法。
受到了一些启发,但当我解决问题回头对比网上方案时,发现要么重复一个字符串数组解法,要么是一些暴力解法(不代表全部结果,仅浏览了几个结果)。
我觉得我的解法与他们的不同,值得给大家看一看,目前的初始版本没使用任何循环以上的知识,入门一小时的新人也能看懂吧,大概。
【题目分析】
普通的递归解法与“九层妖塔”解法在我看来没什么区别。但一种利用字符串的解法启发了我(好吧其实是“字符串”这三个字启发了我,让我想起以前做的「数字翻转」),有思路就上手。
仔细看了下题目,发现数字是有规律的,粗略分析知道极值是「987」及「123」,由于三个数成倍数关系,故 abc 的范围在 [123, 333) 之中(为什么不是「329」?因为要花更多时间算出来,麻烦),另外两个数分别是 abc 的两倍和三倍。这样就确定了循环范围了,一共只有 200 多个可能的值。
基本的思路是,把统计每个数字出现的次数,可以用数组,但我决定直接用一个 9 位的数字就好了,每位代表一个数,结果是 9 个 1 就表示每个数字只用了一次。
既然别人把它当字符来玩,那目的「对每位字符进行验证」就可以变成「如何取出每位数字再进行处理」,取数字简单,模运算大法可解之。
123 % 10 = 3 123 / 10 % 10 = 2 123 / 100 = 1
同理,将三个数字的各位取出来就好了,再逐个验证。
最初的想法是 3 个数字分别取出来,分别取了分别验证,验证就用 switch 结构。但受到统计数字的记性,直接合并成一个大数字简洁多了,至于效率,是优化时候的事。
n = a * 1000000 + a * 2000 + a * 3;
这样,这个 n 就是合并后的数了,再把各位取出来并进行判断就好了。取了各位也是要一个个的统计的,脑中萌生了一个问题「取数的循环与逐一统计的循环能否合并呢?」,可以的,变成每次都取个位就好了,说白了就是取完数就除 10 一下,最后这个数会小到没有(归零,准确说是归 1, 因为最小是 1 开头)。
while(n > 0){ int t = n % 10; //统计 t 是哪个数 n /= 10; }
然后就是统计了,用 switch 还是 if else 还是一堆 if 都没差,我就用一堆 if 好了,一家人就要整整琪琪(误)。
1 while(n > 0){ 2 int t = n % 10; 3 if(n % 10 == 0) break; 4 if(n % 10 == 1) flag += 1; 5 if(n % 10 == 2) flag += 10; 6 if(n % 10 == 3) flag += 100; 7 if(n % 10 == 4) flag += 1000; 8 if(n % 10 == 5) flag += 10000; 9 if(n % 10 == 6) flag += 100000; 10 if(n % 10 == 7) flag += 1000000; 11 if(n % 10 == 8) flag += 10000000; 12 if(n % 10 == 9) flag += 100000000; 13 n /= 10; 14 }
这样一通下来,就能统计到这两百多个 9 位数的数字分布了,每次生成的 flag 如果等于 111111111(9 个 1) 就表示刚好用了一次。另外有个要先处理的问题,查到 0 要直接 out,因为目标数字不由 0 参与组成。
再来是 n 在这个过程中被除掉了,所以事先就要保存起来,后面输出要用。
至此,解法 v1.0 的思路就是这些了,还是很不完善,留下了很多可改进的地方,就以后再说啦。
【解法 v1.0】
完整可运行代码如下,上述的代码都是说明性的,有些不完整。
#include<stdio.h> int main(){ for(int a = 123; a < 333; a ++){ int flag = 0, n, m; m = n = a * 1000000 + a * 2000 + a * 3; while(n > 0){ int t = n % 10; if(n % 10 == 0) break; if(n % 10 == 1) flag += 1; if(n % 10 == 2) flag += 10; if(n % 10 == 3) flag += 100; if(n % 10 == 4) flag += 1000; if(n % 10 == 5) flag += 10000; if(n % 10 == 6) flag += 100000; if(n % 10 == 7) flag += 1000000; if(n % 10 == 8) flag += 10000000; if(n % 10 == 9) flag += 100000000; n /= 10; } if(flag == 111111111){ printf("%d ", m); } } return 0; }
【余留问题】
1. 尝试不合并数字,循环 3 次把数字各位取出来,用其它方法统计?
2. 尝试用数组(非字符数组)来存放这些数字,然后通过其它方法统计?
3. 各位数字的和是固定的,但相同数字也会产生这个和,是否值得探索?
4. 真正的最小循环范围是多少?起码不是 [123, 333)。
5. 那个字符解法是怎么解的?能否用到数字数组模式中去?
6. 这个题目的在哪个 OJ 中有?希望验证一下效率。
7. 在每次统计前查看是否已有?这样用数组或二进制即可。
【其它的话】
不 DIY 的话,默认界面实在不太方便,连等宽字体都不提供,早晚要改掉。
编辑完发发现可以用 MD 来写,诶,MD!