Funciton , niekonkurencyjny
AKTUALIZACJA! Ogromna poprawa wydajności! n = 7 kończy się teraz w mniej niż 10 minut! Zobacz wyjaśnienie na dole!
To była dobra zabawa pisać. To jest solute brute-force dla tego problemu napisanego w Funciton. Niektóre faktoidy:
- Akceptuje liczbę całkowitą na STDIN. Obce białe spacje psują go, w tym nowa linia po liczbie całkowitej.
- Używa liczb od 0 do n - 1 (nie od 1 do n ).
- Wypełnia siatkę „do tyłu”, więc otrzymujesz rozwiązanie, w którym czytany jest dolny wiersz,
3 2 1 0
a nie górny 0 1 2 3
.
- Poprawnie wyprowadza
0
(jedyne rozwiązanie) dla n = 1.
- Pusty wynik dla n = 2 i n = 3.
- Po skompilowaniu do pliku exe zajmuje około 8¼ minuty dla n = 7 (było około godziny przed poprawą wydajności). Bez kompilacji (przy użyciu interpretera) zajmuje to około 1,5 razy dłużej, więc warto skorzystać z kompilatora.
- Jako osobisty kamień milowy, po raz pierwszy napisałem cały program Funciton bez uprzedniego napisania programu w języku pseudokodu. Najpierw jednak napisałem to w rzeczywistym języku C #.
- (Jednak nie po raz pierwszy dokonałem zmiany, aby znacznie poprawić wydajność czegoś w Funciton. Za pierwszym razem zrobiłem to w funkcji silni. Zamiana kolejności operandów mnożenia zrobiła ogromną różnicę z powodu jak działa algorytm mnożenia . Na wszelki wypadek.)
Bez ceregieli:
┌────────────────────────────────────┐ ┌─────────────────┐
│ ┌─┴─╖ ╓───╖ ┌─┴─╖ ┌──────┐ │
│ ┌─────────────┤ · ╟─╢ Ӂ ╟─┤ · ╟───┤ │ │
│ │ ╘═╤═╝ ╙─┬─╜ ╘═╤═╝ ┌─┴─╖ │ │
│ │ └─────┴─────┘ │ ♯ ║ │ │
│ ┌─┴─╖ ╘═╤═╝ │ │
│ ┌────────────┤ · ╟───────────────────────────────┴───┐ │ │
┌─┴─╖ ┌─┴─╖ ┌────╖ ╘═╤═╝ ┌──────────┐ ┌────────┐ ┌─┴─╖│ │
│ ♭ ║ │ × ╟───┤ >> ╟───┴───┘ ┌─┴─╖ │ ┌────╖ └─┤ · ╟┴┐ │
╘═╤═╝ ╘═╤═╝ ╘══╤═╝ ┌─────┤ · ╟───────┴─┤ << ╟─┐ ╘═╤═╝ │ │
┌───────┴─────┘ ┌────╖ │ │ ╘═╤═╝ ╘══╤═╝ │ │ │ │
│ ┌─────────┤ >> ╟─┘ │ └───────┐ │ │ │ │ │
│ │ ╘══╤═╝ ┌─┴─╖ ╔═══╗ ┌─┴─╖ ┌┐ │ │ ┌─┴─╖ │ │
│ │ ┌┴┐ ┌───────┤ ♫ ║ ┌─╢ 0 ║ ┌─┤ · ╟─┤├─┤ ├─┤ Ӝ ║ │ │
│ │ ╔═══╗ └┬┘ │ ╘═╤═╝ │ ╚═╤═╝ │ ╘═╤═╝ └┘ │ │ ╘═╤═╝ │ │
│ │ ║ 1 ╟───┬┘ ┌─┴─╖ └───┘ ┌─┴─╖ │ │ │ │ │ ┌─┴─╖ │
│ │ ╚═══╝ ┌─┴─╖ │ ɓ ╟─────────────┤ ? ╟─┘ │ ┌─┴─╖ │ ├─┤ · ╟─┴─┐
│ ├─────────┤ · ╟─┐ ╘═╤═╝ ╘═╤═╝ ┌─┴────┤ + ╟─┘ │ ╘═╤═╝ │
┌─┴─╖ ┌─┴─╖ ╘═╤═╝ │ ╔═╧═╕ ╔═══╗ ┌───╖ ┌─┴─╖ ┌─┴─╖ ╘═══╝ │ │ │
┌─┤ · ╟─┤ · ╟───┐ └┐ └─╢ ├─╢ 0 ╟─┤ ⌑ ╟─┤ ? ╟─┤ · ╟──────────────┘ │ │
│ ╘═╤═╝ ╘═╤═╝ └───┐ ┌┴┐ ╚═╤═╛ ╚═╤═╝ ╘═══╝ ╘═╤═╝ ╘═╤═╝ │ │
│ │ ┌─┴─╖ ┌───╖ │ └┬┘ ┌─┴─╖ ┌─┘ │ │ │ │
│ ┌─┴───┤ · ╟─┤ Җ ╟─┘ └────┤ ? ╟─┴─┐ ┌─────────────┘ │ │
│ │ ╘═╤═╝ ╘═╤═╝ ╘═╤═╝ │ │╔════╗╔════╗ │ │
│ │ │ ┌──┴─╖ ┌┐ ┌┐ ┌─┴─╖ ┌─┴─╖ │║ 10 ║║ 32 ║ ┌─────────────────┘ │
│ │ │ │ << ╟─┤├─┬─┤├─┤ · ╟─┤ · ╟─┘╚══╤═╝╚╤═══╝ ┌──┴──┐ │
│ │ │ ╘══╤═╝ └┘ │ └┘ ╘═╤═╝ ╘═╤═╝ │ ┌─┴─╖ ┌─┴─╖ ┌─┴─╖ │
│ │ ┌─┴─╖ ┌─┴─╖ ┌─┴─╖ ┌─┴─╖ ╔═╧═╕ └─┤ ? ╟─┤ · ╟─┤ % ║ │
│ └─────┤ · ╟─┤ · ╟──┤ Ӂ ╟──┤ ɱ ╟─╢ ├───┐ ╘═╤═╝ ╘═╤═╝ ╘═╤═╝ │
│ ╘═╤═╝ ╘═╤═╝ ╘═╤═╝ ╘═══╝ ╚═╤═╛ ┌─┴─╖ ┌─┴─╖ │ └────────────────────┘
│ └─────┤ │ └───┤ ‼ ╟─┤ ‼ ║ │ ┌──────┐
│ │ │ ╘═══╝ ╘═╤═╝ │ │ ┌────┴────╖
│ │ │ ┌─┴─╖ │ │ │ str→int ║
│ │ └──────────────────────┤ · ╟───┴─┐ │ ╘════╤════╝
│ │ ┌─────────╖ ╘═╤═╝ │ ╔═╧═╗ ┌──┴──┐
│ └──────────┤ int→str ╟──────────┘ │ ║ ║ │ ┌───┴───┐
│ ╘═════════╝ │ ╚═══╝ │ │ ┌───╖ │
└───────────────────────────────────────────────────────┘ │ └─┤ × ╟─┘
┌──────────────┐ ╔═══╗ │ ╘═╤═╝
╔════╗ │ ╓───╖ ┌───╖ │ ┌───╢ 0 ║ │ ┌─┴─╖ ╔═══╗
║ −1 ║ └─╢ Ӝ ╟─┤ × ╟──┴──────┐ │ ╚═╤═╝ └───┤ Ӂ ╟─╢ 0 ║
╚═╤══╝ ╙───╜ ╘═╤═╝ │ │ ┌─┴─╖ ╘═╤═╝ ╚═══╝
┌─┴──╖ ┌┐ ┌───╖ ┌┐ ┌─┴──╖ ╔════╗ │ │ ┌─┤ ╟───────┴───────┐
│ << ╟─┤├─┤ ÷ ╟─┤├─┤ << ║ ║ −1 ║ │ │ │ └─┬─╜ ┌─┐ ┌─────┐ │
╘═╤══╝ └┘ ╘═╤═╝ └┘ ╘═╤══╝ ╚═╤══╝ │ │ │ └───┴─┘ │ ┌─┴─╖ │
│ └─┘ └──────┘ │ │ └───────────┘ ┌─┤ ? ╟─┘
└──────────────────────────────┘ ╓───╖ └───────────────┘ ╘═╤═╝
┌───────────╢ Җ ╟────────────┐ │
┌────────────────────────┴───┐ ╙───╜ │
│ ┌─┴────────────────────┐ ┌─┴─╖
┌─┴─╖ ┌─┴─╖ ┌─┴─┤ · ╟──────────────────┐
│ ♯ ║ ┌────────────────────┤ · ╟───────┐ │ ╘═╤═╝ │
╘═╤═╝ │ ╘═╤═╝ │ │ │ ┌───╖ │
┌─────┴───┘ ┌─────────────────┴─┐ ┌───┴───┐ ┌─┴─╖ ┌─┴─╖ ┌─┤ × ╟─┴─┐
│ │ ┌─┴─╖ │ ┌───┴────┤ · ╟─┤ · ╟──────────┤ ╘═╤═╝ │
│ │ ┌───╖ ┌───╖ ┌──┤ · ╟─┘ ┌─┴─┐ ╘═╤═╝ ╘═╤═╝ ┌─┴─╖ │ │
│ ┌────┴─┤ ♭ ╟─┤ × ╟──┘ ╘═╤═╝ │ ┌─┴─╖ ┌───╖└┐ ┌──┴─╖ ┌─┤ · ╟─┘ │
│ │ ╘═══╝ ╘═╤═╝ ┌───╖ │ │ │ × ╟─┤ Ӝ ╟─┴─┤ ÷% ╟─┐ │ ╘═╤═╝ ┌───╖ │
│ ┌─────┴───┐ ┌────┴───┤ Ӝ ╟─┴─┐ │ ╘═╤═╝ ╘═╤═╝ ╘══╤═╝ │ │ └───┤ Ӝ ╟─┘
│ ┌─┴─╖ ┌───╖ │ │ ┌────╖ ╘═╤═╝ │ └───┘ ┌─┴─╖ │ │ └────┐ ╘═╤═╝
│ │ × ╟─┤ Ӝ ╟─┘ └─┤ << ╟───┘ ┌─┴─╖ ┌───────┤ · ╟───┐ │ ┌─┴─╖ ┌───╖ │ │
│ ╘═╤═╝ ╘═╤═╝ ╘══╤═╝ ┌───┤ + ║ │ ╘═╤═╝ ├──┴─┤ · ╟─┤ × ╟─┘ │
└───┤ └────┐ ╔═══╗ ┌─┴─╖ ┌─┴─╖ ╘═╤═╝ │ ╔═══╗ ┌─┴─╖ ┌─┴─╖ ╘═╤═╝ ╘═╤═╝ │
┌─┴─╖ ┌────╖ │ ║ 0 ╟─┤ ? ╟─┤ = ║ ┌┴┐ │ ║ 0 ╟─┤ ? ╟─┤ = ║ │ │ ┌────╖ │
│ × ╟─┤ << ╟─┘ ╚═══╝ ╘═╤═╝ ╘═╤═╝ └┬┘ │ ╚═══╝ ╘═╤═╝ ╘═╤═╝ │ └─┤ << ╟─┘
╘═╤═╝ ╘═╤══╝ ┌┐ ┌┐ │ │ └───┘ ┌─┴─╖ ├──────┘ ╘═╤══╝
│ └────┤├──┬──┤├─┘ ├─────────────────┤ · ╟───┘ │
│ └┘┌─┴─╖└┘ │ ┌┐ ┌┐ ╘═╤═╝ ┌┐ ┌┐ │
└────────────┤ · ╟─────────┘ ┌─┤├─┬─┤├─┐ └───┤├─┬─┤├────────────┘
╘═╤═╝ │ └┘ │ └┘ │ └┘ │ └┘
└───────────────┘ │ └────────────┘
Objaśnienie pierwszej wersji
Pierwsza wersja zajęła około godziny n = 7. Poniżej wyjaśniono głównie, jak działała ta wolna wersja. Na dole wyjaśnię, jakie zmiany wprowadziłem, aby uzyskać mniej niż 10 minut.
Wycieczka na kawałki
Ten program potrzebuje bitów. Potrzebuje wielu bitów i potrzebuje ich we wszystkich właściwych miejscach. Doświadczeni programiści Funciton już wiedzą, że jeśli potrzebujesz n bitów, możesz użyć wzoru
które w Funcitonie można wyrazić jako
Podczas optymalizacji wydajności przyszło mi do głowy, że mogę obliczyć tę samą wartość znacznie szybciej, korzystając z tego wzoru:
Mam nadzieję, że wybaczysz mi, że nie zaktualizowałem odpowiednio całej grafiki równania w tym poście.
Powiedzmy, że nie chcesz ciągłego bloku bitów; tak naprawdę chcesz n bitów w regularnych odstępach co k -ty bit, tak jak poniżej:
LSB
↓
00000010000001000000100000010000001
└──┬──┘
k
Wzór na to jest dość prosty, gdy się zorientujesz:
W kodzie funkcja Ӝ
przyjmuje wartości n i k i oblicza tę formułę.
Śledzenie używanych numerów
Na końcowej siatce jest n ² liczb, a każda liczba może być dowolną z n możliwych wartości. Aby śledzić, które liczby są dozwolone w każdej komórce, zachowujemy liczbę składającą się z n ³ bitów, w których ustawiony jest bit wskazujący, że określona wartość jest pobierana. Początkowo liczba ta wynosi oczywiście 0.
Algorytm rozpoczyna się w prawym dolnym rogu. Po „odgadnięciu” pierwszą liczbą jest 0, musimy śledzić fakt, że 0 nie jest już dozwolone w żadnej komórce wzdłuż tego samego wiersza, kolumny i przekątnej:
LSB (example n=5)
↓
10000 00000 00000 00000 10000
00000 10000 00000 00000 10000
00000 00000 10000 00000 10000
00000 00000 00000 10000 10000
10000 10000 10000 10000 10000
↑
MSB
W tym celu obliczamy następujące cztery wartości:
Bieżący wiersz: Potrzebujemy n bitów co n- ty bit (jeden na komórkę), a następnie przesuwamy go do bieżącego wiersza r , pamiętając, że każdy wiersz zawiera n ² bitów:
Bieżąca kolumna: Potrzebujemy n bitów co n ²-ty bit (jeden na wiersz), a następnie przesuwamy ją do bieżącej kolumny c , pamiętając, że każda kolumna zawiera n bitów:
Przekątna do przodu: Potrzebujemy n bitów co ... (czy zwracałeś uwagę? Szybko, wymyśl to!) ... n ( n +1) -ty bit (dobra robota!), Ale tylko jeśli faktycznie jesteśmy na przekątna do przodu:
Przekątna do tyłu: tutaj dwie rzeczy. Po pierwsze, skąd wiemy, czy jesteśmy na przekątnej do tyłu? Matematycznie warunkiem jest c = ( n - 1) - r , co jest takie samo jak c = n + (- r - 1). Hej, czy to ci coś przypomina? Zgadza się, to uzupełnienie dwóch, więc zamiast dekrementacji możemy użyć bitowej negacji (bardzo wydajnej w Funcitonie). Po drugie, powyższy wzór zakłada, że chcemy ustawić najmniej znaczący bit, ale tak nie jest w przypadku przekątnej wstecznej, więc musimy go przesunąć w górę o ... wiesz? ... Zgadza się, n ( n - 1).
Jest to również jedyny przypadek, w którym potencjalnie dzielimy przez 0, jeśli n = 1. Jednak Funciton to nie obchodzi. 0 ÷ 0 to tylko 0, nie wiesz?
W kodzie funkcja Җ
(dolna) przyjmuje n, a indeks (na podstawie którego oblicza r i c przez dzielenie i resztę), oblicza te cztery wartości i or
s je razem.
Algorytm brutalnej siły
Algorytm siły brutalnej jest implementowany przez Ӂ
(funkcja u góry). Zajmuje n (rozmiar siatki), indeks (gdzie na siatce obecnie umieszczamy liczbę) i pobierane (liczba z n ³ bitami mówi nam, które liczby możemy nadal umieścić w każdej komórce).
Ta funkcja zwraca sekwencję ciągów. Każdy ciąg jest pełnym rozwiązaniem dla siatki. To kompletny solver; zwróci wszystkie rozwiązania, jeśli na to pozwolisz, ale zwróci je jako sekwencję leniwą.
Jeśli indeks osiągnął wartość 0, pomyślnie wypełniliśmy całą siatkę, więc zwracamy sekwencję zawierającą pusty ciąg (pojedyncze rozwiązanie, które nie obejmuje żadnej komórki). Jest pusty ciąg 0
i używamy funkcji biblioteki, ⌑
aby przekształcić go w sekwencję jednoelementową.
Sprawdzanie opisane w poniższej poprawie wydajności ma miejsce tutaj.
Jeśli indeks nie osiągnął jeszcze 0, zmniejszamy go o 1, aby uzyskać indeks, pod którym teraz musimy umieścić liczbę (nazwij to IX ).
Używamy ♫
do generowania leniwej sekwencji zawierającej wartości od 0 do n - 1.
Następnie używamy ɓ
(monadic bind) z lambda, która wykonuje następujące czynności w kolejności:
- Najpierw spójrz na odpowiedni bit , aby podjąć decyzję, czy numer jest ważny tutaj, czy nie. Możemy umieścić liczbę i wtedy i tylko wtedy , gdy pobrana & (1 << ( n × ix ) << i ) nie jest jeszcze ustawiona. Jeśli jest ustawiony, wróć
0
(pusta sekwencja).
- Służy
Җ
do obliczania bitów odpowiadających bieżącemu rzędowi, kolumnie i przekątnej. Przesuń go o i, a następnie or
na zabrane .
- Wywołaj rekurencyjnie,
Ӂ
aby pobrać wszystkie rozwiązania dla pozostałych komórek, przekazując mu nowe zajęte i zmniejszone ix . Zwraca sekwencję niekompletnych ciągów; każdy ciąg ma znaki ix (siatka wypełniona do indeksu ix ).
- Użyj
ɱ
(map), aby przejrzeć znalezione w ten sposób rozwiązania, i użyj, ‼
aby połączyć i do końca każdego z nich. Dodaj nowy wiersz, jeśli indeks jest wielokrotnością n , w przeciwnym razie spacją.
Generowanie wyniku
Główne wywołania programu Ӂ
(brutalny forcer) z n , indeks = n ² (pamiętaj, że wypełniamy siatkę do tyłu) i przyjmowane = 0 (początkowo nic nie jest pobierane). Jeśli wynikiem tego jest pusta sekwencja (nie znaleziono rozwiązania), wypisz pusty ciąg. W przeciwnym razie wypisz pierwszy ciąg w sekwencji. Zauważ, że oznacza to, że będzie oceniał tylko pierwszy element sekwencji, dlatego solver nie kontynuuje działania, dopóki nie znajdzie wszystkich rozwiązań.
Poprawa wydajności
(Dla tych, którzy już przeczytali starą wersję objaśnienia: program nie generuje już sekwencji sekwencji, którą należy osobno przekształcić w ciąg wyjściowy; po prostu generuje sekwencję ciągów bezpośrednio. Odpowiednio zredagowałem wyjaśnienie Ale to nie była główna poprawa. Oto nadchodzi.)
Na moim komputerze skompilowane exe pierwszej wersji zajęło prawie dokładnie 1 godzinę na rozwiązanie n = 7. Nie było to w podanym limicie czasu 10 minut, więc nie odpoczywałem. (Właściwie powodem, dla którego nie odpoczywałem, było to, że wpadłem na pomysł, jak to znacznie przyspieszyć.)
Algorytm opisany powyżej zatrzymuje swoje wyszukiwanie i cofa się za każdym razem, że napotyka na komórkę, w którym wszystkie bity w podjętej liczby są ustawione, wskazując, że nic nie można umieścić w tej komórce.
Jednak algorytm będzie nadal bezskutecznie wypełniał siatkę do komórki, w której ustawiono wszystkie te bity. Byłoby znacznie szybciej, gdyby mógł zatrzymać się, gdy jakakolwiek komórka, która ma być wypełniona, ma już ustawione wszystkie bity, co już wskazuje, że nigdy nie możemy rozwiązać reszty siatki bez względu na to, jakie liczby wstawimy to. Ale jak sprawnie sprawdzić, czy jakakolwiek komórka ma ustawione n bitów bez przechodzenia przez wszystkie?
Sztuczka zaczyna się od dodania jednego bitu na komórkę do pobranej liczby. Zamiast tego, co pokazano powyżej, wygląda to tak:
LSB (example n=5)
↓
10000 0 00000 0 00000 0 00000 0 10000 0
00000 0 10000 0 00000 0 00000 0 10000 0
00000 0 00000 0 10000 0 00000 0 10000 0
00000 0 00000 0 00000 0 10000 0 10000 0
10000 0 10000 0 10000 0 10000 0 10000 0
↑
MSB
Zamiast n ³, w tej liczbie jest teraz n ² ( n + 1) bitów. Funkcja wypełniająca bieżący wiersz / kolumnę / przekątną została odpowiednio zmieniona (w rzeczywistości całkowicie przepisana, jeśli mam być szczera). Ta funkcja będzie jednak nadal wypełniać tylko n bitów na komórkę, więc dodatkowy bit, który właśnie dodaliśmy, zawsze będzie 0
.
Powiedzmy, że jesteśmy w połowie obliczeń, właśnie umieściliśmy 1
w środkowej komórce, a pobrana liczba wygląda mniej więcej tak:
current
LSB column (example n=5)
↓ ↓
11111 0 10010 0 01101 0 11100 0 11101 0
00011 0 11110 0 01101 0 11101 0 11100 0
11111 0 11110 0[11101 0]11100 0 11100 0 ← current row
11111 0 11111 0 11111 0 11111 0 11111 0
11111 0 11111 0 11111 0 11111 0 11111 0
↑
MSB
Jak widać, lewa górna komórka (indeks 0) i środkowa lewa komórka (indeks 10) są teraz niemożliwe. Jak najskuteczniej to ustalić?
Rozważ liczbę, w której ustawiony jest zerowy bit każdej komórki, ale tylko do bieżącego indeksu. Taka liczba jest łatwa do obliczenia za pomocą znanej formuły:
Co uzyskalibyśmy, gdyby dodać te dwie liczby razem?
LSB LSB
↓ ↓
11111 0 10010 0 01101 0 11100 0 11101 0 10000 0 10000 0 10000 0 10000 0 10000 0 ╓───╖
00011 0 11110 0 01101 0 11101 0 11100 0 ║ 10000 0 10000 0 10000 0 10000 0 10000 0 ║
11111 0 11110 0 11101 0 11100 0 11100 0 ═══╬═══ 10000 0 10000 0 00000 0 00000 0 00000 0 ═════ ╓─╜
11111 0 11111 0 11111 0 11111 0 11111 0 ║ 00000 0 00000 0 00000 0 00000 0 00000 0 ═════ ╨
11111 0 11111 0 11111 0 11111 0 11111 0 00000 0 00000 0 00000 0 00000 0 00000 0 o
↑ ↑
MSB MSB
Wynik to:
OMG
↓
00000[1]01010 0 11101 0 00010 0 00011 0
10011 0 00001 0 11101 0 00011 0 00010 0
═════ 00000[1]00001 0 00011 0 11100 0 11100 0
═════ 11111 0 11111 0 11111 0 11111 0 11111 0
11111 0 11111 0 11111 0 11111 0 11111 0
Jak widać, dodatek przepełnia dodatkowy bit, który dodaliśmy, ale tylko wtedy, gdy wszystkie bity dla tej komórki są ustawione! Dlatego wszystko, co pozostało do zrobienia, to maskowanie tych bitów (ta sama formuła jak powyżej, ale << n ) i sprawdzenie, czy wynikiem jest 0:
00000[1]01010 0 11101 0 00010 0 00011 0 ╓╖ 00000 1 00000 1 00000 1 00000 1 00000 1 ╓─╖ ╓───╖
10011 0 00001 0 11101 0 00011 0 00010 0 ╓╜╙╖ 00000 1 00000 1 00000 1 00000 1 00000 1 ╓╜ ╙╖ ║
00000[1]00001 0 00011 0 11100 0 11100 0 ╙╥╥╜ 00000 1 00000 1 00000 0 00000 0 00000 0 ═════ ║ ║ ╓─╜
11111 0 11111 0 11111 0 11111 0 11111 0 ╓╜╙╥╜ 00000 0 00000 0 00000 0 00000 0 00000 0 ═════ ╙╖ ╓╜ ╨
11111 0 11111 0 11111 0 11111 0 11111 0 ╙──╨─ 00000 0 00000 0 00000 0 00000 0 00000 0 ╙─╜ o
Jeśli nie jest zero, siatka jest niemożliwa i możemy się zatrzymać.
- Zrzut ekranu przedstawiający rozwiązanie i czas działania dla n = 4 do 7.