Problem Summary

In a hexagonal orchard of order \(n\), every tree lies in one of six congruent triangular sectors around the center. A tree is hidden if another tree lies on the same ray from the center and is closer to the origin. The solution does not compare every pair of trees. Instead it turns visibility into a coprimality condition and then reduces the whole problem to evaluating the summatory Euler totient function.

Mathematical Approach

Step 1: Reduce the Hexagon to One Sector

Because the orchard has sixfold rotational symmetry, it is enough to count hidden trees in one \(60^\circ\) sector and multiply by 6 at the end. In one sector, row \(r\) contains exactly \(r\) trees for \(r=1,2,\dots,n\). Therefore the total number of trees in one sector is the triangular number

$$T(n)=\sum_{r=1}^{n} r=\frac{n(n+1)}{2}.$$

The center itself is only the observation point, so it is not included in this count.

Step 2: Visibility Becomes a GCD Test

Index the trees on row \(r\) by positions \(x=1,2,\dots,r\). In the lattice underlying one hexagonal sector, the tree \((x,r)\) lies on the same ray as \((x/d,r/d)\) whenever \(d\) divides both \(x\) and \(r\). Hence

$$\text{tree }(x,r)\text{ is visible from the center} \iff \gcd(x,r)=1.$$

If \(\gcd(x,r)=d>1\), then \((x/d,r/d)\) is a nearer tree on the same line of sight, so \((x,r)\) is hidden. If \(\gcd(x,r)=1\), there is no intermediate orchard point on that ray. The number of visible trees on row \(r\) is therefore

$$\varphi(r)=\#\{1\le x\le r:\gcd(x,r)=1\},$$

which is Euler's totient function. The number of hidden trees on row \(r\) is then

$$r-\varphi(r).$$

Step 3: Sum Hidden Trees Row by Row

Summing over all rows of one sector gives

$$H_{\mathrm{sector}}(n)=\sum_{r=1}^{n}\bigl(r-\varphi(r)\bigr)=\frac{n(n+1)}{2}-\sum_{r=1}^{n}\varphi(r).$$

Introduce the summatory totient function

$$\Phi(n)=\sum_{k=1}^{n}\varphi(k).$$

Then the hidden-tree count for the entire orchard is

$$\boxed{H(n)=6\,H_{\mathrm{sector}}(n)=6\left(\frac{n(n+1)}{2}-\Phi(n)\right).}$$

This closed form is exactly what the C++, Python, and Java programs evaluate.

Step 4: Derive the Recursive Formula for \(\Phi(n)\)

The code does not build all totients up to \(n\) with a full sieve. Instead it uses the classical divisor identity

$$\sum_{d\mid m}\varphi(d)=m.$$

Summing this identity for \(m=1,2,\dots,n\) yields

$$\sum_{m=1}^{n} m=\sum_{m=1}^{n}\sum_{d\mid m}\varphi(d)=\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor.$$

Since \(\sum_{m=1}^{n} m=\frac{n(n+1)}{2}\), we obtain

$$\frac{n(n+1)}{2}=\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor.$$

Now rewrite the right-hand side by expanding each floor term as a count of prefixes:

$$\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor=\sum_{k=1}^{n}\Phi\left(\left\lfloor\frac{n}{k}\right\rfloor\right).$$

Separating the \(k=1\) term gives the recurrence used in all three implementations:

$$\Phi(n)=\frac{n(n+1)}{2}-\sum_{k=2}^{n}\Phi\left(\left\lfloor\frac{n}{k}\right\rfloor\right).$$

Step 5: Quotient Grouping and Memoization

The recurrence still appears to contain \(n-1\) terms, but many consecutive indices produce the same quotient \(q=\left\lfloor n/k\right\rfloor\). If a block starts at left, its last index is

$$\text{right}=\left\lfloor\frac{n}{q}\right\rfloor.$$

So the entire block

$$k=\text{left},\text{left}+1,\dots,\text{right}$$

contributes

$$(\text{right}-\text{left}+1)\,\Phi(q).$$

This is exactly why the code keeps left, computes quotient = n / left, derives right = n / quotient, subtracts one whole block, and then jumps directly to right + 1. Memoization stores previously computed values of \(\Phi\), so each distinct recursive argument is evaluated only once.

Worked Example: \(n=5\)

For the first five rows we have

$$\varphi(1)=1,\quad \varphi(2)=1,\quad \varphi(3)=2,\quad \varphi(4)=2,\quad \varphi(5)=4,$$

so

$$\Phi(5)=1+1+2+2+4=10.$$

Also

$$T(5)=\frac{5\cdot 6}{2}=15,$$

hence

$$H_{\mathrm{sector}}(5)=15-10=5,\qquad H(5)=6\cdot 5=30.$$

This matches the checkpoint in the C++ file. The same source also checks \(H(10)=138\) and \(H(1000)=1177848\), plus a brute-force cross-check at order 200.

How the Code Works

All three solution files implement the same formula. They initialize a memo table with \(\Phi(0)=0\) and \(\Phi(1)=1\), compute the summatory totient recursively, evaluate the triangular number \(\frac{n(n+1)}{2}\), subtract the visible count in one sector, and multiply the result by 6.

The C++ version encapsulates the recursion in a TotientSummatory class backed by std::unordered_map and uses unsigned __int128 for intermediate arithmetic safety. The Python version uses a nested function with a dictionary, and the Java version uses a static HashMap<Long, Long>. The mathematical logic is identical in all three languages.

Complexity Analysis

A naive method would examine every tree in every row and test visibility with repeated \(\gcd\) calls, which is quadratic in the orchard order. The implemented method avoids that completely. For a fixed argument \(m\), the floor-division grouping visits only the distinct quotients \(\left\lfloor m/k\right\rfloor\), and there are \(O(\sqrt{m})\) of them. If \(S\) is the set of distinct arguments reached by memoized recursion, then the total running time is

$$O\left(\sum_{m\in S}\sqrt{m}\right),$$

with memory usage \(O(|S|)\) for the memo table. In practice this is dramatically smaller than computing and storing every totient value up to \(10^8\).

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=351
  2. Euler's totient function: Wikipedia — Euler's totient function
  3. Summatory totient function: Wikipedia — Summatory totient function
  4. Visible points and coprimality: Wikipedia — Visible point
  5. Triangular numbers: Wikipedia — Triangular number

Problemzusammenfassung

