Problem Summary

On an \(n\times n\) board we place exactly one queen in each row. A queen does not attack forever: it attacks vertically and diagonally only up to row distance \(D\), where

$$D=n-1-w.$$

Let \(Q(n,w)\) be the number of legal placements for weakness parameter \(w\). It is convenient to reparameterize by \(D\) and write \(N(n,D)=Q(n,n-1-D)\). Then the required quantity is

$$S(n)=\sum_{w=0}^{n-1}Q(n,w)=\sum_{D=0}^{n-1}N(n,D).$$

The endpoint cases clarify the model. When \(D=0\), no queen can reach a different row, so every row may use any of the \(n\) columns and \(N(n,0)=n^n\). When \(D=n-1\), every pair of rows can interact, so we recover the ordinary \(n\)-queens condition.

Mathematical Approach

The decisive observation is that a queen more than \(D\) rows above the current row is irrelevant. Therefore a partial placement is completely summarized by the most recent \(D\) chosen columns.

Step 1: Reparameterize by Attack Depth

Instead of summing directly over \(w\), we solve the fixed-depth problem \(N(n,D)\). This removes one layer of notation and matches the actual row-by-row rule: only row pairs whose distance is at most \(D\) can attack each other.

After computing every \(N(n,D)\) independently, the final answer is the outer sum

$$S(n)=\sum_{D=0}^{n-1}N(n,D).$$

Step 2: Describe the Local Forbidden Columns

Let \(c_r\in\{0,1,\dots,n-1\}\) denote the column used in row \(r\). A queen already placed in row \(r-d\) affects row \(r\) only when \(1\le d\le D\). In that case it forbids three possibilities:

$$c_r=c_{r-d},\qquad c_r=c_{r-d}+d,\qquad c_r=c_{r-d}-d,$$

whenever those columns stay inside the board. The first condition is the vertical attack, and the other two are the diagonals. Nothing older than \(D\) rows can matter.

Step 3: Use a Finite-Memory State

After placing the first \(r\) rows, define \(\ell=\min(r,D)\). The future depends only on the tuple

$$\sigma_r=(c_r,c_{r-1},\dots,c_{r-\ell+1}).$$

Two partial boards with the same tuple \(\sigma_r\) are equivalent for all future choices, because every earlier queen is already more than \(D\) rows away from the next row and can never attack again. This turns the search into dynamic programming on recent-history states.

Step 4: Transition to the Next Row

Suppose the current state is \(\sigma=(a_1,\dots,a_\ell)\), with \(a_1\) the most recent column. The blocked set for the next row is

$$B(\sigma)=\bigcup_{d=1}^{\ell}\left(\{a_d\}\cup\{a_d+d\}\cup\{a_d-d\}\right)\cap\{0,1,\dots,n-1\}.$$

Every column in \(\{0,1,\dots,n-1\}\setminus B(\sigma)\) is legal. Choosing such a column \(c\) produces the new recent-history tuple obtained by putting \(c\) in front and dropping the oldest remembered column if necessary.

If \(F_r(\sigma)\) denotes the number of partial boards of height \(r\) that lead to state \(\sigma\), then the next layer is obtained by adding \(F_r(\sigma)\) to every legal successor of \(\sigma\).

Step 5: Merge Equal Recent Histories

Many different partial boards collapse to the same recent-history tuple. Once the next-row successors have been generated, equal tuples are merged and their multiplicities are added. This is the key compression step: the algorithm never distinguishes two histories once their last \(D\) columns agree.

The merging is exact because the future attack pattern is determined entirely by the recent tuple, so no information is lost.

Worked Example: \((n,D)=(4,1)\)

When \(D=1\), each row interacts only with the immediately previous row, so the state is just the last column. If the previous column is \(0\), the next row may use only \(2\) or \(3\). If it is \(1\), only \(3\) is legal. If it is \(2\), only \(0\) is legal. If it is \(3\), only \(0\) or \(1\) are legal.

Starting from the first row, the counts of partial boards ending in columns \(0,1,2,3\) evolve as

$$\begin{aligned} r=1&:\ (1,1,1,1), &&T_1=4,\\ r=2&:\ (2,1,1,2), &&T_2=6,\\ r=3&:\ (3,2,2,3), &&T_3=10,\\ r=4&:\ (5,3,3,5), &&T_4=16. \end{aligned}$$

Hence \(N(4,1)=16\). The same framework also gives \(N(4,0)=4^4=256\), \(N(4,2)=2\), and \(N(4,3)=2\), so

$$S(4)=256+16+2+2=276.$$

How the Code Works

The C++, Python, and Java implementations all use this finite-memory dynamic program. For a fixed \(D\), they begin with the empty state of multiplicity \(1\), process rows from top to bottom, rebuild the blocked columns from the remembered recent columns, and enumerate every legal next column.

The compiled implementations store the remembered columns in a compact integer representation, using four bits per remembered row, which is sufficient for the target size \(n=14\). After each row, the generated successor states are ordered by their encoded state and equal states are merged by adding their counts.

Once all \(n\) rows have been processed, the counts of the surviving states are summed to obtain \(N(n,D)\). The outer routine repeats this for every \(D=0,1,\dots,n-1\) and adds the results. Because these fixed-\(D\) tasks are independent, the compiled implementations distribute them across worker threads. The Python implementation serves as a thin front end to the same compiled computation.

Complexity Analysis

For fixed \(D\), let \(F_r\) be the number of merged states after row \(r\), and let \(G_r\) be the number of generated successors before merging at the next step. Building the blocked set for one parent state scans at most \(D\) remembered queens, so the transition phase at that row costs \(O(F_rD+G_r)\). The implementations then sort the generated states before merging, which adds \(O(G_r\log G_r)\).

Therefore the total time for one value of \(D\) is

$$O\left(\sum_{r=0}^{n-1}\left(F_rD+G_r\log G_r\right)\right),$$

and the memory usage is \(O(\max_r G_r)\). A coarse worst-case bound is exponential in \(D\), since there can be up to \(n^D\) recent-history states, but for the required instance \(n=14\) the state merging keeps the frontier manageable enough for a direct computation.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=534
  2. Eight queens puzzle: Wikipedia — Eight queens puzzle
  3. Dynamic programming: Wikipedia — Dynamic programming
  4. Bitwise operation: Wikipedia — Bitwise operation

Problemzusammenfassung

Auf einem \(n\times n\)-Brett wird genau eine Dame pro Zeile gesetzt. Eine Dame greift nicht unbegrenzt weit an: Vertikale und diagonale Angriffe reichen nur bis zur Zeilendistanz \(D\), wobei

$$D=n-1-w.$$

Sei \(Q(n,w)\) die Zahl der zulassigen Stellungen fur den Schwacheparameter \(w\). ZweckmaBig ist die Umparametrisierung uber \(D\):

$$N(n,D)=Q(n,n-1-D).$$

Dann lautet die gesuchte Summe

$$S(n)=\sum_{w=0}^{n-1}Q(n,w)=\sum_{D=0}^{n-1}N(n,D).$$

