Problem Summary

For every binary HP sequence of length \(n\), we place the chain on the square lattice as a self-avoiding walk and ask for the maximum possible number of H-H contacts. Project Euler asks for the average of that maximum over all \(2^n\) sequences. The code solves the case

$$n=15.$$

Mathematical Approach

1. Folding shapes are self-avoiding walks

A fold is a self-avoiding walk

$$p_0,p_1,\dots,p_{n-1}\in\mathbb Z^2$$

with unit steps and no repeated lattice site. Because translation and rotation do not change contact counts, the implementation fixes

$$p_0=(0,0),\qquad p_1=(1,0),$$

and then performs a DFS over the remaining \(n-2\) steps. This removes the large symmetry factor before enumeration even starts.

2. A fold matters only through its contact map

For a finished walk, the only geometric information relevant to scoring is which non-consecutive monomer indices end up adjacent on the lattice. For \(i<j\) with \(j\ge i+2\), define

$$C_{ij}=1 \iff |p_i-p_j|_1=1.$$

This produces a contact map, which the code stores as a bitset over all pairs \((i,j)\) with \(j\ge i+2\). Different lattice walks can produce the same contact map, and then they have exactly the same score for every HP sequence. So the geometry phase ends by deduplicating maps with hashing.

For example, the solver finds only

$$41$$

distinct contact maps for \(n=8\), and

$$12495$$

distinct contact maps for \(n=15\).

3. Why consecutive H-H pairs are added separately

If positions \(i\) and \(i+1\) are both H, then they are always adjacent in the chain, no matter how the fold bends. Therefore that contribution is independent of the contact map. For an HP mask \(h\in\{0,1\}^n\), the number of consecutive H-H bonds is

$$\operatorname{popcount}(h\ \&\ (h\gg 1)).$$

This explains the extra term in the code:

$$\text{total}(h,C)=\text{nonconsecutive\_contacts}(h,C)+\operatorname{popcount}(h\ \&\ (h\gg 1)).$$

So the optimization over folds only needs to maximize the non-consecutive part.

4. Scoring one contact map by subset DP

Fix one contact map \(C\). The code converts it to adjacency bitmasks \(N_C(i)\), where bit \(j\) is set if \((i,j)\) is a non-consecutive contact in that fold. For an H-mask \(m\), define \(\text{score}_C(m)\) as the number of contact edges whose two endpoints are both H.

Instead of checking every contact edge from scratch for every mask, the implementation uses the recurrence

$$\text{score}_C(m)=\text{score}_C(m\setminus\{i\})+\bigl|N_C(i)\cap(m\setminus\{i\})\bigr|,$$

where \(i\) is the least significant set bit of \(m\). This works because when \(i\) is inserted last, exactly the contacts from \(i\) to already-present H positions are new. Every contact is counted once and only once.

5. Best fold for each HP sequence

Let \(B(m)\) be the best non-consecutive contact count over all unique maps:

$$B(m)=\max_C \text{score}_C(m).$$

The code maintains an array best[mask] and updates it while scanning all deduplicated contact maps. After that, the full optimal score for sequence mask \(m\) is

$$B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1)).$$

6. Exact averaging over all \(2^n\) sequences

The final numerator is

$$\sum_{m=0}^{2^n-1}\left(B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1))\right).$$

Since every HP sequence is equally likely, the desired expectation is

$$\frac{1}{2^n}\sum_{m=0}^{2^n-1}\left(B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1))\right).$$

The denominator is a power of two, so the result is converted to an exact terminating binary-rational decimal by long division.

7. Checkpoints

The published checkpoint is

$$n=8 \quad \Longrightarrow \quad \frac{850}{256}=3.3203125.$$

The full solver for \(n=15\) returns

$$8.0540771484375.$$

How the Code Works

dfs(...) enumerates all self-avoiding walks after fixing the first edge. add_current_contact_map() converts one finished walk to a packed bit key and inserts it into a set. Then each unique map is turned into adjacency masks, and a subset DP computes \(\text{score}_C(m)\) for all \(m\). The array best stores the maximum over all maps, after which the sequence-independent consecutive-HH term is added and averaged.

Complexity Analysis

If \(M\) is the number of unique contact maps, the scoring phase costs

$$O(M\cdot 2^n)$$

with very small constants because every update is just a few bit operations. Memory is

$$O(M+2^n).$$

The expensive combinatorial step is the self-avoiding walk enumeration, but for \(n=15\) it is still manageable once symmetry is fixed and maps are deduplicated.

