问题
《编程之美》第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<BigInt>(); //设置一个队列来实现分层搜索二叉树
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<string> 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>(); // 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 万以下的数字了,逐数测试时,相比前面两法“卡卡”,真可谓酣畅淋漓!
由于从下往上看每级元素数量列呈现“橄榄型”——中间多两头少,在面对更大的数字的时候,未能到达此数字之前,就被困扰大量计算中,类似“广度优先搜索”。那么在需要计算这么大的数字之前,还可以改进此算法,利用“深度优先搜索”、“启发式搜索”,或者遗传算法给出近似解。
作者:Silver,原文链接:http://gpww.blog.163.com/blog/static/11826816420099782740350/
其他文章:
连载1:卡特兰数(Catalan)
连载2:序列 ABAB对应字符串集合
连载3:最长公共子序列
连载4:计算字符串的相似度
该博客介绍了《编程之美》第2.8节的问题,即寻找一个最小的正整数M,使得N * M的十进制表示仅含1和0。文中给出了三种解法:二叉树搜索、试乘法和分级组合法,并讨论了针对大数字的优化策略,如深度优先搜索和启发式搜索。
314