Die Randfalle zeigen die Struktur sofort. Fur \(D=0\) kann keine Dame eine andere Zeile erreichen, also hat jede Zeile frei \(n\) Spalten zur Wahl und damit \(N(n,0)=n^n\). Fur \(D=n-1\) konnen alle Zeilenpaare wechselwirken, also entsteht das gewohnliche Damenproblem.

Mathematischer Ansatz

Die entscheidende Beobachtung ist: Eine Dame, die mehr als \(D\) Zeilen uber der aktuellen Zeile steht, ist vollstandig irrelevant. Deshalb wird eine Teilbelegung vollstandig durch die letzten \(D\) gewahlten Spalten beschrieben.

Step 1: Umparametrisierung uber die Angriffstiefe

Statt direkt uber \(w\) zu summieren, losen wir das Problem fur feste Angriffstiefe \(N(n,D)\). Das passt exakt zur zeilenweisen Konstruktion, denn nur Zeilen mit Abstand hochstens \(D\) konnen einander angreifen.

Nachdem alle Werte \(N(n,D)\) getrennt berechnet sind, liefert

$$S(n)=\sum_{D=0}^{n-1}N(n,D)$$

die gesuchte Endsumme.

Step 2: Lokale verbotene Spalten beschreiben

Sei \(c_r\in\{0,1,\dots,n-1\}\) die Spalte der Dame in Zeile \(r\). Eine Dame in Zeile \(r-d\) beeinflusst Zeile \(r\) nur fur \(1\le d\le D\). Dann verbietet sie drei Moglichkeiten:

$$c_r=c_{r-d},\qquad c_r=c_{r-d}+d,\qquad c_r=c_{r-d}-d,$$

sofern diese Spalten noch auf dem Brett liegen. Die erste Bedingung ist der vertikale Angriff, die beiden anderen sind die Diagonalen. Altere Zeilen spielen keine Rolle mehr.

Step 3: Zustand mit endlichem Gedachtnis

Nach den ersten \(r\) gesetzten Zeilen sei \(\ell=\min(r,D)\). Fur die Zukunft ist nur das Tupel

$$\sigma_r=(c_r,c_{r-1},\dots,c_{r-\ell+1})$$

wichtig. Zwei Teilbelegungen mit demselben Tupel \(\sigma_r\) sind fur alle spateren Entscheidungen aquivalent, weil jede noch fruhere Dame bereits mehr als \(D\) Zeilen entfernt ist und nie wieder angreifen kann. Damit wird die Suche zu einer dynamischen Programmierung uber Zustande mit kurzer Historie.

Step 4: Ubergang zur nachsten Zeile

Sei \(\sigma=(a_1,\dots,a_\ell)\) der aktuelle Zustand, wobei \(a_1\) die jungste Spalte ist. Die verbotene Menge fur die nachste Zeile lautet

$$B(\sigma)=\bigcup_{d=1}^{\ell}\left(\{a_d\}\cup\{a_d+d\}\cup\{a_d-d\}\right)\cap\{0,1,\dots,n-1\}.$$

Jede Spalte aus \(\{0,1,\dots,n-1\}\setminus B(\sigma)\) ist zulassig. Wahlen wir eine solche Spalte \(c\), entsteht der Folgezustand, indem \(c\) vorne eingefugt und bei Bedarf die alteste gemerkte Spalte entfernt wird.

Bezeichnet \(F_r(\sigma)\) die Zahl der Teilbelegungen der Hohe \(r\) mit Zustand \(\sigma\), dann wird \(F_r(\sigma)\) zu jedem legalen Folgezustand addiert.

Step 5: Gleiche Historien zusammenfassen

Viele verschiedene Teilbelegungen enden in derselben kurzen Historie. Nachdem alle Nachfolgezustande erzeugt wurden, werden gleiche Tupel zusammengelegt und ihre Anzahlen addiert. Genau diese Verdichtung macht den Ansatz praktikabel.

Das ist mathematisch exakt, weil das gesamte zukunftige Angriffsmuster allein durch das aktuelle Historientupel bestimmt wird.

Worked Example: \((n,D)=(4,1)\)

Fur \(D=1\) wechselwirkt jede Zeile nur mit der unmittelbar vorherigen Zeile, also besteht der Zustand nur aus der letzten Spalte. Steht die vorherige Dame in Spalte \(0\), dann sind fur die nachste Zeile nur \(2\) und \(3\) moglich. Bei Spalte \(1\) ist nur \(3\) erlaubt, bei Spalte \(2\) nur \(0\), und bei Spalte \(3\) sind \(0\) oder \(1\) moglich.

Ausgehend von der ersten Zeile entwickeln sich die Zahlen der Teilbelegungen mit Endspalten \(0,1,2,3\) zu

$$\begin{aligned} r=1&:\ (1,1,1,1), &&T_1=4,\\ r=2&:\ (2,1,1,2), &&T_2=6,\\ r=3&:\ (3,2,2,3), &&T_3=10,\\ r=4&:\ (5,3,3,5), &&T_4=16. \end{aligned}$$

Also ist \(N(4,1)=16\). Derselbe Rahmen ergibt auBerdem \(N(4,0)=4^4=256\), \(N(4,2)=2\) und \(N(4,3)=2\), also

$$S(4)=256+16+2+2=276.$$

Wie der Code arbeitet

Die C++-, Python- und Java-Implementierungen benutzen alle genau diese DP mit endlichem Gedachtnis. Fur festes \(D\) starten sie mit dem leeren Zustand der Multiplizitat \(1\), gehen Zeile fur Zeile vor, rekonstruieren aus den gemerkten Spalten die blockierten Spalten und enumerieren jede erlaubte nachste Spalte.

Die kompilierten Implementierungen speichern die gemerkten Spalten in einer kompakten Ganzzahldarstellung mit vier Bits pro gemerkter Zeile; das reicht fur die ZielgroBe \(n=14\) aus. Nach jeder Zeile werden die erzeugten Folgezustande nach ihrem kodierten Zustand geordnet und gleiche Zustande durch Addition ihrer Anzahlen verschmolzen.

Nach \(n\) Zeilen werden die Anzahlen aller verbleibenden Zustande summiert; das ist \(N(n,D)\). AuBen herum wird dieser Ablauf fur alle \(D=0,1,\dots,n-1\) wiederholt und addiert. Weil diese Aufgaben fur festes \(D\) unabhangig sind, verteilen die kompilierten Implementierungen sie auf mehrere Worker-Threads. Die Python-Implementierung dient als schlanke Hulle um dieselbe kompilierte Berechnung.

Komplexitätsanalyse

Fur festes \(D\) sei \(F_r\) die Zahl der zusammengefassten Zustande nach Zeile \(r\), und \(G_r\) die Zahl der erzeugten Nachfolger vor dem Zusammenfassen im nachsten Schritt. Das Aufbauen der Sperrmenge fur einen Elternzustand betrachtet hochstens \(D\) gemerkte Damen, daher kostet die Ubergangsphase \(O(F_rD+G_r)\). Danach werden die erzeugten Zustande sortiert, was weitere \(O(G_r\log G_r)\) kostet.

