找符合条件的整数

任意给定一个正整数N,求一个最小的正整数M>1),使得N M的十进制表示形式里只含有1和0。

解法1:

看了题目要求之后,我们的第一想法就是从小到大枚举M的取值,然后再计算M,最后判断它们的乘积是否只含有1和0。大体的思路可以用下面的伪代码来实现:

for(M = 2; ; M++)

{

    product = N * M;

    if(HasOnlyOneAndZero(product))

        output N, M, Product, and return;

}

但是问题很快就出现了,什么时候应该终止循环呢?这个循环会终止吗?即使能够终止,也许这个循环仍需要耗费太多的时间,比如= 99时,= 1 122 334 455 667 789,= 111 111 111 111 111 111。






 

解法2:

题目中的直接做法显然不是一个令人满意的方法。还有没有其他的方法呢?答案是肯定的。

可以做一个问题的转化。由于问题中要求N M的十进制表示形式里只含有1和0,所以N MM相比有明显的特征。我们不妨尝试去搜索它们的乘积*M,这样在某些情况下需要搜索的空间要小很多。另外,搜索N * M,而不去搜索M,其实有一个更加重要的原因,就是当M很大时,特别是当M大于232时,某些机器就可能没法表示M了,我们就得自己实现高精度大整数类。但是考虑N M的特点,可以只需要存储N M的十进制表示中“1”的位置,这样就可以大大缩小为表示N M所需要的空间,从而使程序能处理数值很大的情况。因此,考虑到程序的推广性,选择了以N M为目标进行计算。

换句话说,就是把问题从“求一个最小的正整数M,使得M的十进制表示形式里只含有1和0”变成求一个最小的正整数X,使得X的十进制表示形式里只含有1和0,并且XN整除。

我们先来看一下X的取值,X从小到大有如下的取值:1、10、11、100、101、110、111、1000、1001、1010、1011、1100、1101、1110、1111、10000、……

如果直接对X进行循环,就是先检查X=1是否可以整除N,再检查X=10,然后检查X=11,接着检查X=100…(就像遍历二进制整数一样遍历X的各个取值)。但是这样处理还是比较慢,如果X的最终结果有K位,则要循环搜索2K次。由于我们的目标是寻找最小的X,使得X mod N= 0,我们只要记录mod N =i(0 <= i N)的最小X就可以了。这样通过避免一些不必要的循环,可以达到加速算法的目的。那么如何避免不必要的循环呢?先来看一个例子:

N=3,X=1,再引入一个变量jj=X N。直接遍历X,计算中间结果如表2-1所示:

表2-1

Num

 

 

1

 

 

2

 

 

3

 

 

4

 

 

5

 

 

6

 

 

7

 

 

N

 

 

3

 

 

3

 

 

3

 

 

3

 

 

3

 

 

3

 

 

3

 

 

X

 

 

1

 

 

10

 

 

11

 

 

100

 

 

101

 

 

110

 

 

111

 

 

J

 

 

1

 

 

1

 

 

2

 

 

1

 

 

2

 

 

2

 

 

0

 

 

表2-1计算110 % 3是多余的。原因是1和10对3的余数相同,所以101和110对3的余数相同,那么只需要判断101是否可以整除3就可以了,而不用判断110是否能整除3。并且,如果X的最低3位是110,那么可以通过将101替换110得到一个符合条件的更小正整数。因此,对于mod N同余的数,只需要记录最小的一个。

有些读者可能会问,当X循环到110时,我怎么知道1和10对3的余数相同呢?其实,X=1,X=10是否能整除3,在X循环到110时都已经计算过了,只要在计算X=1,X=10时,保留X N的结果,就可以在X=110时作出判断,从而避免计算N

以上的例子阐明了在计算中保留X除以N的余数信息可以避免不必要的计算。下面给出更加形式化的论述。

