Làm thế nào bạn có thể tìm thấy tất cả các parens không cân bằng trong một chuỗi trong thời gian tuyến tính với bộ nhớ không đổi?


11

Tôi đã được đưa ra vấn đề sau đây trong một cuộc phỏng vấn:

Cung cấp một chuỗi chứa một số hỗn hợp parens (không phải dấu ngoặc hoặc dấu ngoặc - chỉ parens) với các ký tự chữ và số khác, xác định tất cả các parens không có paren phù hợp.

Ví dụ: trong chuỗi ") (ab))", các chỉ số 0 và 5 chứa các parens không có paren phù hợp.

Tôi đưa ra giải pháp O (n) đang hoạt động bằng cách sử dụng bộ nhớ O (n), sử dụng ngăn xếp và đi qua chuỗi một lần thêm parens vào ngăn xếp và loại bỏ chúng khỏi ngăn xếp bất cứ khi nào tôi gặp phải một paren đóng và đỉnh ngăn xếp chứa một paren mở.

Sau đó, người phỏng vấn lưu ý rằng vấn đề có thể được giải quyết trong thời gian tuyến tính với bộ nhớ không đổi (như trong, không sử dụng bộ nhớ bổ sung ngoài những gì được đưa vào bởi đầu vào.)

Tôi hỏi làm thế nào và cô ấy nói điều gì đó về việc đi qua chuỗi một lần từ bên trái xác định tất cả các parens mở, và sau đó lần thứ hai từ bên phải xác định tất cả các parens gần .... hoặc có thể đó là cách khác. Tôi đã không thực sự hiểu và không muốn yêu cầu cô ấy nắm tay tôi vượt qua nó.

Bất cứ ai có thể làm rõ các giải pháp cô đề nghị?


1
Chúng tôi có thể cần một số làm rõ từ bạn đầu tiên. Là các parens đầu tiên hoặc parens thứ hai trong "(()" được coi là không cân bằng? Là các parens cuối cùng hoặc parens thứ hai cuối cùng trong "())" được coi là không cân bằng? Hoặc là đủ để xác định bất kỳ tập hợp parens nào có ít cardinality nhất mà việc loại bỏ chúng sẽ làm cho các parens còn lại cân bằng? Hay cái gì khác? Hay đây là một phần của cuộc phỏng vấn để một câu trả lời có thể đưa ra bất kỳ đặc điểm kỹ thuật chính đáng nào?
John L.

Tôi sẽ nói nó không thành vấn đề, tùy thuộc vào bạn. Loại bỏ bất kỳ bộ nào để phần còn lại cân bằng.
tạm

5
Sau đó loại bỏ tất cả; P
Veedrac

@Veedrac, tất nhiên (như bạn biết) người đăng đã quên từ 'tối thiểu' trong "Xóa mọi tập hợp tối thiểu ."
LSpice

Tôi đã không "quên nó", nhưng sẽ bỏ nó đi vì nó dường như không phải là một đặc điểm kỹ thuật quan trọng đối với tôi vì chỉ có một bộ có thể được gỡ bỏ để làm cho nó cân bằng, bên cạnh "tất cả chúng" tất nhiên là đánh bại mục đích của bài tập.
tạm

Câu trả lời:


17

Vì điều này xuất phát từ một nền tảng lập trình chứ không phải là một bài tập khoa học máy tính lý thuyết, tôi giả định rằng phải mất bộ nhớ để lưu trữ một chỉ mục vào chuỗi. Trong khoa học máy tính lý thuyết, điều này có nghĩa là sử dụng mô hình RAM; với các máy Turing, bạn không thể làm điều này và bạn cần bộ nhớ để lưu chỉ mục vào một chuỗi có độ dài .O(1)Θ(log(n))n

Bạn có thể giữ nguyên tắc cơ bản của thuật toán mà bạn đã sử dụng. Bạn đã bỏ lỡ một cơ hội để tối ưu hóa bộ nhớ.

sử dụng một ngăn xếp và đi qua chuỗi một lần thêm parens vào ngăn xếp và loại bỏ chúng khỏi ngăn xếp bất cứ khi nào tôi gặp một paren đóng và đỉnh của ngăn xếp chứa một paren mở

