Câu hỏi phỏng vấn dễ trở nên khó hơn: đưa ra các số 1..100, tìm (các) số bị thiếu cho chính xác k bị thiếu


1146

Tôi đã có một kinh nghiệm phỏng vấn công việc thú vị một thời gian trở lại. Câu hỏi bắt đầu thực sự dễ dàng:

Q1 : Chúng tôi có một túi chứa các số 1, 2, 3, ..., 100. Mỗi số xuất hiện chính xác một lần, vì vậy có 100 số. Bây giờ một số được chọn ngẫu nhiên ra khỏi túi. Tìm số còn thiếu.

Tôi đã nghe câu hỏi phỏng vấn này trước đây, tất nhiên, vì vậy tôi đã nhanh chóng trả lời dọc theo dòng:

A1 : Chà, tổng của các số 1 + 2 + 3 + … + N(N+1)(N/2)(xem Wikipedia: tổng của chuỗi số học ). Đối với N = 100, tổng là 5050.

Do đó, nếu tất cả các số có mặt trong túi, tổng sẽ chính xác 5050. Vì một số bị thiếu, tổng sẽ nhỏ hơn số này và sự khác biệt là số đó. Vì vậy, chúng ta có thể tìm thấy số còn thiếu trong O(N)thời gian và O(1)không gian.

Tại thời điểm này, tôi nghĩ rằng tôi đã làm tốt, nhưng đột nhiên câu hỏi đã có một bước ngoặt bất ngờ:

Q2 : Điều đó là chính xác, nhưng bây giờ bạn sẽ làm điều này như thế nào nếu thiếu số HAI ?

Tôi chưa bao giờ thấy / nghe / xem xét biến thể này trước đây, vì vậy tôi đã hoảng loạn và không thể trả lời câu hỏi. Người phỏng vấn khăng khăng muốn biết quá trình suy nghĩ của tôi, vì vậy tôi đã đề cập rằng có lẽ chúng ta có thể nhận được nhiều thông tin hơn bằng cách so sánh với sản phẩm dự kiến ​​hoặc có thể thực hiện lần thứ hai sau khi thu thập một số thông tin từ lượt đầu tiên, v.v., nhưng tôi thực sự chỉ đang bắn trong bóng tối hơn là thực sự có một con đường rõ ràng cho giải pháp.

Người phỏng vấn đã cố gắng khuyến khích tôi bằng cách nói rằng có một phương trình thứ hai thực sự là một cách để giải quyết vấn đề. Tại thời điểm này, tôi cảm thấy khó chịu (vì không biết câu trả lời trước khi ra tay) và hỏi liệu đây có phải là một kỹ thuật lập trình chung (đọc: "hữu ích") không, hoặc nếu đó chỉ là một câu trả lời lừa / gotcha.

Câu trả lời của người phỏng vấn làm tôi ngạc nhiên: bạn có thể khái quát kỹ thuật để tìm 3 số còn thiếu. Trong thực tế, bạn có thể khái quát nó để tìm k thiếu số.

Qk : Nếu chính xác số k bị thiếu trong túi, làm thế nào bạn tìm thấy nó hiệu quả?

Đây là một vài tháng trước, và tôi vẫn không thể hiểu được kỹ thuật này là gì. Rõ ràng có một Ω(N)thời gian giới hạn thấp hơn kể từ khi chúng ta phải quét tất cả các con số ít nhất một lần, nhưng người phỏng vấn khẳng định rằng LÚC NÀOSPACE phức tạp của kỹ thuật giải quyết (trừ đi O(N)thời gian đầu vào quét) được định nghĩa trong k không tồn .

Vì vậy, câu hỏi ở đây là đơn giản:

  • Làm thế nào bạn sẽ giải quyết quý 2 ?
  • Làm thế nào bạn sẽ giải quyết quý 3 ?
  • Làm thế nào bạn sẽ giải quyết Qk ?

Làm rõ

  • Nói chung có N số từ 1 .. N , không chỉ 1..100.
  • Tôi không tìm kiếm giải pháp dựa trên tập rõ ràng, ví dụ: sử dụng tập bit , mã hóa sự hiện diện / vắng mặt của mỗi số bằng giá trị của bit được chỉ định, do đó sử dụng O(N)bit trong không gian bổ sung. Chúng ta không thể đủ khả năng bất kỳ tỷ lệ không gian bổ sung cho N .
  • Tôi cũng không tìm kiếm cách tiếp cận đầu tiên rõ ràng. Điều này và cách tiếp cận dựa trên tập hợp đáng được đề cập trong một cuộc phỏng vấn (chúng rất dễ thực hiện, và tùy thuộc vào N , có thể rất thực tế). Tôi đang tìm kiếm giải pháp Chén Thánh (có thể có hoặc không thực tế để thực hiện, tuy nhiên vẫn có các đặc điểm tiệm cận mong muốn).

Vì vậy, một lần nữa, tất nhiên bạn phải quét đầu vào O(N), nhưng bạn chỉ có thể nắm bắt một lượng nhỏ thông tin (được xác định theo k không phải N ), và sau đó phải tìm k thiếu số.


7
@polygenelubricants Cảm ơn bạn đã làm rõ. "Tôi đang tìm kiếm một thuật toán sử dụng không gian O (N) và không gian O (K) trong đó K là số lượng vắng mặt" sẽ rõ ràng ngay từ đầu ;-)
Dave O.

7
Bạn nên chính xác, trong tuyên bố của Q1 rằng bạn không thể truy cập các số theo thứ tự. Điều này có vẻ rõ ràng đối với bạn, nhưng tôi chưa bao giờ nghe về câu hỏi và thuật ngữ "túi" (có nghĩa là "multiset") cũng khá khó hiểu.
Jérémie

7
Vui lòng đọc phần sau đây vì các câu trả lời được cung cấp ở đây thật lố bịch: stackoverflow.com/questions/4406110/

18
Giải pháp tính tổng các số yêu cầu không gian log (N) trừ khi bạn xem xét yêu cầu không gian cho một số nguyên không giới hạn là O (1). Nhưng nếu bạn cho phép các số nguyên không giới hạn, thì bạn có nhiều không gian như bạn muốn chỉ với một số nguyên.
Udo Klein

3
Bằng cách này, giải pháp thay thế khá hay cho Q1 có thể là tính toán XORtất cả các số từ 1đến n, sau đó xending kết quả với tất cả các số trong mảng đã cho. Cuối cùng, bạn có số còn thiếu của bạn. Trong giải pháp này, bạn không cần quan tâm đến việc tràn như khi tóm tắt.
sbeliakov

Câu trả lời:


590

Dưới đây là tóm tắt về liên kết của Dimitris Andreou .

Hãy nhớ tổng các lũy thừa thứ i, trong đó i = 1,2, .., k. Điều này làm giảm vấn đề để giải hệ phương trình

a 1 + a 2 + ... + a k = b 1

a 1 2 + a 2 2 + ... + a k 2 = b 2

...

a 1 k + a 2 k + ... + a k k = b k

Sử dụng danh tính của Newton , biết b i cho phép tính toán

c 1 = a 1 + a 2 + ... a k

c 2 = a 1 a 2 + a 1 a 3 + ... + a k - 1 a k

...

c k = a 1 a 2 ... a k

Nếu bạn mở rộng đa thức (xa 1 ) ... (xa k ), các hệ số sẽ chính xác là c 1 , ..., c k - xem công thức của Viète . Vì mọi yếu tố đa thức duy nhất (vòng đa thức là một miền Euclide ), điều này có nghĩa là một i được xác định duy nhất, cho đến khi hoán vị.

Điều này kết thúc một bằng chứng rằng việc ghi nhớ sức mạnh là đủ để phục hồi các con số. Đối với hằng số k, đây là một cách tiếp cận tốt.

Tuy nhiên, khi k thay đổi, cách tiếp cận trực tiếp của máy tính c 1 , ..., c k rất tốn kém, vì c k là sản phẩm của tất cả các số bị thiếu, cường độ n! / (Nk)!. Để khắc phục điều này, thực hiện phép tính trong Z q lĩnh vực , trong đó q là số nguyên tố sao cho n <= q <2n - nó tồn tại bởi Định đề Bertrand . Bằng chứng không cần phải thay đổi, vì các công thức vẫn giữ và hệ số của đa thức vẫn là duy nhất. Bạn cũng cần một thuật toán để nhân tố hóa trên các trường hữu hạn, ví dụ như thuật toán của Berlekamp hoặc Cantor-Zassenhaus .

Mã giả cấp cao cho hằng số k:

  • Tính năng lượng thứ i của các số đã cho
  • Trừ đi để có được tổng số quyền hạn thứ i của các số chưa biết. Gọi các khoản tiền b i .
  • Sử dụng danh tính của Newton để tính các hệ số từ b i ; gọi họ là tôi . Về cơ bản, c 1 = b 1 ; c 2 = (c 1 b 1 - b 2 ) / 2; xem Wikipedia để biết công thức chính xác
  • Hệ số đa thức x k -c 1 x k - 1 + ... + c k .
  • Nguồn gốc của đa thức là các số cần thiết a 1 , ..., a k .

Để thay đổi k, hãy tìm một số nguyên tố n <= q <2n bằng cách sử dụng ví dụ Miller-Rabin và thực hiện các bước với tất cả các số giảm modulo q.

EDIT: Phiên bản trước của câu trả lời này đã nói rằng thay vì Z q , trong đó q là số nguyên tố, có thể sử dụng trường hữu hạn của đặc tính 2 (q = 2 ^ (log n)). Đây không phải là trường hợp, vì các công thức của Newton yêu cầu chia cho số lên đến k.


6
Bạn không phải sử dụng trường chính, bạn cũng có thể sử dụng q = 2^(log n). (Làm thế nào bạn tạo ra siêu và đăng ký?!)
Heinrich Apfelmus

49
+1 Điều này thực sự, thực sự thông minh. Đồng thời, điều đáng nghi ngờ là liệu nó có thực sự đáng nỗ lực hay không (liệu một phần) giải pháp này cho một vấn đề khá giả tạo có thể được sử dụng lại theo cách khác. Và ngay cả khi đây là một vấn đề trong thế giới thực, trên nhiều nền tảng, O(N^2)giải pháp tầm thường nhất có thể sẽ vượt trội hơn vẻ đẹp này thậm chí còn cao N. Làm cho tôi nghĩ về điều này: tinyurl.com/c8fwgw Tuy nhiên, công việc tuyệt vời! Tôi sẽ không đủ kiên nhẫn để bò qua tất cả các phép toán :)
back2dos

167
Tôi nghĩ rằng đây là một câu trả lời tuyệt vời. Tôi nghĩ rằng điều này cũng minh họa mức độ nghèo nàn của một câu hỏi phỏng vấn sẽ mở rộng những con số còn thiếu vượt quá một. Ngay cả lần đầu tiên là một loại gotchya, nhưng nó đủ phổ biến đến nỗi về cơ bản nó cho thấy "bạn đã thực hiện một số chuẩn bị phỏng vấn." Nhưng để mong đợi một chuyên viên CS biết vượt quá k = 1 (đặc biệt là "tại chỗ" trong một cuộc phỏng vấn) thì hơi ngớ ngẩn.
corsiKa

5
Điều này có hiệu quả thực hiện mã hóa Solomon Solomon trên đầu vào.
David Ehrmann