Further Reading

  1. Problem page: https://projecteuler.net/problem=300
  2. Self-avoiding walk: https://en.wikipedia.org/wiki/Self-avoiding_walk
  3. HP lattice model: https://en.wikipedia.org/wiki/HP_model

Problemzusammenfassung

Für jede binäre HP-Sequenz der Länge \(n\) wird die Kette als selbstvermeidender Weg auf dem quadratischen Gitter gefaltet. Für jede Sequenz sucht man die maximal mögliche Zahl von H-H-Kontakten und mittelt diesen Wert über alle \(2^n\) Sequenzen. Der Code löst

$$n=15.$$

Mathematischer Ansatz

1. Faltungen als self-avoiding walks

Eine Faltung ist ein selbstvermeidender Weg

$$p_0,p_1,\dots,p_{n-1}\in\mathbb Z^2$$

mit Einheitskanten und ohne mehrfach belegte Gitterpunkte. Da Translationen und Rotationen die Kontaktzahl nicht ändern, fixiert der Code

$$p_0=(0,0),\qquad p_1=(1,0)$$

und erzeugt per DFS nur die restlichen \(n-2\) Schritte.

2. Entscheidend ist nur die Kontaktkarte

Für die Bewertung zählt nur, welche nicht-benachbarten Kettenpositionen auf dem Gitter benachbart werden. Für \(i<j\) mit \(j\ge i+2\) definiert man

$$C_{ij}=1 \iff |p_i-p_j|_1=1.$$

So entsteht eine Kontaktkarte, die der Code als Bitmuster über alle zulässigen Paare \((i,j)\) speichert. Verschiedene Wege können dieselbe Kontaktkarte erzeugen; dann liefern sie für jede HP-Sequenz exakt denselben Score. Deshalb dedupliziert die Implementierung diese Karten per Hashing.

Der Solver findet nur

$$41$$

verschiedene Kontaktkarten für \(n=8\) und

$$12495$$

für \(n=15\).

3. Warum benachbarte H-H-Paare separat addiert werden

Wenn die Positionen \(i\) und \(i+1\) beide H sind, dann sind sie entlang der Kette immer benachbart, unabhängig von der geometrischen Faltung. Dieser Beitrag hängt also nicht von der Kontaktkarte ab. Für eine HP-Maske \(h\in\{0,1\}^n\) ist die Zahl solcher festen Beiträge

$$\operatorname{popcount}(h\ \&\ (h\gg 1)).$$

Daher verwendet der Code

$$\text{total}(h,C)=\text{nonconsecutive\_contacts}(h,C)+\operatorname{popcount}(h\ \&\ (h\gg 1)).$$

Optimiert werden muss also nur der nicht-benachbarte Kontaktanteil.

4. Bewertung einer Kontaktkarte per Teilmengen-DP

Für eine feste Kontaktkarte \(C\) erzeugt der Code Nachbarschaftsbitmasken \(N_C(i)\). Für eine H-Maske \(m\) sei \(\text{score}_C(m)\) die Zahl der Kontaktkanten, deren beide Endpunkte H sind.

Anstatt für jede Maske alle Kanten neu zu prüfen, nutzt die Implementierung die Rekursion

$$\text{score}_C(m)=\text{score}_C(m\setminus\{i\})+\bigl|N_C(i)\cap(m\setminus\{i\})\bigr|,$$

wobei \(i\) das niedrigstgesetzte Bit von \(m\) ist. Fügt man \(i\) zuletzt ein, sind genau die Kontakte von \(i\) zu den bereits vorhandenen H-Positionen neu. Jede Kante wird dadurch genau einmal gezählt.

5. Beste Faltung pro HP-Sequenz

Sei \(B(m)\) der beste nicht-benachbarte Kontaktwert über alle deduplizierten Karten:

$$B(m)=\max_C \text{score}_C(m).$$

Der Code hält dafür ein Feld best[mask]. Der vollständige optimale Score für die Sequenzmaske \(m\) ist dann

$$B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1)).$$

6. Exaktes Mittel über alle \(2^n\) Sequenzen

Der endgültige Zähler lautet

$$\sum_{m=0}^{2^n-1}\left(B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1))\right).$$

Da jede HP-Sequenz gleich wahrscheinlich ist, folgt der Erwartungswert als

$$\frac{1}{2^n}\sum_{m=0}^{2^n-1}\left(B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1))\right).$$

Weil der Nenner eine Zweierpotenz ist, kann das Ergebnis exakt per schriftlicher Division ausgegeben werden.

7. Prüfwerte

Der veröffentlichte Kontrollwert ist

$$n=8 \quad \Longrightarrow \quad \frac{850}{256}=3.3203125.$$

