Problem Summary

Start with the single-node tree \(T_1\). For each \(m\ge 2\), form \(T_m\) by making \(m\) the new root and reattaching the chain obtained from \(T_{m-1}\) by starting at the old root and repeatedly following the greatest child. Let \(S(n,k)\) be the sum of the labels on the unique path from node \(k\) to the root \(n\) in \(T_n\).

The implementation must evaluate this path sum for the very large input \((n,k)=\left(10^{17},9^{17}\right)\). The crucial observation is that the recursive tree rule has a direct binary description, so the tree never has to be built explicitly.

Mathematical Approach

The recursive definition looks structural, but the actual parent relation turns out to depend only on powers of two. Once that relation is identified, the path from \(k\) to \(n\) becomes a simple process of clearing the leading bit of the remaining gap.

Step 1: Identify the chain moved at each insertion

An induction on the recursive construction shows that, just before node \(m+1\) is inserted, the chain lifted from the old root is

$$m,\ m-1,\ m-3,\ m-7,\ \dots,\ m+1-2^j,$$

for every power \(2^j\le m\).

Equivalently, when a new root \(x\) is created, its direct children are exactly

$$x-1,\ x-2,\ x-4,\ x-8,\dots.$$

So the recursive tree is secretly organized by offsets that are powers of two.

Step 2: Derive the final parent of any node

A node \(y\) is attached to a later node \(x\) exactly when \(x-y\) is a power of two. Since later insertions may reattach \(y\) again, its parent in the final tree \(T_n\) is the largest such \(x\) with \(x\le n\).

Therefore, for every \(y<n\),

$$\operatorname{parent}_n(y)=y+2^{\lfloor \log_2(n-y)\rfloor}.$$

This is the key greedy rule: from the current node, move upward by the largest power of two that still fits in the remaining distance to \(n\).

Step 3: Rewrite the climb through the remaining gap

Let \(x_0=k\), and define the remaining gap by

$$r_t=n-x_t.$$

Applying the parent formula gives

$$x_{t+1}=x_t+2^{\lfloor \log_2 r_t\rfloor}, \qquad r_{t+1}=r_t-2^{\lfloor \log_2 r_t\rfloor}.$$

Thus each step removes the most significant set bit of the current gap. When the gap becomes \(0\), the climb has reached \(n\).

Step 4: Express the whole path from the binary expansion of \(n-k\)

Write the difference as

$$n-k=\sum_{i=1}^{s}2^{e_i}, \qquad e_1>e_2>\cdots>e_s\ge 0.$$

Because the algorithm removes one leading bit at a time, the visited nodes are exactly

$$x_j=k+\sum_{i=1}^{j}2^{e_i}\qquad (0\le j\le s),$$

where the empty sum for \(j=0\) is \(0\).

Therefore the path sum is

$$S(n,k)=\sum_{j=0}^{s}x_j=(s+1)k+\sum_{i=1}^{s}(s-i+1)2^{e_i}.$$

So the tree problem is equivalent to a weighted sum of the set bits of \(n-k\).

Step 5: Worked example

Take \((n,k)=(10,3)\). Then

$$n-k=7=2^2+2^1+2^0.$$

The remaining gaps are

$$7\to 3\to 1\to 0,$$

so the visited nodes are

$$3\to 7\to 9\to 10.$$

Hence

$$S(10,3)=3+7+9+10=29.$$

This matches the small checkpoint used by the implementation and shows concretely that each move clears one binary digit of the difference.

How the Code Works

The C++, Python, and Java implementations all use the same fast loop. They keep the current node and the running sum, compute the largest power of two not exceeding the remaining gap \(n-\text{current}\), jump upward by that amount, and add the new node to the sum.

No explicit tree nodes, adjacency lists, or recursive traversal are needed for the real input. One implementation also includes tiny cross-checks on small values, confirming that the binary parent rule agrees with the recursive construction.

Complexity Analysis

Let \(s\) be the number of \(1\)-bits in the binary expansion of \(n-k\). Each iteration removes exactly one of those bits, so the running time is \(O(s)\) and the memory usage is \(O(1)\). Since \(s\le \lfloor \log_2(n-k)\rfloor+1\), the method is also \(O(\log n)\) in the worst case.

Footnotes and References

  1. Project Euler problem page: https://projecteuler.net/problem=872
  2. Powers of two: Wikipedia — Power of two
  3. Binary numbers: Wikipedia — Binary number
  4. Hamming weight: Wikipedia — Hamming weight
  5. Tree theory basics: Wikipedia — Tree (graph theory)

Problemzusammenfassung

Man beginnt mit dem Ein-Knoten-Baum \(T_1\). Für jedes \(m\ge 2\) entsteht \(T_m\), indem \(m\) zur neuen Wurzel gemacht wird und die Kette aus \(T_{m-1}\), die man von der alten Wurzel aus durch wiederholtes Folgen des größten Kindes erhält, direkt unter \(m\) umgehängt wird. Sei \(S(n,k)\) die Summe der Knotenwerte auf dem eindeutigen Pfad von \(k\) zur Wurzel \(n\) in \(T_n\).