78
Tôi đặt cược nhập tất cả các số trong một hash setvà lặp lại 1...Nbộ phần mềm bằng cách sử dụng tra cứu để xác định xem có thiếu số nào không, sẽ là cách chung nhất, trung bình nhanh nhất về kcác biến thể, giải pháp dễ hiểu nhất có thể gỡ lỗi và dễ hiểu nhất. Tất nhiên cách toán học rất ấn tượng nhưng đâu đó trên đường bạn cần trở thành một kỹ sư chứ không phải là một nhà toán học. Đặc biệt là khi kinh doanh có liên quan.
v.oddou

243

Bạn sẽ tìm thấy nó bằng cách đọc vài trang của Muthukrishnan - Thuật toán luồng dữ liệu: Câu đố 1: Tìm số bị thiếu . Nó cho thấy chính xác khái quát mà bạn đang tìm kiếm . Có lẽ đây là những gì người phỏng vấn của bạn đọc và tại sao anh ta đặt ra những câu hỏi này.

Bây giờ, nếu chỉ có mọi người sẽ bắt đầu xóa các câu trả lời bị thay thế hoặc thay thế bằng cách đối xử của Muthukrishnan, và làm cho văn bản này dễ tìm thấy hơn. :)


Ngoài ra, hãy xem câu trả lời liên quan trực tiếp của sdcvvc , cũng bao gồm mã giả (Hurrah! Không cần phải đọc các công thức toán khó khăn đó :)) (cảm ơn, công việc tuyệt vời!).


Ôi ... Thật thú vị. Tôi phải thừa nhận rằng tôi đã có một chút bối rối bởi các toán học nhưng tôi đã lướt qua nó. Có thể để nó mở để xem xét thêm sau này. :) Và +1 để có được liên kết này dễ tìm thấy hơn. ;-)
Chris

2
Liên kết sách google không hoạt động đối với tôi. Đây là một phiên bản tốt hơn [Tệp PostScript].
Apfelmus Heinrich

9
Ồ Tôi không mong đợi điều này sẽ được nâng cấp! Lần trước tôi đã đăng một tài liệu tham khảo về giải pháp (Knuth's, trong trường hợp đó) thay vì cố gắng tự giải quyết, nó thực sự bị hạ cấp: stackoverflow.com/questions/3060104/ . Người thủ thư trong tôi vui mừng, cảm ơn :)
Dimitris Andreou

@Apfelmus, lưu ý rằng đây là bản nháp. (Tất nhiên tôi không đổ lỗi cho bạn, tôi đã nhầm lẫn bản nháp cho những thứ thực sự trong gần một năm trước khi tìm thấy cuốn sách). Btw nếu liên kết không hoạt động, bạn có thể truy cập Books.google.com và tìm kiếm "thuật toán luồng dữ liệu Muthukrishnan" (không có dấu ngoặc kép), đây là lần đầu tiên bật lên.
Dimitris Andreou

2
Vui lòng đọc phần sau đây vì các câu trả lời được cung cấp ở đây thật lố bịch: stackoverflow.com/questions/4406110/

174

Chúng ta có thể giải quyết Q2 bằng cách tự tổng hợp cả hai số và bình phương của các số.

Sau đó chúng ta có thể giảm vấn đề xuống

k1 + k2 = x
k1^2 + k2^2 = y

Ở đâu xybao xa các khoản tiền dưới giá trị dự kiến.

Thay thế cho chúng tôi:

(x-k2)^2 + k2^2 = y

Mà sau đó chúng ta có thể giải quyết để xác định số còn thiếu của chúng tôi.


7
+1; Tôi đã thử công thức trong Maple cho các số được chọn và nó hoạt động. Tôi vẫn không thể thuyết phục bản thân mình TẠI SAO nó hoạt động.
đa gen

4
@polygenelubricants: Nếu bạn muốn chứng minh tính đúng đắn, trước tiên bạn sẽ cho thấy rằng nó luôn cung cấp một giải pháp chính xác (nghĩa là nó luôn tạo ra một cặp số mà khi xóa chúng khỏi tập hợp, sẽ dẫn đến phần còn lại của tập hợp có tổng quan sát và tổng bình phương). Từ đó, việc chứng minh tính duy nhất đơn giản như thể hiện rằng nó chỉ tạo ra một cặp số như vậy.
Anon.

5
Bản chất của các phương trình có nghĩa là bạn sẽ nhận được hai giá trị của k2 từ phương trình đó. Tuy nhiên, từ phương trình đầu tiên mà bạn sử dụng để tạo k1, bạn có thể thấy rằng hai giá trị này của k2 sẽ có nghĩa là k1 là giá trị khác để bạn có hai giải pháp có cùng số ngược nhau. Nếu bạn khai báo một cách bất thường rằng k1> k2 thì bạn chỉ có một giải pháp cho phương trình bậc hai và do đó, một giải pháp tổng thể. Và rõ ràng bởi bản chất của câu hỏi, một câu trả lời luôn tồn tại để nó luôn hoạt động.
Chris

3
Với một tổng k1 + k2 đã cho, có nhiều cặp. Chúng ta có thể viết các cặp này là K1 = a + b và K2 = ab trong đó a = (K1 + k2 / 2). a là duy nhất cho một khoản tiền nhất định. Tổng bình phương (a + b) ** 2 + (ab) ** 2 = 2 * (a 2 + b 2). Đối với một tổng K1 + K2 đã cho, một số hạng 2 được cố định và chúng ta thấy rằng tổng bình phương sẽ là duy nhất do số hạng b 2. Do đó, các giá trị x và y là duy nhất cho một cặp số nguyên.
phkahler

8
Điều này thật tuyệt. @ user3281743 đây là một ví dụ. Đặt các số còn thiếu (k1 và k2) là 4 và 6. Tổng (1 -> 10) = 55 và Tổng (1 ^ 2 -> 10 ^ 2) = 385. Bây giờ hãy x = 55 - (Tổng (Tất cả các số còn lại )) và y = 385 - (Tổng (bình phương của tất cả các số còn lại)) do đó x = 10 và y = 52. Thay thế như được hiển thị để lại cho chúng tôi: (10 - k2) ^ 2 + k2 ^ 2 = 52 mà bạn có thể đơn giản hóa thành: 2k ^ 2 - 20k + 48 = 0. Giải phương trình bậc hai cho bạn 4 và 6 là câu trả lời.
AlexKoren

137

Như @j_random_hacker đã chỉ ra, điều này khá giống với Tìm kiếm các bản sao trong không gian thời gian O (n) và O (1) , và việc điều chỉnh câu trả lời của tôi cũng hoạt động ở đây.

Giả sử rằng "túi" được biểu thị bằng một mảng A[]kích thước dựa trên 1 N - k, chúng ta có thể giải quyết Qk trong O(N)thời gian và O(k)không gian bổ sung.

Đầu tiên, chúng tôi mở rộng mảng của chúng tôi A[]bằng kcác phần tử, để bây giờ nó có kích thước N. Đây là O(k)không gian bổ sung. Sau đó chúng tôi chạy thuật toán mã giả sau đây:

for i := n - k + 1 to n
    A[i] := A[1]
end for

for i := 1 to n - k
    while A[A[i]] != A[i] 
        swap(A[i], A[A[i]])
    end while
end for

for i := 1 to n
    if A[i] != i then 
        print i
    end if
end for

Vòng lặp đầu tiên khởi tạo các kmục nhập bổ sung giống như mục nhập đầu tiên trong mảng (đây chỉ là một giá trị thuận tiện mà chúng ta biết đã có trong mảng - sau bước này, bất kỳ mục nào bị thiếu trong mảng kích thước ban đầu N-klà vẫn còn thiếu trong mảng mở rộng).

Vòng lặp thứ hai hoán vị mảng mở rộng để nếu phần tử xcó mặt ít nhất một lần, thì một trong những mục đó sẽ ở vị trí A[x].

Lưu ý rằng mặc dù nó có một vòng lặp lồng nhau, nó vẫn chạy O(N)đúng lúc - một sự hoán đổi chỉ xảy ra nếu có một iđiều đó A[i] != ivà mỗi lần hoán đổi đặt ra ít nhất một yếu tố sao cho A[i] == iđiều đó không đúng trước đây. Điều này có nghĩa là tổng số lần hoán đổi (và do đó tổng số lần thực hiện của whilethân vòng lặp) là nhiều nhất N-1.

Vòng lặp thứ ba in các chỉ mục của mảng ikhông bị chiếm bởi giá trị i- điều này có nghĩa là iphải bị thiếu.


4
Tôi tự hỏi tại sao rất ít người bỏ phiếu cho câu trả lời này và thậm chí không đánh dấu nó là một câu trả lời đúng. Đây là mã trong Python. Nó chạy trong thời gian O (n) và cần thêm không gian O (k). pastebin.com/9jZqnTzV
wall-e

3
@caf điều này khá giống với việc thiết lập các bit và đếm các vị trí có bit là 0. Và tôi nghĩ khi bạn đang tạo ra một mảng số nguyên thì sẽ chiếm nhiều bộ nhớ hơn.
Fox

5
"Đặt bit và đếm các vị trí có bit bằng 0" yêu cầu thêm không gian O (n), giải pháp này cho biết cách sử dụng không gian thừa O (k).
phê

7
Không hoạt động với các luồng làm đầu vào và sửa đổi mảng đầu vào (mặc dù tôi rất thích nó và ý tưởng rất hiệu quả).
comco

3
@ v.oddou: Không, không sao đâu. Việc hoán đổi sẽ thay đổi A[i], điều đó có nghĩa là lần lặp tiếp theo sẽ không so sánh hai giá trị giống như lần trước. Cái mới A[i]sẽ giống như vòng lặp cuối cùng A[A[i]], nhưng cái mới A[A[i]]sẽ là một giá trị mới . Hãy thử nó và xem.
phê

128

Tôi yêu cầu một đứa trẻ 4 tuổi giải quyết vấn đề này. Anh ta sắp xếp các con số và sau đó đếm theo. Điều này có một yêu cầu không gian của O (sàn bếp), và nó hoạt động dễ dàng như vậy tuy nhiên nhiều quả bóng bị thiếu.


20
;) 4 tuổi của bạn phải tiếp cận 5 hoặc / và là một thiên tài. Con gái 4 tuổi của tôi thậm chí không thể đếm đúng đến 4. cũng công bằng mà nói, cuối cùng cô ấy chỉ vừa đủ tích hợp sự tồn tại của "4". nếu không cho đến bây giờ cô sẽ luôn bỏ qua nó. "1,2,3,5,6,7" là trình tự đếm thông thường của cô ấy. Tôi yêu cầu cô ấy thêm bút chì lại với nhau và cô ấy sẽ quản lý 1 + 2 = 3 bằng cách làm lại tất cả một lần nữa từ đầu. Tôi thực sự lo lắng ...: '(meh ..
v.oddou

cách tiếp cận đơn giản nhưng hiệu quả.
PabTorre

6
O (sàn bếp) haha ​​- nhưng đó không phải là O (n ^ 2) sao?

13
O (m2) tôi đoán :)
Viktor Mellgren