Für \(n=15\) liefert der Solver

$$8.0540771484375.$$

Wie der Code arbeitet

dfs(...) erzeugt alle self-avoiding walks nach Fixierung der ersten Kante. add_current_contact_map() wandelt eine fertige Faltung in einen gepackten Bit-Schlüssel um und legt ihn in einer Menge ab. Danach wird jede eindeutige Kontaktkarte in Nachbarschaftsbitmasken umgewandelt, und eine Teilmengen-DP berechnet \(\text{score}_C(m)\) für alle \(m\). Schließlich speichert best das Maximum über alle Karten, addiert den festen benachbarten H-H-Term und mittelt über alle Sequenzen.

Komplexitätsanalyse

Ist \(M\) die Zahl der eindeutigen Kontaktkarten, dann kostet die Bewertungsphase

$$O(M\cdot 2^n)$$

Zeit bei sehr kleinen Konstanten, weil jede Aktualisierung nur aus wenigen Bitoperationen besteht. Der Speicherbedarf ist

$$O(M+2^n).$$

Der teuerste kombinatorische Teil ist die Erzeugung der self-avoiding walks; für \(n=15\) bleibt sie jedoch nach Symmetriebrechung und Deduplikation noch handhabbar.

Weiterführende Hinweise

  1. Problemseite: https://projecteuler.net/problem=300
  2. Self-avoiding walk: https://de.wikipedia.org/wiki/Selbstvermeidender_Weg
  3. HP-Modell: https://en.wikipedia.org/wiki/HP_model

Problem Özeti

Uzunluğu \(n\) olan her ikili HP dizisi için zincir kare örgüde self-avoiding walk olarak katlanır. Her dizi için elde edilebilecek en yüksek H-H temas sayısı alınır ve bu değer tüm \(2^n\) dizi üzerinde ortalanır. Kodun çözdüğü asıl durum

$$n=15$$

olup, amaç bu ortalamanın tam değeridir.

Matematiksel Yaklaşım

1. Katlanmalar self-avoiding walk olarak üretilir

Bir katlanma

$$p_0,p_1,\dots,p_{n-1}\in\mathbb Z^2$$

noktalarından oluşan, birim adımlı ve kendi üzerine basmayan bir yürüyüştür. Öteleme ve dönme temas sayısını değiştirmediğinden kod

$$p_0=(0,0),\qquad p_1=(1,0)$$

seçimini sabitler ve geri kalan \(n-2\) adımı DFS ile üretir. Böylece en başta büyük bir simetri faktörü atılmış olur.

2. Bir katlanmayı asıl belirleyen şey contact map'tir

Skor için geometrinin tamamı değil, sadece zincirde komşu olmayan hangi indeks çiftlerinin örgüde komşu hale geldiği gerekir. \(i<j\) ve \(j\ge i+2\) için

$$C_{ij}=1 \iff |p_i-p_j|_1=1$$

tanımı yapılır. Bu, contact map denen bit-mask temsiline karşılık gelir. Aynı contact map'i veren iki farklı geometri, her HP dizisi için aynı skoru üreteceği için kod bunları hash ile tekilleştirir.

Örneğin çözücü \(n=8\) için yalnızca

$$41$$

farklı contact map, \(n=15\) için ise

$$12495$$

farklı contact map bulur.

3. Ardışık H-H çiftleri neden ayrı ekleniyor?

Dizide \(i\) ve \(i+1\) konumları ikisi de H ise, bunlar hangi katlanma seçilirse seçilsin zincirin üzerinde zaten komşudur. Yani bu katkı contact map'ten bağımsızdır. Bir HP maskesi \(h\in\{0,1\}^n\) için bu sabit katkı

$$\operatorname{popcount}(h\ \&\ (h\gg 1))$$

kadardır. Bu yüzden kod fiilen

$$\text{total}(h,C)=\text{nonconsecutive\_contacts}(h,C)+\operatorname{popcount}(h\ \&\ (h\gg 1))$$

değerini kullanır. Maksimize edilmesi gereken kısım sadece contact map'e bağlı ilk terimdir.

4. Tek bir contact map'in skoru altküme DP ile hesaplanır

Sabit bir contact map \(C\) için kod, her indeks \(i\) adına komşuluk bitmask'i \(N_C(i)\) kurar. Bir H-maskesi \(m\) için \(\text{score}_C(m)\), her iki ucu da H olan temas kenarlarının sayısı olsun.

Her maskede tüm temas kenarlarını baştan saymak yerine şu recurrence kullanılır:

$$\text{score}_C(m)=\text{score}_C(m\setminus\{i\})+\bigl|N_C(i)\cap(m\setminus\{i\})\bigr|,$$

burada \(i\), \(m\)'nin en düşük set bitidir. Çünkü \(i\) en son eklenmiş gibi düşünülürse yeni gelen temaslar tam olarak \(i\)'nin daha önce var olan H düğümleriyle yaptığı temaslardır. Her temas böylece tam bir kez sayılır.

5. Her HP dizisi için en iyi katlanma

Tüm tekil contact map'ler üzerinde

$$B(m)=\max_C \text{score}_C(m)$$

tanımını yapalım. Kodun best[mask] dizisi tam olarak bunu tutar. O zaman maske \(m\) için toplam en iyi skor

$$B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1))$$

olur.

6. Tüm \(2^n\) dizi üzerinde tam ortalama

Nihai pay

$$\sum_{m=0}^{2^n-1}\left(B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1))\right)$$

şeklindedir. Her HP dizisi eş olasılıklı olduğu için beklenen değer

$$\frac{1}{2^n}\sum_{m=0}^{2^n-1}\left(B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1))\right)$$

olur. Payda bir \(2\) kuvveti olduğu için sonuç tam kesir halinde hesaplanıp uzun bölme ile kesin ondalığa çevrilir.

7. Kontrol noktaları

Yayınlanmış checkpoint

$$n=8 \quad \Longrightarrow \quad \frac{850}{256}=3.3203125$$

şeklindedir. Tam çözücü \(n=15\) için

$$8.0540771484375$$

değerini verir.

Kod Nasıl Çalışır?

dfs(...), ilk kenar sabitlenmiş halde tüm SAW'leri üretir. add_current_contact_map() bitmiş bir yürüyüşü paketlenmiş bir bit anahtarına çevirip kümeye ekler. Sonra her tekil harita komşuluk maskelerine dönüştürülür ve subset DP ile tüm \(\text{score}_C(m)\) değerleri hesaplanır. best dizisi haritalar üzerindeki maksimumu tutar; en sonda sabit ardışık-HH katkısı eklenir ve tüm maskeler üzerinde ortalama alınır.

Karmaşıklık Analizi

Tekil contact map sayısı \(M\) ise puanlama evresi

$$O(M\cdot 2^n)$$

zamanda çalışır; çünkü her güncelleme birkaç bit işleminden ibarettir. Bellek karmaşıklığı

$$O(M+2^n)$$

olur. En pahalı kombinatoryal kısım SAW üretimidir; ancak \(n=15\) için simetri kırma ve map tekilleştirmesi sonrası hâlâ yönetilebilir kalır.

İleri Okuma

  1. Problem metni: https://projecteuler.net/problem=300
  2. Self-avoiding walk: https://tr.wikipedia.org/wiki/Kendi_kendini_kesmeyen_yürüyüş
  3. HP modeli: https://en.wikipedia.org/wiki/HP_model

Resumen del Problema

Para cada secuencia HP binaria de longitud \(n\), la cadena se pliega en la red cuadrada como self-avoiding walk. Para esa secuencia se toma el máximo número posible de contactos H-H y luego se promedia sobre las \(2^n\) secuencias. El código resuelve el caso

$$n=15.$$

Enfoque Matemático

1. Los plegados son self-avoiding walks

Un plegado es un camino autoevitante

$$p_0,p_1,\dots,p_{n-1}\in\mathbb Z^2$$

con pasos unitarios y sin reutilizar celdas. Como traslaciones y rotaciones no cambian los contactos, la implementación fija

$$p_0=(0,0),\qquad p_1=(1,0),$$

y después hace DFS sólo sobre los \(n-2\) pasos restantes.

2. Lo importante es el mapa de contactos

Para puntuar, no hace falta toda la geometría: sólo importa qué pares no consecutivos de índices acaban siendo vecinos en la red. Para \(i<j\) con \(j\ge i+2\), definimos

$$C_{ij}=1 \iff |p_i-p_j|_1=1.$$

Eso genera un contact map que el código guarda como bitset. Dos plegados distintos pueden producir el mismo mapa; si eso ocurre, puntúan igual para cualquier secuencia HP. Por eso se deduplican los mapas mediante hashing.

El solver encuentra sólo

$$41$$

mapas distintos para \(n=8\) y

$$12495$$

para \(n=15\).

3. Por qué los pares H-H consecutivos se añaden aparte

Si las posiciones \(i\) e \(i+1\) son ambas H, siempre son adyacentes en la cadena, independientemente del plegado. Esa contribución no depende del contact map. Para una máscara HP \(h\in\{0,1\}^n\), ese término fijo es