In einem hexagonalen Obstgarten der Ordnung \(n\) liegt jeder Baum in einem von sechs kongruenten dreieckigen Sektoren um das Zentrum. Ein Baum ist verdeckt, wenn auf demselben Strahl vom Mittelpunkt ein anderer Baum näher am Ursprung liegt. Die Lösung vergleicht nicht alle Baumpaare direkt, sondern übersetzt Sichtbarkeit in eine Teilerfremdheitsbedingung und reduziert die Aufgabe anschließend auf die Summenfunktion der eulerschen Phi-Funktion.

Mathematischer Ansatz

Schritt 1: Das Hexagon auf einen Sektor reduzieren

Wegen der sechsfachen Rotationssymmetrie genügt es, die verdeckten Bäume in einem \(60^\circ\)-Sektor zu zählen und das Ergebnis am Ende mit 6 zu multiplizieren. In einem Sektor enthält Zeile \(r\) genau \(r\) Bäume für \(r=1,2,\dots,n\). Damit ist die Gesamtzahl der Bäume in einem Sektor die Dreieckszahl

$$T(n)=\sum_{r=1}^{n} r=\frac{n(n+1)}{2}.$$

Das Zentrum selbst wird nicht mitgezählt; es ist nur der Beobachtungspunkt.

Schritt 2: Sichtbarkeit wird zu einem ggT-Test

Nummeriere die Bäume in Zeile \(r\) mit Positionen \(x=1,2,\dots,r\). Im schiefen Gitter eines hexagonalen Sektors liegt der Baum \((x,r)\) genau dann auf demselben Strahl wie \((x/d,r/d)\), wenn \(d\) sowohl \(x\) als auch \(r\) teilt. Daher gilt

$$\text{der Baum }(x,r)\text{ ist vom Zentrum sichtbar} \iff \gcd(x,r)=1.$$

Ist \(\gcd(x,r)=d>1\), dann liegt \((x/d,r/d)\) näher am Zentrum und blockiert die Sicht. Ist \(\gcd(x,r)=1\), gibt es keinen Zwischenpunkt auf diesem Strahl. Die Zahl der sichtbaren Bäume in Zeile \(r\) ist also

$$\varphi(r)=\#\{1\le x\le r:\gcd(x,r)=1\},$$

also die eulersche Phi-Funktion. Die Zahl der verdeckten Bäume in dieser Zeile ist damit

$$r-\varphi(r).$$

Schritt 3: Verdeckte Bäume zeilenweise aufsummieren

Für einen Sektor erhält man

$$H_{\mathrm{sector}}(n)=\sum_{r=1}^{n}\bigl(r-\varphi(r)\bigr)=\frac{n(n+1)}{2}-\sum_{r=1}^{n}\varphi(r).$$

Definiere nun die Summenfunktion

$$\Phi(n)=\sum_{k=1}^{n}\varphi(k).$$

Dann gilt für den gesamten Obstgarten

$$\boxed{H(n)=6\,H_{\mathrm{sector}}(n)=6\left(\frac{n(n+1)}{2}-\Phi(n)\right).}$$

Genau diese Formel werten die C++-, Python- und Java-Programme aus.

Schritt 4: Herleitung der Rekursion für \(\Phi(n)\)

Der Code berechnet nicht alle Totienten bis \(n\) mit einem vollständigen Sieb. Stattdessen verwendet er die klassische Teileridentität

$$\sum_{d\mid m}\varphi(d)=m.$$

Summiert man diese Gleichung für \(m=1,2,\dots,n\), so folgt

$$\sum_{m=1}^{n} m=\sum_{m=1}^{n}\sum_{d\mid m}\varphi(d)=\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor.$$

Mit \(\sum_{m=1}^{n}m=\frac{n(n+1)}{2}\) ergibt sich

$$\frac{n(n+1)}{2}=\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor.$$

Nun schreibt man die rechte Seite als Summe über Präfixsummen um:

$$\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor=\sum_{k=1}^{n}\Phi\left(\left\lfloor\frac{n}{k}\right\rfloor\right).$$

Trennt man den Term \(k=1\) ab, erhält man die im Code verwendete Rekursion:

$$\Phi(n)=\frac{n(n+1)}{2}-\sum_{k=2}^{n}\Phi\left(\left\lfloor\frac{n}{k}\right\rfloor\right).$$

Schritt 5: Quotientenblöcke und Memoisierung

Die Rekursion scheint noch immer \(n-1\) Summanden zu besitzen, aber viele aufeinanderfolgende Indizes erzeugen denselben Quotienten \(q=\left\lfloor n/k\right\rfloor\). Beginnt ein solcher Block bei left, dann endet er bei

$$\text{right}=\left\lfloor\frac{n}{q}\right\rfloor.$$

Der gesamte Block

$$k=\text{left},\text{left}+1,\dots,\text{right}$$

liefert also den Beitrag

$$(\text{right}-\text{left}+1)\,\Phi(q).$$

Genau deshalb hält der Code einen linken Rand fest, berechnet quotient = n / left, bestimmt daraus right = n / quotient, subtrahiert den kompletten Block und springt dann direkt zu right + 1. Durch Memoisierung wird jeder unterschiedliche Rekursionswert nur einmal berechnet.

Beispiel: \(n=5\)

Für die ersten fünf Zeilen gilt

$$\varphi(1)=1,\quad \varphi(2)=1,\quad \varphi(3)=2,\quad \varphi(4)=2,\quad \varphi(5)=4,$$

also

$$\Phi(5)=1+1+2+2+4=10.$$

Außerdem ist

$$T(5)=\frac{5\cdot 6}{2}=15,$$

und damit

$$H_{\mathrm{sector}}(5)=15-10=5,\qquad H(5)=6\cdot 5=30.$$

Das stimmt mit dem Prüfwert in der C++-Datei überein. Dieselbe Quelle testet außerdem \(H(10)=138\) und \(H(1000)=1177848\) sowie einen Brute-Force-Abgleich bei Ordnung 200.

Wie der Code arbeitet

Alle drei Lösungsdateien setzen dieselbe Formel um. Sie initialisieren eine Memo-Tabelle mit \(\Phi(0)=0\) und \(\Phi(1)=1\), berechnen die summierte Phi-Funktion rekursiv, bilden die Dreieckszahl \(\frac{n(n+1)}{2}\), ziehen die sichtbaren Punkte in einem Sektor ab und multiplizieren das Resultat mit 6.

