【问题标题】:Find the Kth least number for expression (2^x)*(3^y)*(5^z)找到表达式 (2^x)*(3^y)*(5^z) 的第 K 个最小数
【发布时间】:2011-11-05 03:08:34
【问题描述】:

在表达式中

2x * 3y * 5z

xyz 可以取非负整数值 (>=0)。

所以函数会生成一系列数字1,2,3,4,5,6,8,9,10,12,15,16....

  • 我有一个蛮力解决方案。
  • 我基本上会在从 1​​ 开始的循环中进行迭代,并且在每次迭代中我会发现当前的数字因子是否仅来自 2,3 或 5 的集合。

我想要的是一个优雅的算法。

这是一道面试题。

【问题讨论】:

标签: java algorithm hamming-numbers


【解决方案1】:

下面是一个有效的基于 java 的解决方案,用于查找因子仅为 2,3 和 5 的第 k 个最小数字。这里 2*3*5 被认为是最小因子。

import java.util.Comparator;
import java.util.PriorityQueue;
public class KthSmallestFactor {

    public static void main(String[] args){

        for(int i=1;i<=10;i++){
            System.out.println(kthSmallest(i));
        }
    }

    private static int kthSmallest(int k){
        PriorityQueue<Triplet> p = new PriorityQueue<Triplet>(10, new Comparator<Triplet>() {
            public int compare(Triplet t1, Triplet t2) {
                int score1 = (int) (Math.pow(2, t1.a) * Math.pow(3, t1.b) * Math.pow(5, t1.c)) ; 
                int score2 = (int) (Math.pow(2, t2.a) * Math.pow(3, t2.b) * Math.pow(5, t2.c));
                return score1 -score2;
            }
        });

        p.add(new Triplet(1, 1, 1));
        int count =1;
        while(count <k){
            Triplet top = p.poll();
            count++;
            int a = top.a;
            int b = top.b;
            int c = top.c;
            Triplet t = new Triplet(a+1, b, c);
            if(!p.contains(t)){
                p.add(t);
            }
            t = new Triplet(a, b+1, c);
            if(!p.contains(t)){
                p.add(t);
            }
            t = new Triplet(a, b, c+1);
            if(!p.contains(t)){
                p.add(t);
            }
        }
        Triplet kth = p.poll();
        System.out.println("a: "+kth.a+"b: "+kth.b+"c: "+kth.c);
        return (int) (Math.pow(2, kth.a) * Math.pow(3, kth.b) * Math.pow(5, kth.c));
    }
}
class Triplet{
    int a ;
    int b;
    int c;

    public Triplet(int a , int b, int c){
        this.a = a;
        this.b=b;
        this.c = c;
    }

    public boolean equals(Object other){
        Triplet t = (Triplet)other;
        return this.a== t.a && this.b==t.b && this.c == t.c; 
    }
}