Damit ist die Gesamtlaufzeit fur ein festes \(D\)

$$O\left(\sum_{r=0}^{n-1}\left(F_rD+G_r\log G_r\right)\right),$$

und der Speicherverbrauch ist \(O(\max_r G_r)\). Eine grobe Schranke ist exponentiell in \(D\), weil es bis zu \(n^D\) Historienzustande geben kann. Fur die geforderte Instanz \(n=14\) reicht die starke Zustandsverschmelzung jedoch fur eine direkte Berechnung aus.

Fußnoten und Quellen

  1. Problemseite: https://projecteuler.net/problem=534
  2. Damenproblem: Wikipedia — Eight queens puzzle
  3. Dynamische Programmierung: Wikipedia — Dynamic programming
  4. Bitweise Operationen: Wikipedia — Bitwise operation

Problem Özeti

\(n\times n\) tahtada her satira tam bir vezir yerlestiriyoruz. Bir vezir sonsuza kadar saldirmaz; dikey ve capraz saldirilar yalnizca \(D\) satirlik mesafeye kadar etkilidir ve

$$D=n-1-w.$$

\(Q(n,w)\), zayiflik parametresi \(w\) icin gecerli yerlestirme sayisi olsun. Hesabi sadeleştirmek icin \(D\) ile yeniden yazariz:

$$N(n,D)=Q(n,n-1-D).$$

Boylece istenen toplam

$$S(n)=\sum_{w=0}^{n-1}Q(n,w)=\sum_{D=0}^{n-1}N(n,D)$$

olur. Uc durumlar problemi iyi aciklar. \(D=0\) iken hicbir vezir baska bir satira ulasamaz; her satir herhangi bir sutunu secebilir ve \(N(n,0)=n^n\) olur. \(D=n-1\) iken her satir cifti etkilesebildigi icin klasik \(n\)-vezir kisiti geri gelir.

Matematiksel Yaklaşım

Kilit gozlem sudur: Guncel satirdan \(D\) satirdan daha yukaridaki bir vezir artik tamamen onemsizdir. Bu nedenle bir kismi yerlestirme, secilen son \(D\) sutun ile tamamen ozetlenebilir.

Step 1: Saldiri derinligi ile yeniden parametrele

Dogrudan \(w\) uzerinden toplamak yerine sabit derinlik problemi \(N(n,D)\) cozulur. Bu, satir satir kurulan gercek kisitla tam uyumludur; cunku yalnizca aralarindaki uzaklik en fazla \(D\) olan satirlar birbirine saldirabilir.

Butun \(N(n,D)\) degerleri bagimsiz hesaplandiktan sonra son cevap

$$S(n)=\sum_{D=0}^{n-1}N(n,D)$$

olur.

Step 2: Yerel yasak sutunlari yaz

\(c_r\in\{0,1,\dots,n-1\}\), \(r\). satirdaki vezirin sutunu olsun. Daha once yerlestirilmis bir vezir ancak \(1\le d\le D\) icin, yani \(r-d\). satirdaysa, \(r\). satiri etkiler. Bu durumda uc olasiligi yasaklar:

$$c_r=c_{r-d},\qquad c_r=c_{r-d}+d,\qquad c_r=c_{r-d}-d,$$

tabii bu sutunlar tahta icinde kaldigi surece. Ilk kosul dikey saldiridir; diger ikisi iki caprazdir. \(D\)'den daha eski satirlarin hicbir etkisi kalmaz.

Step 3: Sonlu hafizali durumu kullan

Ilk \(r\) satir yerlestirildikten sonra \(\ell=\min(r,D)\) olsun. Gelecek kararlar yalnizca

$$\sigma_r=(c_r,c_{r-1},\dots,c_{r-\ell+1})$$

demetine baglidir. Ayni \(\sigma_r\) demetine sahip iki farkli kismi tahta, bundan sonraki butun secimler icin esdegerdir; cunku daha eski her vezir bir sonraki satirdan artik \(D\)'den fazla uzaktadir ve bir daha saldiramaz. Boylece arama, yakin gecmisi tutan bir dinamik programlamaya donusur.

Step 4: Sonraki satira gecis

Guncel durum \(\sigma=(a_1,\dots,a_\ell)\) olsun; burada \(a_1\) en son secilen sutundur. Sonraki satir icin yasak kume

$$B(\sigma)=\bigcup_{d=1}^{\ell}\left(\{a_d\}\cup\{a_d+d\}\cup\{a_d-d\}\right)\cap\{0,1,\dots,n-1\}$$

seklindedir. \(\{0,1,\dots,n-1\}\setminus B(\sigma)\) icindeki her sutun yasaldir. Boyle bir \(c\) secilirse yeni yakin-gecmis durumu, \(c\)'yi basa ekleyip gerekirse en eski kaydi atarak elde edilir.

\(F_r(\sigma)\), yuksekligi \(r\) olan ve durum olarak \(\sigma\)'ya ulasan kismi yerlestirme sayisi ise, sonraki katmanda her yasal ardil duruma \(F_r(\sigma)\) eklenir.

Step 5: Ayni yakin gecmisleri birlestir

Pek cok farkli kismi yerlestirme ayni son \(D\) sutuna cokebilir. Sonraki satir adaylari uretildikten sonra ayni demetler birlestirilir ve cokluklari toplanir. Algoritmayi uygulanabilir yapan temel sikistirma budur.

Bu birlestirme tam dogrudur; cunku gelecekteki saldiri deseni sadece elde kalan yakin-gecmis demeti tarafindan belirlenir.

Worked Example: \((n,D)=(4,1)\)

\(D=1\) oldugunda her satir sadece hemen onceki satirla etkilesir; dolayisiyla durum son sutundan ibarettir. Onceki sutun \(0\) ise sonraki satir yalnizca \(2\) veya \(3\) olabilir. Onceki sutun \(1\) ise yalnizca \(3\), \(2\) ise yalnizca \(0\), \(3\) ise \(0\) veya \(1\) secilebilir.

Ilk satirdan baslayinca, son sutunlari \(0,1,2,3\) olan kismi yerlestirme sayilari su sekilde ilerler:

$$\begin{aligned} r=1&:\ (1,1,1,1), &&T_1=4,\\ r=2&:\ (2,1,1,2), &&T_2=6,\\ r=3&:\ (3,2,2,3), &&T_3=10,\\ r=4&:\ (5,3,3,5), &&T_4=16. \end{aligned}$$

Demek ki \(N(4,1)=16\). Ayni cerceve \(N(4,0)=4^4=256\), \(N(4,2)=2\) ve \(N(4,3)=2\) degerlerini de verir; dolayisiyla

$$S(4)=256+16+2+2=276.$$

Kod Nasıl Çalışır

C++, Python ve Java uygulamalari bu sonlu hafizali dinamik programlamayi kullanir. Sabit bir \(D\) icin bos durum ve cokluk \(1\) ile baslanir, satirlar yukaridan asagi islenir, hatirlanan son sutunlardan yasak sutunlar yeniden kurulur ve her yasal sonraki sutun denenir.