$$\operatorname{popcount}(h\ \&\ (h\gg 1)).$$

Por eso el total se descompone como

$$\text{total}(h,C)=\text{nonconsecutive\_contacts}(h,C)+\operatorname{popcount}(h\ \&\ (h\gg 1)).$$

4. Puntuar un mapa con DP de subconjuntos

Fijado un mapa \(C\), el código construye máscaras de adyacencia \(N_C(i)\). Para una máscara \(m\), sea \(\text{score}_C(m)\) el número de aristas de contacto cuyos dos extremos son H.

La recurrencia usada es

$$\text{score}_C(m)=\text{score}_C(m\setminus\{i\})+\bigl|N_C(i)\cap(m\setminus\{i\})\bigr|,$$

donde \(i\) es el bit menos significativo de \(m\). La idea es que, al insertar \(i\) en último lugar, los únicos contactos nuevos son los de \(i\) con los H ya presentes. Así cada contacto se cuenta exactamente una vez.

5. Mejor plegado para cada secuencia

Definimos

$$B(m)=\max_C \text{score}_C(m).$$

La tabla best[mask] almacena precisamente ese máximo. Entonces el score óptimo total para la máscara \(m\) es

$$B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1)).$$

6. Promedio exacto sobre las \(2^n\) secuencias

El numerador final es

$$\sum_{m=0}^{2^n-1}\left(B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1))\right).$$

Como todas las secuencias HP son equiprobables, la esperanza buscada es

$$\frac{1}{2^n}\sum_{m=0}^{2^n-1}\left(B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1))\right).$$

El denominador es una potencia de \(2\), así que la salida decimal se obtiene de forma exacta por división larga.

7. Checkpoints

El valor publicado para comprobar es

$$n=8 \quad \Longrightarrow \quad \frac{850}{256}=3.3203125.$$

Para \(n=15\), el solver devuelve

$$8.0540771484375.$$

Cómo Funciona el Código

dfs(...) enumera todos los self-avoiding walks tras fijar la primera arista. add_current_contact_map() convierte un plegado terminado en una clave de bits y la inserta en un conjunto. Luego cada mapa único se transforma en máscaras de adyacencia, y una DP de subconjuntos calcula \(\text{score}_C(m)\) para todas las máscaras. La tabla best guarda el máximo sobre todos los mapas; al final se añade el término fijo de H-H consecutivos y se promedia.

Complejidad

Si \(M\) es el número de mapas únicos, la fase de puntuación cuesta

$$O(M\cdot 2^n)$$

con constantes pequeñas porque todo son operaciones bit a bit. La memoria es

$$O(M+2^n).$$

La parte combinatoria dura es la enumeración de self-avoiding walks, pero para \(n=15\) sigue siendo manejable una vez rotas las simetrías y deduplicados los mapas.

Lecturas

  1. Problema: https://projecteuler.net/problem=300
  2. Self-avoiding walk: https://es.wikipedia.org/wiki/Paseo_aleatorio_autoevitante
  3. Modelo HP: https://en.wikipedia.org/wiki/HP_model

问题概述

对长度为 \(n\) 的每一个二元 HP 序列,把链放到方格点上作为自回避游走。对该序列取所有折叠中最大的 H-H 接触数,再对全部 \(2^n\) 个序列求平均。代码处理的是

$$n=15.$$

数学方法

1. 折叠就是自回避游走

一个折叠可表示为

$$p_0,p_1,\dots,p_{n-1}\in\mathbb Z^2$$

这样的单位步长路径,且不能重复占用格点。由于平移和旋转不会改变接触数,程序固定

$$p_0=(0,0),\qquad p_1=(1,0),$$

然后只对剩余的 \(n-2\) 步做 DFS 枚举。

2. 真正重要的是 contact map

计分时并不需要保留整个几何形状,只需要知道哪些“链上不相邻”的位置在格点上变成了相邻。对 \(i<j\) 且 \(j\ge i+2\),定义

$$C_{ij}=1 \iff |p_i-p_j|_1=1.$$

这就得到一个 contact map,代码把它压成 bitset。不同的格点路径可能产生同一个 contact map,而一旦 map 相同,它们对所有 HP 序列的得分都完全一样。因此程序用哈希去重。

对于 \(n=8\),唯一 contact map 只有

$$41$$

个;对于 \(n=15\),则是

$$12495$$

个。

3. 为什么连续的 H-H 对要单独加

若位置 \(i\) 与 \(i+1\) 都是 H,那么它们沿链本来就是相邻的,不依赖于折叠方式。因此这部分贡献与 contact map 无关。对 HP 掩码 \(h\in\{0,1\}^n\),这项固定贡献就是

