In nhỏ nhất tiếp theo từ 2 ^ i * 5 ^ j trong đó i, j> = 0


10

Tôi đã được hỏi câu hỏi này trong một buổi kiểm tra điện thoại kỹ thuật gần đây và không làm tốt. Câu hỏi được bao gồm nguyên văn dưới đây.

Tạo {2^i * 5^j | i,j >= 0}bộ sưu tập được sắp xếp. Liên tục in giá trị nhỏ nhất tiếp theo.

Thí dụ: { 1, 2, 4, 5, 8, 10...}

"Nhỏ nhất tiếp theo" khiến tôi nghĩ rằng một đống nhỏ có liên quan, nhưng tôi thực sự không biết phải đi đâu từ đó và không có sự trợ giúp nào được cung cấp bởi người phỏng vấn.

Có ai có lời khuyên về cách giải quyết vấn đề như vậy?


Tôi nghĩ rằng cuộc phỏng vấn muốn yêu cầu bạn làm điều đó trong bộ nhớ liên tục. Sử dụng bộ nhớ O (n) làm cho điều này khá tầm thường. Hoặc ít nhất là sử dụng bộ nhớ O (logn) vì kích thước mã hóa cho đầu vào n sẽ là logn. Một O (n) cho giải pháp bộ nhớ là một giải pháp bộ nhớ theo cấp số nhân.
Được thông báo vào

Câu trả lời:


14

Hãy giải quyết vấn đề: Xuất ra mọi số từ 1 đến vô cùng sao cho số đó không có yếu tố nào ngoại trừ 2 và 5.

Dưới đây là đoạn mã C # đơn giản:

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);
}

Kilian của / QuestionC của cách tiếp cận là nhiều hơn performant. Đoạn mã C # với cách tiếp cận này:

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 ngăn chặn chèn vào trùng lặp.

Về cơ bản, nó hoạt động bằng cách đảm bảo rằng số tiếp theo trong chuỗi nằm trong itms.

Bằng chứng rằng cách tiếp cận này là hợp lệ:
Thuật toán được mô tả đảm bảo rằng sau khi có bất kỳ đầu ra số nào trong biểu mẫu 2^i*5^j, tập hợp hiện chứa 2^(i+1)*5^j2^i*5^(j+1). Giả sử số tiếp theo trong chuỗi là 2^p*5^q. Phải tồn tại số đầu ra trước đó của biểu mẫu 2^(p-1)*5^(q)hoặc 2^p*5^(q-1)(hoặc cả hai, nếu cả p và q đều không bằng 0). Nếu không, thì đó 2^p*5^qkhông phải là số tiếp theo, vì 2^(p-1)*5^(q)2^p*5^(q-1)cả hai đều nhỏ hơn.

Đoạn mã thứ hai sử dụng O(n)bộ nhớ (trong đó n là số lượng số đã được xuất), vì O(i+j) = O(n)(vì i và j đều nhỏ hơn n) và sẽ tìm thấy n số trong O(n log n)thời gian. Đoạn mã đầu tiên tìm thấy số trong thời gian theo cấp số nhân.


1
Xin chào, bạn có thể thấy lý do tại sao tôi đã bối rối trong cuộc phỏng vấn tôi hy vọng. Trong thực tế, ví dụ được cung cấp là kết quả đầu ra từ tập hợp được mô tả trong câu hỏi. 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.
Justin Skiles

Là những người lặp đi lặp lại .Remove().Add()sẽ kích hoạt hành vi xấu từ người thu gom rác, hoặc nó sẽ tìm ra mọi thứ?
Người tuyết

1
@Snowbody: Câu hỏi của op là một câu hỏi về thuật toán, vì vậy nó có phần không liên quan. Bỏ qua điều đó, mối quan tâm đầu tiên của bạn nên xử lý các số nguyên rất lớn, vì điều này trở thành một vấn đề sớm hơn nhiều so với chi phí thu gom rác.
Brian

8