Derlenmis uygulamalar, hatirlanan sutunlari kompakt bir tamsayi icinde tutar; her kayitli satir icin dort bit kullanilir ve bu \(n=14\) icin yeterlidir. Her satirdan sonra uretilen ardil durumlar kodlanmis durum anahtarina gore siralanir ve ayni durumlar sayilari toplanarak birlestirilir.

Butun \(n\) satir tamamlaninca kalan durumlarin sayilari toplanarak \(N(n,D)\) elde edilir. Dis rutinin yaptigi sey bunu \(D=0,1,\dots,n-1\) icin tekrarlayip sonuclari toplamaktir. Bu sabit-\(D\) gorevleri birbirinden bagimsiz oldugu icin derlenmis uygulamalar bunlari is parcaciklarina dagitir. Python uygulamasi ise ayni derlenmis hesabin ince bir on yuzudur.

Karmaşıklık Analizi

Sabit \(D\) icin \(F_r\), \(r\). satirdan sonra birlesmis durum sayisi; \(G_r\) ise bir sonraki adimda birlestirme oncesi uretilen ardil sayisi olsun. Bir ebeveyn durum icin yasak kumesi en fazla \(D\) hatirlanan veziri tarar; dolayisiyla gecis asamasi \(O(F_rD+G_r)\) maliyetlidir. Ardindan uretilen durumlar siralanir ve bu da \(O(G_r\log G_r)\) ekler.

Boylece sabit bir \(D\) icin toplam sure

$$O\left(\sum_{r=0}^{n-1}\left(F_rD+G_r\log G_r\right)\right)$$

ve bellek kullanimi \(O(\max_r G_r)\) olur. Kaba bir ust sinir \(D\)'ye gore usseldir; cunku yakin-gecmis durumlarinin sayisi en fazla \(n^D\) olabilir. Buna ragmen gerekli ornekte \(n=14\) oldugu icin durum birlestirme on cephesi yeterince kucultur ve dogrudan hesaplama mumkun olur.

Dipnotlar ve Kaynaklar

  1. Problem sayfasi: https://projecteuler.net/problem=534
  2. Sekiz vezir problemi: Wikipedia — Eight queens puzzle
  3. Dinamik programlama: Wikipedia — Dynamic programming
  4. Bit islemleri: Wikipedia — Bitwise operation

Resumen del Problema

En un tablero \(n\times n\) colocamos exactamente una reina en cada fila. Una reina no ataca de forma ilimitada: sus ataques verticales y diagonales solo alcanzan una distancia de \(D\) filas, donde

$$D=n-1-w.$$

Sea \(Q(n,w)\) el numero de configuraciones validas para el parametro de debilidad \(w\). Resulta mas comodo reparametrizar con \(D\) y definir

$$N(n,D)=Q(n,n-1-D).$$

Entonces la cantidad pedida es

$$S(n)=\sum_{w=0}^{n-1}Q(n,w)=\sum_{D=0}^{n-1}N(n,D).$$

Los casos extremos aclaran el modelo. Si \(D=0\), ninguna reina puede alcanzar otra fila, de modo que cada fila puede elegir cualquiera de las \(n\) columnas y \(N(n,0)=n^n\). Si \(D=n-1\), cualquier par de filas puede interactuar y reaparece el problema clasico de las \(n\) reinas.

Enfoque Matemático

La observacion decisiva es que una reina situada a mas de \(D\) filas por encima de la fila actual deja de importar por completo. Por eso una configuracion parcial queda determinada totalmente por las ultimas \(D\) columnas elegidas.

Step 1: Reparametrizar por profundidad de ataque

En lugar de sumar directamente sobre \(w\), resolvemos el problema de profundidad fija \(N(n,D)\). Esto encaja exactamente con la restriccion fila a fila, porque solo pueden atacarse las parejas de filas cuya distancia es como maximo \(D\).

Una vez calculados todos los valores \(N(n,D)\), la respuesta final es

$$S(n)=\sum_{D=0}^{n-1}N(n,D).$$

Step 2: Escribir las columnas prohibidas locales

Sea \(c_r\in\{0,1,\dots,n-1\}\) la columna elegida en la fila \(r\). Una reina ya colocada en la fila \(r-d\) solo afecta a la fila \(r\) cuando \(1\le d\le D\). En ese caso prohíbe tres posibilidades:

$$c_r=c_{r-d},\qquad c_r=c_{r-d}+d,\qquad c_r=c_{r-d}-d,$$

siempre que esas columnas sigan dentro del tablero. La primera condicion corresponde al ataque vertical y las otras dos a las diagonales. Nada mas antiguo que \(D\) filas puede influir.

Step 3: Usar un estado de memoria finita

Tras colocar las primeras \(r\) filas, definimos \(\ell=\min(r,D)\). El futuro depende solo de la tupla

$$\sigma_r=(c_r,c_{r-1},\dots,c_{r-\ell+1}).$$

Dos tableros parciales con la misma tupla \(\sigma_r\) son equivalentes para todas las elecciones futuras, porque cualquier reina mas antigua ya esta a mas de \(D\) filas de distancia y nunca volvera a atacar. Asi, la busqueda se convierte en una programacion dinamica sobre estados de historial reciente.

Step 4: Transicion a la fila siguiente

Supongamos que el estado actual es \(\sigma=(a_1,\dots,a_\ell)\), donde \(a_1\) es la columna mas reciente. El conjunto bloqueado para la fila siguiente es

$$B(\sigma)=\bigcup_{d=1}^{\ell}\left(\{a_d\}\cup\{a_d+d\}\cup\{a_d-d\}\right)\cap\{0,1,\dots,n-1\}.$$

Cualquier columna de \(\{0,1,\dots,n-1\}\setminus B(\sigma)\) es valida. Si elegimos una de ellas, la nueva tupla de historial reciente se obtiene colocando esa columna delante y descartando la entrada mas antigua cuando sea necesario.

Si \(F_r(\sigma)\) denota el numero de tableros parciales de altura \(r\) que llegan al estado \(\sigma\), entonces la capa siguiente se obtiene anadiendo \(F_r(\sigma)\) a cada sucesor legal de \(\sigma\).

Step 5: Fusionar historiales recientes iguales

Muchos tableros parciales distintos terminan en la misma tupla de historial reciente. Una vez generados los sucesores de la siguiente fila, las tuplas iguales se fusionan y sus multiplicidades se suman. Esta es la compresion clave del metodo.

La fusion es exacta porque el patron futuro de ataques queda determinado por completo por la tupla reciente; no se pierde informacion.

Worked Example: \((n,D)=(4,1)\)

Cuando \(D=1\), cada fila solo interactua con la fila inmediatamente anterior, de modo que el estado es solo la ultima columna. Si la columna previa es \(0\), la siguiente fila solo puede usar \(2\) o \(3\). Si es \(1\), solo \(3\) es valida. Si es \(2\), solo \(0\). Si es \(3\), se permiten \(0\) o \(1\).

Partiendo de la primera fila, los recuentos de tableros parciales que terminan en las columnas \(0,1,2,3\) evolucionan como