假设已经遍历了X的十进制表示有K位时的所有情况,而且也搜索了X=10K的情况,设10K%a。现在要搜索XK+1位的情况,即X=10K+Y,(0<Y<10K)。如果用最简单的方法,搜索空间(Y的取值)将有2K-1个数据。但是如果对这个空间进行一下分解,即把Y按照其对N的余数分类,我们的搜索空间将被分成N-1个子空间。对于每个子空间,其实只需要判断其中最小的元素加上10K是否能被N整除即可,而没有必要判断这个子空间里所有元素加上10K是否能被N整除。这样搜索的空间就从2K-1维压缩到了N-1维。但是这种压缩有一个前提,就是在前面的计算中已经保留了余数信息,并且把Y的搜索空间进行了分解。所谓分解,其实,从技术上讲,就是对于“XN”的各种可能结果,保留一个对应的已经出现了的最小的X(即建立一个长度为N的“余数信息数组”,这个数组的第i位保留已经出现的最小的模NiX)。

那么现在的问题就是如何维护这个“余数信息数组”了。假设已经有了X的十进制表示有K位时的所有余数信息。也有了X=10K的余数信息。现在我们要搜索XK+1位的情况,也即X=10K+Y,(0<Y<10K)时,X除以N的余数情况。由于已经有了对Y的按除N的余数进行的空间分解情况,即Y<10K的余数信息数组。我们只需要将10K%N的结果与余数信息数组里非空的元素相加,再去模N,看看会不会出现新的余数即可。如果出现,就在余数信息数组的相应位置增添对应的X。这一步只需要N次循环。

综上所述,假设最终的结果XK位,那么直接遍历X,需要循环2K次,而按照我们保留余数信息避免不必要的循环的方法,最多只需要(K-1)*N步。可以看出,当最终结果比较大时,保留余数信息的算法具有明显的优势。

下面是这个算法的伪代码:BigInt[i]表示模N等于i的十进制表示形式里只含1和0的最小整数。由于BigInt[i]可能很大,又因为它只有0和1,所以,只需要记下1的位置即可。比如,整数1001,记为(0, 3)= 100+103。即BigInt的每个元素是一个变长数组,对于模N等于i的最小X,BigInt的每个元素将存储最小X在十进制中表示“1”的位置。我们的目标就是求BigInt[0]。

代码清单2-17

// 初始化

    for(i = 0; i < N; i++)

        BigInt[i].clear();

    BigInt[1].push_back(0);

    for(i = 1, j = 10 % N; ;i++, j = (j * 10) % N)

    {

        int NoUpdate = 0;

        bool flag = false;

        if(BigInt[j].size() == 0)

        {

            // BigInt[j] = 10^i, (10^i % N = j)

            BigInt[j].clear();

            BigInt[j].push_back(i);

        }

        for(k = 1; k < N; k++)

        {

            if((BigInt[k].size() > 0)

                && (i > BigInt[k][BigInt[k].size() - 1])

                && (BigInt[(k + j) % N].size() == 0))

            {

                // BigInt[(k + j) % N] = 10^i + BigInt[k]

                flag = true;

                BigInt[(k + j) % N] = BigInt[k];

                BigInt[(k + j) % N].push_back(i);

            }

        }

        if(flag == false)

            NoUpdate++;

        // 如果经过一个循环节都没能对BigInt进行更新,就是无解,跳出。

        // 或者BigInt[0] != NULL,已经找到解,也跳出。

        if(NoUpdate == N || BigInt[0].size() > 0)

            break;

    }

    if(BigInt[0].size() == 0)

    {

        // M not exist

    }

    else

    {

        // Find N * M = BigInt[0]

    }

在上面的实现中,循环节取值N(其实循环节小于等于N,循环节就是最小的c,使得10c mod N = 1)。

这个算法其实部分借鉴了动态规划算法的思想。在动态规划算法的经典实例“最短路径问题”中,当处理中间结点时,只需要得到从起点到中间结点的最短路径,而不需要中间结点到那个端点的所有路径信息。“只保留模N的结果相同的X中最小的一个”的方法,与此思路相似。


解法3:

