*(>:^]*(*>{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}*<)]*(:)*=<*)>]
Cần được chạy với các -ln
cờ dòng lệnh (do đó +4 byte). In 0
cho số tổng hợp và 1
cho số nguyên tố.
Hãy thử trực tuyến!
Tôi nghĩ rằng đây là chương trình Stack Mèo không tầm thường đầu tiên.
Giải trình
Giới thiệu về Stack Stack nhanh chóng:
- Stack Mèo hoạt động trên một băng vô hạn của các ngăn xếp, với một đầu băng chỉ vào một ngăn xếp hiện tại. Mỗi ngăn xếp ban đầu được lấp đầy với một số lượng không giới hạn. Nói chung tôi sẽ bỏ qua các số không này trong từ ngữ của tôi, vì vậy khi tôi nói "đáy của ngăn xếp" tôi có nghĩa là giá trị khác không thấp nhất và nếu tôi nói "ngăn xếp trống rỗng", ý tôi là chỉ có các số không trên đó.
- Trước khi chương trình bắt đầu, a
-1
được đẩy lên ngăn xếp ban đầu, và sau đó toàn bộ đầu vào được đẩy lên trên đó. Trong trường hợp này, do-n
cờ, đầu vào được đọc dưới dạng số nguyên thập phân.
- Vào cuối chương trình, ngăn xếp hiện tại được sử dụng cho đầu ra. Nếu có một cái
-1
ở phía dưới, nó sẽ bị bỏ qua. Một lần nữa, do-n
cờ, các giá trị từ ngăn xếp được in đơn giản dưới dạng số nguyên thập phân được phân tách bằng dòng.
- Stack Mèo là một ngôn ngữ chương trình có thể đảo ngược: mọi đoạn mã đều có thể được hoàn tác (không có Stack Mèo theo dõi lịch sử rõ ràng). Cụ thể hơn, để đảo ngược bất kỳ đoạn mã nào, bạn chỉ cần phản chiếu nó, ví dụ như
<<(\-_)
trở thành (_-/)>>
. Mục tiêu thiết kế này đặt ra các hạn chế khá nghiêm trọng đối với các loại toán tử và cấu trúc luồng điều khiển tồn tại trong ngôn ngữ và loại chức năng nào bạn có thể tính toán trên trạng thái bộ nhớ chung.
Trên hết, mọi chương trình Stack Mèo phải tự đối xứng. Bạn có thể nhận thấy rằng đây không phải là trường hợp của mã nguồn trên. Đây là những gì -l
cờ dành cho: nó hoàn toàn phản chiếu mã bên trái, sử dụng ký tự đầu tiên cho trung tâm. Do đó chương trình thực tế là:
[<(*>=*(:)*[(>*{[[>[:<[>>_(_-<<(-!>)>(>-)):]<^:>!->}<*)*[^:<)*(>:^]*(*>{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}*<)]*(:)*=<*)>]
Lập trình hiệu quả với toàn bộ mã rất không tầm thường và không trực quan và chưa thực sự tìm ra cách con người có thể làm điều đó. Chúng tôi đã buộc phải lập trình như vậy cho các nhiệm vụ đơn giản hơn, nhưng sẽ không thể đến bất cứ nơi nào gần đó bằng tay. May mắn thay, chúng tôi đã tìm thấy một mô hình cơ bản cho phép bạn bỏ qua một nửa chương trình. Mặc dù điều này chắc chắn là không tối ưu, nhưng hiện tại đây là cách duy nhất được biết để lập trình hiệu quả trong Stack Mèo.
Vì vậy, trong câu trả lời này, mẫu của mẫu đã nói là mẫu này (có một số thay đổi trong cách thực hiện):
[<(...)*(...)>]
Khi chương trình bắt đầu, băng ngăn xếp trông như thế này (đối với đầu vào 4
, giả sử):
4
... -1 ...
0
^
Việc [
di chuyển đỉnh của ngăn xếp sang trái (và đầu băng dọc) - chúng tôi gọi đây là "đẩy". Và <
di chuyển đầu băng một mình. Vì vậy, sau hai lệnh đầu tiên, chúng ta đã gặp tình huống này:
... 4 -1 ...
0 0 0
^
Bây giờ, (...)
một vòng lặp có thể được sử dụng khá dễ dàng như một điều kiện: vòng lặp được nhập và chỉ còn lại khi đỉnh của ngăn xếp hiện tại là dương. Vì hiện tại nó bằng không, chúng tôi bỏ qua toàn bộ nửa đầu của chương trình. Bây giờ lệnh trung tâm là *
. Điều này chỉ đơn giản XOR 1
, tức là nó bật một chút ít quan trọng nhất của đỉnh ngăn xếp và trong trường hợp này biến 0
thành 1
:
... 1 4 -1 ...
0 0 0
^
Bây giờ chúng ta bắt gặp hình ảnh phản chiếu của (...)
. Lần này phía trên cùng của ngăn xếp là tích cực và chúng tôi làm nhập mã. Trước khi chúng tôi xem xét những gì diễn ra bên trong dấu ngoặc đơn, hãy để tôi giải thích cách chúng tôi kết thúc ở cuối: chúng tôi muốn đảm bảo rằng ở cuối khối này, chúng tôi lại có đầu băng trên một giá trị dương (để vòng lặp kết thúc sau một lần lặp duy nhất và được sử dụng đơn giản như một điều kiện tuyến tính), rằng ngăn xếp bên phải giữ đầu ra và bên phải ngăn xếp đó giữ a -1
. Nếu đó là trường hợp, chúng tôi rời khỏi vòng lặp, >
di chuyển lên giá trị đầu ra và ]
đẩy nó lên -1
để chúng tôi có một ngăn xếp sạch cho đầu ra.
Đó là điều đó. Bây giờ bên trong dấu ngoặc đơn, chúng ta có thể làm bất cứ điều gì chúng ta muốn kiểm tra tính nguyên thủy miễn là chúng ta đảm bảo rằng chúng ta thiết lập mọi thứ như được mô tả trong đoạn trước ở cuối (có thể dễ dàng thực hiện với một số thao tác đẩy và băng đầu). Lần đầu tiên tôi thử giải quyết vấn đề với định lý của Wilson nhưng kết thúc tốt hơn 100 byte, bởi vì tính toán giai đoạn bình phương thực sự khá tốn kém trong Stack Mèo (ít nhất là tôi đã không tìm thấy một cách ngắn). Vì vậy, tôi đã đi với bộ phận thử nghiệm thay thế và điều đó thực sự đơn giản hơn nhiều. Hãy nhìn vào bit tuyến tính đầu tiên:
>:^]
Bạn đã thấy hai trong số các lệnh đó. Ngoài ra, :
hoán đổi hai giá trị trên cùng của ngăn xếp hiện tại và ^
XOR giá trị thứ hai thành giá trị trên cùng. Điều này tạo ra :^
một mô hình chung để nhân đôi một giá trị trên một ngăn xếp trống (chúng ta kéo một số 0 lên trên giá trị và sau đó biến số 0 thành 0 XOR x = x
). Vì vậy, sau này, phần băng của chúng tôi trông như thế này:
4
... 1 4 -1 ...
0 0 0
^
Thuật toán phân chia thử nghiệm mà tôi đã triển khai không hoạt động cho đầu vào 1
, vì vậy chúng ta nên bỏ qua mã trong trường hợp đó. Chúng tôi có thể dễ dàng ánh xạ 1
tới 0
và mọi thứ khác theo các giá trị tích cực *
, vì vậy đây là cách chúng tôi làm điều đó:
*(*...)
Đó là chúng ta biến 1
thành 0
, bỏ qua một phần lớn của mã nếu chúng ta thực sự nhận được 0
, nhưng bên trong chúng ta ngay lập tức hoàn tác *
để chúng ta lấy lại giá trị đầu vào. Chúng ta chỉ cần đảm bảo một lần nữa rằng chúng ta kết thúc một giá trị dương ở cuối dấu ngoặc đơn để chúng không bắt đầu lặp. Trong điều kiện, chúng ta di chuyển một ngăn xếp đúng với >
và sau đó bắt đầu vòng phân chia thử nghiệm chính:
{<-!<:^>[:((-<)<(<!-)>>-_)_<<]>:]<]]}
Dấu ngoặc (trái ngược với dấu ngoặc đơn) xác định một loại vòng lặp khác: đó là vòng lặp do-while, nghĩa là nó luôn chạy trong ít nhất một lần lặp. Sự khác biệt khác là điều kiện kết thúc: khi vào vòng lặp Stack Cat ghi nhớ giá trị trên cùng của ngăn xếp hiện tại ( 0
trong trường hợp của chúng tôi). Vòng lặp sau đó sẽ chạy cho đến khi cùng một giá trị này được nhìn thấy ở cuối vòng lặp. Điều này thuận tiện cho chúng tôi: trong mỗi lần lặp, chúng tôi chỉ cần tính phần còn lại của ước số tiềm năng tiếp theo và di chuyển nó lên ngăn xếp này, chúng tôi sẽ bắt đầu vòng lặp. Khi chúng ta tìm thấy một ước số, phần còn lại là 0
và vòng lặp dừng lại. Chúng tôi sẽ thử các ước số bắt đầu từ n-1
và sau đó giảm chúng xuống 1
. Điều đó có nghĩa là a) chúng tôi biết điều này sẽ chấm dứt khi chúng tôi đạt được1
muộn nhất và b) sau đó chúng ta có thể xác định xem số đó có phải là số nguyên tố hay không bằng cách kiểm tra ước số cuối cùng mà chúng ta đã thử (nếu đó là 1
số nguyên tố, nếu không thì không).
Chúng ta hãy đi đến đó. Có một phần tuyến tính ngắn ở đầu:
<-!<:^>[:
Bạn biết những gì hầu hết những điều đó làm bây giờ. Các lệnh mới là -
và !
. Mèo Stack không có toán tử tăng hoặc giảm. Tuy nhiên, nó có -
(phủ định, tức là nhân với -1
) và !
(bitwise KHÔNG, tức là nhân với -1
và giảm). Chúng có thể được kết hợp thành một mức tăng !-
hoặc giảm -!
. Vì vậy, chúng tôi giải mã bản sao n
trên đầu trang -1
, sau đó tạo một bản sao khác n
trên ngăn xếp bên trái, sau đó lấy bộ chia thử nghiệm mới và đặt nó bên dưới n
. Vì vậy, trong lần lặp đầu tiên, chúng ta nhận được điều này:
4
3
... 1 4 -1 ...
0 0 0
^
Trong các lần lặp tiếp theo, ý 3
chí sẽ được thay thế bằng ước số thử nghiệm tiếp theo, v.v. (trong khi hai bản sao n
sẽ luôn có cùng giá trị tại thời điểm này).
((-<)<(<!-)>>-_)
Đây là tính toán modulo. Vì các vòng lặp chấm dứt trên các giá trị dương, ý tưởng là bắt đầu từ -n
và liên tục thêm ước số thử nghiệm d
vào nó cho đến khi chúng ta nhận được giá trị dương. Khi chúng tôi làm, chúng tôi trừ kết quả d
và điều này cho chúng tôi phần còn lại. Một mẹo nhỏ ở đây là chúng ta không thể đặt một -n
chồng lên trên ngăn xếp và bắt đầu một vòng lặp thêm d
: nếu đỉnh của ngăn xếp là âm, vòng lặp sẽ không được nhập. Đó là những hạn chế của một ngôn ngữ lập trình đảo ngược.
Vì vậy, để khắc phục vấn đề này, chúng tôi bắt đầu với n
trên cùng của ngăn xếp, nhưng chỉ phủ nhận nó ở lần lặp đầu tiên. Một lần nữa, điều đó nghe có vẻ đơn giản hơn hóa ra là ...
(-<)
Khi đỉnh của ngăn xếp là dương (tức là chỉ trong lần lặp đầu tiên), chúng ta phủ nhận nó với -
. Tuy nhiên, chúng tôi không thể làm (-)
vì sau đó chúng tôi sẽ không rời khỏi vòng lặp cho đến khi -
được áp dụng hai lần. Vì vậy, chúng tôi di chuyển một ô còn lại <
bởi vì chúng tôi biết có một giá trị dương ở đó (cái 1
). Được rồi, vì vậy bây giờ chúng tôi đã phủ nhận đáng tin cậy n
trong lần lặp đầu tiên. Nhưng chúng ta có một vấn đề mới: đầu băng bây giờ ở một vị trí khác trên lần lặp đầu tiên so với mọi đầu khác. Chúng ta cần củng cố điều này trước khi chúng ta tiếp tục. Tiếp theo <
di chuyển đầu băng bên trái. Tình huống trên lần lặp đầu tiên:
-4
3
... 1 4 -1 ...
0 0 0 0
^
Và trên lần lặp thứ hai (hãy nhớ rằng chúng tôi đã thêm d
một lần vào -n
bây giờ):
-1
3
... 1 4 -1 ...
0 0 0
^
Điều kiện tiếp theo hợp nhất các đường dẫn này một lần nữa:
(<!-)
Trong lần lặp đầu tiên, đầu băng chỉ vào 0, vì vậy điều này được bỏ qua hoàn toàn. Trên các lần lặp tiếp theo, đầu băng chỉ vào một cái, vì vậy chúng tôi thực hiện điều này, di chuyển sang bên trái và tăng ô ở đó. Vì chúng ta biết ô bắt đầu từ 0, nên nó sẽ luôn dương để chúng ta có thể rời khỏi vòng lặp. Điều này đảm bảo chúng ta luôn kết thúc hai ngăn xếp bên trái của ngăn xếp chính và bây giờ có thể di chuyển trở lại với >>
. Sau đó, vào cuối vòng lặp modulo, chúng tôi làm -_
. Bạn đã biết -
. _
là để trừ đi ^
XOR là gì : nếu đỉnh của ngăn xếp là a
và giá trị bên dưới là b
nó thay thế a
bằng b-a
. Vì lần đầu tiên chúng tôi phủ nhận a
, -_
thay thế a
bằng b+a
, do đó thêmd
vào tổng số chạy của chúng tôi.
Sau khi vòng lặp kết thúc (chúng tôi đã đạt đến giá trị dương), băng trông như thế này:
2
3
... 1 1 4 -1 ...
0 0 0 0
^
Giá trị bên trái nhất có thể là bất kỳ số dương nào. Trong thực tế, đó là số lần lặp lại trừ đi một lần. Bây giờ có một bit tuyến tính ngắn khác:
_<<]>:]<]]
Như tôi đã nói trước đây, chúng ta cần trừ kết quả d
để có được phần còn lại thực tế ( 3-2 = 1 = 4 % 3
), vì vậy chúng ta chỉ cần làm _
lại một lần nữa. Tiếp theo, chúng ta cần dọn sạch ngăn xếp mà chúng ta đã tăng ở bên trái: khi chúng ta thử ước số tiếp theo, nó cần phải bằng 0 lần nữa, để lần lặp đầu tiên hoạt động. Vì vậy, chúng tôi di chuyển đến đó và đẩy giá trị dương đó lên ngăn xếp trợ giúp khác <<]
và sau đó di chuyển trở lại vào ngăn xếp hoạt động của chúng tôi với ngăn xếp khác >
. Chúng tôi kéo lên d
với :
và đẩy nó trở lại vào -1
với ]
và sau đó chúng tôi di chuyển phần còn lại vào ngăn xếp có điều kiện của chúng tôi với <]]
. Đó là kết thúc của vòng phân chia thử nghiệm: điều này tiếp tục cho đến khi chúng ta nhận được phần còn lại bằng 0, trong trường hợp đó, ngăn xếp bên trái chứan
Ước số lớn nhất (khác n
).
Sau khi vòng lặp kết thúc, ngay *<
trước khi chúng ta nối các đường dẫn với đầu vào 1
lại. Đơn *
giản chỉ cần biến số 0 thành a 1
, chúng ta sẽ cần một chút, sau đó chúng ta chuyển sang ước số <
(để chúng ta ở trên cùng một ngăn xếp như với đầu vào 1
).
Tại thời điểm này, nó giúp so sánh ba loại đầu vào khác nhau. Đầu tiên, trường hợp đặc biệt n = 1
mà chúng tôi chưa thực hiện bất kỳ nội dung phân chia thử nghiệm nào:
0
... 1 1 -1 ...
0 0 0
^
Sau đó, ví dụ trước của chúng tôi n = 4
, một số tổng hợp:
2
1 2 1
... 1 4 -1 1 ...
0 0 0 0
^
Và cuối cùng, n = 3
một số nguyên tố:
3
1 1 1
... 1 3 -1 1 ...
0 0 0 0
^
Vì vậy, đối với các số nguyên tố, chúng ta có một số 1
trên ngăn xếp này và đối với các số tổng hợp, chúng ta có một 0
hoặc một số dương lớn hơn 2
. Chúng tôi biến tình huống này thành 0
hoặc 1
chúng tôi cần với đoạn mã cuối cùng sau đây:
]*(:)*=<*
]
chỉ cần đẩy giá trị này sang bên phải. Sau đó *
được sử dụng để đơn giản hóa tình hình có điều kiện rất nhiều: do chuyển đổi qua lại các bit quan trọng nhất, chúng tôi rẽ 1
(nguyên tố) vào 0
, 0
(composite) vào giá trị tích cực 1
, và tất cả các giá trị tích cực khác sẽ vẫn còn dương tính. Bây giờ chúng ta chỉ cần phân biệt giữa 0
và tích cực. Đó là nơi chúng ta sử dụng cái khác (:)
. Nếu đỉnh của ngăn xếp là 0
(và đầu vào là số nguyên tố), thì điều này chỉ đơn giản là bỏ qua. Nhưng nếu đỉnh của ngăn xếp là dương (và đầu vào là số hỗn hợp) thì điều này sẽ hoán đổi nó với 1
, vì vậy bây giờ chúng ta có 0
kết hợp và1
cho các số nguyên tố - chỉ có hai giá trị riêng biệt. Tất nhiên, chúng trái ngược với những gì chúng ta muốn đầu ra, nhưng điều đó dễ dàng được sửa với cái khác *
.
Bây giờ, tất cả những gì còn lại là để khôi phục mô hình các ngăn xếp được dự kiến bởi khung xung quanh của chúng tôi: đầu băng trên một giá trị dương, kết quả ở trên cùng của ngăn xếp bên phải và một -1
ngăn xếp bên phải của ngăn xếp đó . Đây là những gì =<*
dành cho. =
hoán đổi đỉnh của hai ngăn xếp liền kề, do đó di chuyển -1
sang bên phải của kết quả, ví dụ cho đầu vào 4
lại:
2 0
1 3
... 1 4 1 -1 ...
0 0 0 0 0
^
Sau đó, chúng ta chỉ cần di chuyển sang trái <
và biến số 0 thành một với *
. Và đó là điều đó.
Nếu bạn muốn tìm hiểu sâu hơn về cách chương trình hoạt động, bạn có thể sử dụng các tùy chọn gỡ lỗi. Hoặc thêm -d
cờ và chèn "
bất cứ nơi nào bạn muốn để xem trạng thái bộ nhớ hiện tại, ví dụ như thế này hoặc sử dụng -D
cờ để có được một dấu vết hoàn chỉnh của toàn bộ chương trình . Ngoài ra, bạn có thể sử dụng EswericIDE của Timwi's , bao gồm trình thông dịch Stack Mèo với trình gỡ lỗi từng bước.