Die C++-Version kapselt die Rekursion in einer TotientSummatory-Klasse mit std::unordered_map und verwendet unsigned __int128 für sichere Zwischenwerte. Die Python-Version nutzt eine geschachtelte Funktion mit Dictionary, die Java-Version eine statische HashMap<Long, Long>. Die zugrunde liegende Mathematik ist in allen drei Sprachen identisch.

Komplexitätsanalyse

Ein naiver Ansatz würde jeden Baum in jeder Zeile betrachten und Sichtbarkeit mit vielen \(\gcd\)-Aufrufen prüfen; das ist quadratisch in der Gartenordnung. Die implementierte Methode vermeidet das vollständig. Für ein festes Argument \(m\) besucht die Blockzerlegung nur die verschiedenen Quotienten \(\left\lfloor m/k\right\rfloor\), und davon gibt es \(O(\sqrt{m})\). Ist \(S\) die Menge der durch Memoisierung erreichten Argumente, dann beträgt die Gesamtlaufzeit

$$O\left(\sum_{m\in S}\sqrt{m}\right),$$

und der Speicherverbrauch ist \(O(|S|)\) für die Memo-Tabelle. In der Praxis ist das erheblich kleiner als ein vollständiges Totient-Sieb bis \(10^8\).

Fußnoten und Quellen

  1. Problemseite: https://projecteuler.net/problem=351
  2. Eulersche Phi-Funktion: Wikipedia — Eulersche Phi-Funktion
  3. Summierte Phi-Funktion: Wikipedia — Summatory totient function
  4. Sichtbare Gitterpunkte: Wikipedia — Visible point
  5. Dreieckszahlen: Wikipedia — Dreieckszahl

Problem Özeti

Merkezi çevreleyen altı eş üçgensel bölgeden oluşan bir altıgen bahçede, bazı ağaçlar aynı doğrultuda daha yakın bir ağaç tarafından gizlenir. Buradaki amaç, düzenin derecesi \(n\) iken gizli kalan ağaçların sayısını bulmaktır. Çözüm tüm ağaç çiftlerini tek tek karşılaştırmaz; görünürlüğü aralarında asallık koşuluna çevirir ve problemi Euler phi fonksiyonunun kümülatif toplamına indirger.

Matematiksel Yaklaşım

Adım 1: Altıgeni tek bir sektöre indirgeme

Şekil altı kat dönel simetriye sahip olduğu için önce yalnızca bir \(60^\circ\) sektör sayılır, sonra sonuç 6 ile çarpılır. Tek bir sektörde \(r=1,2,\dots,n\) için \(r\). satırda tam olarak \(r\) ağaç vardır. Böylece bir sektördeki toplam ağaç sayısı üçgensel sayı olur:

$$T(n)=\sum_{r=1}^{n} r=\frac{n(n+1)}{2}.$$

Merkez bu toplamın içinde yer almaz; yalnızca bakış noktasıdır.

Adım 2: Görünürlük bir EBOB testidir

\(r\). satırdaki ağaçları \(x=1,2,\dots,r\) diye numaralandıralım. Altıgensel sektörün altında yatan eğik örgüde \((x,r)\) noktası, \(d\) hem \(x\)'i hem \(r\)'yi bölüyorsa \((x/d,r/d)\) ile aynı ışın üzerinde bulunur. Bu yüzden

$$\text{\((x,r)\) ağacı merkezden görünür} \iff \gcd(x,r)=1.$$

Eğer \(\gcd(x,r)=d>1\) ise \((x/d,r/d)\) aynı görüş doğrusu üzerinde daha yakın bir ağaçtır ve \((x,r)\) gizlidir. Eğer \(\gcd(x,r)=1\) ise arada başka bahçe noktası yoktur. Dolayısıyla \(r\). satırdaki görünür ağaç sayısı

$$\varphi(r)=\#\{1\le x\le r:\gcd(x,r)=1\}$$

olur. Bu da Euler phi fonksiyonudur. O halde o satırdaki gizli ağaç sayısı

$$r-\varphi(r).$$

Adım 3: Satırlar boyunca toplam almak

Tek sektördeki gizli ağaç sayısı

$$H_{\mathrm{sector}}(n)=\sum_{r=1}^{n}\bigl(r-\varphi(r)\bigr)=\frac{n(n+1)}{2}-\sum_{r=1}^{n}\varphi(r).$$

Şimdi kümülatif phi toplamını

$$\Phi(n)=\sum_{k=1}^{n}\varphi(k)$$

diye tanımlarsak tüm bahçe için

$$\boxed{H(n)=6\,H_{\mathrm{sector}}(n)=6\left(\frac{n(n+1)}{2}-\Phi(n)\right).}$$

C++, Python ve Java dosyalarının doğrudan hesapladığı ifade tam olarak budur.

Adım 4: \(\Phi(n)\) için özyineli formülün türetilmesi

Kod, \(1\)'den \(n\)'ye kadar tüm phi değerlerini doğrusal bir elekle üretmez. Bunun yerine şu klasik özdeşliği kullanır:

$$\sum_{d\mid m}\varphi(d)=m.$$

Bu özdeşliği \(m=1,2,\dots,n\) için toplarsak

$$\sum_{m=1}^{n} m=\sum_{m=1}^{n}\sum_{d\mid m}\varphi(d)=\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor$$

elde edilir. Sol taraf üçgensel toplam olduğundan

$$\frac{n(n+1)}{2}=\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor.$$

Sağ tarafı önek toplamları cinsinden yeniden yazarsak

$$\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor=\sum_{k=1}^{n}\Phi\left(\left\lfloor\frac{n}{k}\right\rfloor\right)$$

olur. \(k=1\) terimini ayırınca uygulamadaki özyineleme çıkar:

$$\Phi(n)=\frac{n(n+1)}{2}-\sum_{k=2}^{n}\Phi\left(\left\lfloor\frac{n}{k}\right\rfloor\right).$$

Adım 5: Bölüm blokları ve memoization

Bu toplam ilk bakışta hâlâ uzun görünür, fakat birçok ardışık \(k\) aynı \(q=\left\lfloor n/k\right\rfloor\) bölümünü üretir. Bir blok left konumunda başlıyorsa son indeksi

$$\text{right}=\left\lfloor\frac{n}{q}\right\rfloor$$

olur. Böylece

$$k=\text{left},\text{left}+1,\dots,\text{right}$$

