最后一道附加题,当时搞来搞去的,没有搞好,昨天晚上敲了一下,今天拿来跟狄米特同学比试了一下速度,没想到竟然是我的快了(因为我几乎没有比他快过)。
题目描述:
求第k大的因数只有3,5和7的数。比如当k=1,2,3的时候答案应该为3,5,7。
笔试题就是这个样子,也不说k的密集程度(测试的时候会有多少个k),也不说k的范围,搞得人很纠结
但是题上面说了,要求时间复杂度最小。
我的分析和解法:
我们假设k的范围为1-n。那么对于其中的每一个解,如果时间最优的话,最快可能是O(1),那么对于整个k的范围,可以采用O(n)的方法进行预处理,之后对于每一个k进行O(1)的输出。
关键的思想在于对于任意一个满足条件的数字一定是由之前的某个数字*3 、*5或者*7得到的。那么就可以用类似动态规划的思想从3,5,7开始向后更新。取3,5,7三个指针,每次用数字去乘以当前指针所指的数字(要求是这个数字之前没有出现过比如3*5 = 5*3这样会得到相同的结果),比较之后得到最小的,更新结果数组和指针。
设置ans[]用来存放结果。
p[3]表示3,5,7对应的指针(从工程的角度讲应该这样写吧)。
如此这般看来,关键的问题就在于如何判断重复的情况。
判断重复可以用的方法有:
遍历一边之前的表,看是否存在重复的结果(这样的话,算法的复杂度就会退化到O(n*n),因为每一个数字都需要遍历。),不好。
第二种方法,是这样,开一个足够大的数组,进行标记,每次查找某个数字x是否有重复时候就看mark[x]是否为0,如果不为0,那么证明这个数字可以用,并且将其标记为1。这种方法,看似不错,但是还是有一些问题。首先,当n比较大的时候,这个数组需要开十分大,才有可能包含所有的结果。其次,这个数组是稀疏的数组,有很多空间会被浪费(因为只有3,5,7的倍数被标记了)。所以这个办法也不是很可取。
第三种方法,开一个三维数组mark[i][j][k]进行标记。三个维度分别对应某个数字的因数中3,5,7的个数。对于得到的一个数字x,将其分解为3^i*5^j*7^k,如果这个位置的mark为0证明这个数字可用,并且将其标记为1。这种记录方法,很大程度上解决了空间利用率低的问题,因为每一个位置都有可能会被标记。那么于是又有了一个问题,就是 三维数组需要多大的空间?这要取决于n的范围,但是我们可以对比一下两种标记方式可以表示的数字的范围。假设空间为10^6,那么对于上一种方案,能标记的最大数字就是10^6,对于这个方法,我们每一个维度都可以保存到100,那么这个最大的数字就是3^100*5^100*7^100,看看有多大就知道啦(unsigned long long 都没他大了)。
剩下的就是如何得到一个数字对应的3,5,7的因数的个数问题了。
如果每次都直接分解,那么算法复杂度就退化为O(n*log(n))了,显然不够给力。
由于每一个数字(x)都是由前一个数字(y)*3,*5,*7得来的,那么也就是说对于x来说也就是y相应的质因数+1就好(*3的话就在y的3的因数个数+1)。这样,这个问题也解决了,就是在存result的时候也应该把对应数字的因数个数存下来。代价不大可以接受!
贴下我的狗血的代码好了:
1 #include<iostream> 2 #include<cstring> 3 #include<ctime> 4 using namespace std; 5 6 struct Node{//保存结果的数据结构 7 unsigned long long num; 8 int n[3]; 9 Node(unsigned long long _num = 0, int _n3 = 0, int _n5 = 0, int _n7 = 0){ 10 num = _num; 11 n[0] = _n3; 12 n[1] = _n5; 13 n[2] = _n7; 14 } 15 }; 16 17 const int MAXK = 4000+5;//k的范围 18 19 const int MAXN = 500+2;//标记数组的范围 20 21 Node ans[MAXK] = {Node(3,1,0,0),Node(5,0,1,0),Node(7,0,0,1)}; 22 23 int mark[MAXN][MAXN][MAXN]; 24 25 int p[3]= {0,0,0}; 26 27 unsigned long long clc[3] = {3,5,7}; 28 29 30 Node get_num(int x){//得到下标为x的指针的下一个数字 31 Node next = ans[p[x]]; 32 next.num = next.num*clc[x]; 33 next.n[x]++; 34 if ( mark[next.n[0]][next.n[1]][next.n[2]] == 0 ){ 35 return next; 36 } 37 return Node(-1,0,0,0); 38 } 39 40 void Mark(Node node){//标记mark 41 mark[node.n[0]][node.n[1]][node.n[2]] = 1; 42 return; 43 } 44 45 Node get_next(){//得到下一个ans 46 int i = 0; 47 int m = -1; 48 Node Min(-1,0,0,0); 49 int M[3]; 50 while (m == -1 ){ 51 for ( i = 0; i < 3; i++ ){ 52 Node next = get_num(i); 53 while ( next.num == -1 ){ 54 p[i]++; 55 next = get_num(i); 56 } 57 if ( ( next.num != -1 ) && ( ( -1 == m ) || ( next.num < Min.num ) ) ){ 58 m = i; 59 Min = next; 60 } 61 } 62 } 63 p[m]++; 64 Mark(Min); 65 return Min; 66 } 67 68 void pre_process(){//打表 69 int i; 70 i = 3; 71 memset(mark,0,sizeof(mark)); 72 for ( ; i < MAXK; i++ ){ 73 ans[i] = get_next(); 74 } 75 } 76 77 void show(){//输出全部结果 78 for ( int i = 0; i < MAXK; i++ ){ 79 cout<<ans[i].num<<endl; 80 } 81 } 82 83 int main(){ 84 clock_t start,finish; 85 start=clock(); 86 pre_process(); 87 show(); 88 finish=clock(); 89 cout<<(double)(finish-start)/CLOCKS_PER_SEC; 90 }
这一个题目,在考场上没有想那么仔细,就是想到开个三维数组标记一下,然后打表。事后仔细想一想,还是有点意思的,真没想到这个想法的理论依据还是有的。无奈考试的时候不懂得考试的技巧上去就写代码,最后还划掉了重新写了一下思想,但肯定没有现在整理的清楚了。希望明年去笔试能够给力吧。