Đây là một câu hỏi phỏng vấn đủ phổ biến mà nó hữu ích để biết câu trả lời. Đây là mục có liên quan trong bảng cũi cá nhân của tôi:

  • để tạo các số có dạng 3 a 5 b 7 c theo thứ tự , bắt đầu bằng 1, nhét cả ba người kế vị có thể (3, 5, 7) vào một cấu trúc phụ trợ, sau đó thêm số nhỏ nhất từ ​​nó vào danh sách của bạn.

Nói cách khác, bạn cần một cách tiếp cận hai bước với một bộ đệm được sắp xếp bổ sung để giải quyết vấn đề này một cách hiệu quả. (Một mô tả dài hơn là trong Bẻ khóa phỏng vấn mã hóa của Gayle McDowell.


3

Đây là một câu trả lời chạy với bộ nhớ không đổi, với chi phí CPU. Đây không phải là một câu trả lời tốt trong bối cảnh của câu hỏi ban đầu (tức là câu trả lời trong một cuộc phỏng vấn). Nhưng nếu cuộc phỏng vấn kéo dài 24 giờ, thì nó không quá tệ. ;)

Ý tưởng là nếu tôi có n là một câu trả lời hợp lệ, thì câu tiếp theo trong chuỗi sẽ là n lần sức mạnh của hai, chia cho một số sức mạnh bằng 5. Hoặc nếu không n nhân một sức mạnh bằng 5, chia cho a Sức mạnh của hai. Miễn là nó chia đều. (... hoặc số chia có thể là 1;) trong trường hợp bạn chỉ cần nhân với 2 hoặc 5)

Ví dụ: để đi từ 625 đến 640, nhân với 5 ** 4/2 ** 7. Hoặc, nói chung, nhân với một số giá trị của 2 ** m * 5 ** nmột số m, n trong đó một là dương và một là âm hoặc bằng 0 và số nhân chia số đồng đều.

Bây giờ, phần khó khăn là tìm số nhân. Nhưng chúng ta biết a) số chia phải chia đều số, b) số nhân phải lớn hơn một (số tiếp tục tăng) và c) nếu chúng ta chọn hệ số nhân thấp nhất lớn hơn 1 (tức là 1 <f <tất cả các f khác ), sau đó được đảm bảo là bước tiếp theo của chúng tôi. Bước sau đó sẽ là bước thấp nhất.

Phần khó chịu là tìm giá trị của m, n. Chỉ có các khả năng đăng nhập (n), vì chỉ có rất nhiều 2 hoặc 5 để từ bỏ, nhưng tôi đã phải thêm một yếu tố -1 đến +1 như một cách cẩu thả để đối phó với vòng đấu. Vì vậy, chúng tôi chỉ phải lặp qua O (log (n)) mỗi bước. Vì vậy, tổng thể O (n log (n)).

Tin tốt là, bởi vì nó lấy một giá trị và tìm giá trị tiếp theo, bạn có thể bắt đầu ở bất cứ đâu trong chuỗi. Vì vậy, nếu bạn muốn số tiếp theo sau 1 tỷ, bạn chỉ có thể tìm thấy nó bằng cách lặp qua 2/5 hoặc 5/2 và chọn hệ số nhân nhỏ nhất lớn hơn 1.

(trăn)

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

Tôi đã xác thực 10.000 số đầu tiên mà số này tạo ra so với 10.000 số đầu tiên được tạo bởi giải pháp danh sách được sắp xếp và nó hoạt động ít nhất cho đến nay.

BTW tiếp theo sau một nghìn tỷ dường như là 1.024.000.000.000.

...

Hừm. Tôi có thể nhận hiệu suất O (n) - O (1) trên mỗi giá trị (!) - và sử dụng bộ nhớ O (log n) bằng cách xử lý best()như một bảng tra cứu mà tôi mở rộng tăng dần. Ngay bây giờ, nó tiết kiệm bộ nhớ bằng cách lặp lại mỗi lần, nhưng nó đang thực hiện rất nhiều phép tính dự phòng. Bằng cách giữ các giá trị trung gian đó - và một danh sách các giá trị tối thiểu - tôi có thể tránh được công việc trùng lặp & tăng tốc nó lên rất nhiều. Tuy nhiên, danh sách các giá trị trung gian sẽ phát triển với n, do đó bộ nhớ O (log n).