bloğunun katkısı tek seferde

$$(\text{right}-\text{left}+1)\,\Phi(q)$$

şeklinde çıkarılır. Çözümlerde görülen left, quotient ve right değişkenleri tam olarak bu bloklamayı uygular. Daha önce bulunan \(\Phi\) değerleri saklandığı için her farklı argüman yalnızca bir kez hesaplanır.

Örnek: \(n=5\)

İlk beş satır için

$$\varphi(1)=1,\quad \varphi(2)=1,\quad \varphi(3)=2,\quad \varphi(4)=2,\quad \varphi(5)=4,$$

dolayısıyla

$$\Phi(5)=1+1+2+2+4=10.$$

Ayrıca

$$T(5)=\frac{5\cdot 6}{2}=15,$$

olduğu için

$$H_{\mathrm{sector}}(5)=15-10=5,\qquad H(5)=6\cdot 5=30.$$

Bu değer C++ kaynağındaki kontrol noktasıyla aynıdır. Aynı dosya ayrıca \(H(10)=138\), \(H(1000)=1177848\) ve mertebe 200 için kaba kuvvet doğrulaması yapar.

Kodun Çalışma Mantığı

Üç çözüm dosyasının yapısı aynıdır. Önce \(\Phi(0)=0\) ve \(\Phi(1)=1\) başlangıç değerleriyle bir bellek tablosu kurulur. Ardından kümülatif phi fonksiyonu özyineli biçimde hesaplanır, üçgensel sayı \(\frac{n(n+1)}{2}\) bulunur, tek sektördeki görünür ağaç sayısı çıkarılır ve sonuç 6 ile çarpılır.

C++ sürümü bunu TotientSummatory sınıfı ve std::unordered_map ile uygular; ara çarpımlarda güvenlik için unsigned __int128 kullanır. Python sürümü bir iç fonksiyon ve sözlük, Java sürümü ise statik bir HashMap<Long, Long> kullanır. Matematiksel yöntem üçünde de aynıdır.

Karmaşıklık Analizi

Naif yaklaşım, her satırdaki her ağacı inceleyip görünürlüğü tekrar tekrar \(\gcd\) ile test ederdi; bu da bahçe derecesine göre karesel büyür. Gerçek çözüm bunu tamamen atlar. Sabit bir \(m\) için bölme bloklaması yalnızca farklı \(\left\lfloor m/k\right\rfloor\) değerlerini ziyaret eder; bunların sayısı \(O(\sqrt{m})\)'dir. Memoization ile ulaşılan argüman kümesine \(S\) dersek toplam süre

$$O\left(\sum_{m\in S}\sqrt{m}\right)$$

olur. Bellek kullanımı memo tablosu için \(O(|S|)\)'dir. Pratikte bu yaklaşım \(10^8\)'e kadar tüm phi değerlerini saklamaktan çok daha verimlidir.

Dipnotlar ve Kaynakça

  1. Problem sayfası: https://projecteuler.net/problem=351
  2. Euler phi fonksiyonu: Wikipedia — Euler phi fonksiyonu
  3. Summatory totient function: Wikipedia — Summatory totient function
  4. Görünür kafes noktaları: Wikipedia — Visible point
  5. Üçgensel sayılar: Wikipedia — Üçgensel sayı

Resumen del Problema

En un huerto hexagonal de orden \(n\), cada árbol pertenece a uno de seis sectores triangulares congruentes alrededor del centro. Un árbol está oculto si otro árbol se encuentra sobre el mismo rayo desde el centro y está más cerca del origen. La solución no compara todos los pares de árboles. En su lugar, transforma la visibilidad en una condición de coprimalidad y reduce el conteo a la suma acumulada de la función phi de Euler.

Enfoque Matemático

Paso 1: Reducir el hexágono a un solo sector

Por la simetría rotacional de orden 6, basta contar los árboles ocultos en un sector de \(60^\circ\) y multiplicar por 6 al final. En un sector, la fila \(r\) contiene exactamente \(r\) árboles para \(r=1,2,\dots,n\). Por tanto, el número total de árboles en un sector es el número triangular

$$T(n)=\sum_{r=1}^{n} r=\frac{n(n+1)}{2}.$$

El centro no se incluye en esta cantidad; solo es el punto de observación.

Paso 2: La visibilidad equivale a una prueba de MCD

Numeremos los árboles de la fila \(r\) por posiciones \(x=1,2,\dots,r\). En la red oblicua que modela un sector hexagonal, el árbol \((x,r)\) está sobre el mismo rayo que \((x/d,r/d)\) cuando \(d\) divide a la vez a \(x\) y a \(r\). Por eso

$$\text{el árbol }(x,r)\text{ es visible desde el centro} \iff \gcd(x,r)=1.$$

Si \(\gcd(x,r)=d>1\), entonces \((x/d,r/d)\) es un árbol más cercano sobre la misma línea de visión y \((x,r)\) queda oculto. Si \(\gcd(x,r)=1\), no existe ningún punto intermedio. El número de árboles visibles en la fila \(r\) es entonces

$$\varphi(r)=\#\{1\le x\le r:\gcd(x,r)=1\},$$

es decir, la función phi de Euler. Por consiguiente, los árboles ocultos en esa fila son

$$r-\varphi(r).$$

Paso 3: Sumar fila por fila

En un solo sector obtenemos

$$H_{\mathrm{sector}}(n)=\sum_{r=1}^{n}\bigl(r-\varphi(r)\bigr)=\frac{n(n+1)}{2}-\sum_{r=1}^{n}\varphi(r).$$

Definimos ahora la función phi acumulada

$$\Phi(n)=\sum_{k=1}^{n}\varphi(k).$$

Entonces el número de árboles ocultos en todo el huerto es

$$\boxed{H(n)=6\,H_{\mathrm{sector}}(n)=6\left(\frac{n(n+1)}{2}-\Phi(n)\right).}$$

Esta es exactamente la fórmula que calculan los programas en C++, Python y Java.

Paso 4: Derivar la recurrencia de \(\Phi(n)\)

El código no construye todos los valores de \(\varphi\) hasta \(n\) con un cribado completo. En cambio usa la identidad clásica

$$\sum_{d\mid m}\varphi(d)=m.$$

Al sumar esa identidad para \(m=1,2,\dots,n\), se obtiene

$$\sum_{m=1}^{n} m=\sum_{m=1}^{n}\sum_{d\mid m}\varphi(d)=\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor.$$