$$\operatorname{popcount}(h\ \&\ (h\gg 1)).$$

所以总分被拆成

$$\text{total}(h,C)=\text{nonconsecutive\_contacts}(h,C)+\operatorname{popcount}(h\ \&\ (h\gg 1)).$$

4. 用子集 DP 计算一个 map 的分数

固定一个 map \(C\) 后,代码为每个位置 \(i\) 构造邻接位掩码 \(N_C(i)\)。对一个 H 掩码 \(m\),定义 \(\text{score}_C(m)\) 为“两个端点都为 H 的接触边数量”。

程序使用递推

$$\text{score}_C(m)=\text{score}_C(m\setminus\{i\})+\bigl|N_C(i)\cap(m\setminus\{i\})\bigr|,$$

其中 \(i\) 是 \(m\) 的最低位 set bit。其含义是:把 \(i\) 最后加入时,新增的接触边恰好是 \(i\) 与此前已选 H 点之间的那些边。这样每条接触边只被计数一次。

5. 每个 HP 序列的最优折叠

定义

$$B(m)=\max_C \text{score}_C(m).$$

代码中的 best[mask] 正是在所有唯一 map 上取这个最大值。于是掩码 \(m\) 的完整最优得分为

$$B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1)).$$

6. 对全部 \(2^n\) 序列做精确平均

最终分子为

$$\sum_{m=0}^{2^n-1}\left(B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1))\right).$$

由于每个 HP 序列等概率,所求期望就是

$$\frac{1}{2^n}\sum_{m=0}^{2^n-1}\left(B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1))\right).$$

分母是 \(2\) 的幂,因此程序可以通过长除法精确输出十进制结果。

7. 检查点

已发布的校验值是

$$n=8 \quad \Longrightarrow \quad \frac{850}{256}=3.3203125.$$

完整求解器在 \(n=15\) 时给出

$$8.0540771484375.$$

代码原理

dfs(...) 在固定第一条边后枚举全部自回避游走。add_current_contact_map() 把一个完成的折叠转成压缩 bit key 并放进集合。随后每个唯一 map 会被转成邻接掩码,再用子集 DP 求出所有 \(\text{score}_C(m)\)。数组 best 保存对所有 map 取最大值后的结果,最后再加上固定的连续 H-H 项并做平均。

复杂度分析

若唯一 contact map 数为 \(M\),则评分阶段复杂度为

$$O(M\cdot 2^n),$$

常数很小,因为每一步基本只是位运算。空间复杂度是

$$O(M+2^n).$$

最重的组合部分是自回避游走枚举,但对 \(n=15\) 来说,在固定对称性并去重之后仍然可行。

延伸阅读

  1. 题目页面: https://projecteuler.net/problem=300
  2. 自回避游走: https://zh.wikipedia.org/wiki/自避行走
  3. HP 模型: https://en.wikipedia.org/wiki/HP_model

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

Для каждой бинарной HP-последовательности длины \(n\) цепочка укладывается на квадратную решётку как самоизбегающее блуждание. Для данной последовательности берётся максимальное число H-H контактов по всем укладкам, а затем выполняется усреднение по всем \(2^n\) последовательностям. Код решает случай

$$n=15.$$

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

1. Укладки как self-avoiding walk

Укладка — это путь

$$p_0,p_1,\dots,p_{n-1}\in\mathbb Z^2$$

с единичными шагами без повторного посещения вершины. Так как переносы и повороты не меняют число контактов, программа фиксирует

$$p_0=(0,0),\qquad p_1=(1,0),$$

и затем перебирает DFS только оставшиеся \(n-2\) шага.

2. Важна только карта контактов

Для оценки не нужна полная геометрия; важно лишь, какие непоследовательные индексы цепочки стали соседями на решётке. Для \(i<j\) и \(j\ge i+2\) положим

$$C_{ij}=1 \iff |p_i-p_j|_1=1.$$

Это и есть карта контактов, которую код хранит как битовую маску. Разные самоизбегающие пути могут давать одну и ту же карту; тогда они полностью эквивалентны для всех HP-последовательностей. Поэтому карта дедуплицируется хешированием.

Для \(n=8\) получается лишь

$$41$$

различная карта контактов, а для \(n=15\) —

$$12495.$$

3. Почему соседние H-H пары добавляются отдельно

Если позиции \(i\) и \(i+1\) обе гидрофобны, то они всегда соседствуют вдоль самой цепочки, независимо от формы укладки. Этот вклад не зависит от карты контактов. Для HP-маски \(h\in\{0,1\}^n\) он равен