Die Implementierung muss diese Pfadsumme für den sehr großen Eingabefall \((n,k)=\left(10^{17},9^{17}\right)\) berechnen. Die entscheidende Einsicht ist, dass sich die rekursive Baumregel in eine reine Binärbeschreibung übersetzen lässt, sodass der Baum nie explizit erzeugt werden muss.

Mathematischer Ansatz

Die Rekursion sieht zunächst strukturell aus, doch die Elternbeziehung hängt letztlich nur von Zweierpotenzen ab. Sobald diese Beziehung klar ist, wird der Aufstieg von \(k\) nach \(n\) zu einem Prozess, der in jeder Runde das führende gesetzte Bit der Restdifferenz entfernt.

Schritt 1: Die bei jedem Einfügen verschobene Kette bestimmen

Eine Induktion über die rekursive Konstruktion zeigt, dass unmittelbar vor dem Einfügen des Knotens \(m+1\) die von der alten Wurzel gelöste Kette die Form

$$m,\ m-1,\ m-3,\ m-7,\ \dots,\ m+1-2^j$$

hat, und zwar für jede Zweierpotenz \(2^j\le m\).

Äquivalent dazu hat eine neue Wurzel \(x\) als direkte Kinder genau die Knoten

$$x-1,\ x-2,\ x-4,\ x-8,\dots.$$

Damit steckt hinter der Rekursion in Wahrheit eine feste Struktur aus Zweierpotenz-Abständen.

Schritt 2: Die endgültige Elternfunktion herleiten

Ein Knoten \(y\) wird genau dann unter einen späteren Knoten \(x\) gehängt, wenn \(x-y\) eine Zweierpotenz ist. Spätere Einfügungen können \(y\) erneut umhängen, daher ist sein Vater im endgültigen Baum \(T_n\) der größte solche Wert \(x\) mit \(x\le n\).

Also gilt für jedes \(y<n\)

$$\operatorname{parent}_n(y)=y+2^{\lfloor \log_2(n-y)\rfloor}.$$

Genau das ist die zentrale greedy-Regel: Vom aktuellen Knoten springt man um die größte Zweierpotenz nach oben, die noch in die Restlücke bis \(n\) passt.

Schritt 3: Den Aufstieg über die Restlücke formulieren

Setze \(x_0=k\) und definiere die Restlücke durch

$$r_t=n-x_t.$$

Dann liefert die Elternformel

$$x_{t+1}=x_t+2^{\lfloor \log_2 r_t\rfloor}, \qquad r_{t+1}=r_t-2^{\lfloor \log_2 r_t\rfloor}.$$

Jeder Schritt entfernt also das höchstwertige gesetzte Bit der aktuellen Lücke. Sobald die Lücke \(0\) wird, ist \(n\) erreicht.

Schritt 4: Den gesamten Pfad aus der Binärdarstellung von \(n-k\) beschreiben

Schreibe die Differenz als

$$n-k=\sum_{i=1}^{s}2^{e_i}, \qquad e_1>e_2>\cdots>e_s\ge 0.$$

Da der Algorithmus immer genau ein führendes Bit entfernt, sind die besuchten Knoten genau

$$x_j=k+\sum_{i=1}^{j}2^{e_i}\qquad (0\le j\le s),$$

wobei die leere Summe für \(j=0\) gleich \(0\) ist.

Damit ergibt sich für die Pfadsumme

$$S(n,k)=\sum_{j=0}^{s}x_j=(s+1)k+\sum_{i=1}^{s}(s-i+1)2^{e_i}.$$

Die Baumaufgabe ist also äquivalent zu einer gewichteten Summe der gesetzten Bits von \(n-k\).

Schritt 5: Durchgerechnetes Beispiel

Nehmen wir \((n,k)=(10,3)\). Dann ist

$$n-k=7=2^2+2^1+2^0.$$

Die Restlücken verlaufen als

$$7\to 3\to 1\to 0,$$

also lautet der Pfad

$$3\to 7\to 9\to 10.$$

Folglich

$$S(10,3)=3+7+9+10=29.$$

Das stimmt mit dem kleinen Kontrollwert der Implementierung überein und zeigt anschaulich, dass jeder Sprung genau ein Binärbit der Differenz entfernt.

Wie der Code arbeitet

Die C++-, Python- und Java-Implementierungen verwenden alle dieselbe schnelle Schleife. Sie halten den aktuellen Knoten und die laufende Summe, bestimmen die größte Zweierpotenz, die die Restlücke \(n-\text{current}\) nicht überschreitet, springen um genau diesen Betrag nach oben und addieren den neuen Knoten zur Summe.

Für die eigentliche Eingabe sind weder explizite Baumknoten noch Adjazenzlisten noch rekursive Traversierungen nötig. Eine Implementierung enthält zusätzlich kleine Gegenprüfungen für winzige Werte, damit die bitbasierte Elternregel mit der rekursiven Konstruktion abgeglichen werden kann.

Komplexitätsanalyse