Como \(\sum_{m=1}^{n}m=\frac{n(n+1)}{2}\), queda

$$\frac{n(n+1)}{2}=\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor.$$

Ahora reescribimos el lado derecho en términos de sumas prefijo:

$$\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor=\sum_{k=1}^{n}\Phi\left(\left\lfloor\frac{n}{k}\right\rfloor\right).$$

Separando el término \(k=1\), aparece la recurrencia usada por las tres implementaciones:

$$\Phi(n)=\frac{n(n+1)}{2}-\sum_{k=2}^{n}\Phi\left(\left\lfloor\frac{n}{k}\right\rfloor\right).$$

Paso 5: Agrupación por cociente y memoización

Aunque la suma parece larga, muchos valores consecutivos de \(k\) producen el mismo cociente \(q=\left\lfloor n/k\right\rfloor\). Si un bloque empieza en left, su último índice es

$$\text{right}=\left\lfloor\frac{n}{q}\right\rfloor.$$

Por tanto, el bloque completo

$$k=\text{left},\text{left}+1,\dots,\text{right}$$

aporta

$$(\text{right}-\text{left}+1)\,\Phi(q).$$

Eso explica exactamente el bucle de las soluciones: fijan left, calculan quotient = n / left, obtienen right = n / quotient, restan un bloque entero y saltan a right + 1. La memoización evita recalcular valores ya conocidos de \(\Phi\).

Ejemplo: \(n=5\)

Para las cinco primeras filas,

$$\varphi(1)=1,\quad \varphi(2)=1,\quad \varphi(3)=2,\quad \varphi(4)=2,\quad \varphi(5)=4,$$

de modo que

$$\Phi(5)=1+1+2+2+4=10.$$

Además,

$$T(5)=\frac{5\cdot 6}{2}=15,$$

y por ello

$$H_{\mathrm{sector}}(5)=15-10=5,\qquad H(5)=6\cdot 5=30.$$

Este valor coincide con el punto de control del archivo C++. La misma fuente verifica también \(H(10)=138\), \(H(1000)=1177848\) y un contraste por fuerza bruta en orden 200.

Cómo Funciona el Código

Los tres archivos de solución siguen la misma estructura. Inicializan una tabla de memoria con \(\Phi(0)=0\) y \(\Phi(1)=1\), calculan recursivamente la suma acumulada de totientes, forman el número triangular \(\frac{n(n+1)}{2}\), restan los puntos visibles de un sector y multiplican por 6.

La versión en C++ encapsula la recurrencia en una clase TotientSummatory apoyada en std::unordered_map y usa unsigned __int128 para evitar problemas en valores intermedios. La versión en Python usa una función interna con diccionario, y la versión en Java una HashMap<Long, Long> estática. La lógica matemática es la misma en los tres lenguajes.

Análisis de Complejidad

Un método ingenuo revisaría cada árbol de cada fila y comprobaría visibilidad con llamadas repetidas a \(\gcd\), lo cual es cuadrático en el orden del huerto. El método implementado lo evita por completo. Para un argumento fijo \(m\), la descomposición por divisiones enteras visita solo los distintos cocientes \(\left\lfloor m/k\right\rfloor\), y su cantidad es \(O(\sqrt{m})\). Si \(S\) es el conjunto de argumentos distintos alcanzados por la recursión memorizada, el tiempo total es

$$O\left(\sum_{m\in S}\sqrt{m}\right),$$

mientras que la memoria es \(O(|S|)\) para la tabla de memoización. En la práctica esto es muchísimo menor que calcular y almacenar todos los totientes hasta \(10^8\).

Notas y Referencias

  1. Página del problema: https://projecteuler.net/problem=351
  2. Función phi de Euler: Wikipedia — Función phi de Euler
  3. Summatory totient function: Wikipedia — Summatory totient function
  4. Puntos visibles y coprimalidad: Wikipedia — Visible point
  5. Números triangulares: Wikipedia — Número triangular

问题概述

在阶数为 \(n\) 的六边形果园中,所有树点可以按对称性分成六个全等的三角扇区。若某棵树与中心共线, 并且在同一条射线上有另一棵更靠近中心的树,那么这棵树就是被遮挡的。程序并不枚举所有树对,而是先把 可见性转化为互素条件,再把总计数化成欧拉函数前缀和的问题。

数学方法

步骤 1:先只看一个扇区

由于图形具有 6 重旋转对称,只需求出一个 \(60^\circ\) 扇区中的遮挡树数,最后再乘以 6。单个扇区中, 第 \(r\) 行恰好有 \(r\) 棵树,其中 \(r=1,2,\dots,n\)。因此一个扇区的树总数是三角数

$$T(n)=\sum_{r=1}^{n} r=\frac{n(n+1)}{2}.$$

中心点只是观察位置,不计入树的数量。

步骤 2:可见性等价于 \(\gcd\) 为 1

把第 \(r\) 行上的树编号为 \(x=1,2,\dots,r\)。在六边形扇区对应的斜格点模型中,只要 \(d\) 同时整除 \(x\) 与 \(r\),点 \((x,r)\) 就与 \((x/d,r/d)\) 位于同一条从中心发出的射线上。因此

$$\text{树 }(x,r)\text{ 从中心可见} \iff \gcd(x,r)=1.$$

若 \(\gcd(x,r)=d>1\),那么 \((x/d,r/d)\) 就是同一直线方向上更近的树,\((x,r)\) 会被挡住。若 \(\gcd(x,r)=1\),则中间不存在其他格点树。于是第 \(r\) 行可见树数正好是

$$\varphi(r)=\#\{1\le x\le r:\gcd(x,r)=1\},$$

也就是欧拉函数。该行被遮挡的树数因此为

$$r-\varphi(r).$$

步骤 3:按行求和

单个扇区中的遮挡树总数为

$$H_{\mathrm{sector}}(n)=\sum_{r=1}^{n}\bigl(r-\varphi(r)\bigr)=\frac{n(n+1)}{2}-\sum_{r=1}^{n}\varphi(r).$$

定义欧拉函数前缀和

$$\Phi(n)=\sum_{k=1}^{n}\varphi(k).$$

则整个果园的答案满足

$$\boxed{H(n)=6\,H_{\mathrm{sector}}(n)=6\left(\frac{n(n+1)}{2}-\Phi(n)\right).}$$

这正是 C++、Python 与 Java 解法共同实现的公式。

