打印2 ^ i * 5 ^ j中的下一个最小值,其中i,j> = 0


10

最近在一次技术电话筛选中有人问我这个问题,但做得不好。问题包括在下面的逐字记录中。

生成{2^i * 5^j | i,j >= 0}排序的集合。连续打印下一个最小值。

例: { 1, 2, 4, 5, 8, 10...}

“下一个最小的”使我认为涉及到最小的堆,但是我真的不知道从那里去哪里,面试官也没有提供任何帮助。

有人对如何解决此类问题有建议吗?


我认为面试想请您以不变的记忆力去做。使用O(n)内存使这很简单。或者至少使用O(logn)内存,因为输入n的编码大小将为logn。内存解决方案的O(n)是指数内存解决方案。
通知2014年

Answers:


14

让我们重述一下问题:输出从1到无穷大的每个数字,以便该数字除2和5外没有其他因子。

以下是一个简单的C#代码段:

for (int i = 1;;++i)
{
    int num = i;
    while(num%2 == 0) num/=2;
    while(num%5 == 0) num/=5;
    if(num == 1) Console.WriteLine(i);
}

基利安的 / QuestionC的方法是很多更好的性能。使用此方法的C#代码段:

var itms = new SortedSet<int>();
itms.Add(1);
while(true)
{
    int cur = itms.Min;
    itms.Remove(itms.Min);
    itms.Add(cur*2);
    itms.Add(cur*5);
    Console.WriteLine(cur);
}

SortedSet 防止重复插入。

基本上,它通过确保序列中的下一个数字在中来工作itms

证明此方法有效:
所描述的算法可确保在以表格形式输出的任何数字之后2^i*5^j,集合现在包含2^(i+1)*5^j2^i*5^(j+1)。假设序列中的下一个数字是2^p*5^q。必须存在形式为2^(p-1)*5^(q)或的先前输出数字2^p*5^(q-1)(或如果p和q都不等于0,则两者都存在)。如果不是,则2^p*5^q不是下一个数字,因为2^(p-1)*5^(q)2^p*5^(q-1)都较小。

第二个代码段使用O(n)内存(其中n是已输出的数字的数量),因为O(i+j) = O(n)(因为i和j都小于n),并且会O(n log n)及时找到n个数字。第一个代码段以指数时间查找数字。


1
嗨,您可以看到我希望在面试中为何感到困惑的原因。实际上,提供的示例是问题中描述的集合的输出。1 = 2^0*5^0, 2 = 2^1*5^0, 4 = 2^2*5^0, 5 = 2^0*5^1, 8 = 2^3*5^0, 10 = 2^1*5^1
贾斯汀·斯基尔斯

是重复执行这些操作.Remove().Add()触发垃圾收集器的不良行为,还是会解决问题?
Snowbody 2014年

1
@Snowbody:op的问题是算法问题,因此它一点都不相关。忽略这一点,您首先要考虑的是处理非常大的整数,因为这比垃圾回收器开销要早得多。
布莱恩(Brian)

8

这是一个足够常见的面试问题,知道答案很有用。这是我的婴儿床单中的相关条目:

  • 要按顺序生成形式为3 a 5 b 7 c 的数字,请从1开始,将所有三个可能的后继对象(3,5,7)放入辅助结构,然后将其中的最小数字添加到列表中。

换句话说,您需要采用两步方法以及附加的排序缓冲区来有效解决此问题。(更长的描述是在Gayle McDowell撰写的《破解编码访谈》中


3

这是一个以恒定内存运行的答案,但会浪费CPU。在原始问题(即面试中的答案)的背景下,这不是一个很好的答案。但是,如果面试时间是24小时,那还不错。;)

这个想法是,如果我有n个有效答案,那么序列中的下一个将是n的2的幂乘以5的幂,或者n乘以5的幂除以a。二的力量。只要它平均分配。(...或除数可以是1;)在这种情况下,您只需乘以2或5)

例如,要从625变为640,请乘以5 ** 4/2 **7。或者,更一般地说,乘以2 ** m * 5 ** n某个m 的某个值,n为n,其中一个为正,一个为负或零,并且乘数平均除数。

现在,棘手的部分是找到乘数。但是我们知道a)除数必须平均除数; b)乘数必须大于1(数字不断增加); c)如果我们选择大于1 的最低乘数(即1 <f <所有其他f ),那么这肯定是我们的下一步。之后的步骤将是最低的步骤。

令人讨厌的部分是找到m,n的值。只有log(n)的可能性,因为只有太多的2或5可以放弃,但是我不得不添加-1到+1的系数作为处理四舍五入的草率方法。因此,我们只需要在每个步骤中迭代O(log(n))。因此,总体上为O(n log(n))。

好消息是,因为它需要一个值并找到下一个值,所以您可以在序列中的任何位置开始。因此,如果您想要10亿之后的下一个,它可以通过迭代2/5或5/2并选择大于1的最小乘数来找到它。

(蟒蛇)

MAX = 30
F = - math.log(2) / math.log(5)

def val(i, j):
    return 2 ** i * 5 ** j

