Let
$$P=\prod_{p<190} p.$$
We want the largest divisor \(d\mid P\) such that \(d\le \sqrt P\). This divisor is the pseudo square root of \(P\). The implementation finally prints the value modulo \(10^{16}\), but the search itself is done exactly.
Because \(P\) is a product of distinct primes, every divisor corresponds to a subset of the prime list. If \(S\) is the chosen subset, then
$$d=\prod_{p\in S} p.$$
The condition \(d\le \sqrt P\) is equivalent to
$$d^2\le P.$$
So the problem is a subset-product maximization problem under an upper bound.
Multiplication is awkward to compare directly, but logarithms convert products into sums:
$$\log d=\sum_{p\in S}\log p,\qquad \log d\le \tfrac12\log P.$$
That means we want a subset whose log-sum is as close as possible to \(\tfrac12\log P\) from below. This is exactly the shape of a knapsack-style search.
The prime list below 190 has 42 elements, so a brute-force scan over all \(2^{42}\) subsets is too large. The code splits the primes into two halves, enumerates all subset sums in each half, sorts those lists, and then combines them efficiently.
For a left subset \(a\) and a right subset \(b\), the candidate is valid when
$$\log a+\log b\le \tfrac12\log P.$$
Among all valid pairs, we need the one with the greatest total logarithm.
Floating-point logs are used only to narrow the search. They do not decide the final answer. Once the algorithm finds a near-optimal pair of masks, it reconstructs the exact product with big integers and checks
$$d^2\le P$$
using exact arithmetic. This avoids rounding errors and guarantees the final divisor is truly maximal.
Each half has roughly \(2^{21}\) subsets. Enumerating both halves is still feasible, especially because subset log sums can be built incrementally from bit masks:
if a mask removes its lowest set bit, the new subset sum is the previous sum plus the log of the corresponding prime.
After sorting, the code uses a monotone scan over the two lists, then inspects a small neighborhood of right-hand candidates around the current boundary to catch the best exact solution.
Suppose the primes were just \(\{2,3,5,7\}\), split as \(\{2,3\}\) and \(\{5,7\}\). The left subsets give log-values for \(1,2,3,6\); the right subsets give log-values for \(1,5,7,35\).
We then look for the best pair whose combined log does not exceed half of \(\log(2\cdot3\cdot5\cdot7)\). The real problem behaves the same way, just on a much larger prime set.
In this toy instance
$$P=2\cdot3\cdot5\cdot7=210,\qquad \sqrt P\approx 14.49.$$
The best valid divisor is therefore
$$14=2\cdot7,$$
whereas the nearby product \(15=3\cdot5\) already fails because \(15^2=225>210\). This makes the optimization target concrete: we are not looking for the most balanced subset, but for the largest subset product that stays just below the square-root barrier.
The final pseudo square root is enormous, so the program prints
$$d \bmod 10^{16}.$$
The value itself is computed exactly in the internal big integer representation, and only the displayed form is reduced modulo \(10^{16}\).
The program only accepts --skip-checkpoints. The function primes_below(190) generates the 42 primes used in the problem. enumerate_subsets computes every subset mask together with its log-sum and sorts the results. The key implementation trick is incremental subset construction: for a nonzero mask, the code removes its lowest set bit, reuses the previous mask sum, and adds one prime log via __builtin_ctz.
solve_for_primes computes the total log target, splits the primes into two halves, and searches the best valid pair by combining the sorted lists. A monotone pointer finds the boundary in the right list, and then only a very small neighborhood around that boundary is rechecked with exact arithmetic. So logarithms guide the search, but exact integer comparison still decides the winner.
Once a promising pair of masks is found, build_from_masks reconstructs the exact product as a BigInt. The helper square_leq checks whether the square stays below the full product. The BigInt type implements fixed-width multiword multiplication and exact division by \(10^{16}\) for the final printout.
The checkpoint routine compares the fast solver with a brute-force search for smaller prime limits such as 30, 40, and 50. These checks confirm that the meet-in-the-middle logic and the exact validation agree with exhaustive search on small instances.
If there are \(n\) primes, each half has about \(2^{n/2}\) subsets. Enumeration, sorting, and the guided neighborhood scan dominate the cost, so the runtime is about
$$O(2^{n/2}\log 2^{n/2})$$
and the memory usage is about
$$O(2^{n/2}).$$
For \(n=42\), this is feasible, while the naive \(2^{42}\) search is not.
Sei
$$P=\prod_{p<190} p.$$
Gesucht ist der größte Teiler \(d\mid P\) mit \(d\le \sqrt P\). Dieser Teiler heißt Pseudo Square Root von \(P\). Die Implementierung gibt am Ende den Wert modulo \(10^{16}\) aus, rechnet intern aber exakt.
Da \(P\) ein Produkt paarweise verschiedener Primzahlen ist, entspricht jeder Teiler einer Teilmenge der Primliste. Für eine gewählte Teilmenge \(S\) gilt
$$d=\prod_{p\in S} p.$$
Die Schranke \(d\le \sqrt P\) ist äquivalent zu
$$d^2\le P.$$
Mit Logarithmen wird daraus eine Summenbedingung:
$$\log d=\sum_{p\in S}\log p,\qquad \log d\le \tfrac12\log P.$$
Damit wird das Problem zu einer Knapsack-ähnlichen Suche nach der größten zulässigen Teilsumme.
Es gibt 42 Primzahlen unter 190, also wäre eine direkte Suche über \(2^{42}\) Teilmengen zu teuer. Der Code teilt die Primliste in zwei Hälften, erzeugt alle Teilsummen der Logarithmen pro Hälfte, sortiert diese Listen und kombiniert sie anschließend effizient.
Ein linker Teilmengenwert \(a\) und ein rechter \(b\) sind zulässig, wenn
$$\log a+\log b\le \tfrac12\log P.$$
Die Logarithmen dienen nur als Vorfilter. Der eigentliche Sieger wird erst nach exakter Rekonstruktion als Big Integer bestätigt. Dazu prüft der Code für den Kandidaten \(d\) mit Ganzzahlarithmetik, ob
$$d^2\le P$$
gilt. So werden Rundungsfehler ausgeschlossen.
Jede Hälfte enthält etwa \(2^{21}\) Teilmengen. Die Teilsummen werden inkrementell aus Bitmasken aufgebaut, sortiert und dann mit einem monotonen Scan plus kleinem Nachbarschaftsfenster ausgewertet. Dadurch genügt eine einzige sortierte Suche, statt alle Paare bruteforce zu testen.
Wären die Primzahlen nur \(\{2,3,5,7\}\), dann hätte die linke Hälfte \(\{1,2,3,6\}\) und die rechte Hälfte \(\{1,5,7,35\}\). Gesucht wäre wieder das größte Produkt unterhalb von \(\sqrt{2\cdot3\cdot5\cdot7}\). Das reale Problem ist dasselbe Prinzip, nur mit 42 Primzahlen.
In diesem Spielbeispiel ist
$$P=2\cdot3\cdot5\cdot7=210,\qquad \sqrt P\approx 14.49.$$
Der beste zulässige Teiler ist also
$$14=2\cdot7,$$
während das nahe Produkt \(15=3\cdot5\) bereits scheitert, weil \(15^2=225>210\). So wird das Optimierungsziel greifbar: Gesucht ist nicht die „ausgeglichenste“ Teilmenge, sondern das größte Teilmengenprodukt knapp unter der Quadratwurzelgrenze.
Der echte pseudo square root ist riesig, daher gibt das Programm nur
$$d \bmod 10^{16}$$
aus. Intern bleibt das Ergebnis exakt; nur die Darstellung wird reduziert.
Das Programm akzeptiert nur --skip-checkpoints. primes_below(190) erzeugt die 42 Primzahlen. enumerate_subsets berechnet für jede Bitmaske den zugehörigen Logarithmus und sortiert die Ergebnisse. Der zentrale Implementierungstrick ist der inkrementelle Aufbau: Für eine von null verschiedene Maske entfernt der Code das niedrigste gesetzte Bit, übernimmt die Summe der Vorgängermaske und addiert nur einen Primlogarithmus über __builtin_ctz.
solve_for_primes bestimmt zunächst die Zielschwelle \(\tfrac12\log P\), splittet die Primzahlen in zwei Hälften und sucht mit den sortierten Listen das beste zulässige Paar. Ein monotoner Zeiger findet die Grenze in der rechten Liste; danach wird nur ein sehr kleines Nachbarschaftsfenster mit exakter Arithmetik nachgeprüft. Die Logarithmen lenken also die Suche, aber die endgültige Entscheidung bleibt exakt.
Ist ein Kandidat gefunden, rekonstruiert build_from_masks daraus das exakte Produkt als BigInt. square_leq prüft anschließend die Schranke \(d^2\le P\). BigInt implementiert dabei feste Mehrwort-Arithmetik und die Modulo-Ausgabe \(10^{16}\).
Die Checkpoints vergleichen die schnelle Lösung mit Bruteforce für kleinere Primgrenzen wie 30, 40 und 50. So wird sichergestellt, dass die Meet-in-the-middle-Suche und die exakte Prüfung exakt zusammenpassen.
Bei \(n\) Primzahlen entstehen pro Hälfte etwa \(2^{n/2}\) Teilmengen. Enumeration, Sortierung und der gezielte Nachbarschaftsscan dominieren die Laufzeit:
$$O(2^{n/2}\log 2^{n/2})$$
Speicher wird in der Größenordnung
$$O(2^{n/2})$$
benötigt. Für \(n=42\) ist das praktisch, die naive Suche über \(2^{42}\) Möglichkeiten dagegen nicht.
Şu çarpımı düşünelim:
$$P=\prod_{p<190} p.$$
İstenen, \(P\)'yi bölen ve \(\sqrt P\)'yi aşmayan en büyük \(d\) değeridir. Bu değer pseudo square root olarak adlandırılır. Kod, iç hesaplamada tam doğruluk kullanır ve sonucun \(10^{16}\) modunu yazar.
\(P\), farklı asalların çarpımı olduğu için her bölen bir asal altkümesine karşılık gelir:
$$d=\prod_{p\in S} p.$$
\(d\le \sqrt P\) koşulu,
$$d^2\le P$$
şeklinde yazılır.
Çarpımları karşılaştırmak yerine logaritmaları toplarız:
$$\log d=\sum_{p\in S}\log p,\qquad \log d\le \tfrac12\log P.$$
Böylece problem, hedefe alttan en yakın altküme toplamını bulma problemine dönüşür.
190 altındaki asal sayılar 42 tanedir. Tüm altkümeleri doğrudan denemek \(2^{42}\) büyüklüğünde bir arama olurdu; bu fazla büyüktür. Kod asalları iki yarıya böler, her yarının tüm altküme log toplamlarını çıkarır, sıralar ve sonra bu iki listeyi verimli biçimde birleştirir.
Bir sol altküme \(a\) ile bir sağ altküme \(b\) için geçerli koşul
$$\log a+\log b\le \tfrac12\log P$$
olmalıdır.
Logaritma adımı yalnızca aday seçmek içindir. Son karar, aday ürün tam olarak yeniden kurulup
$$d^2\le P$$
koşuluna bakılarak verilir. Böylece kayan nokta yuvarlama hataları tamamen devre dışı kalır.
Her yarıda yaklaşık \(2^{21}\) altküme vardır. Altküme log toplamları bit maskeleriyle artımlı biçimde oluşturulur; ardından sıralı listeler üzerinde tek yönlü bir tarama ve küçük bir komşuluk kontrolü yapılır. Böylece tüm çiftleri tek tek denemek gerekmez.
Asallar yalnızca \(\{2,3,5,7\}\) olsaydı, sol yarı \(\{1,2,3,6\}\) ve sağ yarı \(\{1,5,7,35\}\) olurdu. Yine \(\sqrt{2\cdot3\cdot5\cdot7}\) altında kalan en büyük çarpım aranırdı. Gerçek problem aynı fikrin 42 asallık sürümüdür.
Bu oyuncak örnekte
$$P=2\cdot3\cdot5\cdot7=210,\qquad \sqrt P\approx 14.49$$
olur. Dolayısıyla en iyi geçerli bölen
$$14=2\cdot7$$
değeridir; buna karşılık yakın görünen \(15=3\cdot5\) çarpımı artık geçersizdir çünkü \(15^2=225>210\). Böylece optimizasyon hedefi somutlaşır: en dengeli altküme değil, karekök bariyerinin hemen altında kalan en büyük altküme çarpımı aranır.
Gerçek pseudo square root çok büyük olduğundan program yalnızca
$$d \bmod 10^{16}$$
değerini yazar. İçeride sonuç tamdır; sadece dışarıya gösterilen biçim küçültülür.
Program yalnızca --skip-checkpoints seçeneğini destekler. primes_below(190) 42 asalı üretir. enumerate_subsets her bit maskesi için log toplamını hesaplar ve sonuçları sıralar. Buradaki temel uygulama hilesi artımlı altküme kurulumudur: sıfır olmayan bir maskede kod en düşük set biti kaldırır, önceki maskenin toplamını yeniden kullanır ve __builtin_ctz ile yalnızca tek bir asal logu ekler.
solve_for_primes önce hedef eşiği \(\tfrac12\log P\) bulur, sonra asal listesini ikiye ayırıp sıralı listeler üzerinde en iyi geçerli çifti arar. Monoton bir işaretçi sağ listedeki sınırı bulur; ardından bu sınırın çevresindeki çok küçük bir komşuluk tam sayı aritmetiğiyle tekrar kontrol edilir. Yani logaritmalar aramayı yönlendirir, ama son karar yine tam karşılaştırmayla verilir.
Uygun bir çift bulunduğunda build_from_masks bu maskelerden tam çarpımı BigInt olarak yeniden kurar. square_leq ise \(d^2\le P\) kontrolünü yapar. BigInt sabit genişlikte çok kelimeli aritmetik ve son 10^{16} modunu sağlar.
Checkpoint bölümü, 30, 40 ve 50 altındaki asallar için hızlı çözümü brute force ile karşılaştırır. Böylece meet-in-the-middle aramasının ve kesin doğrulamanın küçük örneklerde tam uyuştuğu doğrulanır.
\(n\) asal için her yarıda yaklaşık \(2^{n/2}\) altküme vardır. Sayma, sıralama ve hedef çevresindeki tarama baskın maliyetlerdir:
$$O(2^{n/2}\log 2^{n/2})$$
Bellek kullanımı ise
$$O(2^{n/2})$$
düzeyindedir. \(n=42\) için bu yapılabilir, ama \(2^{42}\) tam araması yapılamaz.
Sea
$$P=\prod_{p<190} p.$$
Se busca el mayor divisor \(d\mid P\) tal que \(d\le \sqrt P\). Ese divisor es el pseudo square root de \(P\). La implementación calcula el valor exacto y al final imprime \(d \bmod 10^{16}\).
Como \(P\) es producto de primos distintos, cada divisor corresponde a un subconjunto de la lista de primos:
$$d=\prod_{p\in S} p.$$
La condición \(d\le \sqrt P\) equivale a
$$d^2\le P.$$
Tomando logaritmos:
$$\log d=\sum_{p\in S}\log p,\qquad \log d\le \tfrac12\log P.$$
Así, el problema pasa a ser encontrar la suma de logs más grande posible sin superar el límite.
Hay 42 primos menores que 190, así que revisar los \(2^{42}\) subconjuntos completos sería demasiado caro. El código divide la lista de primos en dos mitades, enumera todas las sumas de logs de cada mitad, ordena las listas y luego busca combinaciones válidas de forma eficiente.
Un candidato \((a,b)\) debe cumplir
$$\log a+\log b\le \tfrac12\log P.$$
Los logaritmos solo sirven para filtrar candidatos. La decisión final se toma reconstruyendo el producto exacto con enteros grandes y verificando
$$d^2\le P.$$
Así se elimina cualquier riesgo de error de redondeo.
Cada mitad tiene alrededor de \(2^{21}\) subconjuntos. Sus sumas se construyen incrementalmente a partir de máscaras de bits; después, un barrido monótono y una pequeña ventana de vecinos bastan para encontrar el mejor candidato exacto.
Si los primos fueran solo \(\{2,3,5,7\}\), la parte izquierda produciría \(\{1,2,3,6\}\) y la derecha \(\{1,5,7,35\}\). El objetivo seguiría siendo el mayor producto que no supere \(\sqrt{2\cdot3\cdot5\cdot7}\). El problema real es la misma idea con 42 primos.
En este ejemplo de juguete
$$P=2\cdot3\cdot5\cdot7=210,\qquad \sqrt P\approx 14.49.$$
El mejor divisor válido es entonces
$$14=2\cdot7,$$
mientras que el producto cercano \(15=3\cdot5\) ya falla porque \(15^2=225>210\). Así queda claro el objetivo de optimización: no buscamos el subconjunto más “equilibrado”, sino el mayor producto de subconjunto que permanezca justo por debajo de la barrera \(\sqrt P\).
Como el pseudo square root real es enorme, el programa imprime únicamente
$$d \bmod 10^{16}.$$
El valor interno se mantiene exacto; solo la presentación final se reduce módulo \(10^{16}\).
El programa sólo acepta --skip-checkpoints. primes_below(190) genera los 42 primos necesarios. enumerate_subsets calcula, para cada máscara, la suma de logs y ordena los resultados. El truco central de implementación es la construcción incremental: para una máscara no nula, el código elimina su bit menos significativo, reutiliza la suma de la máscara anterior y añade un único logaritmo primo con __builtin_ctz.
solve_for_primes obtiene el umbral \(\tfrac12\log P\), divide los primos en dos mitades y busca la mejor pareja válida con las listas ordenadas. Un puntero monótono localiza la frontera en la lista derecha y luego sólo se vuelve a comprobar, con aritmética exacta, una vecindad muy pequeña alrededor de esa frontera. Los logaritmos, por tanto, sólo guían la búsqueda; el ganador final sigue decidiéndose de forma exacta.
Cuando encuentra una pareja prometedora, build_from_masks reconstruye el producto exacto como BigInt. La función square_leq comprueba entonces la desigualdad \(d^2\le P\). El tipo BigInt implementa aritmética multiword de tamaño fijo y la reducción final módulo \(10^{16}\).
La rutina de checkpoints compara la solución rápida con brute force para límites pequeños como 30, 40 y 50. Eso confirma que la estrategia meet-in-the-middle y la validación exacta coinciden con una búsqueda exhaustiva en instancias pequeñas.
Si hay \(n\) primos, cada mitad contiene unas \(2^{n/2}\) subconjuntos. La enumeración, el ordenamiento y la búsqueda guiada dominan el coste:
$$O(2^{n/2}\log 2^{n/2})$$
y la memoria es del orden de
$$O(2^{n/2}).$$
Para \(n=42\) esto es viable, mientras que el barrido ingenuo sobre \(2^{42}\) subconjuntos no lo es.
设
$$P=\prod_{p<190} p.$$
要求最大约数 \(d\mid P\),且 \(d\le \sqrt P\)。这个 \(d\) 就是 \(P\) 的 pseudo square root。程序最终输出的是 \(d \bmod 10^{16}\),但搜索过程本身是精确的。
因为 \(P\) 是互不相同素数的乘积,所以每个约数都对应一个素数子集:
$$d=\prod_{p\in S} p.$$
条件 \(d\le \sqrt P\) 等价于
$$d^2\le P.$$
对数将乘积转成和:
$$\log d=\sum_{p\in S}\log p,\qquad \log d\le \tfrac12\log P.$$
于是问题变成:在不超过上界的前提下,寻找尽量接近上界的子集和。
190 以下有 42 个素数,直接枚举所有 \(2^{42}\) 个子集太大。代码把素数列表一分为二,分别枚举两半的所有子集对数和,排序后再组合。
一个左半候选 \(a\) 和一个右半候选 \(b\) 必须满足
$$\log a+\log b\le \tfrac12\log P.$$
对数值只用于缩小候选范围。真正的答案要在重建出精确乘积之后,使用大整数验证
$$d^2\le P.$$
这样就不会因为浮点误差错过正确答案。
每一半大约有 \(2^{21}\) 个子集。子集对数和通过位掩码递推得到,排序后再做单调扫描和局部邻域检查,就能找到最优候选,而不用枚举全部配对。
如果素数只有 \(\{2,3,5,7\}\),那么左半会得到 \(\{1,2,3,6\}\),右半会得到 \(\{1,5,7,35\}\)。我们仍然是在找小于 \(\sqrt{2\cdot3\cdot5\cdot7}\) 的最大乘积。真实问题只是规模更大而已。
在这个玩具例子里
$$P=2\cdot3\cdot5\cdot7=210,\qquad \sqrt P\approx 14.49.$$
最优合法因数因此是
$$14=2\cdot7,$$
而看起来也很接近的 \(15=3\cdot5\) 已经失败,因为 \(15^2=225>210\)。这个小例子把真正的优化目标说得很清楚:我们找的不是最“均衡”的子集,而是仍然压在平方根边界之下的最大子集乘积。
真正的 pseudo square root 非常大,所以程序最后只输出
$$d \bmod 10^{16}.$$
内部计算保持精确,只有最终展示做了取模。
程序只支持 --skip-checkpoints 这个选项。primes_below(190) 生成 42 个素数。enumerate_subsets 为每个位掩码计算对应的对数和并排序。这里最关键的实现技巧是增量式子集构造:对一个非零掩码,代码去掉其最低位的 1,复用前一个掩码的和,再通过 __builtin_ctz 只补上一个素数的对数。
solve_for_primes 先求出阈值 \(\tfrac12\log P\),再把素数分成两半,用两个有序列表寻找最佳合法组合。一个单调指针先找到右侧列表中的边界,然后只在这个边界附近很小的一段范围内做精确整数复检。因此对数只负责导航,最终胜出的候选仍然由精确比较决定。
找到候选后,build_from_masks 用 BigInt 重建精确乘积。square_leq 检查 \(d^2\le P\)。BigInt 实现了固定宽度的多字大整数乘法和最终的 \(10^{16}\) 取模输出。
检查点会把快速算法与小范围暴力搜索比较,例如素数上界 30、40、50。这样可以确认 meet-in-the-middle 搜索和精确校验在小实例上与穷举完全一致。
若有 \(n\) 个素数,每一半大约有 \(2^{n/2}\) 个子集。枚举、排序和有指导的邻域扫描是主要开销:
$$O(2^{n/2}\log 2^{n/2})$$
空间复杂度约为
$$O(2^{n/2}).$$
对于 \(n=42\) 这是可行的,而直接搜索 \(2^{42}\) 个子集则不可行。
Пусть
$$P=\prod_{p<190} p.$$
Нужно найти наибольший делитель \(d\mid P\), для которого \(d\le \sqrt P\). Это и есть pseudo square root. Программа печатает значение по модулю \(10^{16}\), но сам поиск выполняется точно.
Так как \(P\) — произведение различных простых, любой делитель соответствует подмножеству списка простых:
$$d=\prod_{p\in S} p.$$
Условие \(d\le \sqrt P\) эквивалентно
$$d^2\le P.$$
После логарифмирования получаем
$$\log d=\sum_{p\in S}\log p,\qquad \log d\le \tfrac12\log P.$$
Теперь задача сводится к поиску наибольшей допустимой суммы подмножества.
Простых чисел меньше 190 всего 42, поэтому полный перебор \(2^{42}\) подмножеств слишком дорог. Код делит список простых на две половины, строит все суммы логарифмов для каждой половины, сортирует их и затем комбинирует.
Кандидат \((a,b)\) допустим, если
$$\log a+\log b\le \tfrac12\log P.$$
Логарифмы используются только для отбора кандидатов. Финальное решение принимается после точного восстановления произведения и проверки
$$d^2\le P.$$
Таким образом, ошибки округления не влияют на ответ.
В каждой половине примерно \(2^{21}\) подмножеств. Их суммы строятся инкрементально по битовым маскам. Затем используется монотонный проход по отсортированным спискам и небольшой просмотр окрестности границы, чтобы найти лучший точный кандидат.
Если бы простые были только \(\{2,3,5,7\}\), то левая часть дала бы \(\{1,2,3,6\}\), а правая \(\{1,5,7,35\}\). Ищется всё тот же наибольший продукт ниже \(\sqrt{2\cdot3\cdot5\cdot7}\). Реальная задача — это тот же принцип, но для 42 простых.
В этом игрушечном примере
$$P=2\cdot3\cdot5\cdot7=210,\qquad \sqrt P\approx 14.49.$$
Лучший допустимый делитель поэтому равен
$$14=2\cdot7,$$
тогда как близкое произведение \(15=3\cdot5\) уже не подходит, поскольку \(15^2=225>210\). Этот маленький пример делает цель оптимизации наглядной: ищется не самая «сбалансированная» подмножина, а наибольшее произведение подмножины, которое всё ещё остаётся чуть ниже барьера \(\sqrt P\).
Сам pseudo square root слишком велик, поэтому программа выводит только
$$d \bmod 10^{16}.$$
Внутреннее значение хранится точно, а модуль применяется только к печати.
Программа поддерживает только параметр --skip-checkpoints. primes_below(190) генерирует 42 простых числа. enumerate_subsets вычисляет логарифмическую сумму для каждой маски и сортирует результаты. Ключевой технический приём — инкрементальное построение подмножеств: для ненулевой маски код удаляет её младший установленный бит, переиспользует сумму предыдущей маски и добавляет только один логарифм простого через __builtin_ctz.
solve_for_primes сначала находит порог \(\tfrac12\log P\), затем делит простые на две половины и ищет лучшее допустимое сочетание в отсортированных списках. Монотонный указатель находит границу в правом списке, после чего с точной арифметикой перепроверяется лишь очень маленькая окрестность этой границы. Поэтому логарифмы только направляют поиск, а окончательный победитель определяется точно.
Когда найден перспективный набор масок, build_from_masks восстанавливает точное произведение как BigInt. Функция square_leq проверяет условие \(d^2\le P\). Тип BigInt реализует фиксированную многословную арифметику и деление по модулю \(10^{16}\) для финальной печати.
Проверки сравнивают быстрый метод с brute force на более маленьких границах, например для простых ниже 30, 40 и 50. Это подтверждает, что meet-in-the-middle и точная проверка совпадают с полным перебором на малых входах.
При \(n\) простых в каждой половине примерно \(2^{n/2}\) подмножеств. Основные затраты дают перечисление, сортировка и направленный просмотр окрестности:
$$O(2^{n/2}\log 2^{n/2})$$
Память порядка
$$O(2^{n/2}).$$
Для \(n=42\) это выполнимо, а прямой перебор \(2^{42}\) подмножеств — нет.
لنعرّف
$$P=\prod_{p<190} p.$$
المطلوب هو أكبر قاسم \(d\mid P\) بحيث \(d\le \sqrt P\). هذا هو pseudo square root لـ \(P\). البرنامج يطبع في النهاية القيمة بتعديل \(10^{16}\)، لكن البحث نفسه يتم بدقة تامة.
بما أن \(P\) حاصل ضرب أعداد أولية متميزة، فإن كل قاسم يقابل مجموعة جزئية من قائمة الأوليات:
$$d=\prod_{p\in S} p.$$
والشرط \(d\le \sqrt P\) يكافئ
$$d^2\le P.$$
بعد أخذ اللوغاريتم نحصل على
$$\log d=\sum_{p\in S}\log p,\qquad \log d\le \tfrac12\log P.$$
وهكذا تصبح المسألة بحثًا عن أكبر مجموع جزئي لا يتجاوز الحد.
عدد الأوليات الأصغر من 190 هو 42، لذلك فإن فحص \(2^{42}\) مجموعة جزئية كاملة سيكون مكلفًا جدًا. الكود يقسم قائمة الأوليات إلى نصفين، ويولد جميع مجاميع اللوغاريتمات الجزئية لكل نصف، ثم يرتب القوائم ويجمعها بكفاءة.
يكون الزوج \((a,b)\) صالحًا إذا حقق
$$\log a+\log b\le \tfrac12\log P.$$
اللوغاريتمات تُستخدم فقط لتقليص فضاء البحث. القرار النهائي يُتخذ بعد إعادة بناء الناتج بدقة تامة والتحقق من
$$d^2\le P.$$
وبذلك لا تؤثر أخطاء التقريب العشري في الإجابة.
كل نصف يحتوي تقريبًا على \(2^{21}\) مجموعة جزئية. تُبنى مجاميع اللوغاريتمات تدريجيًا من الأقنعة الثنائية، ثم يُجرى مسح أحادي الاتجاه مع فحص نطاق صغير حول الحد للعثور على أفضل مرشح دقيق.
لو كانت الأوليات فقط \(\{2,3,5,7\}\)، فسيعطي النصف الأيسر \(\{1,2,3,6\}\) والنصف الأيمن \(\{1,5,7,35\}\). ونظل نبحث عن أكبر حاصل ضرب دون \(\sqrt{2\cdot3\cdot5\cdot7}\). المسألة الحقيقية هي نفس الفكرة ولكن مع 42 أوليًا.
في هذا المثال المصغر يكون
$$P=2\cdot3\cdot5\cdot7=210,\qquad \sqrt P\approx 14.49.$$
ولذلك فإن أفضل قاسم صالح هو
$$14=2\cdot7,$$
بينما الحاصل القريب \(15=3\cdot5\) يفشل بالفعل لأن \(15^2=225>210\). يوضح هذا المثال الصغير هدف التحسين بدقة: لسنا نبحث عن المجموعة الجزئية الأكثر "توازنًا"، بل عن أكبر حاصل ضرب لمجموعة جزئية يبقى مباشرة تحت حاجز الجذر التربيعي.
القيمة الحقيقية لـ pseudo square root كبيرة جدًا، لذلك يطبع البرنامج فقط
$$d \bmod 10^{16}.$$
أما القيمة الداخلية فتبقى دقيقة، ويُستخدم التعديل فقط عند الطباعة.
البرنامج يدعم فقط الخيار --skip-checkpoints. primes_below(190) يولد 42 عددًا أوليًا. enumerate_subsets يحسب مجموع اللوغاريتم لكل قناع ويقوم بترتيب النتائج. والحيلة التنفيذية الأساسية هنا هي البناء التزايدي للمجموعات الجزئية: فبالنسبة إلى قناع غير صفري يزيل الكود أقل بت مضبوط، ويعيد استخدام مجموع القناع السابق، ثم يضيف لوغاريتم عدد أولي واحد فقط بواسطة __builtin_ctz.
solve_for_primes يحدد أولًا الحد \(\tfrac12\log P\)، ثم يقسم الأوليات إلى نصفين ويبحث عن أفضل زوج صالح داخل القوائم المرتبة. ويجد مؤشر أحادي الاتجاه الحد في القائمة اليمنى، ثم يُعاد فحص جوار صغير جدًا حول هذا الحد بحساب صحيح دقيق. لذلك فاللوغاريتمات توجه البحث فقط، أما الاختيار النهائي للفائز فيبقى دقيقًا بالكامل.
عند إيجاد مرشح واعد، يعيد build_from_masks بناء الحاصل بدقة ككائن BigInt. بعد ذلك تتحقق square_leq من الشرط \(d^2\le P\). نوع BigInt ينفذ حسابات متعددة الكلمات ويعطي أيضًا التعديل النهائي على \(10^{16}\).
تقوم نقاط التحقق بمقارنة الحل السريع مع brute force عند حدود أصغر مثل 30 و40 و50. وهذا يؤكد أن meet-in-the-middle والتحقق الدقيق يطابقان الفحص الكامل في الحالات الصغيرة.
إذا كان عدد الأوليات \(n\)، فلكل نصف حوالي \(2^{n/2}\) مجموعة جزئية. الإنشاء والترتيب والمسح الموجه حول الحد هي الكلفة المهيمنة:
$$O(2^{n/2}\log 2^{n/2})$$
أما الذاكرة فهي من رتبة
$$O(2^{n/2}).$$
عند \(n=42\) يكون هذا ممكنًا، بينما البحث المباشر في \(2^{42}\) مجموعة غير عملي.