步骤 4:推导 \(\Phi(n)\) 的递推式

代码并没有对 \(1\) 到 \(n\) 的所有 \(\varphi\) 做完整筛法,而是使用经典恒等式

$$\sum_{d\mid m}\varphi(d)=m.$$

把它对 \(m=1,2,\dots,n\) 求和,可得

$$\sum_{m=1}^{n} m=\sum_{m=1}^{n}\sum_{d\mid m}\varphi(d)=\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor.$$

左边就是三角和,因此

$$\frac{n(n+1)}{2}=\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor.$$

再把右侧改写成前缀和形式:

$$\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor=\sum_{k=1}^{n}\Phi\left(\left\lfloor\frac{n}{k}\right\rfloor\right).$$

把 \(k=1\) 这一项单独移到左边,就得到三份代码都在使用的递推关系:

$$\Phi(n)=\frac{n(n+1)}{2}-\sum_{k=2}^{n}\Phi\left(\left\lfloor\frac{n}{k}\right\rfloor\right).$$

步骤 5:按商分块与记忆化

虽然递推式表面上仍有很多项,但大量连续的 \(k\) 会产生相同的商 \(q=\left\lfloor n/k\right\rfloor\)。如果某个块从 left 开始,那么它的末尾位置就是

$$\text{right}=\left\lfloor\frac{n}{q}\right\rfloor.$$

于是整段

$$k=\text{left},\text{left}+1,\dots,\text{right}$$

可以一次性贡献

$$(\text{right}-\text{left}+1)\,\Phi(q).$$

这正对应解法中的做法:维护 left,计算 quotient = n / left, 再令 right = n / quotient,一次减去整个块,然后直接跳到 right + 1。 由于带有记忆化,每个不同的 \(\Phi\) 参数只会真正计算一次。

例子:\(n=5\)

前五行的欧拉函数值为

$$\varphi(1)=1,\quad \varphi(2)=1,\quad \varphi(3)=2,\quad \varphi(4)=2,\quad \varphi(5)=4,$$

因此

$$\Phi(5)=1+1+2+2+4=10.$$

同时

$$T(5)=\frac{5\cdot 6}{2}=15,$$

于是

$$H_{\mathrm{sector}}(5)=15-10=5,\qquad H(5)=6\cdot 5=30.$$

这与 C++ 源文件中的检查值一致。该文件还验证了 \(H(10)=138\)、\(H(1000)=1177848\),并在 阶数 200 时进行了暴力交叉验证。

代码实现说明

三个解法文件的结构完全一致。它们先建立 \(\Phi(0)=0\)、\(\Phi(1)=1\) 的记忆表,然后递归计算前缀和 \(\Phi(n)\),再求三角数 \(\frac{n(n+1)}{2}\),减去一个扇区中的可见树数,最后乘以 6。

C++ 版本把递推封装在 TotientSummatory 类里,底层使用 std::unordered_map,并用 unsigned __int128 保护中间运算。Python 版本使用 带字典的内部函数,Java 版本使用静态 HashMap<Long, Long>。三种语言的数学逻辑完全相同。

复杂度分析

朴素做法需要检查每一行中的每一棵树,并频繁调用 \(\gcd\) 判断是否可见,时间复杂度近似二次。实际实现 完全避免了这一点。对固定参数 \(m\),整除分块只会访问不同的 \(\left\lfloor m/k\right\rfloor\) 值, 其数量是 \(O(\sqrt{m})\)。若 \(S\) 表示记忆化递归最终访问到的所有不同参数集合,则总时间可写成

$$O\left(\sum_{m\in S}\sqrt{m}\right),$$

而内存复杂度为保存记忆表所需的 \(O(|S|)\)。在实际规模下,这比保存到 \(10^8\) 的全部欧拉函数值节省得多。

参考资料

  1. 题目页面: https://projecteuler.net/problem=351
  2. 欧拉函数: Wikipedia — 欧拉函数
  3. Summatory totient function: Wikipedia — Summatory totient function
  4. 可见格点: Wikipedia — Visible point
  5. 三角数: Wikipedia — 三角形数

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

В шестиугольном саду порядка \(n\) все деревья разбиваются на шесть одинаковых треугольных секторов вокруг центра. Дерево считается скрытым, если на том же луче из центра уже стоит другое дерево, расположенное ближе к началу координат. Решение не перебирает все пары деревьев. Вместо этого оно переводит видимость в условие взаимной простоты и сводит задачу к вычислению сумматорной функции Эйлера.

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

Шаг 1: свести шестиугольник к одному сектору

Из-за шестикратной вращательной симметрии достаточно посчитать скрытые деревья в одном секторе \(60^\circ\), а затем умножить результат на 6. В одном секторе строка \(r\) содержит ровно \(r\) деревьев для \(r=1,2,\dots,n\). Поэтому общее число деревьев в секторе равно треугольному числу

$$T(n)=\sum_{r=1}^{n} r=\frac{n(n+1)}{2}.$$

Центральная точка не входит в этот подсчет; это только точка наблюдения.

Шаг 2: видимость эквивалентна условию \(\gcd(x,r)=1\)

Пронумеруем деревья в строке \(r\) позициями \(x=1,2,\dots,r\). В косой решетке, соответствующей одному сектору, точка \((x,r)\) лежит на том же луче, что и \((x/d,r/d)\), если число \(d\) делит и \(x\), и \(r\). Поэтому

$$\text{дерево }(x,r)\text{ видно из центра} \iff \gcd(x,r)=1.$$

Если \(\gcd(x,r)=d>1\), то \((x/d,r/d)\) находится ближе на той же линии обзора, и дерево \((x,r)\) скрыто. Если \(\gcd(x,r)=1\), промежуточной точки на этом луче нет. Следовательно, число видимых деревьев в строке \(r\) равно

$$\varphi(r)=\#\{1\le x\le r:\gcd(x,r)=1\},$$

то есть функции Эйлера. Тогда число скрытых деревьев в этой строке равно

$$r-\varphi(r).$$

Шаг 3: суммирование по строкам

Для одного сектора получаем

$$H_{\mathrm{sector}}(n)=\sum_{r=1}^{n}\bigl(r-\varphi(r)\bigr)=\frac{n(n+1)}{2}-\sum_{r=1}^{n}\varphi(r).$$

Введем сумматорную функцию

$$\Phi(n)=\sum_{k=1}^{n}\varphi(k).$$