Sei \(s\) die Anzahl der Eins-Bits in der Binärdarstellung von \(n-k\). Jede Schleifenrunde entfernt genau eines dieser Bits, also beträgt die Laufzeit \(O(s)\) und der Speicherverbrauch \(O(1)\). Da \(s\le \lfloor \log_2(n-k)\rfloor+1\) gilt, ist das Verfahren im Worst Case auch \(O(\log n)\).

Fußnoten und Quellen

  1. Project-Euler-Problemseite: https://projecteuler.net/problem=872
  2. Zweierpotenzen: Wikipedia — Zweierpotenz
  3. Binärsystem: Wikipedia — Binärsystem
  4. Hamming-Gewicht: Wikipedia — Hamming-Gewicht
  5. Baum in der Graphentheorie: Wikipedia — Baum (Graphentheorie)

Problem Özeti

Başlangıçta tek düğümlü \(T_1\) ağacı vardır. Her \(m\ge 2\) için \(T_m\), \(m\) yeni kök yapılacak şekilde kurulur; eski kökten başlayıp her adımda en büyük çocuğu izleyerek elde edilen zincir koparılır ve doğrudan \(m\)'in altına bağlanır. \(S(n,k)\), \(T_n\) ağacında \(k\) düğümünden kök \(n\)'e giden tek yol üzerindeki etiketlerin toplamı olsun.

Uygulama bu toplamı çok büyük \((n,k)=\left(10^{17},9^{17}\right)\) girdisi için hesaplar. Kritik nokta, rekürsif ağaç tanımının doğrudan ikili sayı sistemiyle ifade edilebilmesidir; böylece ağacı açıkça kurmaya gerek kalmaz.

Matematiksel Yaklaşım

İlk bakışta yapı tamamen ağaç rekürsiyonu gibi görünür, fakat gerçek ebeveyn ilişkisi yalnızca iki kuvvetleriyle belirlenir. Bu ilişki ortaya çıkarılınca \(k\)'dan \(n\)'e tırmanış, kalan farkın en yüksek bitini her adımda silen basit bir sürece dönüşür.

Adım 1: Her eklemede taşınan zinciri belirle

Rekürsif kurulum üzerine yapılacak bir tümevarım, \(m+1\) düğümü eklenmeden hemen önce eski kökten alınan zincirin

$$m,\ m-1,\ m-3,\ m-7,\ \dots,\ m+1-2^j$$

şeklinde olduğunu gösterir; burada \(2^j\le m\) olan tüm iki kuvvetleri kullanılır.

Buna eşdeğer olarak, yeni kök \(x\)'in doğrudan çocukları tam olarak

$$x-1,\ x-2,\ x-4,\ x-8,\dots$$

olur. Yani rekürsif ağaç aslında iki kuvvetleri cinsinden sabit bir ofset düzeni taşır.

Adım 2: Son ebeveyn formülünü çıkar

Bir \(y\) düğümü, ancak \(x-y\) bir iki kuvveti olduğunda daha büyük bir \(x\) düğümünün altına bağlanır. Daha sonraki eklemeler \(y\)'yi tekrar başka yere taşıyabildiğinden, nihai \(T_n\) ağacındaki ebeveyni \(x\le n\) koşulunu sağlayan en büyük böyle \(x\) olur.

Dolayısıyla her \(y<n\) için

$$\operatorname{parent}_n(y)=y+2^{\lfloor \log_2(n-y)\rfloor}.$$

Bu, çözümün greedy kuralıdır: mevcut düğümden \(n\)'e kalan mesafeye sığan en büyük iki kuvveti kadar yukarı zıpla.

Adım 3: Tırmanışı kalan fark üzerinden yaz

\(x_0=k\) olsun ve kalan farkı

$$r_t=n-x_t$$

ile tanımlayalım. Ebeveyn formülü şu güncellemeyi verir:

$$x_{t+1}=x_t+2^{\lfloor \log_2 r_t\rfloor}, \qquad r_{t+1}=r_t-2^{\lfloor \log_2 r_t\rfloor}.$$

Böylece her adım, kalan farkın en yüksek değerli \(1\) bitini temizler. Fark \(0\) olduğunda \(n\)'e ulaşılmış olur.

Adım 4: Tüm yolu \(n-k\)'nin ikili açılımından çıkar

Farkı

$$n-k=\sum_{i=1}^{s}2^{e_i}, \qquad e_1>e_2>\cdots>e_s\ge 0$$

şeklinde yazalım.

Algoritma her adımda bir lider biti kaldırdığı için ziyaret edilen düğümler tam olarak

$$x_j=k+\sum_{i=1}^{j}2^{e_i}\qquad (0\le j\le s)$$

olur; burada \(j=0\) için toplam boştur ve \(0\)'dır.

Buna göre yol toplamı

$$S(n,k)=\sum_{j=0}^{s}x_j=(s+1)k+\sum_{i=1}^{s}(s-i+1)2^{e_i}$$

şeklini alır. Yani soru, \(n-k\)'nin \(1\) bitlerinin ağırlıklı toplamına indirgenir.

Adım 5: Çözümlü örnek