$$\begin{aligned} r=1&:\ (1,1,1,1), &&T_1=4,\\ r=2&:\ (2,1,1,2), &&T_2=6,\\ r=3&:\ (3,2,2,3), &&T_3=10,\\ r=4&:\ (5,3,3,5), &&T_4=16. \end{aligned}$$

Por tanto, \(N(4,1)=16\). El mismo esquema tambien da \(N(4,0)=4^4=256\), \(N(4,2)=2\) y \(N(4,3)=2\), asi que

$$S(4)=256+16+2+2=276.$$

Cómo Funciona el Código

Las implementaciones en C++, Python y Java utilizan exactamente esta programacion dinamica de memoria finita. Para un \(D\) fijo comienzan con el estado vacio y multiplicidad \(1\), avanzan fila por fila, reconstruyen las columnas bloqueadas a partir de las columnas recientes recordadas y enumeran cada columna siguiente que sea legal.

Las implementaciones compiladas guardan las columnas recordadas en una representacion entera compacta, usando cuatro bits por fila recordada, lo cual basta para el caso objetivo \(n=14\). Despues de cada fila, los estados sucesores generados se ordenan por su estado codificado y los estados iguales se fusionan sumando sus recuentos.

Una vez procesadas las \(n\) filas, se suman los recuentos de todos los estados supervivientes y asi se obtiene \(N(n,D)\). El procedimiento exterior repite esto para \(D=0,1,\dots,n-1\) y acumula los resultados. Como estas tareas de \(D\) fijo son independientes, las implementaciones compiladas las reparten entre varios hilos de trabajo. La implementacion en Python actua como una capa ligera sobre el mismo calculo compilado.

Análisis de Complejidad

Para un \(D\) fijo, sea \(F_r\) el numero de estados fusionados despues de la fila \(r\), y \(G_r\) el numero de sucesores generados antes de fusionar en el paso siguiente. Construir el conjunto bloqueado para un estado padre inspecciona como maximo \(D\) reinas recordadas, asi que la fase de transicion cuesta \(O(F_rD+G_r)\). Luego las implementaciones ordenan los estados generados antes de fusionarlos, lo que anade \(O(G_r\log G_r)\).

En consecuencia, el tiempo total para un valor fijo de \(D\) es

$$O\left(\sum_{r=0}^{n-1}\left(F_rD+G_r\log G_r\right)\right),$$

y la memoria es \(O(\max_r G_r)\). Una cota grosera en el peor caso es exponencial en \(D\), porque puede haber hasta \(n^D\) estados de historial reciente. Aun asi, para la instancia requerida con \(n=14\), la fusion de estados mantiene la frontera en un tamano manejable para un calculo directo.

Notas y Referencias

  1. Pagina del problema: https://projecteuler.net/problem=534
  2. Problema de las ocho reinas: Wikipedia — Eight queens puzzle
  3. Programacion dinamica: Wikipedia — Dynamic programming
  4. Operaciones a nivel de bits: Wikipedia — Bitwise operation

问题概述

在一个 \(n\times n\) 的棋盘上,每一行恰好放置一个皇后。这里的皇后不是无限攻击:它只会在纵向和两条对角线方向上攻击距离自己不超过 \(D\) 行的位置,其中

$$D=n-1-w.$$

记 \(Q(n,w)\) 为弱度参数 \(w\) 下的合法摆放数量。为了书写和推导更自然,可以改用攻击深度 \(D\) 作为参数,定义

$$N(n,D)=Q(n,n-1-D).$$

那么题目要求的总量就是

$$S(n)=\sum_{w=0}^{n-1}Q(n,w)=\sum_{D=0}^{n-1}N(n,D).$$

两个边界情形可以帮助理解这个模型。当 \(D=0\) 时,任意皇后都无法攻击到别的行,因此每一行都可以自由选择 \(n\) 个列位置之一,于是 \(N(n,0)=n^n\)。当 \(D=n-1\) 时,任意两行之间都可能互相作用,这就退化成普通的 \(n\) 皇后问题。

数学方法

核心观察是:如果某个皇后位于当前行上方超过 \(D\) 行的位置,那么它以后再也不会影响当前决策。于是,一个部分摆放是否会产生相同的未来,只取决于最近的 \(D\) 行放在了哪些列上。

Step 1: 用攻击深度重新参数化

与其直接对 \(w\) 求和,不如先解决固定攻击深度的计数问题 \(N(n,D)\)。这样做的好处是,约束条件本来就是按“行距是否不超过 \(D\)”来定义的,所以固定 \(D\) 后,状态转移会变得非常清晰。

当所有 \(N(n,D)\) 都分别算出之后,最终答案只需要再做一次外层求和:

$$S(n)=\sum_{D=0}^{n-1}N(n,D).$$

Step 2: 写出当前行的局部禁用列

设第 \(r\) 行皇后所在的列为 \(c_r\in\{0,1,\dots,n-1\}\)。一枚已经放在第 \(r-d\) 行的皇后,只有在 \(1\le d\le D\) 时才会对第 \(r\) 行产生影响。此时它会禁掉三种可能:

$$c_r=c_{r-d},\qquad c_r=c_{r-d}+d,\qquad c_r=c_{r-d}-d,$$

当然前提是这些列号仍然落在棋盘范围内。第一项表示同列攻击,后两项表示两条对角线攻击。只要行距超过 \(D\),这枚旧皇后就再也不需要考虑。

Step 3: 只保留最近 \(D\) 行的有限记忆状态

当前已经放好了前 \(r\) 行时,记 \(\ell=\min(r,D)\)。接下来的所有合法性判断,只依赖于最近的 \(\ell\) 个列位置:

$$\sigma_r=(c_r,c_{r-1},\dots,c_{r-\ell+1}).$$

这一步是整个方法成立的原因。如果两个不同的部分棋盘有完全相同的 \(\sigma_r\),那么它们在未来每一行面对的攻击模式也完全相同,因为更早的那些皇后已经远在 \(D\) 行之外,不会再产生任何新的限制。于是,原本巨大的搜索树可以压缩成“最近历史状态”上的动态规划。

Step 4: 从一个状态转移到下一行

设当前状态为 \(\sigma=(a_1,\dots,a_\ell)\),其中 \(a_1\) 表示最近一行使用的列。下一行的禁用列集合为

$$B(\sigma)=\bigcup_{d=1}^{\ell}\left(\{a_d\}\cup\{a_d+d\}\cup\{a_d-d\}\right)\cap\{0,1,\dots,n-1\}.$$

于是,\(\{0,1,\dots,n-1\}\setminus B(\sigma)\) 中的每一列都是合法选择。若下一行选择列 \(c\),那么新状态就是把 \(c\) 放到最前面,并在长度超过 \(D\) 时删掉最旧的一项。

如果 \(F_r(\sigma)\) 表示放完前 \(r\) 行后到达状态 \(\sigma\) 的部分摆放数量,那么转移到下一层时,只需把 \(F_r(\sigma)\) 加到 \(\sigma\) 的每一个合法后继状态上。

Step 5: 合并相同的最近历史

