问题
《编程之美》第2.8节:任意给定一个正整数N,求一个最小的正整数M(M >1),使得N * M 的十进制表示形式里只含有1 和0,例如:
1 * 1 = 1 2 * 5 = 10
3 * 37 = 111 4 * 25 = 100
5 * 2 = 10 6 * 185 = 1,110
7 * 143 = 1,001 8 * 125 = 1,000
9 * 12,345,679 = 111,111,111
解法一
既然乘积是类似101011…的形式,那么可以使用二叉树去遍历所有乘积,从小到大,直到找到满足条件的。例如,下图是N=3 时搜索的情况:
需要说明的是,在搜索二叉树的同一层中,相同余数的节点以后 发展出来的节点余数也是相同的(若X≡Y (mod N),则X*10≡Y *10 (mod N) X*10+1≡Y *10+1 (mod N)),所以只需要保留一个节点即可。
关键代码如下:
var remainders = new int[n]; //设置一个数组来保存每层的余数(N 的余数个数≤N) var queue = new Queue(); //设置一个队列来实现分层搜索二叉树 queue.Enqueue((BigInt)1); while (queue.Count > 0) { var v = queue.Dequeue(); var r = v % n; if (r == 0) { Console.WriteLine("{0} * {1} = {2}", n, v / n, v); break; } if (remainders[r] == v.Digit) continue; else remainders[r] = v.Digit; v *= 10; queue.Enqueue(v); queue.Enqueue(v + 1); }
其中:BigInt 是自习开发辅助类库中的个,用于计算非常大的整数。
解法二
我们也可以反过来考虑,去遍历乘数,例如,当N=13 时,采用“试乘”的办法去得到M——13 的个位数是3,能够让3 变成1 的数只有7(3×7=21),依次类推:
需要注意的是,个位不可以“试乘”0,因为这样得到的M 不是最小的(还可以缩小10 倍),其它高位都可以“试乘0。
另外,还可能出现无限循环的“试乘”:
当N 为d 位数的时候,“试乘”可能会出现d 位循环出现,需要排除,关键代码如下:
void Find_01_Format(BigInt x, BigInt y, BigInt z, int digit, Dictionary cycleDic) { for (int m = digit > 0 ? 0 : 1; m < 10; m++) { var sum = m * x[0] + z[digit]; var u = sum % 10; if (u == 1 || u == 0)//找到一个能将最后乘积digit 位变为0 或1 的数 { y[digit] = m; var p = (x * m).LeftShift(digit).Add(z); if (min.Digit > 0 && p >= min) continue;//大于当前最小值,剪枝 //<检查是否找到结果> <检查是否出现循环> Find_01_Format(x, y.Clone(), p, digit + 1, copyDic); } } }
其中:
x 是被乘数,y 是乘数,z 是乘积
digit 是当前“试乘”的位数
cycleDic 是用于判断是否出现循环的哈希表
BigInt. LeftShift(k)是将10 进制数左移k 位,等价于×10k
BigInt.Add(n)是将本数字加上n 后返回自身。
min 保存的当前搜索到的最小的z
解法三
当N 比较大的时候,两种方法都比较慢,例如N=987654 时:987654 * 1,113,861,738,118,815 = 1,100,110,001,100,000,110,010
能否在秒级解决10 万以下的数呢?
可以这样考虑,我们要得到的乘积只包含0 和1,即:
z = b1 * 10^0 + b2 * 10^1 + b3 * 10^2 + … + bk * 10^k-1
其中,bi = 0 或1,i = 1,2,3,…,k
考虑无穷数列:10^0 % N,10^1 % N,10^2 % N,…,由于N 的余数总是< N,所以这个数列必将存在循环节,例如,当N = 13 时,此数列为:
1, 10, 9, 12, 3, 4, 1, 10, 9, 12, 3, 4, 1, 10, 9, 12, 3, 4, 1, 10…
现在问题就可以转化为:在这个无穷数列中,挑选一些数出来,使得他们的和能够整除N,一种挑选方案实际上对应于b1 b2 b3…的一种真值指派(每一位选中为1,否则为0)。
到此我们已经发现,这个问题可以采用“分级组合法”:
给定无穷序列100 % N,101 % N,102 % N,…求1~n 级的情况。第i 级保存序列中i 个数相加所能得到和,直到找到最小的一个和能够整除N。
代码如下:
var heap = new List(); // SumSet 类保存数列中n 个数相加所能得到的和 heap.Add(new SumSet()); heap[0].Add(0); int d = 1, b = 1, s = 0, cycleStart = -1; SumSet.Sum min = null; while (true) { var r = b % n; //找到循环节开始的那个元素 if (cycleStart < 0 && heap.Count > 1 && heap[1].ContainsKey(r)) cycleStart = r; if (r == cycleStart) s++; //进入下一个循环周期,前s 级不需要再扩展 bool found = false; heap.Add(new SumSet()); for (int lvl = d - 1; lvl >= s; lvl--) foreach (var sum in heap[lvl]) { var newSum = sum.Add(r, d - 1); if (newSum.value % n == 0) { found = true; if (min == null || newSum < min) min = newSum; } heap[lvl + 1].Renew(newSum); } if (found) break; b *= 10 % n; b %= n; d++; }
其中:
n 是给定的数字N
d 表示当前计算到无穷数列的哪一位
b 依次是10^0,10^1,10^2,10^3…,为了防止越界,计算过程中先求余,结果不变。
讨论
通过“分级组合法”基本能够秒级解决10 万以下的数字了,逐数测试时,相比前面两法“卡卡”,真可谓酣畅淋漓!
由于从下往上看每级元素数量列呈现“橄榄型”——中间多两头少,在面对更大的数字的时候,未能到达此数字之前,就被困扰大量计算中,类似“广度优先搜索”。那么在需要计算这么大的数字之前,还可以改进此算法,利用“深度优先搜索”、“启发式搜索”,或者遗传算法给出近似解。