Python 2 i PuLP - 2 644 648 kwadratów (optymalnie zminimalizowane); 10 753 553 kwadraty (optymalnie zmaksymalizowane)
Minimalnie grał w golfa do 1152 bajtów
from pulp import*
x=0
f=open("c","r")
g=open("s","w")
for k,m in enumerate(f):
if k%2:
b=map(int,m.split())
p=LpProblem("Nn",LpMinimize)
q=map(str,range(18))
ir=q[1:18]
e=LpVariable.dicts("c",(q,q),0,1,LpInteger)
rs=LpVariable.dicts("rs",(ir,ir),0,1,LpInteger)
cs=LpVariable.dicts("cs",(ir,ir),0,1,LpInteger)
p+=sum(e[r][c] for r in q for c in q),""
for i in q:p+=e["0"][i]==0,"";p+=e[i]["0"]==0,"";p+=e["17"][i]==0,"";p+=e[i]["17"]==0,""
for o in range(289):i=o/17+1;j=o%17+1;si=str(i);sj=str(j);l=e[si][str(j-1)];ls=rs[si][sj];p+=e[si][sj]<=l+ls,"";p+=e[si][sj]>=l-ls,"";p+=e[si][sj]>=ls-l,"";p+=e[si][sj]<=2-ls-l,"";l=e[str(i-1)][sj];ls=cs[si][sj];p+=e[si][sj]<=l+ls,"";p+=e[si][sj]>=l-ls,"";p+=e[si][sj]>=ls-l,"";p+=e[si][sj]<=2-ls-l,""
for r,z in enumerate(a):p+=lpSum([rs[str(r+1)][c] for c in ir])==2*z,""
for c,z in enumerate(b):p+=lpSum([cs[r][str(c+1)] for r in ir])==2*z,""
p.solve()
for r in ir:
for c in ir:g.write(str(int(e[r][c].value()))+" ")
g.write('\n')
g.write('%d:%d\n\n'%(-~k/2,value(p.objective)))
x+=value(p.objective)
else:a=map(int,m.split())
print x
(Uwaga: mocno wcięte linie zaczynają się od tabulatorów, a nie spacji.)
Przykładowe dane wyjściowe: https://drive.google.com/file/d/0B-0NVE9E8UJiX3IyQkJZVk82Vkk/view?usp=sharing
Okazuje się, że takie problemy można łatwo przekształcić w całkowite programy liniowe i potrzebowałem podstawowego problemu, aby nauczyć się korzystać z PuLP - interfejsu python dla różnych programów LP - do własnego projektu. Okazuje się również, że PuLP jest niezwykle łatwy w użyciu, a niegodziwy konstruktor LP działał idealnie przy pierwszym uruchomieniu.
Dwie miłe rzeczy związane z zatrudnieniem rozgałęzionego solwera IP do ciężkiej pracy w rozwiązaniu tego problemu (oprócz konieczności implementacji rozgałęzionego i związanego solwera) to:
- Skonstruowane specjalnie do tego celu solwery są naprawdę szybkie. Ten program rozwiązuje wszystkie 50000 problemów w ciągu około 17 godzin na moim stosunkowo niskim komputerze domowym. Każde wystąpienie zajęło od 1-1,5 sekundy do rozwiązania.
- Tworzą gwarantowane optymalne rozwiązania (lub mówią, że tego nie zrobili). Dlatego mogę być pewien, że nikt nie pokona mojego wyniku w kwadracie (chociaż ktoś może go powiązać i pokonać mnie w części golfowej).
Jak korzystać z tego programu
Najpierw musisz zainstalować PuLP. pip install pulp
powinien załatwić sprawę, jeśli masz zainstalowany pip.
Następnie musisz umieścić w pliku o nazwie „c”: https://drive.google.com/file/d/0B-0NVE9E8UJiNFdmYlk1aV9aYzQ/view?usp=sharing
Następnie uruchom ten program w dowolnej późniejszej wersji Pythona 2 z tego samego katalogu. W niecały dzień otrzymasz plik o nazwie „s”, który zawiera 50 000 rozwiązanych nieogramowych siatek (w czytelnym formacie), z których każda zawiera podaną liczbę wypełnionych kwadratów.
Jeśli zamiast tego chcesz zmaksymalizować liczbę wypełnionych kwadratów, zmień LpMinimize
linię 8 na LpMaximize
zamiast. Otrzymasz wynik bardzo podobny do tego: https://drive.google.com/file/d/0B-0NVE9E8UJiYjJ2bzlvZ0RXcUU/view?usp=sharing
Format wejściowy
Ten program używa zmodyfikowanego formatu wejściowego, ponieważ Joe Z. powiedział, że będziemy mogli ponownie zakodować format wejściowy, jeśli chcemy w komentarzu do OP. Kliknij powyższy link, aby zobaczyć, jak to wygląda. Składa się z 10000 linii, z których każda zawiera 16 cyfr. Linie parzyste to wielkości wierszy danego wystąpienia, a linie nieparzyste to wielkości kolumn tego samego wystąpienia, co linia nad nimi. Ten plik został wygenerowany przez następujący program:
from bitqueue import *
with open("nonograms_b64.txt","r") as f:
with open("nonogram_clues.txt","w") as g:
for line in f:
q = BitQueue(line.decode('base64'))
nonogram = []
for i in range(256):
if not i%16: row = []
row.append(q.nextBit())
if not -~i%16: nonogram.append(row)
s=""
for row in nonogram:
blocks=0 #magnitude counter
for i in range(16):
if row[i]==1 and (i==0 or row[i-1]==0): blocks+=1
s+=str(blocks)+" "
print >>g, s
nonogram = map(list, zip(*nonogram)) #transpose the array to make columns rows
s=""
for row in nonogram:
blocks=0
for i in range(16):
if row[i]==1 and (i==0 or row[i-1]==0): blocks+=1
s+=str(blocks)+" "
print >>g, s
(Ten program do ponownego kodowania dał mi również dodatkową okazję do przetestowania mojej niestandardowej klasy BitQueue, którą utworzyłem dla tego samego projektu wspomnianego powyżej. Jest to po prostu kolejka, do której dane mogą być wypychane jako sekwencje bitów LUB bajtów i z których dane mogą może być wyświetlany po jednym bajcie lub bajcie naraz. W tym przypadku działało idealnie).
Ponownie zakodowałem dane wejściowe z konkretnego powodu, że do zbudowania ILP dodatkowe informacje o siatkach, które zostały użyte do wygenerowania wielkości, są całkowicie bezużyteczne. Wielkości są jedynymi ograniczeniami, więc wielkości są wszystkim, do czego potrzebowałem dostępu.
Niegolfowany konstruktor ILP
from pulp import *
total = 0
with open("nonogram_clues.txt","r") as f:
with open("solutions.txt","w") as g:
for k,line in enumerate(f):
if k%2:
colclues=map(int,line.split())
prob = LpProblem("Nonogram",LpMinimize)
seq = map(str,range(18))
rows = seq
cols = seq
irows = seq[1:18]
icols = seq[1:18]
cells = LpVariable.dicts("cell",(rows,cols),0,1,LpInteger)
rowseps = LpVariable.dicts("rowsep",(irows,icols),0,1,LpInteger)
colseps = LpVariable.dicts("colsep",(irows,icols),0,1,LpInteger)
prob += sum(cells[r][c] for r in rows for c in cols),""
for i in rows:
prob += cells["0"][i] == 0,""
prob += cells[i]["0"] == 0,""
prob += cells["17"][i] == 0,""
prob += cells[i]["17"] == 0,""
for i in range(1,18):
for j in range(1,18):
si = str(i); sj = str(j)
l = cells[si][str(j-1)]; ls = rowseps[si][sj]
prob += cells[si][sj] <= l + ls,""
prob += cells[si][sj] >= l - ls,""
prob += cells[si][sj] >= ls - l,""
prob += cells[si][sj] <= 2 - ls - l,""
l = cells[str(i-1)][sj]; ls = colseps[si][sj]
prob += cells[si][sj] <= l + ls,""
prob += cells[si][sj] >= l - ls,""
prob += cells[si][sj] >= ls - l,""
prob += cells[si][sj] <= 2 - ls - l,""
for r,clue in enumerate(rowclues):
prob += lpSum([rowseps[str(r+1)][c] for c in icols]) == 2 * clue,""
for c,clue in enumerate(colclues):
prob += lpSum([colseps[r][str(c+1)] for r in irows]) == 2 * clue,""
prob.solve()
print "Status for problem %d: "%(-~k/2),LpStatus[prob.status]
for r in rows[1:18]:
for c in cols[1:18]:
g.write(str(int(cells[r][c].value()))+" ")
g.write('\n')
g.write('Filled squares for %d: %d\n\n'%(-~k/2,value(prob.objective)))
total += value(prob.objective)
else:
rowclues=map(int,line.split())
print "Total number of filled squares: %d"%total
Jest to program, który faktycznie stworzył „przykładowe wyjście” połączone powyżej. Stąd wyjątkowo długie sznurki na końcu każdej siatki, które obciąłem podczas gry w golfa. (Wersja do gry w golfa powinna dawać identyczne wyniki, bez słów "Filled squares for "
)
Jak to działa
cells = LpVariable.dicts("cell",(rows,cols),0,1,LpInteger)
rowseps = LpVariable.dicts("rowsep",(irows,icols),0,1,LpInteger)
colseps = LpVariable.dicts("colsep",(irows,icols),0,1,LpInteger)
Używam siatki 18 x 18, a środkowa część 16 x 16 jest właściwym rozwiązaniem układanki. cells
to ta siatka. Pierwszy wiersz tworzy 324 zmienne binarne: „cell_0_0”, „cell_0_1” i tak dalej. Tworzę również siatki „przestrzeni” pomiędzy i wokół komórek w części rozwiązania siatki. rowseps
wskazuje na 289 zmiennych, które symbolizują spacje oddzielające komórki w poziomie, podczas gdy colseps
podobnie wskazuje na zmienne, które oznaczają spacje oddzielające komórki w pionie. Oto schemat Unicode:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
W 0
y i □
y to wartości binarne śledzone przez cell
zmienne, |
a wartości binarne są śledzone przez rowsep
wartości zmiennych i -
y to wartości binarne śledzone przez colsep
zmiennych.
prob += sum(cells[r][c] for r in rows for c in cols),""
To jest funkcja celu. Tylko suma wszystkich cell
zmiennych. Ponieważ są to zmienne binarne, jest to dokładnie liczba wypełnionych kwadratów w rozwiązaniu.
for i in rows:
prob += cells["0"][i] == 0,""
prob += cells[i]["0"] == 0,""
prob += cells["17"][i] == 0,""
prob += cells[i]["17"] == 0,""
To po prostu ustawia komórki wokół zewnętrznej krawędzi siatki na zero (dlatego przedstawiłem je jako zera powyżej). Jest to najbardziej celowy sposób śledzenia liczby „bloków” komórek, które są wypełnione, ponieważ zapewnia, że każdej zmianie z niewypełnionej na wypełnioną (przechodzącej przez kolumnę lub wiersz) odpowiada odpowiednia zmiana z wypełnionej na niewypełnioną (i odwrotnie ), nawet jeśli pierwsza lub ostatnia komórka w wierszu jest wypełniona. Jest to jedyny powód, dla którego warto zastosować siatkę 18x18. To nie jedyny sposób na liczenie bloków, ale myślę, że jest najprostszy.
for i in range(1,18):
for j in range(1,18):
si = str(i); sj = str(j)
l = cells[si][str(j-1)]; ls = rowseps[si][sj]
prob += cells[si][sj] <= l + ls,""
prob += cells[si][sj] >= l - ls,""
prob += cells[si][sj] >= ls - l,""
prob += cells[si][sj] <= 2 - ls - l,""
l = cells[str(i-1)][sj]; ls = colseps[si][sj]
prob += cells[si][sj] <= l + ls,""
prob += cells[si][sj] >= l - ls,""
prob += cells[si][sj] >= ls - l,""
prob += cells[si][sj] <= 2 - ls - l,""
To jest prawdziwe mięso logiki ILP. Zasadniczo wymaga, aby każda komórka (inna niż w pierwszym rzędzie i kolumnie) była logicznym xor komórki i separatora bezpośrednio po lewej stronie w swoim rzędzie i bezpośrednio nad nią w kolumnie. Dostałem ograniczenia, które symulują xor w programie liczb całkowitych {0,1} z tej wspaniałej odpowiedzi: /cs//a/12118/44289
Aby wyjaśnić nieco więcej: to ograniczenie xor sprawia, że separatory mogą mieć wartość 1 wtedy i tylko wtedy, gdy znajdują się między komórkami, które są 0 i 1 (oznaczając zmianę z niewypełnionego na wypełnione lub odwrotnie). Zatem w wierszu lub kolumnie będzie dokładnie dwa razy więcej separatorów 1-wartościowych niż liczba bloków w tym wierszu lub kolumnie. Innymi słowy, suma separatorów w danym rzędzie lub kolumnie jest dokładnie dwa razy większa niż wielkość tego rzędu / kolumny. Stąd następujące ograniczenia:
for r,clue in enumerate(rowclues):
prob += lpSum([rowseps[str(r+1)][c] for c in icols]) == 2 * clue,""
for c,clue in enumerate(colclues):
prob += lpSum([colseps[r][str(c+1)] for r in irows]) == 2 * clue,""
I to tyle. Reszta prosi tylko domyślny solver o rozwiązanie ILP, a następnie formatuje powstałe rozwiązanie, zapisując je do pliku.