1
@phuclv: câu trả lời cho biết "Điều này có yêu cầu về không gian của O (sàn bếp)". Nhưng trong mọi trường hợp, đây là một trường hợp mà việc sắp xếp thể đạt được trong thời gian O (n) --- xem cuộc thảo luận này .
Anthony Labarre

36

Không chắc chắn, nếu đó là giải pháp hiệu quả nhất, nhưng tôi sẽ lặp qua tất cả các mục và sử dụng một bitet để ghi nhớ, những số nào được đặt, sau đó kiểm tra 0 bit.

Tôi thích các giải pháp đơn giản - và tôi thậm chí tin rằng, nó có thể nhanh hơn việc tính tổng, hoặc tổng bình phương, v.v.


11
Tôi đã đề xuất câu trả lời rõ ràng này, nhưng đây không phải là điều người phỏng vấn muốn. Tôi đã nói rõ ràng trong câu hỏi rằng đây không phải là câu trả lời tôi đang tìm kiếm. Một câu trả lời rõ ràng khác: sắp xếp đầu tiên. Cả O(N)loại sắp xếp đếm cũng không phải loại O(N log N)so sánh là những gì tôi đang tìm kiếm, mặc dù cả hai đều là những giải pháp rất đơn giản.
đa gen

@polygenelubricants: Tôi không thể tìm thấy nơi bạn nói điều đó trong câu hỏi của bạn. Nếu bạn coi bitet là kết quả, thì không có lần thứ hai. Độ phức tạp là (nếu chúng ta coi N là hằng số, như người phỏng vấn gợi ý bằng cách nói rằng độ phức tạp là "được xác định trong k không phải N") O (1) và nếu bạn cần xây dựng một kết quả "sạch" hơn, bạn lấy O (k), đây là mức tốt nhất bạn có thể nhận được, bởi vì bạn luôn cần O (k) để tạo kết quả sạch.
Chris Lercher

"Lưu ý rằng tôi không tìm kiếm giải pháp dựa trên tập rõ ràng (ví dụ: sử dụng tập bit". Đoạn cuối thứ hai từ câu hỏi ban đầu.
hrnt

9
@hmt: Vâng, câu hỏi đã được chỉnh sửa vài phút trước. Tôi chỉ đưa ra câu trả lời mà tôi mong đợi từ một người được phỏng vấn ... Xây dựng một giải pháp tối ưu phụ (bạn không thể đánh bại thời gian O (n) + O (k), bất kể bạn làm gì) không ' không có ý nghĩa với tôi - ngoại trừ nếu bạn không đủ khả năng cho O (n) không gian bổ sung, nhưng câu hỏi không rõ ràng về điều đó.
Chris Lercher

3
Tôi đã chỉnh sửa câu hỏi một lần nữa để làm rõ hơn. Tôi đánh giá cao phản hồi / câu trả lời.
đa gen

33

Tôi đã không kiểm tra các phép toán, nhưng tôi nghi ngờ rằng việc tính toán Σ(n^2)trong cùng một lượt khi chúng ta tính toán Σ(n)sẽ cung cấp đủ thông tin để có được hai số bị thiếu, Σ(n^3)cũng như nếu có ba, v.v.


15

Vấn đề với các giải pháp dựa trên tổng số là chúng không tính đến chi phí lưu trữ và làm việc với các số có số mũ lớn ... trong thực tế, để nó hoạt động với n rất lớn, một thư viện số lớn sẽ được sử dụng . Chúng ta có thể phân tích việc sử dụng không gian cho các thuật toán này.

Chúng ta có thể phân tích độ phức tạp về thời gian và không gian của các thuật toán của sdcvvc và Dimitris Andreou.

Lưu trữ:

l_j = ceil (log_2 (sum_{i=1}^n i^j))
l_j > log_2 n^j  (assuming n >= 0, k >= 0)
l_j > j log_2 n \in \Omega(j log n)