很多不同的早期摆放过程,走到某一行之后会拥有相同的最近 \(D\) 行列模式。一旦这一点发生,它们在未来就完全等价,没有必要再分开保存。因此,在生成出下一行的所有候选状态以后,可以把相同的状态合并起来,并把它们的方案数相加。

这个合并不会丢失任何信息,因为未来的攻击关系完全由保留下来的最近历史决定。这也是算法能够处理目标规模的关键压缩步骤。

Worked Example: \((n,D)=(4,1)\)

当 \(D=1\) 时,每一行只会受到上一行的影响,所以状态只需要记录“上一行在哪一列”。如果上一行在列 \(0\),那么下一行只能放在列 \(2\) 或 \(3\)。如果上一行在列 \(1\),下一行只能放在列 \(3\)。如果上一行在列 \(2\),下一行只能放在列 \(0\)。如果上一行在列 \(3\),下一行可以放在列 \(0\) 或 \(1\)。

从第一行开始,末列分别为 \(0,1,2,3\) 的部分摆放数量按行演化如下:

$$\begin{aligned} r=1&:\ (1,1,1,1), &&T_1=4,\\ r=2&:\ (2,1,1,2), &&T_2=6,\\ r=3&:\ (3,2,2,3), &&T_3=10,\\ r=4&:\ (5,3,3,5), &&T_4=16. \end{aligned}$$

因此 \(N(4,1)=16\)。同一套框架还给出 \(N(4,0)=4^4=256\)、\(N(4,2)=2\)、\(N(4,3)=2\),于是

$$S(4)=256+16+2+2=276.$$

代码如何工作

C++、Python 和 Java 实现都遵循上述有限记忆动态规划。对于固定的 \(D\),它们从“空状态、计数为 \(1\)”开始,自上而下逐行处理,依据最近记住的列位置重建当前的禁用列集合,然后枚举所有合法的下一列。

编译型实现会把最近的列历史压缩编码到一个整数中。由于目标规模是 \(n=14\),每个记忆的行位置用四位二进制就足够表示。每处理完一行,就把生成出来的后继状态按照编码后的状态值排序,并把相同状态的计数合并相加。

当 \(n\) 行全部处理完之后,把所有存活状态的计数加总,就得到这个固定 \(D\) 下的 \(N(n,D)\)。最外层再对 \(D=0,1,\dots,n-1\) 重复这一过程并累加结果。由于不同 \(D\) 的任务彼此独立,编译型实现会把这些任务分发给多个工作线程。Python 实现则作为一个很薄的前端,调用同一套编译后的核心计算。

复杂度分析

对固定的 \(D\),记 \(F_r\) 为处理完第 \(r\) 行后合并得到的状态数,记 \(G_r\) 为下一步在合并之前生成的后继状态数。对每个父状态,构造禁用集合需要扫描至多 \(D\) 个仍被记住的皇后,因此转移阶段的代价为 \(O(F_rD+G_r)\)。随后实现会先对生成的状态排序,再做合并,这一步再增加 \(O(G_r\log G_r)\) 的时间。

所以,对单个 \(D\) 而言,总时间复杂度可以写成

$$O\left(\sum_{r=0}^{n-1}\left(F_rD+G_r\log G_r\right)\right),$$

空间复杂度为 \(O(\max_r G_r)\)。从最粗略的最坏情形看,算法对 \(D\) 仍然是指数级的,因为最近历史状态最多可能达到 \(n^D\) 个。不过在题目要求的 \(n=14\) 情况下,大量状态会在每一层被合并,实际前沿规模仍然足够可控。

注释与参考资料

  1. 题目页面: https://projecteuler.net/problem=534
  2. 八皇后问题: Wikipedia — Eight queens puzzle
  3. 动态规划: Wikipedia — Dynamic programming
  4. 位运算: Wikipedia — Bitwise operation

Краткое описание

На доске \(n\times n\) нужно поставить ровно по одной ферзю в каждую строку. Ферзь атакует не бесконечно далеко: вертикальные и диагональные атаки действуют только на расстоянии не больше \(D\) строк, где

$$D=n-1-w.$$

Пусть \(Q(n,w)\) обозначает число допустимых расстановок для параметра слабости \(w\). Удобно переписать задачу через глубину атаки \(D\), введя

$$N(n,D)=Q(n,n-1-D).$$

Тогда искомая сумма равна

$$S(n)=\sum_{w=0}^{n-1}Q(n,w)=\sum_{D=0}^{n-1}N(n,D).$$

Крайние случаи хорошо показывают смысл модели. При \(D=0\) ни один ферзь не может дотянуться до другой строки, поэтому в каждой строке можно свободно выбрать любой из \(n\) столбцов и \(N(n,0)=n^n\). При \(D=n-1\) взаимодействовать может любая пара строк, и мы получаем обычную задачу о \(n\) ферзях.

Математический подход

Ключевое наблюдение состоит в том, что ферзь, стоящий более чем на \(D\) строк выше текущей, уже никогда не повлияет на будущие решения. Значит, частичная расстановка полностью определяется последними \(D\) выбранными столбцами.

Step 1: Перепараметризация через глубину атаки

Вместо прямого суммирования по \(w\) удобнее решать задачу для фиксированной глубины \(N(n,D)\). Это точно совпадает с реальным ограничением построчного построения: атаковать друг друга могут только строки на расстоянии не больше \(D\).

После того как все значения \(N(n,D)\) посчитаны независимо, окончательный ответ дает формула

$$S(n)=\sum_{D=0}^{n-1}N(n,D).$$

Step 2: Описать локально запрещенные столбцы

Пусть \(c_r\in\{0,1,\dots,n-1\}\) — столбец, выбранный в строке \(r\). Ферзь в строке \(r-d\) влияет на строку \(r\) только если \(1\le d\le D\). Тогда он запрещает три варианта:

$$c_r=c_{r-d},\qquad c_r=c_{r-d}+d,\qquad c_r=c_{r-d}-d,$$

при условии, что соответствующие столбцы остаются на доске. Первая возможность отвечает вертикальной атаке, а две другие — диагоналям. Более старые строки уже не важны.

Step 3: Состояние с конечной памятью

После размещения первых \(r\) строк положим \(\ell=\min(r,D)\). Все будущее зависит только от кортежа

$$\sigma_r=(c_r,c_{r-1},\dots,c_{r-\ell+1}).$$

Если две разные частичные доски имеют один и тот же кортеж \(\sigma_r\), то для всех будущих выборов они эквивалентны: любой еще более ранний ферзь находится уже дальше чем на \(D\) строк и больше никогда не сможет атаковать. Так полный перебор превращается в динамическое программирование по состояниям недавней истории.

Step 4: Переход к следующей строке

Пусть текущее состояние равно \(\sigma=(a_1,\dots,a_\ell)\), где \(a_1\) — самый недавний столбец. Тогда множество запрещенных столбцов для следующей строки равно

$$B(\sigma)=\bigcup_{d=1}^{\ell}\left(\{a_d\}\cup\{a_d+d\}\cup\{a_d-d\}\right)\cap\{0,1,\dots,n-1\}.$$