$$\operatorname{popcount}(h\ \&\ (h\gg 1)).$$

Поэтому полная оценка раскладывается в

$$\text{total}(h,C)=\text{nonconsecutive\_contacts}(h,C)+\operatorname{popcount}(h\ \&\ (h\gg 1)).$$

4. Оценка одной карты через subset DP

Для фиксированной карты \(C\) код строит битмаски соседей \(N_C(i)\). Для маски \(m\) величина \(\text{score}_C(m)\) — это число контактных рёбер, оба конца которых принадлежат H.

Вместо пересчёта всех рёбер для каждой маски используется рекурсия

$$\text{score}_C(m)=\text{score}_C(m\setminus\{i\})+\bigl|N_C(i)\cap(m\setminus\{i\})\bigr|,$$

где \(i\) — младший установленный бит маски \(m\). Идея проста: если добавить вершину \(i\) последней, то новыми являются только контакты между \(i\) и уже выбранными H-вершинами. Каждое ребро считается ровно один раз.

5. Лучшая укладка для каждой HP-последовательности

Определим

$$B(m)=\max_C \text{score}_C(m).$$

Массив best[mask] хранит именно этот максимум по всем дедуплицированным картам. Тогда полный оптимальный счёт для маски \(m\) равен

$$B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1)).$$

6. Точное усреднение по всем \(2^n\) последовательностям

Итоговый числитель имеет вид

$$\sum_{m=0}^{2^n-1}\left(B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1))\right).$$

Так как все HP-последовательности равновероятны, искомое ожидание равно

$$\frac{1}{2^n}\sum_{m=0}^{2^n-1}\left(B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1))\right).$$

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

7. Контрольные значения

Опубликованный checkpoint таков:

$$n=8 \quad \Longrightarrow \quad \frac{850}{256}=3.3203125.$$

Для \(n=15\) решатель выдаёт

$$8.0540771484375.$$

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

dfs(...) перечисляет все self-avoiding walks после фиксации первого ребра. add_current_contact_map() переводит готовую укладку в упакованный битовый ключ и добавляет его в множество. Затем каждая уникальная карта превращается в маски соседства, а subset DP вычисляет \(\text{score}_C(m)\) для всех \(m\). Массив best хранит максимум по всем картам; после этого прибавляется фиксированный вклад соседних H-H по цепочке и берётся среднее.

Сложность

Если \(M\) — число уникальных карт контактов, фаза оценки стоит

$$O(M\cdot 2^n)$$

по времени, причём константы малы, так как используются лишь битовые операции. Память равна

$$O(M+2^n).$$

Самая тяжёлая комбинаторная часть — перечисление самоизбегающих блужданий, но для \(n=15\) после снятия симметрий и дедупликации это всё ещё практично.

Дополнительные материалы

  1. Условие: https://projecteuler.net/problem=300
  2. Самоизбегающее блуждание: https://ru.wikipedia.org/wiki/Самоизбегающее_блуждание
  3. HP-модель: https://en.wikipedia.org/wiki/HP_model

ملخص المسألة

لكل سلسلة HP ثنائية بطول \(n\)، نضع السلسلة على الشبكة المربعة على شكل مشي ذاتيّ التجنب، ثم نأخذ أكبر عدد ممكن من تماسّات H-H عبر جميع الطيّات، وبعد ذلك نحسب المتوسط على جميع \(2^n\) سلاسل. الكود يعالج الحالة

$$n=15.$$

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

1. الطيّات هي self-avoiding walks

الطيّة تمثل مسارًا

$$p_0,p_1,\dots,p_{n-1}\in\mathbb Z^2$$

بخطوات وحدية ومن دون زيارة نفس الخلية مرتين. ولأن الإزاحة والدوران لا يغيران عدد التماسات، فإن التنفيذ يثبت

$$p_0=(0,0),\qquad p_1=(1,0),$$

ثم يجري DFS على الخطوات \(n-2\) الباقية فقط.

2. المهم فعليًا هو خريطة التماس

في مرحلة التقييم، لا نحتاج الشكل الهندسي الكامل، بل فقط معرفة أي الأزواج غير المتجاورة على السلسلة أصبحت متجاورة على الشبكة. إذا كان \(i<j\) و\(j\ge i+2\)، فنعرّف

$$C_{ij}=1 \iff |p_i-p_j|_1=1.$$

وهذا يعطي contact map يخزنها الكود كقناع بتات. قد تعطي طيّات مختلفة الخريطة نفسها، وعندها تكون متكافئة تمامًا لكل سلاسل HP. لذلك تُزال الخرائط المكررة بالتجزئة.