Vậy ngăn xếp này chứa gì? Nó sẽ không bao giờ chứa ()(dấu ngoặc đơn mở theo sau là dấu ngoặc đơn đóng), vì bất cứ khi nào )xuất hiện, bạn sẽ bật (thay vì đẩy phím ). Vì vậy, ngăn xếp luôn có dạng )…)(…(- một loạt các dấu ngoặc đơn đóng theo sau là một loạt các dấu ngoặc đơn mở.

Bạn không cần một ngăn xếp để thể hiện điều này. Chỉ cần nhớ số lượng dấu ngoặc đóng và số dấu ngoặc mở.

Nếu bạn xử lý chuỗi từ trái sang phải, sử dụng hai bộ đếm này, cái bạn có ở cuối là số dấu ngoặc đóng đóng không khớp và số dấu ngoặc mở mở không khớp.

Nếu bạn muốn báo cáo vị trí của dấu ngoặc đơn không khớp ở cuối, bạn sẽ cần nhớ vị trí của từng dấu ngoặc đơn. Điều đó sẽ đòi hỏi bộ nhớ trong trường hợp xấu nhất. Nhưng bạn không cần đợi đến khi kết thúc để tạo đầu ra. Ngay sau khi bạn tìm thấy dấu ngoặc đơn đóng không khớp, bạn sẽ biết rằng nó không khớp, vì vậy hãy xuất ngay bây giờ. Và sau đó, bạn sẽ không sử dụng số lượng dấu ngoặc đơn đóng không khớp cho bất cứ điều gì, vì vậy chỉ cần giữ một bộ đếm các dấu ngoặc đơn mở chưa từng có.Θ(n)

Tóm lại: xử lý chuỗi từ trái sang phải. Duy trì một bộ đếm của dấu ngoặc đơn mở chưa từng có. Nếu bạn thấy dấu ngoặc đơn mở, hãy tăng bộ đếm. Nếu bạn thấy dấu ngoặc đơn đóng và bộ đếm là khác không, hãy giảm bộ đếm. Nếu bạn thấy dấu ngoặc đơn đóng và bộ đếm bằng 0, hãy xuất chỉ mục hiện tại dưới dạng dấu ngoặc đóng đóng không khớp.

Giá trị cuối cùng của bộ đếm là số dấu ngoặc đơn mở không khớp, nhưng điều này không cung cấp cho bạn vị trí của chúng. Lưu ý rằng vấn đề là đối xứng. Để liệt kê các vị trí của dấu ngoặc đơn mở không khớp, chỉ cần chạy thuật toán theo hướng ngược lại.

Bài tập 1: viết nó xuống trong một ký hiệu chính thức (toán, mã giả hoặc ngôn ngữ lập trình yêu thích của bạn).

Bài tập 2: thuyết phục bản thân rằng đây là thuật toán tương tự Apass.Jack , chỉ giải thích khác nhau.


Oh Gilles rất tốt, giải thích rất tốt. Tôi hiểu hoàn hảo bây giờ. Đã được một vài năm kể từ khi tôi nhận được câu trả lời từ bạn về một trong những câu hỏi của tôi.
temporary_user_name

"Nếu bạn muốn báo cáo vị trí của dấu ngoặc đơn không khớp ở cuối, bạn sẽ cần nhớ vị trí của từng dấu ngoặc đơn." Không hẳn. Thời gian tuyến tính không có nghĩa là vượt qua đơn. Bạn có thể thực hiện một lượt đi thứ hai để tìm bất kỳ dấu ngoặc nào ở phía không khớp và đánh dấu chúng.
Vịt Mooing

Đối với bước cuối cùng, bạn không cần phải chạy ngược lại, bạn chỉ cần đánh dấu chữ N cuối cùng "(" là không khớp.
Vịt Mooing

1
@MooingDuck Điều đó không hiệu quả. Ví dụ (().
orlp

Trong khi tôi thực sự thích câu trả lời này, một cái gì đó tiếp tục làm phiền tôi về nó. Đó là một cái gì đó là "Tôi bằng cách nào đó cần phải nhớ vị trí Và tôi nghĩ vấn đề tôi gặp phải là: làm thế nào để bạn" xuất chỉ mục hiện tại "mà không tiêu thụ bộ nhớ (hoặc bối cảnh khá cụ thể nơi đầu ra của bạn được tiêu thụ theo cách đó thứ tự đầu ra của bạn không thành vấn đề).
Édouard

8

Vì chúng ta chỉ có thể bỏ qua tất cả các ký tự chữ và số, nên chúng ta sẽ giả sử chuỗi chỉ chứa dấu ngoặc đơn từ bây giờ. Như trong câu hỏi, chỉ có một loại dấu ngoặc đơn, "()".

Nếu chúng ta tiếp tục loại bỏ các dấu ngoặc đơn cân bằng cho đến khi không thể xóa các dấu ngoặc cân bằng hơn, tất cả các dấu ngoặc đơn còn lại phải trông giống như "))) (((((tất cả các dấu ngoặc đơn không cân bằng. Quan sát này cho thấy rằng chúng ta nên tìm thấy bước ngoặt đầu tiên , trước đó chúng ta chỉ có dấu ngoặc đơn đóng không cân bằng và sau đó chúng ta chỉ có dấu ngoặc đơn mở không cân bằng.

Đây là thuật toán. Tóm lại, nó tính toán bước ngoặt đầu tiên. Sau đó, nó xuất ra dấu ngoặc đơn đóng thêm, quét chuỗi từ đầu sang phải cho đến khi bước ngoặt. Đối xứng, nó xuất ra dấu ngoặc đơn mở thêm, quét từ đầu đến bên trái cho đến khi bước ngoặt.


Đặt strchuỗi là một mảng các ký tự, có kích thước là .n

Khởi tạo turning_point=0, maximum_count=0, count=0. Đối với mỗi itừ 0để n-1làm như sau.

  1. Nếu str[i] = ')', thêm 1 vào count; mặt khác, trừ 1.
  2. Nếu count > maximum_count, đặt turning_point=imaximum_count=count.

Bây giờ turning_pointlà chỉ số của bước ngoặt.

Đặt lại maximum_count=0, count=0. Đối với mỗi itừ 0để turning_pointlàm như sau.

  1. Nếu str[i] = ')', thêm 1 vào count; mặt khác, trừ 1.
  2. Nếu count > maximum_count, đặt maximum_count = count. Đầu ra ilà chỉ số của dấu ngoặc đơn đóng không cân bằng.

Đặt lại maximum_count=0, count=0. Đối với mỗi itừ n-1để turning_point+1xuống làm như sau.

  1. Nếu str[j] = '(', thêm 1 vào count; mặt khác, trừ 1.
  2. Nếu count > maximum_count, đặt maximum_count = count. Đầu ra ilà chỉ số của dấu ngoặc đơn mở không cân bằng.

Rõ ràng là thuật toán chạy trong thời gian và bộ nhớ phụ và bộ nhớ đầu ra , trong đó là số dấu ngoặc đơn không cân bằng.O(n)O(1)O(u)u


Nếu chúng ta phân tích thuật toán ở trên, chúng ta sẽ thấy rằng, trên thực tế, chúng ta không cần phải tìm và sử dụng bước ngoặt nào cả. Quan sát tốt đẹp rằng tất cả các dấu ngoặc đóng không cân bằng xảy ra trước khi tất cả các dấu ngoặc mở không cân bằng có thể bị bỏ qua mặc dù thú vị.

Đây là mã trong Python .

Chỉ cần nhấn "chạy" để xem một số kết quả thử nghiệm.


Bài tập 1. Chỉ ra rằng thuật toán trên sẽ xuất ra một tập các dấu ngoặc đơn với số lượng thẻ ít nhất sao cho các dấu ngoặc đơn còn lại được cân bằng.

Bài toán 1. Chúng ta có thể khái quát thuật toán cho trường hợp khi chuỗi chứa hai loại dấu ngoặc đơn như "() []" không? Chúng ta phải xác định cách nhận biết và xử lý tình huống mới, trường hợp xen kẽ, "([)]".


Lol, bài tập 1 và bài toán 1, dễ thương. Logic của thuật toán mà bạn mô tả rất khó hình dung. Tôi sẽ phải viết mã này vào ngày mai để có được nó.
tạm

Có vẻ như tôi đã bỏ lỡ lời giải thích khá rõ ràng nhưng quan trọng nhất. Logic trên thực tế là rất đơn giản. Đầu tiên, chúng tôi xuất ra mỗi dấu ngoặc đơn mở thêm. Khi chúng tôi đã vượt qua bước ngoặt, chúng tôi xuất ra từng dấu ngoặc đơn đóng thêm. Làm xong.
John L.

Tìm dấu ngoặc mở không cân bằng là không chính xác. Tức là nếu mảng của bạn là "())", p là 2 và p + 1 nằm ngoài ranh giới mảng. Chỉ là một ý tưởng - để tìm các dấu ngoặc mở không cân bằng, bạn có thể đảo ngược mảng và sử dụng một phần của thuật toán để tìm các dấu ngoặc đóng không cân bằng (tất nhiên, với các chỉ mục được điều chỉnh ngược).
OzrenTkalcecKrznaric

@OzrenTkalcecKrznaric Chính xác vì nằm ngoài ranh giới, không có dấu ngoặc đơn mở không cân bằng trong "())". p+1
John L.

Tôi đã hiểu một chút để hiểu điều này, nhưng tôi thích nó, nó khá thông minh .. và hoạt động ít nhất cho mọi trường hợp tôi đã nghĩ
dquijada
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.