\((n,k)=(10,3)\) alalım. Bu durumda

$$n-k=7=2^2+2^1+2^0.$$

Kalan farklar

$$7\to 3\to 1\to 0$$

şeklindedir; dolayısıyla ziyaret edilen düğümler

$$3\to 7\to 9\to 10$$

olur. Sonuç olarak

$$S(10,3)=3+7+9+10=29.$$

Bu değer uygulamadaki küçük kontrol örneğiyle aynıdır ve her sıçramanın farkın bir ikili basamağını sildiğini açıkça gösterir.

Kod Nasıl Çalışıyor

C++, Python ve Java uygulamalarının üçü de aynı hızlı döngüyü kullanır. Mevcut düğüm ve kısmi toplam tutulur; \(n-\text{current}\) farkını aşmayan en büyük iki kuvveti hesaplanır; bu kadar yukarı çıkılır ve yeni düğüm toplamın içine eklenir.

Gerçek girdi için açık ağaç düğümleri, komşuluk listeleri veya özyinelemeli dolaşım hiç gerekmez. Uygulamalardan biri ayrıca küçük değerlerde bu bit tabanlı ebeveyn kuralını rekürsif yapıyla karşılaştıran kısa doğrulamalar da içerir.

Karmaşıklık Analizi

\(s\), \(n-k\)'nin ikili gösterimindeki \(1\) bit sayısı olsun. Her iterasyon bu bitlerden tam bir tanesini yok eder; dolayısıyla süre \(O(s)\), bellek ise \(O(1)\)'dir. Ayrıca \(s\le \lfloor \log_2(n-k)\rfloor+1\) olduğundan en kötü durumda yöntem \(O(\log n)\) olarak da ifade edilebilir.

Dipnotlar ve Kaynakça

  1. Project Euler problem sayfası: https://projecteuler.net/problem=872
  2. İki kuvvetleri: Wikipedia — 2'nin kuvvetleri
  3. İkili sayı sistemi: Wikipedia — İkili sayı sistemi
  4. Hamming ağırlığı: Wikipedia — Hamming weight
  5. Ağaç kuramı: Wikipedia — Ağaç (graf kuramı)

Resumen del Problema

Se empieza con el árbol \(T_1\), que contiene solo el nodo \(1\). Para cada \(m\ge 2\), el árbol \(T_m\) se obtiene haciendo de \(m\) la nueva raíz y recolgando debajo de ella la cadena que, en \(T_{m-1}\), se obtiene al comenzar en la antigua raíz y seguir siempre el hijo de mayor valor. Sea \(S(n,k)\) la suma de las etiquetas en el camino único desde \(k\) hasta la raíz \(n\) en \(T_n\).

La implementación debe calcular esta suma para la entrada enorme \((n,k)=\left(10^{17},9^{17}\right)\). La observación decisiva es que la regla recursiva del árbol tiene una descripción binaria directa, así que nunca hace falta construir el árbol completo.

Enfoque Matemático

A primera vista la definición es puramente estructural, pero la relación de parentesco final depende solo de potencias de dos. Una vez encontrada esa relación, el ascenso desde \(k\) hasta \(n\) se convierte en un proceso de borrar el bit más significativo de la diferencia restante.

Paso 1: Identificar la cadena que se mueve en cada inserción

Una inducción sobre la construcción recursiva muestra que, justo antes de insertar el nodo \(m+1\), la cadena extraída desde la antigua raíz es

$$m,\ m-1,\ m-3,\ m-7,\ \dots,\ m+1-2^j,$$

para toda potencia de dos \(2^j\le m\).

De manera equivalente, cuando aparece una nueva raíz \(x\), sus hijos directos son exactamente

$$x-1,\ x-2,\ x-4,\ x-8,\dots.$$

Por tanto, la estructura recursiva está gobernada en realidad por desplazamientos que son potencias de dos.

Paso 2: Obtener el padre final de un nodo

Un nodo \(y\) queda colgado de un nodo posterior \(x\) exactamente cuando \(x-y\) es una potencia de dos. Como inserciones aún más tardías pueden volver a recolgar \(y\), su padre en el árbol final \(T_n\) es el mayor de esos valores \(x\) con \(x\le n\).

Así, para todo \(y<n\),

$$\operatorname{parent}_n(y)=y+2^{\lfloor \log_2(n-y)\rfloor}.$$

Esa es la regla greedy esencial: desde el nodo actual se sube con la mayor potencia de dos que todavía cabe en la distancia que falta hasta \(n\).

Paso 3: Reescribir la subida en función de la brecha restante

Tomemos \(x_0=k\) y definamos la brecha restante por

$$r_t=n-x_t.$$

Aplicando la fórmula del padre se obtiene

$$x_{t+1}=x_t+2^{\lfloor \log_2 r_t\rfloor}, \qquad r_{t+1}=r_t-2^{\lfloor \log_2 r_t\rfloor}.$$

Cada paso elimina, por tanto, el bit más significativo encendido de la brecha actual. Cuando la brecha llega a \(0\), ya se alcanzó \(n\).