Câu trả lời chính xác. Tôi có ý tưởng tương tự mà tôi chưa mã hóa. Trong ý tưởng này, tôi giữ một trình theo dõi cho 2 và 5. Điều này sẽ theo dõi mức tối đa nmđã được sử dụng thông qua các con số trong chuỗi cho đến nay. Ở mỗi lần lặp, nhoặc mcó thể hoặc không thể đi lên. Chúng tôi tạo một số mới khi 2^(max_n+1)*5^(max_m+1)sau đó giảm số này theo cách đệ quy toàn diện, mỗi cuộc gọi giảm số mũ xuống 1 cho đến khi chúng tôi nhận được mức tối thiểu lớn hơn số hiện tại. Chúng tôi cập nhật max_n, max_mkhi cần thiết. Đây là mem liên tục. Có thể là O(log^2(n))mem nếu bộ đệm DP được sử dụng trong cuộc gọi giảm
InformedA

Hấp dẫn. Tối ưu hóa ở đây là không cần phải xem xét tất cả các cặp m & n, bởi vì chúng ta biết m, n sẽ tạo ra hệ số nhân gần nhất với 1. Vì vậy, tôi chỉ cần đánh giá m = -i với max_i và tôi chỉ có thể tính n, ném vào một số rác để làm tròn (Tôi đã cẩu thả và chỉ lặp -1 đến 1, nhưng nó mang nhiều suy nghĩ hơn;)).
Rob

Tuy nhiên, tôi có suy nghĩ giống như bạn ... trình tự sẽ mang tính quyết định ... nó thực sự giống như một tam giác Pascal lớn i + 1 theo một hướng và j + 1 theo hướng khác. Vì vậy, trình tự nên được xác định theo toán học. Đối với bất kỳ nút nào trong tam giác, sẽ luôn có một nút tiếp theo được xác định theo toán học.
Rob

1
Có thể có một công thức cho cái tiếp theo, chúng ta có thể không cần phải tìm kiếm. Tôi không biết chắc chắn.
Được thông báo vào

Khi tôi nghĩ về nó, dạng đại số của cái tiếp theo có thể không tồn tại (không phải tất cả các bài toán xác định đều có dạng đại số cho các giải pháp) ngoài ra khi có nhiều số nguyên tố hơn chỉ 2 và 5, công thức có thể khá khó tìm nếu một thực sự muốn làm việc ra công thức này. Nếu ai đó biết công thức, có lẽ tôi sẽ đọc về nó một chút, điều này nghe có vẻ thú vị.
Thông báo

2

Brian hoàn toàn đúng - câu trả lời khác của tôi quá phức tạp. Đây là một cách đơn giản và nhanh hơn để làm điều đó.

Hãy tưởng tượng Quadrant I của mặt phẳng Euclide, giới hạn trong các số nguyên. Gọi một trục là trục i và trục kia là trục j.

Rõ ràng, các điểm gần điểm gốc sẽ được chọn trước các điểm ở xa điểm gốc. Cũng lưu ý rằng khu vực hoạt động sẽ di chuyển ra khỏi trục i trước khi nó di chuyển ra khỏi trục j.

Khi một điểm đã được sử dụng, nó sẽ không bao giờ được sử dụng lại. Và một điểm chỉ có thể được sử dụng nếu điểm ngay bên dưới nó hoặc bên trái của điểm đó đã được sử dụng.

Đặt những thứ này lại với nhau, bạn có thể tưởng tượng một "biên giới" hoặc "cạnh đầu" bắt đầu xung quanh gốc tọa độ và chúng lan rộng lên và sang phải, trải dọc theo trục i xa hơn trên trục j.