Любой столбец из \(\{0,1,\dots,n-1\}\setminus B(\sigma)\) допустим. Если выбрать такой столбец \(c\), то новое состояние получается добавлением \(c\) в начало и удалением самого старого запомненного столбца, если длина стала больше \(D\).

Если \(F_r(\sigma)\) — число частичных расстановок высоты \(r\), приводящих к состоянию \(\sigma\), то на следующем слое величина \(F_r(\sigma)\) добавляется ко всем допустимым потомкам \(\sigma\).

Step 5: Сливать одинаковые недавние истории

Многие разные частичные расстановки приводят к одному и тому же кортежу последних столбцов. После генерации всех состояний следующей строки одинаковые кортежи можно объединить и сложить их кратности. Именно это сжатие делает алгоритм практичным.

Слияние абсолютно корректно, потому что весь будущий рисунок атак полностью определяется сохраненным кортежем недавней истории.

Worked Example: \((n,D)=(4,1)\)

При \(D=1\) каждая строка взаимодействует только с непосредственно предыдущей строкой, поэтому состояние состоит лишь из последнего столбца. Если предыдущий столбец равен \(0\), то в следующей строке можно ставить ферзя только в \(2\) или \(3\). Если предыдущий столбец равен \(1\), разрешен только \(3\). Если он равен \(2\), разрешен только \(0\). Если он равен \(3\), разрешены \(0\) или \(1\).

Начиная с первой строки, количества частичных расстановок, заканчивающихся столбцами \(0,1,2,3\), развиваются так:

$$\begin{aligned} r=1&:\ (1,1,1,1), &&T_1=4,\\ r=2&:\ (2,1,1,2), &&T_2=6,\\ r=3&:\ (3,2,2,3), &&T_3=10,\\ r=4&:\ (5,3,3,5), &&T_4=16. \end{aligned}$$

Следовательно, \(N(4,1)=16\). Та же схема дает \(N(4,0)=4^4=256\), \(N(4,2)=2\) и \(N(4,3)=2\), поэтому

$$S(4)=256+16+2+2=276.$$

Как работает код

Реализации на C++, Python и Java используют именно эту динамику с конечной памятью. Для фиксированного \(D\) они стартуют с пустого состояния кратности \(1\), затем обрабатывают строки сверху вниз, восстанавливают запрещенные столбцы по запомненным недавним столбцам и перебирают каждый допустимый следующий столбец.

Компилируемые реализации хранят недавнюю историю столбцов в компактном целочисленном кодировании, по четыре бита на каждую запомненную строку; для целевого случая \(n=14\) этого достаточно. После каждой строки порожденные состояния сортируются по закодированному ключу, а совпадающие состояния сливаются сложением их счетчиков.

После обработки всех \(n\) строк суммы по оставшимся состояниям дают \(N(n,D)\). Внешняя часть повторяет этот процесс для всех \(D=0,1,\dots,n-1\) и складывает результаты. Поскольку задачи для разных \(D\) независимы, компилируемые реализации распределяют их между рабочими потоками. Python-реализация выступает тонкой оболочкой над тем же скомпилированным вычислением.

Анализ сложности

Для фиксированного \(D\) обозначим через \(F_r\) число слитых состояний после строки \(r\), а через \(G_r\) — число порожденных потомков до слияния на следующем шаге. Построение множества запретов для одного родительского состояния просматривает не более \(D\) запомненных ферзей, поэтому фаза переходов стоит \(O(F_rD+G_r)\). Затем реализации сортируют порожденные состояния перед слиянием, что добавляет \(O(G_r\log G_r)\).

Итак, полное время для одного значения \(D\) равно

$$O\left(\sum_{r=0}^{n-1}\left(F_rD+G_r\log G_r\right)\right),$$

а память равна \(O(\max_r G_r)\). В грубой худшей оценке алгоритм экспоненциален по \(D\), поскольку состояний недавней истории может быть до \(n^D\). Однако для требуемого случая \(n=14\) слияние состояний достаточно сильно сжимает фронт, чтобы прямой расчет оставался выполнимым.

Сноски и ссылки

  1. Страница задачи: https://projecteuler.net/problem=534
  2. Задача о восьми ферзях: Wikipedia — Eight queens puzzle
  3. Динамическое программирование: Wikipedia — Dynamic programming
  4. Побитовые операции: Wikipedia — Bitwise operation

ملخص المسألة

على لوحة \(n\times n\)، نضع وزيرة واحدة بالضبط في كل صف. الوزيرة هنا لا تهاجم إلى ما لا نهاية؛ فالهجوم الرأسي والقطري يمتدان فقط حتى مسافة \(D\) صفوف، حيث

$$D=n-1-w.$$

ولنرمز بـ \(Q(n,w)\) إلى عدد الترتيبات الصحيحة عند قيمة الضعف \(w\). ومن الأنسب إعادة كتابة المسألة بدلالة عمق الهجوم \(D\) عبر التعريف

$$N(n,D)=Q(n,n-1-D).$$

وعندئذ تصبح الكمية المطلوبة

$$S(n)=\sum_{w=0}^{n-1}Q(n,w)=\sum_{D=0}^{n-1}N(n,D).$$

الحالتان الطرفيتان توضحان الفكرة بسرعة. عندما يكون \(D=0\)، لا تستطيع أي وزيرة الوصول إلى صف آخر، لذلك يملك كل صف حرية اختيار أي واحد من الأعمدة \(n\)، ومن ثم \(N(n,0)=n^n\). وعندما يكون \(D=n-1\)، يمكن لأي زوج من الصفوف أن يتفاعل، فنعود إلى مسألة الملكات التقليدية.

المنهج الرياضي

الملاحظة الحاسمة هي أن أي وزيرة تقع أعلى الصف الحالي بأكثر من \(D\) صفوف لا يمكنها أن تؤثر في أي قرار لاحق. لذلك فإن أي ترتيب جزئي يمكن تلخيصه بالكامل بواسطة الأعمدة المختارة في آخر \(D\) صفوف فقط.

Step 1: إعادة صياغة المسألة بعمق الهجوم

بدل أن نجمع مباشرة على \(w\)، نحل مسألة العد عند قيمة ثابتة لـ \(D\)، أي \(N(n,D)\). هذا يطابق القيد الحقيقي في البناء الصفّي، لأن الهجوم لا يحدث إلا بين صفين يفصل بينهما مسافة لا تتجاوز \(D\).

وبعد حساب جميع القيم \(N(n,D)\) على حدة، يكون الجواب النهائي هو

$$S(n)=\sum_{D=0}^{n-1}N(n,D).$$

Step 2: تحديد الأعمدة الممنوعة محلياً

ليكن \(c_r\in\{0,1,\dots,n-1\}\) هو العمود المختار في الصف \(r\). الوزيرة الموجودة في الصف \(r-d\) لا تؤثر في الصف \(r\) إلا إذا كان \(1\le d\le D\). في هذه الحالة تمنع ثلاثة احتمالات:

$$c_r=c_{r-d},\qquad c_r=c_{r-d}+d,\qquad c_r=c_{r-d}-d,$$