Paso 4: Describir todo el camino con la expansión binaria de \(n-k\)

Escribamos

$$n-k=\sum_{i=1}^{s}2^{e_i}, \qquad e_1>e_2>\cdots>e_s\ge 0.$$

Como el algoritmo borra un bit líder en cada iteración, los nodos visitados son exactamente

$$x_j=k+\sum_{i=1}^{j}2^{e_i}\qquad (0\le j\le s),$$

donde la suma vacía para \(j=0\) vale \(0\).

Por consiguiente, la suma del camino es

$$S(n,k)=\sum_{j=0}^{s}x_j=(s+1)k+\sum_{i=1}^{s}(s-i+1)2^{e_i}.$$

La cuestión del árbol recursivo se transforma así en una suma ponderada de los bits encendidos de \(n-k\).

Paso 5: Ejemplo trabajado

Tomemos \((n,k)=(10,3)\). Entonces

$$n-k=7=2^2+2^1+2^0.$$

Las brechas restantes son

$$7\to 3\to 1\to 0,$$

de modo que el camino es

$$3\to 7\to 9\to 10.$$

Por tanto,

$$S(10,3)=3+7+9+10=29.$$

Esto coincide con la pequeña comprobación usada por la implementación y muestra de forma concreta que cada salto elimina un dígito binario de la diferencia.

Cómo funciona el código

Las implementaciones en C++, Python y Java usan exactamente el mismo bucle rápido. Mantienen el nodo actual y la suma acumulada, calculan la mayor potencia de dos que no supera la brecha \(n-\text{current}\), suben esa cantidad y añaden el nuevo nodo a la suma.

Para la entrada real no hacen falta nodos explícitos, listas de adyacencia ni recorridos recursivos. Una de las implementaciones también contiene pequeñas verificaciones para valores muy reducidos, con el fin de confirmar que la regla binaria del padre coincide con la construcción recursiva.

Complejidad

Sea \(s\) el número de bits iguales a \(1\) en la expansión binaria de \(n-k\). Cada iteración elimina exactamente uno de esos bits, así que el tiempo es \(O(s)\) y la memoria es \(O(1)\). Como además \(s\le \lfloor \log_2(n-k)\rfloor+1\), el método también es \(O(\log n)\) en el peor caso.

Notas y Referencias

  1. Página del problema: https://projecteuler.net/problem=872
  2. Potencias de dos: Wikipedia — Potencia de dos
  3. Sistema binario: Wikipedia — Sistema binario
  4. Peso de Hamming: Wikipedia — Peso de Hamming
  5. Árbol en teoría de grafos: Wikipedia — Árbol (teoría de grafos)

问题概述

先从只含一个节点的树 \(T_1\) 开始。对每个 \(m\ge 2\),把 \(m\) 作为新根,并把 \(T_{m-1}\) 中从旧根出发、每一步都选择“编号最大的孩子”所得到的那条链整条摘下来,直接挂到 \(m\) 下面,于是得到 \(T_m\)。记 \(S(n,k)\) 为 \(T_n\) 中从节点 \(k\) 到根节点 \(n\) 的唯一简单路径上所有标签之和。

实现需要处理的实际输入是 \((n,k)=\left(10^{17},9^{17}\right)\),规模极大,绝不可能真的把整棵树建出来。真正有效的做法,是把这条递归建树规则转写成一个只涉及二进制差值的父节点公式。

数学方法

这道题表面上是树结构题,但核心并不在于维护树,而在于识别“谁会成为谁的父亲”。一旦父节点关系被写成闭式,路径求和就会退化成对 \(n-k\) 的二进制展开逐步清除最高位的过程。

步骤 1:找出每次插入时被整体上提的那条链

对递归构造做归纳,可以得到一个非常整齐的结论:在插入节点 \(m+1\) 之前,从旧根被取出的那条链恰好是

$$m,\ m-1,\ m-3,\ m-7,\ \dots,\ m+1-2^j,$$

其中 \(2^j\le m\) 的所有二次幂都会出现。

把这个结论换一种说法就是:当新根 \(x\) 建立时,它的直接孩子正好是

$$x-1,\ x-2,\ x-4,\ x-8,\dots.$$

于是递归树并不是随意变化的结构,而是始终受“与根的差是二次幂”这一规则支配。

步骤 2:推出最终树中的父节点公式

对于某个节点 \(y\),当且仅当 \(x-y\) 是二次幂时,它会在插入 \(x\) 的那一步被挂到 \(x\) 下面。但以后如果又出现更大的这样的 \(x\),它还会再次被改挂。因此在最终树 \(T_n\) 中,\(y\) 的父节点就是所有满足 \(x-y\) 为二次幂且 \(x\le n\) 的值中最大的那个。

因此,对任意 \(y<n\),都有

$$\operatorname{parent}_n(y)=y+2^{\lfloor \log_2(n-y)\rfloor}.$$

这正是快速算法的贪心规则:从当前节点往上跳,跳的距离就是“不超过剩余距离 \(n-y\) 的最大二次幂”。