def best(i, j):
    f = 100
    m = 0
    n = 0
    max_i = (int)(math.log(val(i, j)) / math.log(2) + 1) if i + j else 1
    #print((val(i, j), max_i, x))
    for mm in range(-i, max_i + 1):
        for rr in {-1, 0, 1}:
            nn = (int)(mm * F + rr)
            if nn < -j: continue
            ff = val(mm, nn)
            #print('  ' + str((ff, mm, nn, rr)))
            if ff > 1 and ff < f:
                f = ff
                m = mm
                n = nn
    return m, n

def detSeq():

    i = 0
    j = 0
    got = [val(i, j)]

    while len(got) < MAX:
        m, n = best(i, j)

        i += m
        j += n
        got.append(val(i, j))

        #print('* ' + str((val(i, j), m, n)))
        #print('- ' + str((v, i, j)))

    return got

我对排序列表解决方案生成的前10,000个数字进行了验证,从而验证了生成的前10,000个数字,并且它至少可以工作到现在。

顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句,顺便说一句:顺便说一句,顺便说一句,顺便说一句:顺便说一句,顺便说一句。

...

嗯 通过将best()我当作增量扩展的查找表,我可以获得O(n)性能-每个值(!)O(1)-和O(log n)内存使用率。现在,它通过每次迭代来节省内存,但是它进行了大量的冗余计算。通过保留这些中间值以及最小值列表,我可以避免重复的工作并将其大大提高。但是,中间值的列表将随n增长,因此O(log n)内存。


好答案。我有没有编码的类似想法。在这个想法中,我保留了2和5的跟踪器。这将跟踪最大值nm并且到目前为止已经使用了整个序列中的数字。在每次迭代中,nm可能会或可能不会上去。我们创建一个新数字,2^(max_n+1)*5^(max_m+1)然后以穷举递归的方式减少该数字,每次调用将指数减小1,直到得到的最小数字大于当前数字。我们更新max_nmax_m根据需要。这是不变的记忆。O(log^2(n))如果在还原调用中使用了DP缓存,可能会成为记忆
InformedA 2014年

有趣。这里的优化是它不需要考虑所有m&n对,因为我们知道正确的m,n将产生最接近1的乘数。因此,我只需要对max_i求m = -i即可,可以只计算n,并舍入一些垃圾进行四舍五入(我很草率,只是将-1迭代为1,但需要更多思考;)。
罗布

但是,我有点像您的想法...顺序将是确定性的...它的确像一个大帕斯卡三角形,一个方向i + 1,另一个方向j + 1。因此,该序列应在数学上确定。对于三角形中的任何节点,总是会有一个数学上确定的下一个节点。
罗布

1
下一个可能会有一个公式,我们可能不需要搜索。我不确定
知情的2014年

当我考虑它时,下一个形式的代数形式可能不存在(并非所有确定性问题都具有解的代数形式),另外,当质数比2和5多时,很难找到公式真的很想计算这个公式。如果有人知道这个公式,我可能会读一点,听起来很有趣。
通知2014年

2

Brian绝对正确-我的其他答案太过复杂了。这是一种更简单,更快捷的方法。

想象一下欧几里德平面的象限I,限于整数。将一个轴称为i轴,将另一个轴称为j轴。

显然,靠近原点的点将在远离原点的点之前被拾取。另请注意,活动区域将在离开j轴之前离开i轴。

一旦使用了一个点,就不会再使用它。并且只有在直接位于其下方或左侧的点已被使用时,才能使用该点。

将它们放在一起,您可以想象一个围绕原点开始的“边界”或“前沿”,它们向上和向右扩展,沿着i轴的传播比在j轴上的传播更远。

实际上,我们可以发现更多的东西:对于任何给定的i值,在边界/边缘最多会有一个点。(必须将i递增2倍以上才能等于j的递增。)因此,我们可以将边界表示为一个列表,其中每个i坐标包含一个元素,仅随j坐标和函数值而变化。

每次通过时,我们在前缘选择最小的元素,然后在j方向上移动一次。如果碰巧要提高最后一个元素,则添加一个新的最后一个more元素,该元素的i值和j值均为0。

using System;
using System.Collections.Generic;
using System.Text;

namespace TwosFives
{
    class LatticePoint : IComparable<LatticePoint>
    {
      public int i;
      public int j;
      public double value;
      public LatticePoint(int ii, int jj, double vvalue)
      {
          i = ii;
          j = jj;
          value = vvalue;
      }
      public int CompareTo(LatticePoint rhs)
      {
          return value.CompareTo(rhs.value);
      }
    }


    class Program
    {
        static void Main(string[] args)
        {
            LatticePoint startPoint = new LatticePoint(0, 0, 1);

            var leadingEdge = new List<LatticePoint> { startPoint } ;

            while (true)
            {
                LatticePoint min = leadingEdge.Min();
                Console.WriteLine(min.value);
                if (min.j + 1 == leadingEdge.Count)
                {
                    leadingEdge.Add(new LatticePoint(0, min.j + 1, min.value * 2));
                }
                min.i++;
                min.value *= 5;
            }
        }
    }
}