وذلك ما دامت هذه الأعمدة ضمن حدود اللوحة. الشرط الأول هو الهجوم الرأسي، والشرطان الآخران هما الهجومان القطريان. أما الصفوف الأقدم من ذلك فلا يبقى لها أي أثر.

Step 3: استعمال حالة ذات ذاكرة محدودة

بعد وضع أول \(r\) صفوف، نأخذ \(\ell=\min(r,D)\). كل ما يهم للمستقبل هو المتتالية

$$\sigma_r=(c_r,c_{r-1},\dots,c_{r-\ell+1}).$$

إذا كان لدينا ترتيبان جزئيان مختلفان لكنهما يملكان المتتالية نفسها \(\sigma_r\)، فهما متكافئان تماماً بالنسبة إلى جميع القرارات اللاحقة، لأن أي وزيرة أقدم من ذلك أصبحت أبعد من \(D\) صفوف ولن تتمكن من الهجوم مرة أخرى. بهذه الفكرة يتحول البحث الكامل إلى برمجة ديناميكية على حالات تمثل التاريخ القريب فقط.

Step 4: الانتقال إلى الصف التالي

لنفرض أن الحالة الحالية هي \(\sigma=(a_1,\dots,a_\ell)\)، حيث \(a_1\) هو أحدث عمود. مجموعة الأعمدة المحظورة في الصف التالي هي

$$B(\sigma)=\bigcup_{d=1}^{\ell}\left(\{a_d\}\cup\{a_d+d\}\cup\{a_d-d\}\right)\cap\{0,1,\dots,n-1\}.$$

كل عمود في \(\{0,1,\dots,n-1\}\setminus B(\sigma)\) هو اختيار قانوني. وعند اختيار عمود \(c\)، تتكون الحالة الجديدة بوضع \(c\) في البداية ثم حذف أقدم عمود محفوظ إذا تجاوز الطول قيمة \(D\).

وإذا رمزنا بـ \(F_r(\sigma)\) إلى عدد الترتيبات الجزئية ذات الارتفاع \(r\) التي تصل إلى الحالة \(\sigma\)، فإن الطبقة التالية تُبنى بإضافة \(F_r(\sigma)\) إلى كل تابع قانوني لهذه الحالة.

Step 5: دمج التواريخ القريبة المتطابقة

كثير من الترتيبات الجزئية المختلفة تنتهي إلى المتتالية نفسها من آخر \(D\) أعمدة. بعد توليد حالات الصف التالي، يمكن دمج الحالات المتطابقة وجمع عدد طرقها. هذه هي خطوة الضغط الأساسية التي تجعل الخوارزمية عملية.

هذا الدمج دقيق تماماً، لأن نمط الهجمات في المستقبل يتحدد بالكامل بواسطة التاريخ القريب المحفوظ، لذلك لا تضيع أي معلومة.

Worked Example: \((n,D)=(4,1)\)

عندما يكون \(D=1\)، فإن كل صف يتفاعل فقط مع الصف السابق مباشرة، ولهذا تصبح الحالة مجرد العمود الأخير. إذا كان العمود السابق هو \(0\)، فالصف التالي لا يستطيع الاختيار إلا بين \(2\) و\(3\). وإذا كان العمود السابق هو \(1\)، فالاختيار الوحيد هو \(3\). وإذا كان \(2\)، فالاختيار الوحيد هو \(0\). وإذا كان \(3\)، فيجوز اختيار \(0\) أو \(1\).

ابتداءً من الصف الأول، تتطور أعداد الترتيبات الجزئية المنتهية بالأعمدة \(0,1,2,3\) كما يلي:

$$\begin{aligned} r=1&:\ (1,1,1,1), &&T_1=4,\\ r=2&:\ (2,1,1,2), &&T_2=6,\\ r=3&:\ (3,2,2,3), &&T_3=10,\\ r=4&:\ (5,3,3,5), &&T_4=16. \end{aligned}$$

إذن \(N(4,1)=16\). والإطار نفسه يعطي أيضاً \(N(4,0)=4^4=256\)، و\(N(4,2)=2\)، و\(N(4,3)=2\)، ومن ثم

$$S(4)=256+16+2+2=276.$$

كيف تعمل الشيفرة

تستخدم تطبيقات C++ وPython وJava هذه البرمجة الديناميكية ذات الذاكرة المحدودة نفسها. عند قيمة ثابتة لـ \(D\)، تبدأ من الحالة الفارغة ذات التعدد \(1\)، ثم تعالج الصفوف من الأعلى إلى الأسفل، وتعيد بناء الأعمدة المحظورة من الأعمدة الحديثة المحفوظة، وتعدد كل عمود تالٍ قانوني.

التطبيقات المترجمة تحفظ تاريخ الأعمدة الحديث داخل تمثيل عددي مضغوط، مع أربع بتات لكل صف محفوظ، وهذا يكفي للحالة الهدف \(n=14\). وبعد كل صف، ترتب الحالات الناتجة بحسب مفتاحها المشفر، ثم تدمج الحالات المتساوية بجمع أعدادها.

بعد معالجة جميع الصفوف \(n\)، يُجمع عدد الطرق في كل الحالات الباقية للحصول على \(N(n,D)\). ثم تكرر الطبقة الخارجية ذلك لكل \(D=0,1,\dots,n-1\) وتجمع النتائج. ولأن مسائل \(D\) المختلفة مستقلة تماماً، توزع التطبيقات المترجمة هذه الأعمال على عدة خيوط تنفيذ. أما تطبيق Python فيعمل كواجهة خفيفة فوق الحساب المترجم نفسه.

تحليل التعقيد

لثابت \(D\)، لنرمز بـ \(F_r\) إلى عدد الحالات بعد الدمج عقب الصف \(r\)، وبـ \(G_r\) إلى عدد الحالات المتولدة قبل الدمج في الخطوة التالية. بناء مجموعة المنع لحالة أب واحدة يتطلب فحص ما يصل إلى \(D\) وزيرات محفوظات، ولذلك تكلف مرحلة الانتقال \(O(F_rD+G_r)\). بعد ذلك ترتب التطبيقات الحالات المتولدة قبل دمجها، مما يضيف زمناً مقداره \(O(G_r\log G_r)\).

وبالتالي فإن الزمن الكلي لقيمة واحدة من \(D\) هو

$$O\left(\sum_{r=0}^{n-1}\left(F_rD+G_r\log G_r\right)\right),$$

بينما يساوي استهلاك الذاكرة \(O(\max_r G_r)\). وهناك حد خشن في أسوأ الأحوال يكون أسيّاً في \(D\)، لأن عدد حالات التاريخ القريب قد يصل إلى \(n^D\). ومع ذلك، في الحالة المطلوبة حيث \(n=14\)، يبقى الحجم العملي للجبهة قابلاً للإدارة بفضل الدمج القوي للحالات.

حواش ومراجع

  1. صفحة المسألة: https://projecteuler.net/problem=534
  2. مسألة الملكات الثماني: Wikipedia — Eight queens puzzle
  3. البرمجة الديناميكية: Wikipedia — Dynamic programming
  4. العمليات البتية: Wikipedia — Bitwise operation