【讨论】:

    【解决方案2】:

    我能想到的最直接的解决方案:

        int[] factors = {2, 3, 5};
        int[] elements = new int[k];
        elements[0] = 1;
        int[] nextIndex = new int[factors.length];
        int[] nextFrom = new int[factors.length];
        for (int j = 0; j < factors.length; j++) {
            nextFrom[j] = factors[j];
        }
        for (int i = 1; i < k; i++) {
            int nextNumber = Integer.MAX_VALUE;
            for (int j = 0; j < factors.length; j++) {
                if (nextFrom[j] < nextNumber) {
                    nextNumber = nextFrom[j];
                }
            }
            elements[i] = nextNumber;
            for (int j = 0; j < factors.length; j++) {
                if (nextFrom[j] == nextNumber) {
                    nextIndex[j]++;
                    nextFrom[j] = elements[nextIndex[j]] * factors[j];
                }
            }
        }
        System.out.println(Arrays.toString(elements));
    

    这会在 O(k) 空间和时间中按升序生成该集合的第一个 k 元素。

    请注意,有必要从 all j 中消耗 nextNumber 以消除重复项(毕竟 2*3 = 3*2)。

    编辑:该算法使用与 n.m. 发布的 haskell 相同的方法。

    【讨论】:

    【解决方案3】:

    对于这类问题有一个非常优雅的解决方案。算法和编码很简单。 时间复杂度为 O(n)

    我在某处看到了类似的问题。问题是按升序生成 2^x.3^y 形式的数字。

    就这样吧。

    int kthsmallest(int k){
    
        int two = 0, three = 0, five = 0;
        int A[k];
        A[0] = 1;
        for (int i=1; i<k; i++){
            int min = (A[two] * 2 <= A[three] * 3)? A[two] * 2: A[three] * 3;
            min = (min <= A[five] * 5)? min: A[five] * 5;
            A[i] = min;
            if (min == A[two] * 2)
                two++;
            if (min == A[three] * 3)
                three++;
            if (min == A[five] * 5)
                five++;
        }
        return A[k-1];
    }
    

    算法基本上是-为xyz保留三个指针。在代码中,我使用了 235。在每次迭代中,检查哪个更小(2^x3^y5^z)。将该数字放在 ith 索引中,并增加 xyz 的相应值。如果最小值超过 1,则增加两个指针。

    【讨论】:

      【解决方案4】:

      This page 列出了无数种编程语言的解决方案。像往常一样,Haskell 版本特别紧凑和直接:

      hamming = 1 : map (2*) hamming `merge` map (3*) hamming `merge` map (5*) hamming
           where merge (x:xs) (y:ys)
                  | x < y = x : xs `merge` (y:ys)
                  | x > y = y : (x:xs) `merge` ys
                  | otherwise = x : xs `merge` ys
      

      更新正如 Will Ness 所说,Data.List.Ordered 中有一个现成的函数,它比我的 merge 更好(而且它的名字也更好)。

      import Data.List.Ordered (union)
      hamming = 1 : map (2*) hamming `union` map (3*) hamming `union` map (5*) hamming
      

      【讨论】:

      • 懒惰确实让这很优雅。
      • “使用“循环迭代器”的替代版本”是一个非常漂亮的 Python 解决方案,适合任何决定阅读哪种 Python 解决方案的人。
      • 这个去重合并函数现在叫做union。它在Data.List.Ordered 包中。名称 merge 应该留给保留重复的变体,作为 mergesort 的一部分。
      • @NeilG 看起来像“循环迭代器”中使用的 Python 的 tee() 函数创建了序列的三个副本,每个副本都以自己的速度使用 - 不像 Haskell 为所有三个使用共享存储。跨度>
      【解决方案5】:

      这些是Hamming numbers,我在SRFI-41 中用作示例。这是我在那里使用的代码:

      (define hamming
        (stream-cons 1
          (stream-unique =
            (stream-merge <
              (stream-map (lsec * 2) hamming)
              (stream-map (lsec * 3) hamming)
              (stream-map (lsec * 5) hamming)))))
      

      【讨论】:

      • 只有切线相关,保留重复的 stream-merge 可以(应该?)通过一点调整轻松更改为删除重复的 stream-union,以便 stream-unique 调用不会根本不需要。
      【解决方案6】:

      这可以使用优先级队列来解决,其中存储三元组 (x, y, z) 按键 2x3 排序y5z.

      1. 仅从队列中的三元组 (0, 0, 0) 开始。

      2. 从队列中移除键最小的三元组(x, y, z)

      3. 插入三个三元组 (x+1, y, z), (x, y+1, z)(x, y, z+1) 在队列中。确保您没有插入任何已经存在的内容。

      4. 从第 2 步开始重复,直到删除 k 个三元组。删除的最后一个是您的答案。

      实际上,这变成了这个有向无环图的排序遍历。 (这里显示的前三个级别,实际的图形当然是无限的)。

      【讨论】:

      • 这行不通,因为例如 2^2=4 出现在 5^1 = 5 之前
      • @Yochai,它会起作用,因为该解决方案使用 priority 队列。
      • 所以你将优先级定义为三元组中最低的结果......好的,记住哪个组合给了你结果,这样你就可以添加接下来的三个三元组......
      • 该解决方案需要 O(k log k) 时间,因为优先级队列的大小将达到 O(k)。我的解决方案更快:-)
      • @hammar 您可以使用 O(ln n) 中的二进制搜索检查重复项,这与插入优先级队列的成本相同,因此不会改变算法复杂度。
      【解决方案7】:

      既然问题可以转化为找到第K个最少的数

       f(x,y,z) = x log(2) + y log(3) + z log(5),
      

      算法可能会遵循

      1. 以 f(x,y,z) = f(0,0,0) 开头
      2. 给定当前最小数 f(i,j,k) = v,你必须找到 (x,y,z) 使得 f(x,y,z) 最接近 v 并且 > v。 自从

        log(2)&lt;log(3)&lt;2log(2)&lt;log(5)

        我们可以说

        0&lt;=i-2&lt;=x&lt;=i+2, 0&lt;=j-1&lt;=y&lt;=j+1 &amp; 0&lt;=k-1&lt;=z&lt;=k+1 such that f(x,y,z) &gt; v

      因此,因为这是在每个步骤中找到 45 个值的最小值,所以我会说这是 O(K) 算法。当然,可以通过施加更多的条件来减少数字 45,例如 (x,y,z)!=(i,j,k)。

      【讨论】:

      • 这是错误的,尽管在正确的方向思考(有 对此的本地解决方案,但我仍然没有掌握自己)。要了解错误的原因,请考虑与元组 (64,0,0) 对应的数字 2^64 及其邻居。 (i,j,k) 的差异将远远超过 3 或 5。
      【解决方案8】:

      这可能测试的不仅仅是您对算法的了解,还包括您如何思考、解决问题以及如何在团队中工作。

      在开始之前有一个适当的问题说明很重要。如上所述,一些未知数包括:

      • K 有界限吗?
      • 您想要一个已知的算法还是临时蛮力可以?
      • 内存使用与计算时间? (可能是其中之一)
      • 计算速度有多快与开发它需要多少时间?
      • 应该缓存结果吗?

      向面试官询问其中部分或全部问题可能至少与能够回答所提出的问题一样重要。当然,你可以用这种方式把自己画到一个角落里,这甚至可以成为测试的一部分......

      【讨论】:

      • +1... 你说对了。在这些“面试问题”中一直让我感到震惊的是缺乏规格,这使得问题通常非常愚蠢。这就是为什么像 TopCoder 或 SPOJ 提出的问题只是 soooo 比愚蠢的面试官提出的大多数愚蠢的面试问题要好得多(而且,是的,我一直在进行面试,是的,他们看起来比如 TopCoder 或 SPOJ 问题;)
      【解决方案9】:

      从 x = y = z = 0 开始; 在每次迭代中计算三个 n:

      nx = 2^(x+1)*3^y*5^z
      ny = 2^x*3^(y+1)*5^z
      nz = 2^x*3^y*5^(z+1)
      

      找出三个中最小的n个:

      n = min(nx, ny, nz).
      

      增加 x、y 或 z:

      If n == nx -> x = x + 1
      If n == ny -> y = y + 1
      If n == nz -> z = z + 1
      

      在第 K 次迭代后停止并返回 n。

      【讨论】:

      • 这样,您只能生成2^x 形式的数字。递增x 总是比递增yz 生成更小的数字。
      • 我认为这行不通,看看 8 到 9 。 8 = 2^3 和 9 = 3^2 .. 你会找到 2^4。 (或者我错过了什么?)
      • 看起来是一个不正确的解决方案。在第二次迭代中,我有 x=1,y=0,z=0。现在在第三次迭代中,nx = 4,ny=6,nz=10。其中最少的是 4 (nx)。但这里的期望值应该是 3 而不是 4。
      • 假设 x = 1, y=0, z=0。没有办法从你的算法中得到 x = 0, y = 1, z = 0。
      猜你喜欢
      • 1970-01-01
      • 2019-01-07
      • 2016-10-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-04
      • 2017-12-27
      相关资源
      最近更新 更多