从网上别人处搜来的方法:

      书中的方法很好,但是个人认为判断条件过多从而不容易掌握。可以利用BFS+剪枝即可,容易理解。《编程之美读书笔记》中有关于此题的BFS解法,但是我认为他剪得不彻底,可以再多剪剪。 
   
  标准BFS搜索m*n,所有0,1组成的数可以构造出一颗二叉树从而进行BFS。每层的数字长度相同,每个节点的左右孩子为其末尾分别添加0,1。BFS可保证是从小到大进行测试。 
   
  由于是按层序遍历的,因此可以保证如果bitmap中某个余数的对应项被置为1,则表示对应该余数的最小n*m已经找到。今后只需对它扩展即可。因为假设该数为x,之后遍历到的与它同余的数位y,则x<y。由y扩展出来的子树和x扩展出来的子树相同,因此可以把y对应的子树剪枝 
   
  bitmap[i]==1表示mod n余i的最小数已经被找到并且已经放入队列中继续做其子树的扩展。 
  之后再遇到余i的数,其子树和已找到的的最小的数相同,所以不需要再进行扩展,因此可以舍弃不处理(即不入队) 
   
  复杂度:若最终结果为k位,每一层最多有n个节点(余数为0...n-1), 
  因此空间复杂度为n(队列中最多有n个不同余数的节点),时间复杂度为O(kn) 
   
  code: 
   
  struct slot 
  { 
   slot():val(0),remainder(0){} 
   slot( __int64 v, __int64 r ):val(v),remainder(r){} 
   __int64 val ; __int64 remainder ; 
  }; 
   
  void OnlyHasOneAndZero2( __int64 n ) 
  { 
   slot *Q = new slot[n+1] ; 
   char *bitmap = new char[n] ; 
   memset( bitmap, 0, sizeof(char)*n ) ; 
   
   slot a ; 
   __int64 rear, front, capacity ; 
   __int64 left, right ; 
   __int64 leftR, rightR ; 
   
   rear = 0 ; front = 1 ; capacity = n ; 
   if( n==1 ) //***注意当n为1时,1%1=0,此为特殊情况 
   Q[++rear] = slot(1,0) ; 
   else 
   Q[++rear] = slot(1,1) ; 
   
   while( true ) 
   { 
   a = Q[front] ; //出栈做处理、判断 
   if( ++front == capacity ) 
   front = 0 ; 
   
   if( a.remainder == 0 ) 
   { 
   printf( "n*m=%I64d , m=%I64d\n", a.val, a.val/n ) ; 
   delete []Q ; 
   delete []bitmap ; 
   return ; 
   } 
   
   left = a.val*10 ; leftR = a.remainder*10%n ; 
   right = a.val*10+1 ; rightR = (a.remainder*10+1)%n ; 
   
   //只要bitmap中某位i被置1,则意味着mod n余i的最小数已经被找到 
   //若其左孩子的余数未出现过,则将其bitmap位置1,并加入队列做下面的扩展 
   if( !bitmap[leftR] ) 
   { 
   bitmap[leftR] = 1 ; 
   
   if( ++rear == capacity ) 
   rear = 0 ; 
   Q[rear] = slot( left, leftR ) ; 
   } 
   if( !bitmap[rightR] ) 
   { 
   bitmap[rightR] = 1 ; 
   
   if( ++rear == capacity ) 
   rear = 0 ; 
   Q[rear] = slot( right, rightR ) ; 
   } 
   } 
  }













扩展问题

1.   对于任意的N,一定存在M,使得N M的乘积的十进制表示只有0和1吗?

对于任意的N,一定存在M,使得N*M的乘积的十进制表示只有01吗?

我百度到的答案是肯定存在,所以我的代码就假设一定存在解。

实际上书中的代码用了抽屉原理判断是否有解。



2.   怎样找出满足题目要求的NM,使得N M < 216,且N+M最大?

         根据N定出M的上界,并在这个范围内进行查找。

用如上的思路都可以解决。

BFS:先拓展值较大的点,每层同余的数只保留值大的。

书中的方法:也是要保留大值。

这些都还要加上边界的判断。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值