Trong thực tế, chúng ta có thể tìm ra một cái gì đó nhiều hơn: sẽ có nhiều nhất một điểm trên biên giới / cạnh cho bất kỳ giá trị i nào. (Bạn phải tăng i hơn 2 lần để tăng bằng j.) Vì vậy, chúng ta có thể biểu diễn biên giới dưới dạng danh sách chứa một phần tử cho mỗi tọa độ i, chỉ thay đổi theo tọa độ j và giá trị hàm.

Mỗi lần vượt qua, chúng tôi chọn phần tử tối thiểu trên cạnh đầu, và sau đó di chuyển nó theo hướng j một lần. Nếu chúng ta tình cờ nâng phần tử cuối cùng, chúng ta thêm vào phần tử mới cuối cùng với giá trị i tăng và giá trị j là 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;
            }
        }
    }
}

Không gian: O (n) về số lượng phần tử được in cho đến nay.

Tốc độ: O (1) chèn, nhưng chúng không được thực hiện mỗi lần. (Đôi khi lâu hơn khi List<>phải phát triển, nhưng vẫn bị khấu hao (1). Thời gian lớn là tìm kiếm tối thiểu, O (n) về số lượng phần tử được in cho đến nay.


1
Thuật toán này sử dụng thuật toán gì? Tại sao nó hoạt động? Phần quan trọng của câu hỏi đang được hỏi là Does anyone have advice on how to solve such a problem?trong nỗ lực để hiểu được vấn đề tiềm ẩn. Một bãi chứa mã không trả lời tốt câu hỏi đó.

Điểm tốt, tôi giải thích suy nghĩ của tôi.
Người tuyết

+1 Trong khi điều này gần tương đương với đoạn trích thứ hai của tôi, việc bạn sử dụng các cạnh không thay đổi sẽ làm cho nó rõ ràng hơn về cách số lượng cạnh tăng lên.
Brian

Điều này chắc chắn chậm hơn đoạn trích đã được sửa đổi của Brian, nhưng hành vi sử dụng bộ nhớ của nó sẽ tốt hơn rất nhiều vì nó không liên tục xóa và thêm các yếu tố. (Trừ khi CLR hoặc Sắp xếp <> có một số phương pháp sử dụng lại các yếu tố mà tôi không biết)
Snowbody

1

Giải pháp dựa trên tập hợp có lẽ là điều mà người phỏng vấn của bạn đang tìm kiếm, tuy nhiên, nó có hậu quả đáng tiếc là có O(n)bộ nhớ và O(n lg n)tổng thời gian để sắp xếp ncác yếu tố.

Một chút toán học giúp chúng ta tìm ra giải pháp O(1)không gian và O(n sqrt(n))thời gian. Lưu ý rằng 2^i * 5^j = 2^(i + j lg 5). Tìm các nphần tử đầu tiên của {i,j > 0 | 2^(i + j lg 5)}giảm để tìm các nphần tử đầu tiên {i,j > 0 | i + j lg 5}vì hàm (x -> 2^x)này hoàn toàn tăng đơn điệu, vì vậy cách duy nhất cho một số a,bđó 2^a < 2^blà nếu a < b.

Bây giờ, chúng ta chỉ cần một thuật toán để tìm chuỗi i + j lg 5, i,jsố tự nhiên ở đâu . Nói cách khác, với giá trị hiện tại của chúng tôi i, j, những gì giảm thiểu bước tiếp theo (nghĩa là cho chúng tôi số tiếp theo trong chuỗi), là một số tăng trong một trong các giá trị (nói j += 1) cùng với việc giảm giá trị khác ( i -= 2). Điều duy nhất hạn chế chúng tôi là điều đó i,j > 0.

Chỉ có hai trường hợp để xem xét - ităng hoặc jtăng. Một trong số chúng phải tăng vì trình tự của chúng tôi đang tăng và cả hai đều không tăng vì nếu không, chúng tôi bỏ qua thuật ngữ mà chúng tôi chỉ có một i,jmức tăng. Do đó, một cái tăng, và cái kia giữ nguyên hoặc giảm. Được thể hiện trong C ++ 11, toàn bộ thuật toán và so sánh của nó với giải pháp thiết lập có sẵn ở đây .

Điều này đạt được bộ nhớ không đổi vì chỉ có một lượng đối tượng được phân bổ trong phương thức, ngoài mảng đầu ra (xem liên kết). Phương thức đạt được thời gian logarit mỗi lần lặp vì đối với bất kỳ lần nào (i,j), nó đi qua cặp tốt nhất (a, b)sao cho (i + a, j + b)giá trị tăng nhỏ nhất i + j lg 5. Truyền tải này là 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)