Тогда для всего сада имеем

$$\boxed{H(n)=6\,H_{\mathrm{sector}}(n)=6\left(\frac{n(n+1)}{2}-\Phi(n)\right).}$$

Именно эту формулу вычисляют программы на C++, Python и Java.

Шаг 4: вывод рекуррентной формулы для \(\Phi(n)\)

Код не строит все значения \(\varphi\) до \(n\) полным решетом. Вместо этого используется классическое тождество

$$\sum_{d\mid m}\varphi(d)=m.$$

Просуммируем его по всем \(m=1,2,\dots,n\):

$$\sum_{m=1}^{n} m=\sum_{m=1}^{n}\sum_{d\mid m}\varphi(d)=\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor.$$

Поскольку \(\sum_{m=1}^{n}m=\frac{n(n+1)}{2}\), получаем

$$\frac{n(n+1)}{2}=\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor.$$

Теперь перепишем правую часть через префиксные суммы:

$$\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor=\sum_{k=1}^{n}\Phi\left(\left\lfloor\frac{n}{k}\right\rfloor\right).$$

Выделяя слагаемое \(k=1\), получаем рекурсию, совпадающую с реализацией:

$$\Phi(n)=\frac{n(n+1)}{2}-\sum_{k=2}^{n}\Phi\left(\left\lfloor\frac{n}{k}\right\rfloor\right).$$

Шаг 5: группировка по одинаковому частному и мемоизация

Хотя сумма кажется длинной, многие подряд идущие значения \(k\) дают один и тот же частный \(q=\left\lfloor n/k\right\rfloor\). Если блок начинается в left, его последний индекс равен

$$\text{right}=\left\lfloor\frac{n}{q}\right\rfloor.$$

Значит, весь блок

$$k=\text{left},\text{left}+1,\dots,\text{right}$$

вносит вклад

$$(\text{right}-\text{left}+1)\,\Phi(q).$$

Именно поэтому код хранит левую границу, вычисляет quotient = n / left, затем right = n / quotient, вычитает весь блок сразу и переходит к right + 1. Мемоизация гарантирует, что каждое различное значение аргумента \(\Phi\) вычисляется только один раз.

Пример: \(n=5\)

Для первых пяти строк имеем

$$\varphi(1)=1,\quad \varphi(2)=1,\quad \varphi(3)=2,\quad \varphi(4)=2,\quad \varphi(5)=4,$$

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

$$\Phi(5)=1+1+2+2+4=10.$$

Кроме того,

$$T(5)=\frac{5\cdot 6}{2}=15,$$

и потому

$$H_{\mathrm{sector}}(5)=15-10=5,\qquad H(5)=6\cdot 5=30.$$

Это совпадает с контрольным значением в файле C++. Там же проверяются \(H(10)=138\), \(H(1000)=1177848\) и полный перебор для порядка 200.

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

Все три файла решения устроены одинаково. Сначала инициализируется таблица памяти значениями \(\Phi(0)=0\) и \(\Phi(1)=1\), затем рекурсивно вычисляется сумматорная функция Эйлера, строится треугольное число \(\frac{n(n+1)}{2}\), из него вычитается число видимых точек в одном секторе, после чего результат умножается на 6.

Версия на C++ оформляет рекурсию в классе TotientSummatory на базе std::unordered_map и использует unsigned __int128 для промежуточных вычислений. В Python используется вложенная функция со словарем, а в Java статическая HashMap<Long, Long>. Математическая идея во всех случаях одинаковая.

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

Наивный способ проверял бы каждое дерево в каждой строке и многократно вызывал бы \(\gcd\), что дает квадратичную зависимость от порядка сада. Реализованный алгоритм этого полностью избегает. Для фиксированного аргумента \(m\) разбиение по целочисленному делению посещает только различные значения \(\left\lfloor m/k\right\rfloor\), а их число равно \(O(\sqrt{m})\). Если \(S\) обозначает множество различных аргументов, реально достигнутых мемоизированной рекурсией, то суммарное время равно

$$O\left(\sum_{m\in S}\sqrt{m}\right),$$

а память составляет \(O(|S|)\) для хранения таблицы. На практике это несравнимо меньше, чем вычислять и хранить все значения функции Эйлера до \(10^8\).

Примечания и источники

  1. Страница задачи: https://projecteuler.net/problem=351
  2. Функция Эйлера: Wikipedia — Функция Эйлера
  3. Summatory totient function: Wikipedia — Summatory totient function
  4. Видимые точки решетки: Wikipedia — Visible point
  5. Треугольные числа: Wikipedia — Треугольное число

ملخص المسألة

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

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

الخطوة 1: اختزال السداسي إلى قطاع واحد

بسبب التناظر الدوراني من الدرجة 6 يكفي أن نعد الأشجار المحجوبة في قطاع واحد زاويته \(60^\circ\)، ثم نضرب النتيجة في 6. في هذا القطاع يحتوي الصف \(r\) على \(r\) أشجار تمامًا عندما \(r=1,2,\dots,n\). لذلك يكون عدد الأشجار في قطاع واحد هو العدد المثلثي

$$T(n)=\sum_{r=1}^{n} r=\frac{n(n+1)}{2}.$$

النقطة المركزية ليست ضمن هذا العد، فهي مجرد موضع الرصد.

الخطوة 2: الرؤية تعادل اختبار \(\gcd\)

لنرقم أشجار الصف \(r\) بالمواقع \(x=1,2,\dots,r\). في الشبكة المائلة التي تمثل قطاعًا سداسيًا، تكون النقطة \((x,r)\) على الشعاع نفسه الذي تمر به \((x/d,r/d)\) متى كان \(d\) يقسم كلًا من \(x\) و\(r\). لذلك تكون الشجرة \((x,r)\) مرئية من المركز إذا وفقط إذا كان

$$\gcd(x,r)=1.$$

إذا كان \(\gcd(x,r)=d>1\)، فإن \((x/d,r/d)\) تمثل شجرة أقرب على خط النظر نفسه، وبالتالي تكون \((x,r)\) محجوبة. وإذا كان \(\gcd(x,r)=1\)، فلا توجد نقطة وسيطة على ذلك الشعاع. ومن ثم فإن عدد الأشجار المرئية في الصف \(r\) هو

$$\varphi(r)=\#\{1\le x\le r:\gcd(x,r)=1\},$$