空格:到目前为止已打印的元素数为O(n)。

速度:O(1)插入,但并非每次都插入。(当List<>必须增长时,有时会更长,但仍要摊销O(1))。最大的耗时是要搜索到目前为止已打印的元素数量中的最小值O(n)。


1
这使用什么算法?为什么行得通?要提出的问题的关键部分是Does anyone have advice on how to solve such a problem?试图加深对基本问题的理解。代码转储不能很好地回答该问题。

好点,我解释了我的想法。
Snowbody 2014年

+1虽然这大致相当于我的第二个片段,但是您使用不可变的边缘可以使边缘计数的增长更加清晰。
布莱恩(Brian)

这绝对比Brian修改后的代码片段慢,但是它的内存使用行为应该好很多,因为它不会不断地删除和添加元素。(除非CLR或SortedSet <>有一些重用我不知道的元素的方法)
Snowbody 2014年

1

基于集合的解决方案可能是您的面试官想要的,但是,不幸的结果是,要对元素进行排序会O(n)占用内存和O(n lg n)总时间n

一点数学可以帮助我们找到一个O(1)时空O(n sqrt(n))解决方案。注意2^i * 5^j = 2^(i + j lg 5)。找到第一个n元素{i,j > 0 | 2^(i + j lg 5)}减少到找到第一个n要素{i,j > 0 | i + j lg 5},因为函数(x -> 2^x)是严格单调递增的,所以对于一些的唯一途径a,b2^a < 2^b是如果a < b

现在,我们只需要一种算法即可找到的序列i + j lg 5,其中i,j是自然数。换句话说,在给定当前值的情况下i, j,将下一个移动减至最小(即,给我们序列中的下一个数字)的是,其中一个值(例如j += 1)有所增加,而另一个值()有所减少i -= 2。唯一限制我们的是i,j > 0

只有两种情况需要考虑- i增加或j增加。其中之一必须增加,因为我们的序列在增加,而两者都不会增加,因为否则我们将跳过只有一个i,j增加的术语。因此,一个增加,另一个保持不变或减少。在C ++ 11中表示的整个算法及其与集合解决方案的比较在此处可用

这实现了恒定的内存,因为除了输出数组(参见链接)之外,该方法中仅分配了恒定数量的对象。该方法每次迭代都达到对数时间,因为对于任何给定而言(i,j),它遍历最佳对(a, b),使得(i + a, j + b)值的增加最小i + j lg 5。遍历为O(i + j)

Attempt to increase i:
++i
current difference in value CD = 1
while (j > 0)
  --j
  mark difference in value for
     current (i,j) as CD -= lg 5
  while (CD < 0) // Have to increase the sequence
    ++i          // This while will end in three loops at most.
    CD += 1
find minimum among each marked difference ((i,j) -> CD)

Attempt to increase j:
++j
current difference in value CD = lg 5
while (j > 0)
  --i
  mark difference in value for
     current (i,j) as CD -= 1
  while (CD < 0) // have to increase the sequence
    ++j          // This while will end in one loop at most.
    CD += lg 5
find minimum among each marked difference ((i,j) -> CD)

每次迭代都会尝试更新i,然后更新,然后j进行两者中较小的更新。

由于ij最多O(sqrt(n)),我们有总O(n sqrt(n))时间。ij生长在广场的速度n,因为对于任何最大valiues imaxjmax存在O(i j)从中使我们的序列,如果我们的序列是唯一对n条款,i以及j彼此(的一些常数因子中生长,因为该指数是由线性的组合FO两个),我们知道,ijO(sqrt(n))

不必担心浮点错误-由于术语呈指数增长,因此在翻牌错误赶上我们之前,我们必须处理溢出问题。如果有时间,我将对此进行更多讨论。


很好的答案,我认为在增加任何质数的序列方面也有一种模式
InformedA

@random谢谢。经过进一步的思考,我得出的结论是,按照目前的状态,我的算法并没有我想象的那么快。如果有一种更快的评估“尝试增加i / j”的方法,我认为这是获取对数时间的关键。
VF1

我当时在想:我们知道要增加数量,我们必须增加素数之一的数量。例如,增加的一种方法是将mul除以8,然后除以5。因此,我们得到了增加和减少数目的所有方法的集合。这将仅包含基本方法,例如mul 8 div 5,而不是mul 16 div5。还有另一组基本方法可以降低。按两组的增加或减少因子对它们进行排序。给定一个数字,可以通过从增量集中找到最小的因数找到适用的增量方式来找到下一个。
InformedA 2014年

..适用的意思是有足够的素数来执行mul和div。然后我们找到一种减少新数的方法,因此从减少最多的数开始。继续使用新的减少方法,当新数字小于原始给定数字时,我们将停止。由于素数集是恒定的,因此这意味着两组素数是恒定的。这也需要一点证明,但是对我来说,每个数字看起来就像是恒定的时间,恒定的记忆。因此,用于打印n个数字的内存和线性时间保持不变。
知悉2014年

@randomA您从哪里获得分裂的?您介意回答一个完整的问题-我不太理解您的评论。
VF1
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.