任意给定一个正整数N,求一个最小的正整数M(M
解法1:
看了题目要求之后,我们的第一想法就是从小到大枚举M的取值,然后再计算N
for(M = 2; ; M++)
{
}
但是问题很快就出现了,什么时候应该终止循环呢?这个循环会终止吗?即使能够终止,也许这个循环仍需要耗费太多的时间,比如N
解法2:
题目中的直接做法显然不是一个令人满意的方法。还有没有其他的方法呢?答案是肯定的。
可以做一个问题的转化。由于问题中要求N
换句话说,就是把问题从“求一个最小的正整数M,使得N
我们先来看一下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
设N=3,X=1,再引入一个变量j,j=X
表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
有些读者可能会问,当X循环到110时,我怎么知道1和10对3的余数相同呢?其实,X=1,X=10是否能整除3,在X循环到110时都已经计算过了,只要在计算X=1,X=10时,保留X
以上的例子阐明了在计算中保留X除以N的余数信息可以避免不必要的计算。下面给出更加形式化的论述。
假设已经遍历了X的十进制表示有K位时的所有情况,而且也搜索了X=10K的情况,设10K%N
那么现在的问题就是如何维护这个“余数信息数组”了。假设已经有了X的十进制表示有K位时的所有余数信息。也有了X=10K的余数信息。现在我们要搜索X有K+1位的情况,也即X=10K+Y,(0<Y<10K)时,X除以N的余数情况。由于已经有了对Y的按除N的余数进行的空间分解情况,即Y<10K的余数信息数组。我们只需要将10K%N的结果与余数信息数组里非空的元素相加,再去模N,看看会不会出现新的余数即可。如果出现,就在余数信息数组的相应位置增添对应的X。这一步只需要N次循环。
综上所述,假设最终的结果X有K位,那么直接遍历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
// 初始化
在上面的实现中,循环节取值N(其实循环节小于等于N,循环节就是最小的c,使得10c
这个算法其实部分借鉴了动态规划算法的思想。在动态规划算法的经典实例“最短路径问题”中,当处理中间结点时,只需要得到从起点到中间结点的最短路径,而不需要中间结点到那个端点的所有路径信息。“只保留模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吗?
我百度到的答案是肯定存在,所以我的代码就假设一定存在解。
实际上书中的代码用了抽屉原理判断是否有解。
2.
根据N定出M的上界,并在这个范围内进行查找。
用如上的思路都可以解决。
BFS:先拓展值较大的点,每层同余的数只保留值大的。
书中的方法:也是要保留大值。
这些都还要加上边界的判断。