أي دالة أويلر. وعدد الأشجار المحجوبة في الصف نفسه يساوي

$$r-\varphi(r).$$

الخطوة 3: الجمع على جميع الصفوف

إذن عدد الأشجار المحجوبة في قطاع واحد هو

$$H_{\mathrm{sector}}(n)=\sum_{r=1}^{n}\bigl(r-\varphi(r)\bigr)=\frac{n(n+1)}{2}-\sum_{r=1}^{n}\varphi(r).$$

ونعرف الآن المجموع التراكمي لدالة أويلر على الصورة

$$\Phi(n)=\sum_{k=1}^{n}\varphi(k).$$

وعندئذ يصبح عدد الأشجار المحجوبة في البستان كله

$$\boxed{H(n)=6\,H_{\mathrm{sector}}(n)=6\left(\frac{n(n+1)}{2}-\Phi(n)\right).}$$

وهذه هي الصيغة نفسها التي تحسبها ملفات C++ وPython وJava.

الخطوة 4: اشتقاق العلاقة العودية لـ \(\Phi(n)\)

الكود لا يبني كل قيم \(\varphi\) حتى \(n\) باستخدام غربال كامل، بل يعتمد على الهوية الكلاسيكية

$$\sum_{d\mid m}\varphi(d)=m.$$

وبجمع هذه الهوية من \(m=1\) إلى \(n\) نحصل على

$$\sum_{m=1}^{n} m=\sum_{m=1}^{n}\sum_{d\mid m}\varphi(d)=\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor.$$

وبما أن \(\sum_{m=1}^{n}m=\frac{n(n+1)}{2}\)، فإن

$$\frac{n(n+1)}{2}=\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor.$$

ثم نعيد كتابة الطرف الأيمن بدلالة المجاميع الأولية:

$$\sum_{d=1}^{n}\varphi(d)\left\lfloor\frac{n}{d}\right\rfloor=\sum_{k=1}^{n}\Phi\left(\left\lfloor\frac{n}{k}\right\rfloor\right).$$

وبفصل الحد الموافق لـ \(k=1\) تظهر العلاقة العودية المستخدمة في اللغات الثلاث:

$$\Phi(n)=\frac{n(n+1)}{2}-\sum_{k=2}^{n}\Phi\left(\left\lfloor\frac{n}{k}\right\rfloor\right).$$

الخطوة 5: تجميع حدود القسمة الصحيحة مع الحفظ

قد يبدو أن العلاقة السابقة تحتوي عددًا كبيرًا من الحدود، لكن قيمًا متتالية كثيرة من \(k\) تعطي خارج القسمة نفسه \(q=\left\lfloor n/k\right\rfloor\). إذا بدأ مقطع عند left فإن آخر فهرس فيه يساوي

$$\text{right}=\left\lfloor\frac{n}{q}\right\rfloor.$$

وعليه فإن المقطع الكامل

$$k=\text{left},\text{left}+1,\dots,\text{right}$$

يسهم بمقدار

$$(\text{right}-\text{left}+1)\,\Phi(q).$$

وهذا يفسر مباشرة ما يفعله الكود: يثبت left، ثم يحسب quotient = n / left، وبعد ذلك يستنتج right = n / quotient، ويطرح كتلة كاملة دفعة واحدة، ثم يقفز إلى right + 1. وباستخدام الحفظ في الذاكرة لا يُحسب كل وسيط مختلف لـ \(\Phi\) إلا مرة واحدة.

مثال: \(n=5\)

في الصفوف الخمسة الأولى نجد

$$\varphi(1)=1,\quad \varphi(2)=1,\quad \varphi(3)=2,\quad \varphi(4)=2,\quad \varphi(5)=4,$$

ومن ثم

$$\Phi(5)=1+1+2+2+4=10.$$

وكذلك

$$T(5)=\frac{5\cdot 6}{2}=15,$$

وبالتالي

$$H_{\mathrm{sector}}(5)=15-10=5,\qquad H(5)=6\cdot 5=30.$$

وهذا يطابق نقطة الفحص الموجودة في ملف C++. كما أن المصدر نفسه يتحقق أيضًا من \(H(10)=138\) و\(H(1000)=1177848\)، ويجري مقارنة بالقوة الغاشمة عندما تكون الرتبة 200.

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

الملفات الثلاثة تستخدم البنية نفسها. فهي تبدأ بجدول حفظ يحوي \(\Phi(0)=0\) و\(\Phi(1)=1\)، ثم تحسب \(\Phi(n)\) عوديًا، وبعد ذلك تكوّن العدد المثلثي \(\frac{n(n+1)}{2}\)، وتطرح عدد النقاط المرئية في قطاع واحد، ثم تضرب الناتج في 6.

نسخة C++ تضع العلاقة العودية داخل الصنف TotientSummatory المعتمد على std::unordered_map، وتستخدم unsigned __int128 لحماية الحسابات الوسيطة. نسخة Python تستخدم دالة داخلية مع قاموس، أما نسخة Java فتستخدم HashMap<Long, Long> ساكنة. المنطق الرياضي واحد في جميع اللغات.

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

الطريقة الساذجة كانت ستفحص كل شجرة في كل صف وتستدعي \(\gcd\) مرارًا، وهذا يعني تعقيدًا تربيعيًا في رتبة البستان. أما الطريقة المنفذة فتتجنب ذلك بالكامل. بالنسبة إلى وسيط ثابت \(m\)، فإن تجميع القسمة الصحيحة يزور فقط القيم المختلفة لـ \(\left\lfloor m/k\right\rfloor\)، وعددها \(O(\sqrt{m})\). وإذا رمزنا بـ \(S\) إلى مجموعة الوسائط المختلفة التي تصل إليها العودية مع الحفظ، فإن زمن التنفيذ الكلي هو

$$O\left(\sum_{m\in S}\sqrt{m}\right),$$

بينما يكون استهلاك الذاكرة \(O(|S|)\) لجدول الحفظ. عمليًا هذا أصغر بكثير من حساب جميع قيم دالة أويلر حتى \(10^8\) وتخزينها.

مراجع

  1. صفحة المسألة: https://projecteuler.net/problem=351
  2. دالة أويلر: Wikipedia — دالة أويلر
  3. Summatory totient function: Wikipedia — Summatory totient function
  4. النقاط المرئية على الشبكة: Wikipedia — Visible point
  5. الأعداد المثلثية: Wikipedia — عدد مثلثي