l_j < log_2 ((sum_{i=1}^n i)^j) + 1
l_j < j log_2 (n) + j log_2 (n + 1) - j log_2 (2) + 1
l_j < j log_2 n + j + c \in O(j log n)`

Vì thế l_j \in \Theta(j log n)

Tổng dung lượng sử dụng: \sum_{j=1}^k l_j \in \Theta(k^2 log n)

Không gian được sử dụng: giả sử rằng máy tính a^jcần có ceil(log_2 j)thời gian, tổng thời gian:

t = k ceil(\sum_i=1^n log_2 (i)) = k ceil(log_2 (\prod_i=1^n (i)))
t > k log_2 (n^n + O(n^(n-1)))
t > k log_2 (n^n) = kn log_2 (n)  \in \Omega(kn log n)
t < k log_2 (\prod_i=1^n i^i) + 1
t < kn log_2 (n) + 1 \in O(kn log n)

Tổng thời gian sử dụng: \Theta(kn log n)

Nếu thời gian và không gian này thỏa đáng, bạn có thể sử dụng thuật toán đệ quy đơn giản. Đặt b! I là mục thứ i trong túi, n số lượng trước khi xóa và k số lần xóa. Trong cú pháp Haskell ...

let
  -- O(1)
  isInRange low high v = (v >= low) && (v <= high)
  -- O(n - k)
  countInRange low high = sum $ map (fromEnum . isInRange low high . (!)b) [1..(n-k)]
  findMissing l low high krange
    -- O(1) if there is nothing to find.
    | krange=0 = l
    -- O(1) if there is only one possibility.
    | low=high = low:l
    -- Otherwise total of O(knlog(n)) time
    | otherwise =
       let
         mid = (low + high) `div` 2
         klow = countInRange low mid
         khigh = krange - klow
       in
         findMissing (findMissing low mid klow) (mid + 1) high khigh
in
  findMising 1 (n - k) k

Lưu trữ được sử dụng: O(k)cho danh sách, O(log(n))cho ngăn xếp: O(k + log(n)) Thuật toán này trực quan hơn, có cùng độ phức tạp thời gian và sử dụng ít không gian hơn.


1
+1, có vẻ tốt nhưng bạn đã mất tôi đi từ dòng 4 đến dòng 5 trong đoạn số 1 - bạn có thể giải thích thêm không? Cảm ơn!
j_random_hacker

isInRangeO (log n) , không phải O (1) : nó so sánh các số trong phạm vi 1..n, vì vậy nó phải so sánh các bit O (log n) . Tôi không biết mức độ lỗi này ảnh hưởng đến phần còn lại của phân tích.
jcsahnwaldt nói GoFundMonica

14

Đợi tí. Như câu hỏi được nêu, có 100 số trong túi. Cho dù k lớn đến đâu, vấn đề có thể được giải quyết trong thời gian không đổi bởi vì bạn có thể sử dụng một tập hợp và loại bỏ các số khỏi tập hợp trong tối đa 100 - k lần lặp của một vòng lặp. 100 là hằng số. Tập hợp các số còn lại là câu trả lời của bạn.

Nếu chúng ta khái quát hóa giải pháp cho các số từ 1 đến N, không có gì thay đổi ngoại trừ N không phải là hằng số, vì vậy chúng ta đang ở trong thời gian O (N - k) = O (N). Chẳng hạn, nếu chúng ta sử dụng một tập bit, chúng ta đặt các bit thành 1 trong thời gian O (N), lặp qua các số, đặt các bit thành 0 khi chúng ta đi (O (Nk) = O (N)) và sau đó chúng ta có câu trả lời

Dường như với tôi rằng người phỏng vấn đã hỏi bạn làm thế nào để in ra nội dung của tập cuối cùng trong thời gian O (k) thay vì thời gian O (N). Rõ ràng, với một tập bit, bạn phải lặp qua tất cả các bit N để xác định xem bạn có nên in số đó hay không. Tuy nhiên, nếu bạn thay đổi cách triển khai tập hợp, bạn có thể in ra các số trong k lần lặp. Điều này được thực hiện bằng cách đặt các số vào một đối tượng sẽ được lưu trữ trong cả bộ băm và danh sách liên kết đôi. Khi bạn xóa một đối tượng khỏi bộ băm, bạn cũng xóa nó khỏi danh sách. Các câu trả lời sẽ được để lại trong danh sách hiện có độ dài k.


9
Câu trả lời này quá đơn giản và tất cả chúng ta đều biết rằng những câu trả lời đơn giản không hiệu quả! ;) Nghiêm túc mà nói, câu hỏi ban đầu có lẽ nên nhấn mạnh yêu cầu không gian O (k).
ĐK.

Vấn đề không đơn giản mà là bạn sẽ phải sử dụng bộ nhớ bổ sung O (n) cho bản đồ. Vấn đề khiến tôi phải giải quyết trong thời gian liên tục và bộ nhớ liên tục
Mojo Risin

3
Tôi cá là bạn có thể chứng minh giải pháp tối thiểu là ít nhất là O (N). bởi vì ít hơn, có nghĩa là bạn thậm chí không NHÌN một số số và vì không có thứ tự nào được chỉ định, nên việc xem TẤT CẢ các số là bắt buộc.
v.oddou

Nếu chúng ta xem đầu vào là một luồng và n quá lớn để giữ trong bộ nhớ, thì yêu cầu bộ nhớ O (k) có ý nghĩa. Chúng ta vẫn có thể sử dụng băm mặc dù: Chỉ cần tạo k ^ 2 thùng và sử dụng thuật toán tổng đơn giản trên mỗi cái. Đó chỉ là k ^ 2 bộ nhớ và một vài thùng nữa có thể được sử dụng để có xác suất thành công cao.
Thomas Ahle

8

Để giải quyết câu hỏi thiếu 2 (và 3) số, bạn có thể sửa đổi quickselect, trung bình chạy vào O(n)và sử dụng bộ nhớ không đổi nếu phân vùng được thực hiện tại chỗ.

  1. Phân vùng tập hợp liên quan đến một trục ngẫu nhiên pthành các phân vùng l, chứa các số nhỏ hơn trục, và r, chứa các số lớn hơn trục.

  2. Xác định phân vùng nào có 2 số bị thiếu bằng cách so sánh giá trị trục với kích thước của từng phân vùng ( p - 1 - count(l) = count of missing numbers in ln - count(r) - p = count of missing numbers in r)

  3. a) Nếu mỗi phân vùng bị thiếu một số, thì hãy sử dụng sự khác biệt của cách tiếp cận tổng để tìm từng số bị thiếu.

    (1 + 2 + ... + (p-1)) - sum(l) = missing #1((p+1) + (p+2) ... + n) - sum(r) = missing #2

    b) Nếu một phân vùng bị thiếu cả hai số và phân vùng trống, thì các số bị thiếu là (p-1,p-2)hoặc(p+1,p+2) tùy thuộc vào phân vùng nào thiếu các số đó.

    Nếu một phân vùng bị thiếu 2 số nhưng không trống, thì hãy lặp lại trên partiton đó.

Chỉ với 2 số bị thiếu, thuật toán này luôn loại bỏ ít nhất một phân vùng, do đó, nó giữ lại O(n)độ phức tạp thời gian trung bình của quickselect. Tương tự, với 3 số bị thiếu, thuật toán này cũng loại bỏ ít nhất một phân vùng với mỗi lần vượt qua (vì với 2 số bị thiếu, nhiều nhất chỉ có 1 phân vùng sẽ chứa nhiều số bị thiếu). Tuy nhiên, tôi không chắc hiệu suất giảm bao nhiêu khi thêm nhiều số bị thiếu.

Đây là một triển khai không sử dụng phân vùng tại chỗ, vì vậy ví dụ này không đáp ứng yêu cầu về không gian nhưng nó minh họa các bước của thuật toán:

<?php

  $list = range(1,100);
  unset($list[3]);
  unset($list[31]);

  findMissing($list,1,100);

  function findMissing($list, $min, $max) {
    if(empty($list)) {
      print_r(range($min, $max));
      return;
    }

    $l = $r = [];
    $pivot = array_pop($list);

    foreach($list as $number) {
      if($number < $pivot) {
        $l[] = $number;
      }
      else {
        $r[] = $number;
      }
    }

    if(count($l) == $pivot - $min - 1) {
      // only 1 missing number use difference of sums
      print array_sum(range($min, $pivot-1)) - array_sum($l) . "\n";
    }
    else if(count($l) < $pivot - $min) {
      // more than 1 missing number, recurse
      findMissing($l, $min, $pivot-1);
    }

    if(count($r) == $max - $pivot - 1) {
      // only 1 missing number use difference of sums
      print array_sum(range($pivot + 1, $max)) - array_sum($r) . "\n";
    } else if(count($r) < $max - $pivot) {
      // mroe than 1 missing number recurse
      findMissing($r, $pivot+1, $max);
    }
  }

Bản giới thiệu


Phân vùng tập hợp giống như sử dụng không gian tuyến tính. Ít nhất là nó sẽ không hoạt động trong một thiết lập phát trực tuyến.
Thomas Ahle

@ThomasAhle xem en.wikipedia.org/wiki/Selection_alerskym#Space_complexity . chia tay tập hợp tại chỗ chỉ yêu cầu không gian bổ sung O (1) - không phải không gian tuyến tính. Trong cài đặt phát trực tuyến, đó sẽ là không gian bổ sung O (k), tuy nhiên, câu hỏi ban đầu không đề cập đến phát trực tuyến.
FuzzyTree

Không trực tiếp, nhưng anh ta viết "bạn phải quét đầu vào trong O (N), nhưng bạn chỉ có thể nắm bắt một lượng nhỏ thông tin (được định nghĩa theo k không phải N)" thường là định nghĩa phát trực tuyến. Di chuyển tất cả các số để phân vùng là không thực sự có thể trừ khi bạn có một mảng kích thước N. Chỉ là câu hỏi có rất nhiều câu trả lời phù thủy dường như bỏ qua ràng buộc này.
Thomas Ahle

1
Nhưng như bạn nói, hiệu suất có thể giảm khi thêm số lượng? Chúng ta cũng có thể sử dụng thuật toán trung bình thời gian tuyến tính, để luôn có một đường cắt hoàn hảo, nhưng nếu các số k được trải đều trong 1, ..., n, bạn sẽ không phải đi sâu về mức logk "sâu" trước khi bạn có thể cắt tỉa chi nhánh nào?
Thomas Ahle

2
Thời gian chạy trong trường hợp xấu nhất thực sự là nlogk vì bạn cần xử lý toàn bộ đầu vào tại hầu hết các lần đăng nhập, và sau đó là một chuỗi hình học (bắt đầu bằng hầu hết n phần tử). Các yêu cầu không gian được ghi lại khi được thực hiện với đệ quy đơn giản, nhưng chúng có thể được tạo O (1) bằng cách chạy quickselect thực tế và đảm bảo độ dài chính xác của mỗi phân vùng.
emu

7

Đây là một giải pháp sử dụng k bit lưu trữ bổ sung, không có bất kỳ thủ thuật thông minh nào và chỉ đơn giản. Thời gian thực hiện O (n), không gian thêm O (k). Chỉ để chứng minh rằng điều này có thể được giải quyết mà không cần đọc giải pháp trước hoặc là một thiên tài:

void puzzle (int* data, int n, bool* extra, int k)
{
    // data contains n distinct numbers from 1 to n + k, extra provides
    // space for k extra bits. 

    // Rearrange the array so there are (even) even numbers at the start
    // and (odd) odd numbers at the end.
    int even = 0, odd = 0;
    while (even + odd < n)
    {
        if (data [even] % 2 == 0) ++even;
        else if (data [n - 1 - odd] % 2 == 1) ++odd;
        else { int tmp = data [even]; data [even] = data [n - 1 - odd]; 
               data [n - 1 - odd] = tmp; ++even; ++odd; }
    }

    // Erase the lowest bits of all numbers and set the extra bits to 0.
    for (int i = even; i < n; ++i) data [i] -= 1;
    for (int i = 0; i < k; ++i) extra [i] = false;

    // Set a bit for every number that is present
    for (int i = 0; i < n; ++i)
    {
        int tmp = data [i];
        tmp -= (tmp % 2);
        if (i >= even) ++tmp;
        if (tmp <= n) data [tmp - 1] += 1; else extra [tmp - n - 1] = true;
    }

    // Print out the missing ones
    for (int i = 1; i <= n; ++i)
        if (data [i - 1] % 2 == 0) printf ("Number %d is missing\n", i);
    for (int i = n + 1; i <= n + k; ++i)
        if (! extra [i - n - 1]) printf ("Number %d is missing\n", i);

    // Restore the lowest bits again.
    for (int i = 0; i < n; ++i) {
        if (i < even) { if (data [i] % 2 != 0) data [i] -= 1; }
        else { if (data [i] % 2 == 0) data [i] += 1; }
    }
}

Bạn có muốn (data [n - 1 - odd] % 2 == 1) ++odd;không
Charles

2
Bạn có thể giải thích làm thế nào điều này hoạt động? Tôi không hiểu
Teepeemm

Giải pháp sẽ rất, rất, đơn giản nếu tôi có thể sử dụng một mảng booleans (n ​​+ k) để lưu trữ tạm thời, nhưng điều đó không được phép. Vì vậy, tôi sắp xếp lại dữ liệu, đặt các số chẵn ở đầu và các số lẻ ở cuối mảng. Bây giờ các bit thấp nhất của n số đó có thể được sử dụng để lưu trữ tạm thời, bởi vì tôi biết có bao nhiêu số chẵn và số lẻ và có thể tái tạo lại các bit thấp nhất! Các bit n và các bit phụ k này chính xác là các booleans (n ​​+ k) mà tôi cần.
gnasher729

2
Điều này sẽ không hoạt động nếu dữ liệu quá lớn để giữ trong bộ nhớ và bạn chỉ xem đó là luồng. Mặc dù rất ngon lành :)
Thomas Ahle

Độ phức tạp không gian có thể là O (1). Trong lần đầu tiên, bạn xử lý tất cả các số <(n - k) bằng chính xác thuật toán này mà không cần sử dụng 'thêm'. Trong lần chuyển thứ hai, bạn xóa các bit chẵn lẻ một lần nữa và sử dụng các vị trí k đầu tiên để lập chỉ mục số (nk) .. (n).
emu

5

Bạn có thể kiểm tra nếu mỗi số tồn tại? Nếu có, bạn có thể thử điều này:

S = tổng của tất cả các số trong túi (S <5050)
Z = tổng các số còn thiếu 5050 - S

nếu các số bị thiếu xysau đó:

x = Z - y và
max (x) = Z - 1

Vì vậy, bạn kiểm tra phạm vi từ 1đến max(x)và tìm số


1
max(x)nghĩa là gì , khi nào xlà một số?
Thomas Ahle

2
anh ta có thể có nghĩa là tối đa từ bộ số
JavaHopper

nếu chúng tôi có nhiều hơn 2 số, giải pháp này sẽ được trả lại
ozgeneral

4

Có thể thuật toán này có thể hoạt động cho câu hỏi 1:

  1. Tính toán trước xor của 100 số nguyên đầu tiên (val = 1 ^ 2 ^ 3 ^ 4 .... 100)
  2. xor các phần tử khi chúng tiếp tục đến từ luồng đầu vào (val1 = val1 ^ next_input)
  3. câu trả lời cuối cùng = val ^ val1

Hoặc thậm chí tốt hơn:

def GetValue(A)
  val=0
  for i=1 to 100
    do
      val=val^i
    done
  for value in A:
    do
      val=val^value 
    done
  return val

Thuật toán này trong thực tế có thể được mở rộng cho hai số còn thiếu. Bước đầu tiên vẫn giữ nguyên. Khi chúng ta gọi GetValue với hai số bị thiếu, kết quả sẽ a1^a2là hai số bị thiếu. Hãy cùng nói nào

val = a1^a2

Bây giờ để sàng ra a1 và a2 từ val, chúng ta lấy bất kỳ bit set nào trong val. Hãy nói rằng ithbit được đặt trong val. Điều đó có nghĩa là a1 và a2 có tính chẵn lẻ khác nhau ở ithvị trí bit. Bây giờ chúng ta thực hiện một bước lặp khác trên mảng ban đầu và giữ hai giá trị xor. Một cho các số có tập bit thứ i và số khác không có tập thứ i. Bây giờ chúng ta có hai thùng số, và bảo đảm của nó a1 and a2sẽ nằm trong các thùng khác nhau. Bây giờ lặp lại tương tự những gì chúng ta đã làm để tìm một phần tử bị thiếu trên mỗi nhóm.


Điều này chỉ giải quyết vấn đề cho k=1, phải không? Nhưng tôi thích sử dụng xorhơn tổng, nó có vẻ nhanh hơn một chút.
Thomas Ahle

@ThomasAhle Có. Tôi đã gọi nó ra trong câu trả lời của tôi.
bashrc

Đúng. Bạn có biết xor "thứ hai" có thể là gì không, với k = 2? Tương tự như sử dụng bình phương cho tổng, chúng ta có thể "bình phương" cho xor không?
Thomas Ahle

1
@ThomasAhle Sửa đổi nó để làm việc cho 2 số bị thiếu.
bashrc

đây là cách yêu thích của tôi :)
vua robert

3

Bạn có thể giải quyết Q2 nếu bạn có tổng của cả hai danh sách và sản phẩm của cả hai danh sách.

(l1 là bản gốc, l2 là danh sách đã sửa đổi)

d = sum(l1) - sum(l2)
m = mul(l1) / mul(l2)

Chúng ta có thể tối ưu hóa điều này vì tổng của một chuỗi số học gấp n lần trung bình của các điều khoản đầu tiên và cuối cùng:

n = len(l1)
d = (n/2)*(n+1) - sum(l2)

Bây giờ chúng ta biết rằng (nếu a và b là các số bị loại bỏ):

a + b = d
a * b = m

Vì vậy, chúng ta có thể sắp xếp lại để:

a = s - b
b * (s - b) = m

Và nhân lên:

-b^2 + s*b = m

Và sắp xếp lại để phía bên phải bằng không:

-b^2 + s*b - m = 0

Sau đó, chúng ta có thể giải quyết với công thức bậc hai:

b = (-s + sqrt(s^2 - (4*-1*-m)))/-2
a = s - b

Mã Python 3 mẫu:

from functools import reduce
import operator
import math
x = list(range(1,21))
sx = (len(x)/2)*(len(x)+1)
x.remove(15)
x.remove(5)
mul = lambda l: reduce(operator.mul,l)
s = sx - sum(x)
m = mul(range(1,21)) / mul(x)
b = (-s + math.sqrt(s**2 - (-4*(-m))))/-2
a = s - b
print(a,b) #15,5

Tôi không biết độ phức tạp của hàm sqrt, hàm giảm và tổng nên tôi không thể tìm ra độ phức tạp của giải pháp này (nếu có ai biết xin vui lòng bình luận bên dưới.)


Nó sử dụng bao nhiêu thời gian và bộ nhớ để tính toán x1*x2*x3*...?
Thomas Ahle

@ThomasAhle Đó là O (n) -time và O (1) -space về độ dài của danh sách, nhưng trên thực tế, nó giống như phép nhân (ít nhất là trong Python) là O (n ^ 1.6) -thời lượng số và số là O (log n) -space trên chiều dài của chúng.
Tuomas Laakkonen

@ThomasAhle Không, log (a ^ n) = n * log (a) vì vậy bạn sẽ có O (l log k) -space để lưu số. Vì vậy, đưa ra một danh sách chiều dài l và số gốc có độ dài k, bạn sẽ có không gian O (l) nhưng hệ số không đổi (log k) sẽ thấp hơn so với việc chỉ viết ra tất cả. (Tôi không nghĩ rằng phương pháp của tôi là một cách đặc biệt tốt để trả lời câu hỏi.)
Tuomas Laakkonen

3

Đối với Q2, đây là một giải pháp kém hiệu quả hơn một chút so với các giải pháp khác, nhưng vẫn có thời gian chạy O (N) và chiếm không gian O (k).

Ý tưởng là chạy thuật toán gốc hai lần. Trong số đầu tiên, bạn nhận được tổng số bị thiếu, cung cấp cho bạn giới hạn trên của các số bị thiếu. Hãy gọi số này N. Bạn biết rằng hai số còn thiếu sẽ được tính tổng N, vì vậy số thứ nhất chỉ có thể nằm trong khoảng [1, floor((N-1)/2)]trong khi số thứ hai sẽ ở [floor(N/2)+1,N-1].

Do đó, bạn lặp lại trên tất cả các số một lần nữa, loại bỏ tất cả các số không được bao gồm trong khoảng đầu tiên. Những người đó, bạn theo dõi tổng của họ. Cuối cùng, bạn sẽ biết một trong hai số còn thiếu và bằng cách mở rộng số thứ hai.

Tôi có cảm giác rằng phương pháp này có thể được khái quát hóa và có thể nhiều tìm kiếm chạy "song song" trong một lần vượt qua đầu vào, nhưng tôi vẫn chưa tìm ra cách.


Ahaha vâng, đây là giải pháp tương tự mà tôi đã đưa ra cho quý 2, chỉ bằng cách tính tổng lại lấy số âm cho tất cả các số dưới N / 2, nhưng điều này thậm chí còn tốt hơn!
xjcl

2

Tôi nghĩ rằng điều này có thể được thực hiện mà không có bất kỳ phương trình và lý thuyết toán học phức tạp. Dưới đây là một đề xuất cho một giải pháp phức tạp tại chỗ và thời gian O (2n):

Giả định hình thức đầu vào:

# số trong túi = n

Số thiếu = k

Các số trong túi được thể hiện bằng một mảng có chiều dài n

Độ dài của mảng đầu vào cho algo = n

Các mục bị thiếu trong mảng (số lấy ra khỏi túi) được thay thế bằng giá trị của phần tử đầu tiên trong mảng.

Ví dụ. Ban đầu túi trông giống như [2,9,3,7,8,6,4,5,1,10]. Nếu 4 được lấy ra, giá trị của 4 sẽ trở thành 2 (phần tử đầu tiên của mảng). Do đó, sau khi lấy ra 4 cái túi sẽ trông giống như [2,9,3,7,8,6,2,5,1,10]

Chìa khóa cho giải pháp này là gắn thẻ INDEX của một số đã truy cập bằng cách phủ định giá trị tại INDEX đó khi mảng được dịch chuyển ngang.

    IEnumerable<int> GetMissingNumbers(int[] arrayOfNumbers)
    {
        List<int> missingNumbers = new List<int>();
        int arrayLength = arrayOfNumbers.Length;

        //First Pass
        for (int i = 0; i < arrayLength; i++)
        {
            int index = Math.Abs(arrayOfNumbers[i]) - 1;
            if (index > -1)
            {
                arrayOfNumbers[index] = Math.Abs(arrayOfNumbers[index]) * -1; //Marking the visited indexes
            }
        }

        //Second Pass to get missing numbers
        for (int i = 0; i < arrayLength; i++)
        {                
            //If this index is unvisited, means this is a missing number
            if (arrayOfNumbers[i] > 0)
            {
                missingNumbers.Add(i + 1);
            }
        }

        return missingNumbers;
    }

Điều này sử dụng quá nhiều bộ nhớ.
Thomas Ahle

2

Có một cách chung để khái quát các thuật toán phát trực tuyến như thế này. Ý tưởng là sử dụng một chút ngẫu nhiên để hy vọng 'lan truyền' các kyếu tố vào các vấn đề phụ độc lập, trong đó thuật toán ban đầu của chúng tôi giải quyết vấn đề cho chúng tôi. Kỹ thuật này được sử dụng trong tái tạo tín hiệu thưa thớt, trong số những thứ khác.

  • Tạo một mảng, akích thước u = k^2.
  • Chọn bất kỳ hàm băm phổ quát , h : {1,...,n} -> {1,...,u}. (Giống như tăng ca )
  • Đối với mỗi itrong 1, ..., ntănga[h(i)] += i
  • Đối với mỗi số xtrong luồng đầu vào, giảm dần a[h(x)] -= x.

Nếu tất cả các số bị thiếu đã được băm vào các nhóm khác nhau, các phần tử khác không của mảng sẽ chứa các số bị thiếu.

Xác suất mà một cặp cụ thể được gửi đến cùng một nhóm, nhỏ hơn 1/uđịnh nghĩa của hàm băm phổ quát. Vì có khoảng k^2/2cặp, nên chúng tôi có xác suất lỗi nhiều nhất k^2/2/u=1/2. Đó là, chúng tôi thành công với xác suất ít nhất 50% và nếu chúng tôi tăng, uchúng tôi sẽ tăng cơ hội.

Lưu ý rằng thuật toán này lấy k^2 lognbit không gian (Chúng ta cần lognbit trên mỗi mảng mảng.) Điều này khớp với không gian được yêu cầu bởi câu trả lời của @Dimitris Andreou (Đặc biệt là yêu cầu không gian của hệ số đa thức, cũng xảy ra ngẫu nhiên.) Thuật toán này cũng có hằng số thời gian cho mỗi lần cập nhật, thay vì thời gian ktrong trường hợp tổng điện.

Trên thực tế, chúng ta có thể còn hiệu quả hơn phương pháp tổng công suất bằng cách sử dụng thủ thuật được mô tả trong các bình luận.


Lưu ý: Chúng tôi cũng có thể sử dụng xortrong mỗi nhóm, thay vì sum, nếu điều đó nhanh hơn trên máy của chúng tôi.
Thomas Ahle

Thú vị nhưng tôi nghĩ điều này chỉ tôn trọng giới hạn không gian khi k <= sqrt(n)- ít nhất là nếu u=k^2? Giả sử k = 11 và n = 100, thì bạn sẽ có 121 thùng và thuật toán cuối cùng sẽ tương tự như có một mảng 100 bit mà bạn kiểm tra khi bạn đọc từng # từ luồng. Tăng ucải thiện cơ hội thành công nhưng có giới hạn về số tiền bạn có thể tăng trước khi vượt quá giới hạn không gian.
FuzzyTree

1
Vấn đề có ý nghĩa nhất đối với nlớn hơn nhiều so với k, tôi nghĩ, nhưng bạn thực sự có thể thu hẹp không gian k lognbằng một phương pháp rất giống với phương pháp băm được mô tả, trong khi vẫn có cập nhật thời gian liên tục. Nó được mô tả trong gnunet.org/eppstein-set-reconcestion , giống như phương pháp tổng quyền, nhưng về cơ bản, bạn băm vào 'hai trong số các k' với hàm băm mạnh như băm bảng, đảm bảo rằng một số nhóm sẽ chỉ có một phần tử . Để giải mã, bạn xác định thùng đó và loại bỏ phần tử khỏi cả hai nhóm của nó, điều này (có khả năng) giải phóng một nhóm khác và cứ thế
Thomas Ahle

2

Một giải pháp rất đơn giản cho Q2 mà tôi ngạc nhiên không ai trả lời được. Sử dụng phương pháp từ Q1 để tìm tổng của hai số còn thiếu. Hãy biểu thị nó bằng S, sau đó một trong những số bị thiếu nhỏ hơn S / 2 và số còn lại lớn hơn S / 2 (duh). Tính tổng tất cả các số từ 1 đến S / 2 và so sánh nó với kết quả của công thức (tương tự như phương pháp trong Q1) để tìm mức thấp hơn giữa các số còn thiếu. Trừ nó khỏi S để tìm số còn thiếu lớn hơn.


Tôi nghĩ rằng điều này giống như câu trả lời của Svalorzen , nhưng bạn đã giải thích nó bằng những từ tốt hơn. Có ý tưởng nào để khái quát nó cho Qk không?
John McClane

Xin lỗi vì đã bỏ lỡ câu trả lời khác. Tôi không chắc có thể khái quát nó thành $ Q_k $ hay không vì trong trường hợp đó bạn không thể ràng buộc phần tử bị thiếu nhỏ nhất vào một phạm vi nào đó. Bạn có biết rằng một số phần tử phải nhỏ hơn $ S / k $ nhưng điều đó có thể đúng với nhiều phần tử
Gilad Deutsch

1

Vấn đề rất tốt đẹp. Tôi sẽ sử dụng một sự khác biệt được thiết lập cho Qk. Rất nhiều ngôn ngữ lập trình thậm chí còn hỗ trợ cho nó, như trong Ruby:

missing = (1..100).to_a - bag

Đây có lẽ không phải là giải pháp hiệu quả nhất nhưng đó là giải pháp tôi sẽ sử dụng trong cuộc sống thực nếu tôi phải đối mặt với một nhiệm vụ như vậy trong trường hợp này (ranh giới đã biết, ranh giới thấp). Nếu tập hợp số sẽ rất lớn thì tôi sẽ xem xét một thuật toán hiệu quả hơn, tất nhiên, nhưng cho đến lúc đó, giải pháp đơn giản sẽ đủ cho tôi.


1
Điều này sử dụng quá nhiều không gian.
Thomas Ahle

@ThomasAhle: Tại sao bạn lại thêm những bình luận vô ích vào mỗi câu trả lời thứ hai? Bạn có ý nghĩa gì với việc sử dụng quá nhiều không gian?
DarkDust

Bởi vì câu hỏi nói rằng "Chúng tôi không thể có bất kỳ không gian bổ sung nào tỷ lệ thuận với N." Giải pháp này thực hiện chính xác điều đó.
Thomas Ahle

1

Bạn có thể thử sử dụng Bộ lọc Bloom . Chèn từng số trong túi để nở, sau đó lặp lại trên bộ 1-k hoàn chỉnh cho đến khi không tìm thấy báo cáo từng số. Điều này có thể không tìm thấy câu trả lời trong tất cả các kịch bản, nhưng có thể là một giải pháp đủ tốt.


Ngoài ra còn có bộ lọc nở, cho phép xóa. Sau đó, bạn có thể chỉ cần thêm tất cả các số và xóa những số bạn nhìn thấy trong luồng.
Thomas Ahle

Haha đây có lẽ là một trong những câu trả lời thiết thực hơn, nhưng ít được chú ý.
ldog

1

Tôi sẽ có một cách tiếp cận khác cho câu hỏi đó và thăm dò người phỏng vấn để biết thêm chi tiết về vấn đề lớn hơn mà anh ta đang cố gắng giải quyết. Tùy thuộc vào vấn đề và các yêu cầu xung quanh nó, giải pháp dựa trên tập hợp rõ ràng có thể là điều đúng đắn và cách tiếp cận tạo ra một danh sách và chọn-qua-sau đó có thể không.

Ví dụ, có thể người phỏng vấn sẽ gửi ntin nhắn và cần biết . Sau đó, tập hợp chứa danh sách các phần tử bị thiếu và không có xử lý bổ sung nào được thực hiện.k rằng điều đó không dẫn đến việc trả lời và cần biết nó trong thời gian đồng hồ treo tường càng ít càng tốt sau khin-k trả lời đến. Chúng ta cũng nói rằng bản chất của kênh thông báo là ngay cả khi chạy hết công suất, vẫn có đủ thời gian để xử lý giữa các tin nhắn mà không có bất kỳ tác động nào đến việc mất bao lâu để tạo ra kết quả cuối cùng sau khi trả lời cuối cùng. Thời gian đó có thể được đưa vào để sử dụng chèn một số khía cạnh xác định của mỗi tin nhắn đã gửi vào một bộ và xóa nó khi mỗi câu trả lời tương ứng đến. Khi đã có câu trả lời cuối cùng, điều duy nhất cần làm là xóa định danh của nó khỏi tập hợp, trong các triển khai thông thường sẽ mấtO(log k+1)k

Đây chắc chắn không phải là cách tiếp cận nhanh nhất để xử lý hàng loạt các túi số được tạo trước bởi vì toàn bộ hoạt động O((log 1 + log 2 + ... + log n) + (log n + log n-1 + ... + log k)). Nhưng nó hoạt động với bất kỳ giá trị nào k(ngay cả khi nó không được biết trước) và trong ví dụ trên, nó được áp dụng theo cách giảm thiểu khoảng thời gian quan trọng nhất.


Điều này có hoạt động không nếu bạn chỉ có bộ nhớ phụ O (k ^ 2)?
Thomas Ahle

1

Bạn có thể thúc đẩy giải pháp bằng cách suy nghĩ về nó theo các đối xứng (nhóm, trong ngôn ngữ toán học). Bất kể thứ tự của bộ số, câu trả lời phải giống nhau. Nếu bạn sẽ sử dụng các khàm để giúp xác định các phần tử bị thiếu, bạn nên suy nghĩ về các hàm có thuộc tính đó: đối xứng. Hàm s_1(x) = x_1 + x_2 + ... + x_nnày là một ví dụ về hàm đối xứng, nhưng có những hàm khác có mức độ cao hơn. Đặc biệt, hãy xem xét các chức năng đối xứng cơ bản . Hàm đối xứng cơ bản của bậc 2 là s_2(x) = x_1 x_2 + x_1 x_3 + ... + x_1 x_n + x_2 x_3 + ... + x_(n-1) x_ntổng của tất cả các sản phẩm của hai phần tử. Tương tự cho các chức năng đối xứng cơ bản của cấp 3 trở lên. Chúng rõ ràng là đối xứng. Hơn nữa, hóa ra chúng là các khối xây dựng cho tất cả các chức năng đối xứng.

Bạn có thể xây dựng các hàm đối xứng cơ bản khi bạn đi bằng cách lưu ý điều đó s_2(x,x_(n+1)) = s_2(x) + s_1(x)(x_(n+1)). Suy nghĩ xa hơn sẽ thuyết phục bạn rằng s_3(x,x_(n+1)) = s_3(x) + s_2(x)(x_(n+1))và cứ thế, để chúng có thể được tính toán trong một lần.

Làm thế nào để chúng ta biết những mục bị thiếu trong mảng? Hãy nghĩ về đa thức (z-x_1)(z-x_2)...(z-x_n). Nó đánh giá 0nếu bạn đặt bất kỳ số nào x_i. Mở rộng đa thức, bạn nhận được z^n-s_1(x)z^(n-1)+ ... + (-1)^n s_n. Các hàm đối xứng cơ bản cũng xuất hiện ở đây, điều này thực sự không có gì đáng ngạc nhiên, vì đa thức sẽ giữ nguyên nếu chúng ta áp dụng bất kỳ hoán vị nào cho gốc.

Vì vậy, chúng ta có thể xây dựng đa thức và cố gắng tính hệ số để tìm ra số nào không có trong tập hợp, như những người khác đã đề cập.

Cuối cùng, nếu chúng ta lo ngại về việc tràn bộ nhớ với số lượng lớn (đa thức đối xứng thứ n sẽ theo thứ tự 100!), chúng ta có thể thực hiện các phép tính này mod ptrong đó pmột số nguyên tố lớn hơn 100. Trong trường hợp đó, chúng ta đánh giá đa thức mod pvà thấy rằng nó lại đánh giá đến 0khi đầu vào là một số trong tập hợp và nó ước tính thành giá trị khác không khi đầu vào là một số không có trong tập hợp. Tuy nhiên, như những người khác đã chỉ ra, để đưa các giá trị ra khỏi đa thức trong thời gian phụ thuộc vào k, không N, chúng ta phải tính đến đa thức mod p.


1

Tuy nhiên, một cách khác là sử dụng lọc đồ thị dư.

Giả sử chúng ta có các số từ 1 đến 4 và 3 bị thiếu. Các đại diện nhị phân như sau,

1 = 001b, 2 = 010b, 3 = 011b, 4 = 100b

Và tôi có thể tạo một biểu đồ dòng chảy như sau.

                   1
             1 -------------> 1
             |                | 
      2      |     1          |
0 ---------> 1 ----------> 0  |
|                          |  |
|     1            1       |  |
0 ---------> 0 ----------> 0  |
             |                |
      1      |      1         |
1 ---------> 0 -------------> 1

Lưu ý rằng biểu đồ luồng chứa x nút, trong khi x là số bit. Và số cạnh tối đa là (2 * x) -2.

Vì vậy, đối với số nguyên 32 bit, nó sẽ lấy không gian O (32) hoặc không gian O (1).

Bây giờ nếu tôi loại bỏ dung lượng cho mỗi số bắt đầu từ 1,2,4 thì tôi còn lại một biểu đồ dư.

0 ----------> 1 ---------> 1

Cuối cùng tôi sẽ chạy một vòng lặp như sau,

 result = []
 for x in range(1,n):
     exists_path_in_residual_graph(x)
     result.append(x)

Bây giờ kết quả là trong resultcác số cũng không bị thiếu (dương tính giả). Nhưng k <= (kích thước của kết quả) <= n khi kthiếu các phần tử.

Tôi sẽ đi qua danh sách đã cho lần cuối để đánh dấu kết quả bị thiếu hay không.

Vậy độ phức tạp thời gian sẽ là O (n).

Cuối cùng, chúng ta có thể giảm số lượng dương tính giả (và không gian cần thiết) bằng cách lấy các nút 00, 01, 11, 10thay vì chỉ 01.


Tôi không hiểu sơ đồ của bạn. Các nút, cạnh và số đại diện cho cái gì? Tại sao một số cạnh được định hướng mà không phải là những người khác?
dain

Trong thực tế tôi không thực sự hiểu câu trả lời của bạn, bạn có thể làm rõ thêm một số?
dain

1

Có lẽ bạn cần làm rõ ý nghĩa của O (k).

Đây là một giải pháp tầm thường cho k tùy ý: với mỗi v trong tập hợp số của bạn, hãy tích lũy tổng 2 ^ v. Cuối cùng, vòng lặp i từ 1 đến N. Nếu tổng bitwise ANDed với 2 ^ i bằng 0, thì tôi bị thiếu. (Hoặc bằng số, nếu sàn của tổng chia cho 2 ^ i là chẵn. Hoặc sum modulo 2^(i+1)) < 2^i.)

Dễ thôi phải không? Lưu trữ thời gian O (N), O (1) và nó hỗ trợ k tùy ý.

Ngoại trừ việc bạn đang tính toán những con số khổng lồ mà trên một máy tính thực tế, mỗi máy sẽ yêu cầu không gian O (N). Trong thực tế, giải pháp này giống hệt với một vectơ bit.

Vì vậy, bạn có thể khéo léo và tính tổng và tổng bình phương và tổng khối ... cho đến tổng của v ^ k, và làm phép toán ưa thích để rút ra kết quả. Nhưng đó cũng là những con số lớn, điều này đặt ra câu hỏi: chúng ta đang nói về mô hình hoạt động trừu tượng nào? Bao nhiêu phù hợp trong không gian O (1) và mất bao lâu để tổng hợp các số có kích thước bất kỳ bạn cần?


Câu trả lời tốt đẹp! Một điều nhỏ: "Nếu tổng modulo 2 ^ i bằng 0, thì tôi bị thiếu" là không chính xác. Nhưng nó rõ ràng những gì được dự định. Tôi nghĩ rằng "nếu tổng modulo 2 ^ (i + 1) nhỏ hơn 2 ^ i, thì tôi bị thiếu" sẽ đúng. (Tất nhiên, trong hầu hết các ngôn ngữ lập trình, chúng tôi sẽ sử dụng dịch chuyển bit thay vì tính toán modulo. Đôi khi, ngôn ngữ lập trình có một chút biểu cảm hơn so với ký hiệu toán học thông thường. :-))
jcsahnwaldt nói GoFundMonica

1
Cảm ơn, bạn hoàn toàn đúng! Đã sửa, mặc dù tôi lười biếng và đi lạc từ ký hiệu toán học ... oh, và tôi cũng đã làm hỏng nó. Sửa lại ...
sfink 27/11/18

1

Đây là một giải pháp không dựa vào toán học phức tạp như câu trả lời của sdcvvc / Dimitris Andreou, không thay đổi mảng đầu vào như quán cà phê và Đại tá Panic đã làm, và không sử dụng các bit có kích thước khổng lồ như Chris Lercher, JeremyP và nhiều người khác đã làm. Về cơ bản, tôi bắt đầu với ý tưởng của Svalorzen's / Gilad Deutch cho quý 2, khái quát nó cho trường hợp phổ biến Qk và triển khai trong Java để chứng minh rằng thuật toán hoạt động.

Ý tưởng

Giả sử chúng ta có một khoảng I tùy ý mà chúng ta chỉ biết rằng nó chứa ít nhất một trong các số còn thiếu. Sau một đi qua các mảng đầu vào, chỉ nhìn vào những con số từ tôi , chúng ta có thể có được cả hai tổng S và số lượng Q của thiếu số từ tôi . Chúng tôi thực hiện điều này bằng cách giảm độ dài của tôi mỗi lần chúng tôi gặp một số từ I (để lấy Q ) và bằng cách giảm tổng số được tính toán trước của tất cả các số trong I theo số đó gặp phải mỗi lần (để lấy S ).

Bây giờ chúng ta nhìn vào SQ . Nếu Q = 1 , có nghĩa là sau đó tôi chỉ chứa một trong những con số mất tích, và con số này rõ ràng là S . Chúng tôi đánh dấu tôi là đã hoàn thành (nó được gọi là "không rõ ràng" trong chương trình) và để nó ra khỏi sự xem xét thêm. Mặt khác, nếu Q> 1 , chúng ta có thể tính toán mức trung bình của A = S / Q của số mất tích chứa trong tôi . Như tất cả các số là khác biệt, ít nhất một trong số đó là đúng ít hơn Một và ít nhất một là nghiêm chỉnh lớn hơn Một . Bây giờ chúng tôi chia tôi trong Athành hai khoảng nhỏ hơn, mỗi khoảng chứa ít nhất một số bị thiếu. Lưu ý rằng không quan trọng trong khoảng thời gian nào chúng ta gán A trong trường hợp đó là số nguyên.

Chúng tôi thực hiện vượt qua mảng tiếp theo tính toán SQ cho từng khoảng riêng biệt (nhưng trong cùng một khoảng) và sau khoảng thời gian đánh dấu đó với Q = 1 và các khoảng chia với Q> 1 . Chúng tôi tiếp tục quá trình này cho đến khi không có khoảng "mơ hồ" mới, nghĩa là chúng tôi không có gì để phân chia vì mỗi khoảng chứa chính xác một số bị thiếu (và chúng tôi luôn biết số này vì chúng tôi biết S ). Chúng tôi bắt đầu từ khoảng "toàn bộ phạm vi" duy nhất chứa tất cả các số có thể (như [1..N] trong câu hỏi).

Phân tích độ phức tạp thời gian và không gian

Tổng số lần vượt qua p chúng ta cần thực hiện cho đến khi quá trình dừng lại không bao giờ lớn hơn số lượng bị thiếu k . Bất đẳng thức p <= k có thể được chứng minh một cách nghiêm ngặt. Mặt khác, cũng có một p <log 2 N + 3 theo kinh nghiệm trên hữu ích cho các giá trị lớn của k . Chúng ta cần thực hiện tìm kiếm nhị phân cho mỗi số của mảng đầu vào để xác định khoảng thời gian mà nó thuộc về. Điều này thêm số nhân log k vào độ phức tạp thời gian.

Tổng cộng, độ phức tạp thời gian là O (N ᛫ min (k, log N) log k) . Lưu ý rằng đối với k lớn , điều này tốt hơn đáng kể so với phương pháp của sdcvvc / Dimitris Andreou, đó là O (N ᛫ k) .

Đối với công việc của nó, thuật toán yêu cầu không gian bổ sung O (k) để lưu trữ tại hầu hết các khoảng k , tốt hơn đáng kể so với O (N) trong các giải pháp "bitet".

Triển khai Java

Đây là một lớp Java thực hiện thuật toán trên. Nó luôn luôn trả về một sắp xếp mảng các số mất tích. Ngoài ra, nó không yêu cầu các số bị thiếu đếm k vì nó tính toán nó trong lần đầu tiên. Toàn bộ phạm vi số được cho bởi các tham số minNumbermaxNumber(ví dụ 1 và 100 cho ví dụ đầu tiên trong câu hỏi).

public class MissingNumbers {
    private static class Interval {
        boolean ambiguous = true;
        final int begin;
        int quantity;
        long sum;

        Interval(int begin, int end) { // begin inclusive, end exclusive
            this.begin = begin;
            quantity = end - begin;
            sum = quantity * ((long)end - 1 + begin) / 2;
        }

        void exclude(int x) {
            quantity--;
            sum -= x;
        }
    }

    public static int[] find(int minNumber, int maxNumber, NumberBag inputBag) {
        Interval full = new Interval(minNumber, ++maxNumber);
        for (inputBag.startOver(); inputBag.hasNext();)
            full.exclude(inputBag.next());
        int missingCount = full.quantity;
        if (missingCount == 0)
            return new int[0];
        Interval[] intervals = new Interval[missingCount];
        intervals[0] = full;
        int[] dividers = new int[missingCount];
        dividers[0] = minNumber;
        int intervalCount = 1;
        while (true) {
            int oldCount = intervalCount;
            for (int i = 0; i < oldCount; i++) {
                Interval itv = intervals[i];
                if (itv.ambiguous)
                    if (itv.quantity == 1) // number inside itv uniquely identified
                        itv.ambiguous = false;
                    else
                        intervalCount++; // itv will be split into two intervals
            }
            if (oldCount == intervalCount)
                break;
            int newIndex = intervalCount - 1;
            int end = maxNumber;
            for (int oldIndex = oldCount - 1; oldIndex >= 0; oldIndex--) {
                // newIndex always >= oldIndex
                Interval itv = intervals[oldIndex];
                int begin = itv.begin;
                if (itv.ambiguous) {
                    // split interval itv
                    // use floorDiv instead of / because input numbers can be negative
                    int mean = (int)Math.floorDiv(itv.sum, itv.quantity) + 1;
                    intervals[newIndex--] = new Interval(mean, end);
                    intervals[newIndex--] = new Interval(begin, mean);
                } else
                    intervals[newIndex--] = itv;
                end = begin;
            }
            for (int i = 0; i < intervalCount; i++)
                dividers[i] = intervals[i].begin;
            for (inputBag.startOver(); inputBag.hasNext();) {
                int x = inputBag.next();
                // find the interval to which x belongs
                int i = java.util.Arrays.binarySearch(dividers, 0, intervalCount, x);
                if (i < 0)
                    i = -i - 2;
                Interval itv = intervals[i];
                if (itv.ambiguous)
                    itv.exclude(x);
            }
        }
        assert intervalCount == missingCount;
        for (int i = 0; i < intervalCount; i++)
            dividers[i] = (int)intervals[i].sum;
        return dividers;
    }
}

Để công bằng, lớp này nhận đầu vào dưới dạng NumberBagđối tượng. NumberBagkhông cho phép sửa đổi mảng và truy cập ngẫu nhiên và cũng tính số lần mảng được yêu cầu để duyệt tuần tự. Nó cũng phù hợp hơn cho thử nghiệm mảng lớn hơn Iterable<Integer>vì nó tránh được các intgiá trị nguyên thủy và cho phép bọc một phần lớn int[]để chuẩn bị kiểm tra thuận tiện. Không khó để thay thế, nếu muốn, NumberBagbằng int[]hoặc Iterable<Integer>findchữ ký, bằng cách thay đổi hai vòng lặp for thành vòng lặp foreach.

import java.util.*;

public abstract class NumberBag {
    private int passCount;

    public void startOver() {
        passCount++;
    }

    public final int getPassCount() {
        return passCount;
    }

    public abstract boolean hasNext();

    public abstract int next();

    // A lightweight version of Iterable<Integer> to avoid boxing of int
    public static NumberBag fromArray(int[] base, int fromIndex, int toIndex) {
        return new NumberBag() {
            int index = toIndex;

            public void startOver() {
                super.startOver();
                index = fromIndex;
            }

            public boolean hasNext() {
                return index < toIndex;
            }

            public int next() {
                if (index >= toIndex)
                    throw new NoSuchElementException();
                return base[index++];
            }
        };
    }

    public static NumberBag fromArray(int[] base) {
        return fromArray(base, 0, base.length);
    }

    public static NumberBag fromIterable(Iterable<Integer> base) {
        return new NumberBag() {
            Iterator<Integer> it;

            public void startOver() {
                super.startOver();
                it = base.iterator();
            }

            public boolean hasNext() {
                return it.hasNext();
            }

            public int next() {
                return it.next();
            }
        };
    }
}

Xét nghiệm

Các ví dụ đơn giản thể hiện việc sử dụng các lớp này được đưa ra dưới đây.

import java.util.*;

public class SimpleTest {
    public static void main(String[] args) {
        int[] input = { 7, 1, 4, 9, 6, 2 };
        NumberBag bag = NumberBag.fromArray(input);
        int[] output = MissingNumbers.find(1, 10, bag);
        System.out.format("Input: %s%nMissing numbers: %s%nPass count: %d%n",
                Arrays.toString(input), Arrays.toString(output), bag.getPassCount());

        List<Integer> inputList = new ArrayList<>();
        for (int i = 0; i < 10; i++)
            inputList.add(2 * i);
        Collections.shuffle(inputList);
        bag = NumberBag.fromIterable(inputList);
        output = MissingNumbers.find(0, 19, bag);
        System.out.format("%nInput: %s%nMissing numbers: %s%nPass count: %d%n",
                inputList, Arrays.toString(output), bag.getPassCount());

        // Sieve of Eratosthenes
        final int MAXN = 1_000;
        List<Integer> nonPrimes = new ArrayList<>();
        nonPrimes.add(1);
        int[] primes;
        int lastPrimeIndex = 0;
        while (true) {
            primes = MissingNumbers.find(1, MAXN, NumberBag.fromIterable(nonPrimes));
            int p = primes[lastPrimeIndex]; // guaranteed to be prime
            int q = p;
            for (int i = lastPrimeIndex++; i < primes.length; i++) {
                q = primes[i]; // not necessarily prime
                int pq = p * q;
                if (pq > MAXN)
                    break;
                nonPrimes.add(pq);
            }
            if (q == p)
                break;
        }
        System.out.format("%nSieve of Eratosthenes. %d primes up to %d found:%n",
                primes.length, MAXN);
        for (int i = 0; i < primes.length; i++)
            System.out.format(" %4d%s", primes[i], (i % 10) < 9 ? "" : "\n");
    }
}

Kiểm tra mảng lớn có thể được thực hiện theo cách này:

import java.util.*;

public class BatchTest {
    private static final Random rand = new Random();
    public static int MIN_NUMBER = 1;
    private final int minNumber = MIN_NUMBER;
    private final int numberCount;
    private final int[] numbers;
    private int missingCount;
    public long finderTime;

    public BatchTest(int numberCount) {
        this.numberCount = numberCount;
        numbers = new int[numberCount];
        for (int i = 0; i < numberCount; i++)
            numbers[i] = minNumber + i;
    }

    private int passBound() {
        int mBound = missingCount > 0 ? missingCount : 1;
        int nBound = 34 - Integer.numberOfLeadingZeros(numberCount - 1); // ceil(log_2(numberCount)) + 2
        return Math.min(mBound, nBound);
    }

    private void error(String cause) {
        throw new RuntimeException("Error on '" + missingCount + " from " + numberCount + "' test, " + cause);
    }

    // returns the number of times the input array was traversed in this test
    public int makeTest(int missingCount) {
        this.missingCount = missingCount;
        // numbers array is reused when numberCount stays the same,
        // just Fisher–Yates shuffle it for each test
        for (int i = numberCount - 1; i > 0; i--) {
            int j = rand.nextInt(i + 1);
            if (i != j) {
                int t = numbers[i];
                numbers[i] = numbers[j];
                numbers[j] = t;
            }
        }
        final int bagSize = numberCount - missingCount;
        NumberBag inputBag = NumberBag.fromArray(numbers, 0, bagSize);
        finderTime -= System.nanoTime();
        int[] found = MissingNumbers.find(minNumber, minNumber + numberCount - 1, inputBag);
        finderTime += System.nanoTime();
        if (inputBag.getPassCount() > passBound())
            error("too many passes (" + inputBag.getPassCount() + " while only " + passBound() + " allowed)");
        if (found.length != missingCount)
            error("wrong result length");
        int j = bagSize; // "missing" part beginning in numbers
        Arrays.sort(numbers, bagSize, numberCount);
        for (int i = 0; i < missingCount; i++)
            if (found[i] != numbers[j++])
                error("wrong result array, " + i + "-th element differs");
        return inputBag.getPassCount();
    }

    public static void strideCheck(int numberCount, int minMissing, int maxMissing, int step, int repeats) {
        BatchTest t = new BatchTest(numberCount);
        System.out.println("╠═══════════════════════╬═════════════════╬═════════════════╣");
        for (int missingCount = minMissing; missingCount <= maxMissing; missingCount += step) {
            int minPass = Integer.MAX_VALUE;
            int passSum = 0;
            int maxPass = 0;
            t.finderTime = 0;
            for (int j = 1; j <= repeats; j++) {
                int pCount = t.makeTest(missingCount);
                if (pCount < minPass)
                    minPass = pCount;
                passSum += pCount;
                if (pCount > maxPass)
                    maxPass = pCount;
            }
            System.out.format("║ %9d  %9d  ║  %2d  %5.2f  %2d  ║  %11.3f    ║%n", missingCount, numberCount, minPass,
                    (double)passSum / repeats, maxPass, t.finderTime * 1e-6 / repeats);
        }
    }

    public static void main(String[] args) {
        System.out.println("╔═══════════════════════╦═════════════════╦═════════════════╗");
        System.out.println("║      Number count     ║      Passes     ║  Average time   ║");
        System.out.println("║   missimg     total   ║  min  avg   max ║ per search (ms) ║");
        long time = System.nanoTime();
        strideCheck(100, 0, 100, 1, 20_000);
        strideCheck(100_000, 2, 99_998, 1_282, 15);
        MIN_NUMBER = -2_000_000_000;
        strideCheck(300_000_000, 1, 10, 1, 1);
        time = System.nanoTime() - time;
        System.out.println("╚═══════════════════════╩═════════════════╩═════════════════╝");
        System.out.format("%nSuccess. Total time: %.2f s.%n", time * 1e-9);
    }
}

Hãy thử chúng trên Ideone


0

Tôi tin rằng tôi có một thuật toán O(k)thời gian và O(log(k))không gian, với điều kiện là bạn có các hàm floor(x)log2(x)hàm cho các số nguyên lớn tùy ý có sẵn:

Bạn có một ksố nguyên dài -bit (do đó là log8(k)khoảng trắng) nơi bạn thêm x^2, trong đó x là số tiếp theo bạn tìm thấy trong túi: s=1^2+2^2+...Điều này làm mất O(N)thời gian (không phải là vấn đề đối với người phỏng vấn). Cuối cùng, bạn nhận được j=floor(log2(s))con số lớn nhất mà bạn đang tìm kiếm. Sau đó s=s-jvà bạn làm lại như trên:

for (i = 0 ; i < k ; i++)
{
  j = floor(log2(s));
  missing[i] = j;
  s -= j;
}

Bây giờ, bạn thường không có chức năng sàn và log2 cho 2756số nguyên -bit mà thay vào đó là nhân đôi. Vì thế? Đơn giản, với mỗi 2 byte (hoặc 1 hoặc 3 hoặc 4), bạn có thể sử dụng các hàm này để có được các số mong muốn, nhưng điều này thêm một O(N)yếu tố cho độ phức tạp thời gian


0

Điều này nghe có vẻ ngu ngốc, nhưng, trong bài toán đầu tiên được trình bày cho bạn, bạn sẽ phải xem tất cả các số còn lại trong túi để thực sự thêm chúng để tìm số còn thiếu bằng phương trình đó.

Vì vậy, vì bạn có thể thấy tất cả các số, chỉ cần tìm số còn thiếu. Điều tương tự cũng xảy ra khi thiếu hai số. Tôi nghĩ khá đơn giản. Không có điểm nào trong việc sử dụng một phương trình khi bạn có thể nhìn thấy các số còn lại trong túi.


2
Tôi nghĩ lợi ích của việc tóm tắt chúng là bạn không cần phải nhớ những con số nào bạn đã thấy (ví dụ: không có yêu cầu thêm bộ nhớ). Mặt khác, tùy chọn duy nhất là giữ lại một tập hợp tất cả các giá trị được nhìn thấy và sau đó lặp lại bộ đó để tìm lại giá trị còn thiếu.
Dan Tao

3
Câu hỏi này thường được hỏi với quy định về độ phức tạp không gian O (1).

Tổng của các số N đầu tiên là N (N + 1) / 2. Với N = 100, Tổng = 100 * (101) / 2 = 5050;
tmarthal

0

Tôi nghĩ rằng điều này có thể được khái quát như thế này:

Suy ra S, M là các giá trị ban đầu cho tổng chuỗi số học và phép nhân.

S = 1 + 2 + 3 + 4 + ... n=(n+1)*n/2
M = 1 * 2 * 3 * 4 * .... * n 

Tôi nên nghĩ về một công thức để tính toán điều này, nhưng đó không phải là vấn đề. Dù sao, nếu thiếu một số, bạn đã cung cấp giải pháp. Tuy nhiên, nếu thiếu hai số thì hãy biểu thị tổng mới và tổng bội số của S1 và M1, như sau:

S1 = S - (a + b)....................(1)

Where a and b are the missing numbers.

M1 = M - (a * b)....................(2)

Vì bạn biết S1, M1, M và S, phương trình trên có thể giải được để tìm a và b, các số còn thiếu.

Bây giờ cho ba số bị thiếu:

S2 = S - ( a + b + c)....................(1)

Where a and b are the missing numbers.

M2 = M - (a * b * c)....................(2)

Bây giờ ẩn số của bạn là 3 trong khi bạn chỉ có hai phương trình bạn có thể giải.


Phép nhân trở nên khá lớn mặc dù vậy .. Ngoài ra, làm thế nào để bạn khái quát hóa hơn 2 số bị thiếu?
Thomas Ahle

Tôi đã thử các công thức này theo trình tự rất đơn giản với N = 3 và các số bị thiếu = {1, 2}. Tôi đã không làm việc, vì tôi tin rằng lỗi là ở công thức (2) nên đọc M1 = M / (a * b)(xem câu trả lời đó ). Sau đó, nó hoạt động tốt.
dma_k

0

Tôi không biết liệu điều này có hiệu quả hay không nhưng tôi muốn đề xuất giải pháp này.

  1. Tính xor của 100 phần tử
  2. Tính xor của 98 phần tử (sau khi loại bỏ 2 phần tử)
  3. Bây giờ (kết quả của 1) XOR (kết quả của 2) cung cấp cho bạn xor của hai số bị thiếu i..ea XOR b nếu a và b là các phần tử bị thiếu
    4.Nhận tổng số Nos bị thiếu với cách tiếp cận thông thường của bạn về tổng công thức diff và cho biết diff là d.

Bây giờ hãy chạy một vòng lặp để có được các cặp có thể (p, q) cả hai nằm trong [1, 100] và tính tổng thành d.

Khi một cặp được lấy, kiểm tra xem (kết quả của 3) XOR p = q và nếu có, chúng ta đã hoàn thành.

Vui lòng sửa cho tôi nếu tôi sai và cũng nhận xét về độ phức tạp của thời gian nếu điều này đúng


2
Tôi không nghĩ tổng và xor xác định duy nhất hai số. Chạy một vòng lặp để có được tất cả các k-tup có thể tính tổng đến d mất thời gian O (C (n, k-1)) = O (n <sup> k-1 </ sup>), trong đó, với k> 2, là xấu.
Teepeemm

0

Chúng ta có thể thực hiện Q1 và Q2 trong O (log n) hầu hết thời gian.

Giả sử của chúng tôi memory chipbao gồm các mảng nsố lượng test tubes. Và một số xtrong ống nghiệm được thể hiện bằng x milliliterchất lỏng hóa học.

Giả sử bộ xử lý của chúng tôi là a laser light. Khi chúng ta chiếu sáng tia laser, nó đi qua tất cả các ống vuông góc với chiều dài của nó. Mỗi khi nó đi qua chất lỏng hóa học, độ sáng bị giảm đi 1. Và vượt qua ánh sáng ở mốc mililit nhất định là một hoạt động của O(1).

Bây giờ nếu chúng ta chiếu tia laser vào giữa ống nghiệm và thu được độ sáng

  • bằng với một giá trị được tính toán trước (được tính khi không có số nào bị thiếu), thì các số bị thiếu lớn hơn n/2.
  • Nếu đầu ra của chúng tôi nhỏ hơn, thì có ít nhất một số bị thiếu nhỏ hơn n/2. Chúng tôi cũng có thể kiểm tra xem độ sáng có bị giảm đi 1hay không 2. nếu nó giảm đi 1thì một số còn thiếu sẽ nhỏ hơn n/2và số khác lớn hơn n/2. Nếu nó giảm đi 2thì cả hai số đều nhỏ hơn n/2.

Chúng ta có thể lặp lại quá trình trên một lần nữa và thu hẹp miền vấn đề của chúng ta. Trong mỗi bước, chúng tôi làm cho miền nhỏ hơn một nửa. Và cuối cùng chúng ta có thể đi đến kết quả của chúng tôi.

Các thuật toán song song đáng được đề cập (vì chúng thú vị),

  • sắp xếp theo một số thuật toán song song, ví dụ, hợp nhất song song có thể được thực hiện trong O(log^3 n)thời gian. Và sau đó số lượng còn thiếu có thể được tìm thấy bằng cách tìm kiếm nhị phân trong O(log n)thời gian.
  • Về mặt lý thuyết, nếu chúng ta có nbộ xử lý thì mỗi quá trình có thể kiểm tra một trong các đầu vào và đặt một số cờ xác định số (thuận tiện trong một mảng). Và trong bước tiếp theo, mỗi quy trình có thể kiểm tra từng cờ và cuối cùng xuất ra số không được gắn cờ. Toàn bộ quá trình sẽ mất O(1)thời gian. Nó có thêm O(n)yêu cầu không gian / bộ nhớ.

Lưu ý rằng hai thuật toán song song được cung cấp ở trên có thể cần thêm không gian như được đề cập trong bình luận .


Mặc dù phương pháp laser ống nghiệm thực sự rất thú vị, tôi hy vọng bạn đồng ý rằng nó không dịch tốt theo hướng dẫn phần cứng và vì vậy rất khó có thể có O(logn)trên máy tính.
SirGuy

1
Đối với phương pháp sắp xếp của bạn, sẽ cần thêm một lượng không gian phụ thuộc Nvà hơn O(N)thời gian (về sự phụ thuộc của nó vào N), mà chúng tôi dự định sẽ làm tốt hơn.
SirGuy

@SirGuy Tôi đánh giá cao sự quan tâm của bạn về khái niệm ống nghiệm và chi phí bộ nhớ xử lý song song. Bài viết của tôi là để chia sẻ suy nghĩ của tôi về vấn đề này. Bộ xử lý GPU hiện đang thực hiện xử lý song song. Ai biết được, nếu khái niệm ống nghiệm sẽ không có sẵn trong tương lai.
shuva
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.