البرنامج يجد فقط

$$41$$

خريطة تماس مميزة عند \(n=8\)، و

$$12495$$

خريطة عند \(n=15\).

3. لماذا تُضاف أزواج H-H المتتالية بشكل منفصل؟

إذا كان الموضعان \(i\) و\(i+1\) كلاهما H، فهما متجاوران دائمًا على طول السلسلة نفسها مهما كانت الطيّة. إذن هذه المساهمة لا تعتمد على خريطة التماس أصلًا. لقناع HP المسمى \(h\in\{0,1\}^n\)، تكون هذه المساهمة الثابتة

$$\operatorname{popcount}(h\ \&\ (h\gg 1)).$$

ولهذا تُكتب الدرجة على الصورة

$$\text{total}(h,C)=\text{nonconsecutive\_contacts}(h,C)+\operatorname{popcount}(h\ \&\ (h\gg 1)).$$

4. تقييم خريطة واحدة باستعمال subset DP

بعد تثبيت خريطة تماس \(C\)، يبني الكود أقنعة جوار \(N_C(i)\). وللقناع \(m\)، نعرّف \(\text{score}_C(m)\) على أنه عدد حواف التماس التي يكون طرفاها من النوع H.

بدلًا من فحص كل الحواف من جديد لكل قناع، يستخدم البرنامج العلاقة

$$\text{score}_C(m)=\text{score}_C(m\setminus\{i\})+\bigl|N_C(i)\cap(m\setminus\{i\})\bigr|,$$

حيث \(i\) هو أقل بت مرفوع في \(m\). الفكرة أن \(i\) إذا أُضيف أخيرًا، فإن التماسات الجديدة بالضبط هي تماساته مع المواضع H الموجودة مسبقًا. وهكذا تُحسب كل حافة مرة واحدة فقط.

5. أفضل طيّة لكل سلسلة HP

لنعرّف

$$B(m)=\max_C \text{score}_C(m).$$

المصفوفة best[mask] في الكود تحتفظ بهذا الحد الأقصى عبر جميع الخرائط المميزة. وعندئذ تكون الدرجة المثلى الكاملة للقناع \(m\) هي

$$B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1)).$$

6. المتوسط الدقيق على جميع \(2^n\) سلاسل

البسط النهائي هو

$$\sum_{m=0}^{2^n-1}\left(B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1))\right).$$

وبما أن كل سلسلة HP متساوية الاحتمال، فإن القيمة المطلوبة هي

$$\frac{1}{2^n}\sum_{m=0}^{2^n-1}\left(B(m)+\operatorname{popcount}(m\ \&\ (m\gg 1))\right).$$

ولأن المقام قوة للعدد \(2\)، فإن النتيجة تُكتب بدقة تامة كعدد عشري عبر القسمة الطويلة.

7. نقاط التحقق

قيمة التحقق المنشورة هي

$$n=8 \quad \Longrightarrow \quad \frac{850}{256}=3.3203125.$$

أما الحل الكامل عند \(n=15\) فيعطي

$$8.0540771484375.$$

كيف يعمل الكود

تقوم الدالة dfs(...) بتعداد كل self-avoiding walks بعد تثبيت الحافة الأولى. ثم تحول add_current_contact_map() كل طية مكتملة إلى مفتاح بتات مضغوط وتضعه في مجموعة. بعد ذلك تتحول كل خريطة فريدة إلى أقنعة جوار، وتُستخدم subset DP لحساب \(\text{score}_C(m)\) لكل الأقنعة. تحتفظ best بأفضل قيمة عبر جميع الخرائط، ثم يضاف الحد الثابت الخاص بأزواج H-H المتتالية ويؤخذ المتوسط.

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

إذا كان عدد خرائط التماس المميزة هو \(M\)، فإن مرحلة التقييم تكلف

$$O(M\cdot 2^n)$$

زمنًا، بثوابت صغيرة لأن التحديثات تعتمد على عمليات بتات فقط. أما الذاكرة فهي

$$O(M+2^n).$$

وأثقل جزء تجميعي هو تعداد self-avoiding walks، لكنه يبقى عمليًا عند \(n=15\) بعد كسر التماثل وإزالة الخرائط المكررة.

مراجع إضافية

  1. صفحة المسألة: https://projecteuler.net/problem=300
  2. المشي ذاتي التجنب: https://en.wikipedia.org/wiki/Self-avoiding_walk
  3. نموذج HP: https://en.wikipedia.org/wiki/HP_model