Mỗi lần lặp lại cố gắng cập nhật i, sau đó j, và đi với bản cập nhật nhỏ hơn của cả hai.

ijnhiều nhất O(sqrt(n)), chúng tôi có tổng O(n sqrt(n))thời gian. ijtăng theo tỷ lệ bình phương nvì đối với bất kỳ giá trị tối đa nào imaxjmaxtồn tại O(i j)các cặp duy nhất để tạo chuỗi nếu chuỗi của chúng là ncác số hạng ijphát triển trong một số yếu tố không đổi của nhau (vì số mũ bao gồm một tuyến tính kết hợp cho cả hai), chúng tôi biết rằng ijđược O(sqrt(n)).

Không có quá nhiều lo lắng về lỗi dấu phẩy động - vì các thuật ngữ tăng theo cấp số nhân, chúng tôi sẽ phải xử lý lỗi tràn trước khi lỗi flop bắt kịp chúng tôi, theo một số cường độ. Tôi sẽ thêm thảo luận về vấn đề này nếu tôi có thời gian.


câu trả lời tuyệt vời, tôi nghĩ đó cũng là một mô hình trong việc tăng trình tự cho bất kỳ số nguyên tố nào
InformedA

@randomA Cảm ơn. Sau khi suy nghĩ thêm, tôi đã đi đến kết luận rằng hiện tại thuật toán của tôi không nhanh như tôi nghĩ. Nếu có cách đánh giá nhanh hơn "Cố gắng tăng i / j", tôi nghĩ đó là chìa khóa để có được thời gian logarit.
VF1

Tôi đã suy nghĩ ở chỗ: chúng ta biết rằng để tăng số lượng, chúng ta phải tăng số lượng của một trong các số nguyên tố. Ví dụ, một cách để tăng là mul với 8 và chia cho 5. Vì vậy, chúng ta có được tất cả các cách để tăng và giảm số lượng. Điều này sẽ chỉ chứa các cách cơ bản như mul 8 div 5 chứ không phải mul 16 div 5. Ngoài ra còn có một số cách cơ bản khác để giảm. Sắp xếp hai bộ này theo hệ số tăng hoặc giảm của chúng. Cho một số, có thể tìm thấy số tiếp theo bằng cách tìm cách tăng có thể áp dụng với hệ số nhỏ nhất từ ​​tập tăng ..
InformedA

.. áp dụng có nghĩa là có đủ số nguyên tố để thực hiện mul và div. Sau đó, chúng tôi tìm thấy một cách giảm đến số mới, vì vậy bắt đầu với một cách giảm nhiều nhất. Tiếp tục sử dụng những cách mới để giảm và chúng tôi dừng lại khi số mới nhỏ hơn số ban đầu. Vì tập hợp các số nguyên là không đổi, điều này có nghĩa là kích thước không đổi cho hai bộ. Điều này cũng cần một chút bằng chứng, nhưng có vẻ như thời gian không đổi, bộ nhớ không đổi ở mỗi số đối với tôi. Vì vậy, bộ nhớ không đổi và thời gian tuyến tính để in n số.
Được thông báo vào

@randomA bạn lấy phân chia từ đâu? Bạn có phiền khi đưa ra một câu trả lời đầy đủ - tôi không hiểu ý kiến ​​của bạn.
VF1
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.