步骤 3:把向上爬的过程改写成“消掉剩余差值的最高位”

设起点 \(x_0=k\),并把当前到根的剩余差值记为

$$r_t=n-x_t.$$

把上面的父节点公式代入,就得到

$$x_{t+1}=x_t+2^{\lfloor \log_2 r_t\rfloor}, \qquad r_{t+1}=r_t-2^{\lfloor \log_2 r_t\rfloor}.$$

也就是说,每一步都会删去当前剩余差值二进制表示里的最高位 \(1\)。当剩余差值变成 \(0\) 时,说明已经到达根 \(n\)。

步骤 4:用 \(n-k\) 的二进制展开写出整条路径

把差值写成

$$n-k=\sum_{i=1}^{s}2^{e_i}, \qquad e_1>e_2>\cdots>e_s\ge 0.$$

由于算法每次删去一个最高位,所以访问到的节点依次就是

$$x_j=k+\sum_{i=1}^{j}2^{e_i}\qquad (0\le j\le s),$$

其中 \(j=0\) 时空和为 \(0\)。

于是路径和可以直接写成

$$S(n,k)=\sum_{j=0}^{s}x_j=(s+1)k+\sum_{i=1}^{s}(s-i+1)2^{e_i}.$$

这说明题目真正要求的,并不是复杂的树遍历,而是对 \(n-k\) 中所有置位比特做一次带权求和。

步骤 5:完整示例

取 \((n,k)=(10,3)\)。这时

$$n-k=7=2^2+2^1+2^0.$$

剩余差值的变化为

$$7\to 3\to 1\to 0,$$

所以实际经过的节点是

$$3\to 7\to 9\to 10.$$

因此

$$S(10,3)=3+7+9+10=29.$$

这个结果与实现中的小规模校验完全一致,也直观展示了“每跳一次就删掉一个二进制最高位”到底是什么意思。

代码如何工作

C++、Python 和 Java 三个实现使用的是同一个快速循环。它们维护当前节点和累计和,计算不超过剩余差值 \(n-\text{current}\) 的最大二次幂,向上跳这么远,然后把新到达的节点值加入总和。

对真实输入来说,完全不需要显式维护树节点、孩子列表或递归遍历。只有一个实现额外保留了非常小范围的交叉验证,用于确认这个二进制父节点规则和递归建树定义确实给出同一条祖先链。

复杂度分析

设 \(s\) 为 \(n-k\) 的二进制表示中 \(1\) 的个数。每次循环恰好去掉其中一个 \(1\),因此时间复杂度是 \(O(s)\),空间复杂度是 \(O(1)\)。又因为 \(s\le \lfloor \log_2(n-k)\rfloor+1\),所以最坏情况下也可以写成 \(O(\log n)\)。

参考资料

  1. Project Euler 题目页面: https://projecteuler.net/problem=872
  2. 二次幂: Wikipedia — 2 的幂
  3. 二进制: Wikipedia — 二进制
  4. 汉明重量: Wikipedia — 汉明重量
  5. 图论中的树: Wikipedia — 树(图论)

Краткое описание задачи

Начинаем с дерева \(T_1\), состоящего из единственной вершины \(1\). Для каждого \(m\ge 2\) дерево \(T_m\) строится так: вершина \(m\) становится новым корнем, а цепочка, полученная в \(T_{m-1}\) из старого корня при последовательном выборе наибольшего ребенка, целиком перевешивается прямо под \(m\). Обозначим через \(S(n,k)\) сумму меток на единственном пути от вершины \(k\) к корню \(n\) в дереве \(T_n\).

Нужно вычислить эту сумму для очень большого входа \((n,k)=\left(10^{17},9^{17}\right)\). Главное наблюдение состоит в том, что рекурсивное правило построения дерева имеет прямую двоичную интерпретацию, поэтому реальное дерево строить не требуется.

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

На вид задача кажется чисто структурной, но окончательная функция родителя определяется только степенями двойки. Как только эта функция записана явно, подъем от \(k\) к \(n\) превращается в процесс последовательного удаления старшего установленного бита из оставшейся разности.

Шаг 1: Найти цепочку, которая переносится при каждом добавлении новой вершины

Индукция по рекурсивному построению показывает, что непосредственно перед вставкой вершины \(m+1\) от старого корня отделяется цепочка вида

$$m,\ m-1,\ m-3,\ m-7,\ \dots,\ m+1-2^j,$$

где берутся все степени двойки \(2^j\le m\).

Эквивалентно этому, у нового корня \(x\) прямыми детьми оказываются ровно вершины

$$x-1,\ x-2,\ x-4,\ x-8,\dots.$$

Значит, скрытая геометрия рекурсивного дерева полностью управляется сдвигами на степени двойки.

Шаг 2: Вывести окончательную формулу для родителя вершины

Вершина \(y\) подвешивается под более позднюю вершину \(x\) тогда и только тогда, когда \(x-y\) является степенью двойки. Поскольку еще более поздние вставки могут снова переподвесить \(y\), в конечном дереве \(T_n\) ее родителем будет максимальное такое \(x\), не превосходящее \(n\).

