【问题标题】:Ugly Numbers — DP approach丑陋的数字——DP方法
【发布时间】:2018-06-11 02:42:07
【问题描述】:

问题: 丑数是仅素因数为 2、3 或 5 的数。序列 1、2、3、4、5、6、8、9、10、12、15……显示前 11 个丑数。按照惯例,包括 1。

给定一个号码n,任务是找到n’th丑号码。 (https://www.geeksforgeeks.org/ugly-numbers/)

答案:以上链接中的动态编程方法

伪代码:

1 Declare an array for ugly numbers:  ugly[n]
2 Initialize first ugly no:  ugly[0] = 1
3 Initialize three array index variables i2, i3, i5 to point to 
   1st element of the ugly array: 
        i2 = i3 = i5 =0; 
4 Initialize 3 choices for the next ugly no:
         next_mulitple_of_2 = ugly[i2]*2;
         next_mulitple_of_3 = ugly[i3]*3
         next_mulitple_of_5 = ugly[i5]*5;
5 Now go in a loop to fill all ugly numbers till 150:
For (i = 1; i < 150; i++ ) 
{
    /* These small steps are not optimized for good 
      readability. Will optimize them in C program */
    next_ugly_no  = Min(next_mulitple_of_2,
                        next_mulitple_of_3,
                        next_mulitple_of_5); 

    ugly[i] =  next_ugly_no       

    if (next_ugly_no  == next_mulitple_of_2) 
    {             
        i2 = i2 + 1;        
        next_mulitple_of_2 = ugly[i2]*2;
    } 
    if (next_ugly_no  == next_mulitple_of_3) 
    {             
        i3 = i3 + 1;        
        next_mulitple_of_3 = ugly[i3]*3;
     }            
     if (next_ugly_no  == next_mulitple_of_5)
     {    
        i5 = i5 + 1;        
        next_mulitple_of_5 = ugly[i5]*5;
     } 

}/* end of for loop */ 
6.return next_ugly_no

疑问:

我不理解这种情况下的 DP 方法。

  1. 子问题是什么?
  2. 这种情况下的递归是什么?
  3. 子问题如何重叠?
  4. 我们为什么要跟踪当前的丑数?为什么不通过 2,3,5 的倍数,每个点都取最小值。

    像 2,3,5。然后是 4,3,5,然后是 4,6,5..不断增加每个的倍数。

相关问题:nth ugly number(我浏览了答案,但对我提到的上述问题感到困惑。)

【问题讨论】:

  • 您需要在此处发布该文章中的问题。当该链接失效时,整个问题和答案对每个人都变得毫无用处。
  • @Rob 包括在内,感谢您的建议。包括答案。如果还有问题,请告诉我。

标签: algorithm dynamic-programming


【解决方案1】:
t = int(input())

# function to get all the numbers with prime factors within a given range

def prime(n):
    global l
    a = []
    i = 1
    while (i <= n):
        k = 0
        if (n % i == 0):
            j = 1
            while (j <= i):
                if (i % j == 0):
                    k = k + 1
                j = j + 1
            if (k == 2):
                a.append(i)
        i = i + 1
    check(a)

#function to filter all non-ugly numbers

def check(a):
    if (all(x in l for x in a)):
        everything.append(n)

# main code

while t >0:
    h = int(input())
    l = [2, 3, 5]
    everything = []
    for n in range(1, h**2):
        prime(n)
    print(everything[h - 1])
    t-=1

【讨论】:

  • SO 不是代码转储站点。请用几句话解释你的方法。还应该注意的是,虽然这个算法产生了正确的结果,但总运行时间对于大值来说不是最佳的——计算第 100 个丑数需要几秒钟。您的解决方案的时间复杂度是多少?
  • OP 正在寻找已知解决方案的解释,而不是替代解决方案。
【解决方案2】:

这就是我将如何根据子问题来表述问题:

第i个丑数是通过将另一个丑数(严格小于第i个)乘以2、3或5得到的。第一个丑数是1。

因此:

// dp[i] ... the i-th ugly number
dp[1] = 1;
i2 = i3 = i5 = 1;
for i in 2 .. n:
  dp[i] = min(2*dp[i2], 3*dp[i3], 5*dp[i5]);
  // where: 1 <= i2, i3, i5 < i

  i2 = update(dp[i], i2, 2);
  i3 = update(dp[i], i3, 3);
  i5 = update(dp[i], i5, 5);
return dp[n];
update(ref, j, k):
  if (ref == k * dp[j]):
    return j + 1;
  return j;

以下是建议算法的运行方式:

子问题确实重叠。以dp[6]dp[4] 为例。两者都是通过使用dp[2] 生成的,分别乘以 3 和 2。

必须使用 自下而上 方法构建解决方案(请参阅 Dynamic ProgrammingComputer programming 部分下的“自下而上方法”项目符号),以建立丑陋数字的索引。在计算 dp[i] 时,您可以将 dp[i2]dp[i3]dp[i5] 视为 memoized 递归调用。

GeeksforGeeks 实施说明

  • 不要进行堆分配;太慢了

    • 最好的方法是分配一次dp数组(确保它是 足够大)并在所有测试用例中重复使用它
  • 不要使用子程序;太慢了

    • min(min(a, b), c)还可以,min({a, b, c})(C++)会超过时间限制

    • update()逻辑必须集成到主算法​​中

  • 输入很大(10k 行);考虑使用某种缓冲 读取实用程序


确实有一个更简单但速度较慢的 (O (n log n)) 解决方案,您暗示并在this question 的答案中指出。这就是它在 Java 中的样子:

TreeSet<Long> multiples = new TreeSet<Long>();

long first = 1;
for (int i = 1; i < n; i++) {
    multiples.add(first * 2);
    multiples.add(first * 3);
    multiples.add(first * 5);
    first = multiples.pollFirst();
}
return first;

注意

这样的解决方案不太可能通过 GeeksforGeeks 设定的时间限制。

给定 t 测试用例,每个测试用例都由要计算的丑数的序数 (n) 指定,我们可以表达两种解决方案的时间复杂度:

  • t * n - 用于线性解

  • t * n * log2(n) - 用于“TreeSet”解决方案

将两者相除会产生“TreeSet”解决方案的速度慢多少倍 相对于线性:

t * n * log2(n)
--------------- = log2(n)
t * n

线性解决方案的运行时间约为 600 到 800 毫秒;平均 700 个。

如果我们将它与log2(10000) (.7s * log2(10000) = 6.98s) 相乘,我们会得到“TreeSet”解决方案的运行时近似值,它远高于设定的时间限制:

Expected Time Limit &lt; 1.2736sec

【讨论】:

    【解决方案3】:
    1. 子问题:找出所有 2、3、5 的倍数的数
    2. 递归:使用以前的丑数来计算未来的丑数。动态编程解决方案通过使用存储来缩短对递归的需求。
    3. 重叠:输出需要按数字顺序排列,因此不清楚哪个子问题将按顺序生成下一个。
    4. 为什么要跟踪:如果不重复使用以前的丑陋数字,您只会得到指数结果 2^n、3^n、5^n

    进一步优化:可以通过修剪丑陋的数组以取消设置任何

    【讨论】:

    • 有两个选项可以按顺序跟踪 - 跟踪未来或跟踪过去。发布的解决方案跟踪过去。就您而言,您可以为每个发现的丑数计算 3 个未来的丑数,但是您必须存储这些未来的数字并在前进时删除重复的数字。在这种情况下,未来的存储似乎比过去的存储更复杂。
    • ideone.com/q3fma 显然是目前最好和理想的解决方案,因为它还能够估计第 N 个数字的近似范围,而不是一直计算在那里。
    • 你能更详细地解释一下递归吗,现在很难从你的答案中弄清楚。
    • 递归是调用同一个函数来获得子问题答案的概念。所以有两种方法可以做到这一点:一种是花时间运行算法,另一种是进行查找。所以查找阶段是递归
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多