寻找符合条件的整数(《编程之美》第2.8节)

问题

    《编程之美》第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 万以下的数字了,逐数测试时,相比前面两法“卡卡”,真可谓酣畅淋漓!
    由于从下往上看每级元素数量列呈现“橄榄型”——中间多两头少,在面对更大的数字的时候,未能到达此数字之前,就被困扰大量计算中,类似“广度优先搜索”。那么在需要计算这么大的数字之前,还可以改进此算法,利用“深度优先搜索”、“启发式搜索”,或者遗传算法给出近似解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值