Следовательно, для любого \(y<n\)

$$\operatorname{parent}_n(y)=y+2^{\lfloor \log_2(n-y)\rfloor}.$$

Это и есть главный greedy-переход: из текущей вершины нужно подниматься на наибольшую степень двойки, которая еще помещается в оставшийся зазор до \(n\).

Шаг 3: Переписать подъем через оставшуюся разность

Пусть \(x_0=k\), а оставшаяся разность равна

$$r_t=n-x_t.$$

Тогда из формулы родителя сразу следует

$$x_{t+1}=x_t+2^{\lfloor \log_2 r_t\rfloor}, \qquad r_{t+1}=r_t-2^{\lfloor \log_2 r_t\rfloor}.$$

Иными словами, на каждом шаге удаляется старший установленный бит текущей разности. Когда разность становится равной \(0\), путь дошел до вершины \(n\).

Шаг 4: Описать весь путь через двоичное разложение \(n-k\)

Запишем

$$n-k=\sum_{i=1}^{s}2^{e_i}, \qquad e_1>e_2>\cdots>e_s\ge 0.$$

Так как алгоритм на каждом ходе убирает один ведущий бит, посещаемые вершины имеют вид

$$x_j=k+\sum_{i=1}^{j}2^{e_i}\qquad (0\le j\le s),$$

где при \(j=0\) пустая сумма равна \(0\).

Отсюда получаем формулу для суммы на пути:

$$S(n,k)=\sum_{j=0}^{s}x_j=(s+1)k+\sum_{i=1}^{s}(s-i+1)2^{e_i}.$$

Таким образом, задача о рекурсивном дереве сводится к взвешенной сумме единичных битов числа \(n-k\).

Шаг 5: Разобранный пример

Возьмем \((n,k)=(10,3)\). Тогда

$$n-k=7=2^2+2^1+2^0.$$

Оставшиеся разности меняются так:

$$7\to 3\to 1\to 0,$$

поэтому путь состоит из вершин

$$3\to 7\to 9\to 10.$$

Следовательно,

$$S(10,3)=3+7+9+10=29.$$

Это совпадает с маленькой контрольной проверкой в реализации и хорошо показывает, что каждый переход уничтожает ровно один двоичный разряд разности.

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

Реализации на C++, Python и Java используют один и тот же быстрый цикл. Они хранят текущую вершину и накопленную сумму, находят наибольшую степень двойки, не превосходящую разность \(n-\text{current}\), переходят вверх на этот шаг и добавляют новую вершину к ответу.

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

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

Пусть \(s\) обозначает число единичных битов в двоичной записи \(n-k\). Каждая итерация убирает ровно один такой бит, поэтому время работы равно \(O(s)\), а память составляет \(O(1)\). Так как \(s\le \lfloor \log_2(n-k)\rfloor+1\), в худшем случае это также \(O(\log n)\).

Сноски и источники

  1. Страница задачи Project Euler: https://projecteuler.net/problem=872
  2. Степени двойки: Wikipedia — Степени двойки
  3. Двоичная система счисления: Wikipedia — Двоичная система счисления
  4. Вес Хэмминга: Wikipedia — Вес Хэмминга
  5. Дерево в теории графов: Wikipedia — Дерево (теория графов)

ملخص المسألة

نبدأ بالشجرة \(T_1\) التي تحتوي على العقدة \(1\) فقط. ولكل \(m\ge 2\)، تُبنى الشجرة \(T_m\) بجعل \(m\) هو الجذر الجديد، ثم تؤخذ السلسلة التي نحصل عليها في \(T_{m-1}\) إذا بدأنا من الجذر القديم واتبعنا في كل مرة الابن الأكبر قيمة، وتُعلَّق هذه السلسلة مباشرة تحت \(m\). ولتكن \(S(n,k)\) هي مجموع القيم على المسار الوحيد من العقدة \(k\) إلى الجذر \(n\) في الشجرة \(T_n\).

المطلوب هو حساب هذا المجموع للحالة الكبيرة جدًا \((n,k)=\left(10^{17},9^{17}\right)\). الفكرة الحاسمة هي أن قاعدة بناء الشجرة تملك وصفًا ثنائيًا مباشرًا، ولذلك لا حاجة إلى بناء الشجرة كاملةً بشكل صريح.

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

تبدو المسألة في ظاهرها مسألة بنية شجرية تكرارية، لكن علاقة الأبوة النهائية تعتمد فقط على قوى العدد \(2\). وما إن تُكتب هذه العلاقة بصيغة مباشرة حتى يصبح الصعود من \(k\) إلى \(n\) مجرد عملية حذف للبت الأعلى من الفارق المتبقي في كل خطوة.

الخطوة 1: تحديد السلسلة التي تُنقل عند كل إدراج

باستقراء البناء التكراري يتبين أنه قبل إدراج العقدة \(m+1\) مباشرة، تكون السلسلة المأخوذة من الجذر القديم هي

$$m,\ m-1,\ m-3,\ m-7,\ \dots,\ m+1-2^j,$$

وذلك لكل قوة للعدد \(2\) تحقق \(2^j\le m\).

وبصياغة مكافئة، فإن الجذر الجديد \(x\) تكون له أبناء مباشرون هم بالضبط

$$x-1,\ x-2,\ x-4,\ x-8,\dots.$$

إذن البنية التكرارية مخفية داخل نمط بسيط تحكمه فروق تساوي قوى للعدد \(2\).

الخطوة 2: اشتقاق صيغة الأب النهائي لأي عقدة

ترتبط العقدة \(y\) بعقدة لاحقة \(x\) إذا وفقط إذا كان \(x-y\) قوة للعدد \(2\). وبما أن إدراجات أحدث قد تعيد ربط \(y\) مرة أخرى، فإن أباها في الشجرة النهائية \(T_n\) هو أكبر قيمة من هذا النوع لا تتجاوز \(n\).

ولذلك، لكل \(y<n\)، لدينا

$$\operatorname{parent}_n(y)=y+2^{\lfloor \log_2(n-y)\rfloor}.$$

وهذه هي القاعدة الجشعة الأساسية: من العقدة الحالية نصعد بمقدار أكبر قوة للعدد \(2\) لا تزال ضمن المسافة المتبقية حتى \(n\).

الخطوة 3: إعادة كتابة الصعود بدلالة الفارق المتبقي

لنأخذ \(x_0=k\)، ولنعرّف الفارق المتبقي بـ

$$r_t=n-x_t.$$

عندها تعطي صيغة الأب التحديث التالي:

$$x_{t+1}=x_t+2^{\lfloor \log_2 r_t\rfloor}, \qquad r_{t+1}=r_t-2^{\lfloor \log_2 r_t\rfloor}.$$

أي أن كل خطوة تمحو أعلى بت مضبوط في الفارق الحالي. وعندما يصبح الفارق \(0\)، نكون قد وصلنا إلى \(n\).

الخطوة 4: وصف المسار كاملًا من التوسع الثنائي لـ \(n-k\)

لنكتب الفرق على الصورة

$$n-k=\sum_{i=1}^{s}2^{e_i}, \qquad e_1>e_2>\cdots>e_s\ge 0.$$

وبما أن الخوارزمية تحذف في كل مرة بتًا قياديًا واحدًا، فإن العقد المزارة هي تمامًا

$$x_j=k+\sum_{i=1}^{j}2^{e_i}\qquad (0\le j\le s),$$

حيث تكون المجموعة الفارغة عند \(j=0\) مساوية لـ \(0\).

ومن ثم فإن مجموع المسار يساوي

$$S(n,k)=\sum_{j=0}^{s}x_j=(s+1)k+\sum_{i=1}^{s}(s-i+1)2^{e_i}.$$

وهكذا تتحول مسألة الشجرة التكرارية إلى مجموع موزون للبتات المساوية لـ \(1\) في العدد \(n-k\).

الخطوة 5: مثال محلول

خذ \((n,k)=(10,3)\). عندئذ

$$n-k=7=2^2+2^1+2^0.$$

ويتغير الفارق المتبقي وفق

$$7\to 3\to 1\to 0,$$

لذلك يكون المسار

$$3\to 7\to 9\to 10.$$

ومن ثم

$$S(10,3)=3+7+9+10=29.$$

وهذا يطابق نقطة الفحص الصغيرة المستخدمة في التنفيذ، ويُظهر بوضوح أن كل قفزة تزيل رقمًا ثنائيًا واحدًا من الفرق.

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

تنفذ تطبيقات C++ وPython وJava الحلقة السريعة نفسها. فهي تحتفظ بالعقدة الحالية وبالمجموع الجاري، ثم تحسب أكبر قوة للعدد \(2\) لا تتجاوز الفارق \(n-\text{current}\)، وتصعد بهذا المقدار، ثم تضيف العقدة الجديدة إلى المجموع.

لا حاجة مع المدخل الحقيقي إلى عقد شجرية صريحة أو قوائم أبناء أو اجتياز تكراري. كما أن أحد التطبيقات يتضمن اختبارات صغيرة جدًا للمقارنة بين هذه القاعدة الثنائية وبين البناء التكراري نفسه.

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

ليكن \(s\) هو عدد البتات التي تساوي \(1\) في التمثيل الثنائي للعدد \(n-k\). كل دورة تحذف بتًا واحدًا بالضبط، ولذلك يكون الزمن \(O(s)\) والذاكرة \(O(1)\). وبما أن \(s\le \lfloor \log_2(n-k)\rfloor+1\)، فيمكن أيضًا كتابة التعقيد في أسوأ حالة على صورة \(O(\log n)\).

الهوامش والمراجع

  1. صفحة مسألة Project Euler: https://projecteuler.net/problem=872
  2. قوى العدد 2: Wikipedia — قوى العدد 2
  3. النظام الثنائي: Wikipedia — نظام ثنائي
  4. وزن هامنج: Wikipedia — Hamming weight
  5. الشجرة في نظرية الرسوم: Wikipedia — شجرة (نظرية الرسوم)