docs: unify algorithm explanation and update teaching video
This commit is contained in:
parent
a1e43bb679
commit
dd5fab9540
15 changed files with 1680 additions and 1050 deletions
881
show/DOCUMENT.md
Normal file
881
show/DOCUMENT.md
Normal file
|
|
@ -0,0 +1,881 @@
|
|||
# 史莱姆区块搜索:从 CPU 枚举到种子解析求解
|
||||
|
||||
本文从单个区块的判定开始,依次介绍 CPU 枚举、CUDA 并行、坐标项复用,再推导当前 [`bench/slime-bench`](../bench/README.md) 搜索算法的数学原理。前四个版本说明如何让同一批检查执行得更快;后面的算法利用 Random 的结构,减少需要检查的候选。
|
||||
|
||||
当前算法的核心思路是:**先用输出奇偶筛掉不可能的低位,再成组排除不可能的中间位,最后通过区间求交求出高位。** 给定一个位置,找出让指定区域全部成为史莱姆区块的世界种子;给定位置范围,就对范围内各位置执行这套搜索。
|
||||
|
||||
候选生成要求每格“首次随机输出能被 10 整除”。最终执行完整 Java 判断,保证输出的种子确实满足要求;当前入口仍可能漏掉依赖拒绝重抽的解,具体边界见第 14 节。
|
||||
|
||||
正文给出一般性的公式和推导;引用块中的“贯穿案例”统一使用同一个实心 5×5 窗口,在有效种子归一化一节引入后逐步展开。性能表引用已有实验,分别注明任务与计时范围。算法固定常量、位数与枚举范围属于一般性内容,符号与常量集中列在[文末总表](#17-符号与常量总表)。
|
||||
|
||||
## 0. 史莱姆区块的判定流程
|
||||
|
||||
本文研究的是 Java 版的史莱姆区块判定。一个区块在水平方向覆盖 $16\times16$ 个方块;下面的 $(x,z)$ 都是**区块坐标**,不是方块坐标。
|
||||
|
||||
判定由**世界种子和区块坐标**共同决定:先计算坐标项 $T(x,z)$,与世界种子组合后初始化 `Random`,再调用 `nextInt(10)`。**返回 0 就是史莱姆区块,返回 1 到 9 则不是。**
|
||||
|
||||
先看直接使用 `Random` 的版本:
|
||||
|
||||
```java
|
||||
import java.util.Random;
|
||||
|
||||
boolean isSlimeChunk(long worldSeed, int x, int z) {
|
||||
long coordinateTerm = (long) (x * x * 4987142)
|
||||
+ (long) (x * 5947611)
|
||||
+ (long) (z * z) * 4392871L
|
||||
+ (long) (z * 389711);
|
||||
|
||||
long randomSeed = (worldSeed + coordinateTerm) ^ 987234911L;
|
||||
return new Random(randomSeed).nextInt(10) == 0;
|
||||
}
|
||||
```
|
||||
|
||||
再展开 `Random` 内部的过程:初始化 48 位状态,按“乘 $A$、加 11、保留低 48 位”更新,取高 31 位,再取余得到 0 到 9;极少数输出需要拒绝重抽。下面的版本与上面的判定等价:
|
||||
|
||||
```java
|
||||
boolean isSlimeChunk(long worldSeed, int x, int z) {
|
||||
final long A = 0x5DEECE66DL;
|
||||
final long mask48 = (1L << 48) - 1; // 低 48 位全为 1
|
||||
|
||||
// 先计算坐标项 T(x, z),保留原公式的 int 溢出行为。
|
||||
long coordinateTerm = (long) (x * x * 4987142)
|
||||
+ (long) (x * 5947611)
|
||||
+ (long) (z * z) * 4392871L
|
||||
+ (long) (z * 389711);
|
||||
|
||||
// 1. 史莱姆公式先做一次 XOR,Random 初始化再做一次 XOR。
|
||||
long randomSeed = (worldSeed + coordinateTerm) ^ 987234911L;
|
||||
long u = (randomSeed ^ A) & mask48;
|
||||
// 合并写法:u = ((worldSeed + coordinateTerm) ^ C) & mask48
|
||||
// 其中 C = 987234911L ^ A。
|
||||
|
||||
while (true) {
|
||||
// 2. 更新状态:乘 A、加 11,只保留低 48 位。
|
||||
long v = (A * u + 11) & mask48;
|
||||
|
||||
// 3. 丢掉低 17 位,取高 31 位。
|
||||
int q = (int) (v >>> 17);
|
||||
|
||||
// 4. 取余,并检查这次输出是否可以接受。
|
||||
int value = q % 10;
|
||||
if ((long) q - value + 9 <= 0x7FFFFFFFL) {
|
||||
return value == 0;
|
||||
}
|
||||
|
||||
// 本次输出被拒绝,从刚得到的状态继续取数。
|
||||
u = v;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
代码中的 `^` 是按位异或,`& mask48` 保留低 48 位,`>>> 17` 取高 31 位。
|
||||
|
||||
后文沿用代码中的符号:$A$ 是固定乘数,$C$ 是两次 XOR 合并后的常量;$u$、$v$ 是更新前后的状态,$q$ 是原始输出。
|
||||
|
||||
### 坐标项与 Java 整数回绕
|
||||
|
||||
坐标 $(x,z)$ 指区块坐标。先用一个记号表示 Java 有符号 32 位整数回绕:
|
||||
|
||||
$$J_{32}(\xi)=((\xi+2^{31})\bmod2^{32})-2^{31}.$$
|
||||
|
||||
它表示乘法溢出后保留低 32 位,再按有符号数解释。
|
||||
|
||||
本文的 `mod` 一律取非负余数。模 $2^n$ 相当于只保留低 $n$ 位,其中 $n$ 表示保留的位数。
|
||||
|
||||
原始坐标项是:
|
||||
|
||||
$$
|
||||
T(x,z)=J_{32}(4987142x^2)+J_{32}(5947611x)
|
||||
+4392871J_{32}(z^2)+J_{32}(389711z).
|
||||
$$
|
||||
|
||||
X 的平方与系数相乘后才转换为 long;Z 的平方在乘系数前就转换为 long。因此不能把它随意改成没有回绕的二次多项式。
|
||||
|
||||
**相同种子与坐标总会得到相同结果。** 单个区块成为史莱姆区块的概率约为 $1/10$,相邻区块却不能直接视为独立。本项目反向利用这些关系,搜索让给定窗口内所有区块都返回 0 的世界种子。
|
||||
|
||||
## 1. CPU:按种子枚举位置
|
||||
|
||||
最直接的方法是枚举世界种子和区块坐标,调用第 0 节的判定函数。要找一片全部满足要求的区域,就检查该位置及其周围各格;只要一格失败,这个候选便可提前退出:
|
||||
|
||||
```text
|
||||
for worldSeed in seeds:
|
||||
for (x, z) in positions:
|
||||
allSlime = true
|
||||
for (dx, dz) in requiredOffsets:
|
||||
if not isSlimeChunk(worldSeed, x + dx, z + dz):
|
||||
allSlime = false
|
||||
break
|
||||
if allSlime:
|
||||
record(worldSeed, x, z)
|
||||
```
|
||||
|
||||
`requiredOffsets` 列出待检查区域内各格相对 `(x, z)` 的偏移。`break` 只结束当前区域的逐格检查,随后继续检查下一个位置。
|
||||
|
||||
CPU 多线程把种子区间分给不同工作线程。每个线程拿到一批种子,逐个扫描同一张坐标地图;不同候选之间互不依赖,适合并行处理。
|
||||
|
||||
这一步的主要成本是候选太多。即使绝大多数候选在前几格就失败,也仍要逐个访问“种子数 × 位置数”这么多种组合。
|
||||
|
||||
早期入口是根目录的 [`slime.cpp`](../slime.cpp)。用于受控比较的教学版本是 [`01_cpu.cpp`](01_cpu.cpp),它与下面的 CUDA 版本使用相同的 Java 算术、搜索边界及完整命中记录规则。
|
||||
|
||||
## 2. CUDA:每个线程负责一个种子
|
||||
|
||||
第一版 CUDA 保留上面的循环结构,让一个 GPU 线程负责一个种子,在线程内部扫描整个坐标矩形:
|
||||
|
||||
```text
|
||||
GPU thread(worldSeed):
|
||||
for (x, z) in positions:
|
||||
check and record this candidate
|
||||
```
|
||||
|
||||
GPU 可以同时安排大量这样的任务。这里的 thread 是执行一份任务的线程,block 是一组线程,kernel 是 GPU 执行的函数。把种子分给更多线程,便能同时检查更多候选。
|
||||
|
||||
这一版改变了执行硬件,搜索的候选集合保持一致,每个线程仍要完成一整张地图的扫描。早期实现见 [`slime.cu`](../slime.cu),统一比较所用的版本见 [`02_cuda_seed.cuh`](02_cuda_seed.cuh)。
|
||||
|
||||
> **实测对照 A:CPU 与按种子分工的 CUDA。** 同样搜索 65536 个种子、257×257 个候选位置,目标都是实心 5×5;两版完整命中列表相同。
|
||||
>
|
||||
> | 版本 | 搜索时间中位数 |
|
||||
> | --- | ---: |
|
||||
> | CPU,16 个逻辑线程 | 3550.398 ms |
|
||||
> | CUDA,每线程一个种子 | 111.110 ms |
|
||||
>
|
||||
> 约加速 31.95 倍。CPU 时间包含线程创建、搜索与结果收集;GPU 使用 scan 时间,含清零、同步及回传,不含一次性的上下文创建和显存分配。GPU 为 RTX 3060 Laptop,详细记录见 [BENCHMARK.md](BENCHMARK.md)。
|
||||
|
||||
## 3. CUDA:固定坐标,在线程内遍历种子
|
||||
|
||||
还可以交换两层循环的分工:让一个工作单元固定一个坐标,在线程内部遍历种子。实现使用 grid-stride loop,一个线程完成当前坐标后,还可能继续处理其他坐标。
|
||||
|
||||
```text
|
||||
之前:thread(worldSeed) -> 扫描全部坐标
|
||||
之后:thread(x, z) -> 遍历全部种子
|
||||
```
|
||||
|
||||
这样,位置数量也能提供并行任务。种子较少、地图很大时,尤其能避免只有少量线程各自承担很长的扫描循环。
|
||||
|
||||
坐标固定后,还能复用与种子无关的计算。将第 0 节的坐标项按变量分组:
|
||||
|
||||
$$F(x)=J_{32}(4987142x^2)+J_{32}(5947611x),$$
|
||||
|
||||
$$G(z)=4392871J_{32}(z^2)+J_{32}(389711z),$$
|
||||
|
||||
$$T(x,z)=F(x)+G(z).$$
|
||||
|
||||
$F$ 只依赖 X,$G$ 只依赖 Z,拆分后仍严格保留原来的整数回绕规则。每个线程先算出待检查区域需要的全部 X 项和 Z 项,再遍历种子,反复使用这些结果。
|
||||
|
||||
这一版见 [`03_cuda_position.cuh`](03_cuda_position.cuh)。它还按图案大小展开模板,便于编译器处理私有轴项与检查循环。因此下面的实测同时包含分工调整、坐标项复用和编译期展开的效果。
|
||||
|
||||
> **实测对照 B:在更大的地图上比较两种分工。** 同样搜索 65536 个种子、4097×4097 个位置、实心 5×5,重新测量 02 与 03:
|
||||
>
|
||||
> | 版本 | GPU kernel 时间中位数 |
|
||||
> | --- | ---: |
|
||||
> | 02,每线程一个种子 | 31598.539 ms |
|
||||
> | 03,固定坐标并复用轴项 | 17600.387 ms |
|
||||
>
|
||||
> 约加速 1.795 倍,完整 GPU 命中列表相同。独立 CPU 核对其中的 257×257 范围,所有 GPU 命中再用独立 Java 算术核验。这组地图大于 A 组,两组之间不计算速度比。
|
||||
|
||||
交换分工能否获益,还取决于任务规模、寄存器用量和调度成本。两版仍检查相同的种子与位置组合,也都保留逐格提前退出。
|
||||
|
||||
## 4. Outer sum:相邻线程共享坐标轴项
|
||||
|
||||
上一版的轴项只供当前线程使用。相邻位置需要的 X、Z 坐标大量重叠,还可以让相邻线程共享它们。
|
||||
|
||||
由于 $F$ 只依赖 X、$G$ 只依赖 Z,完整坐标项满足:
|
||||
|
||||
$$T(x,z)=F(x)+G(z).$$
|
||||
|
||||
把所有 X 项排成一列、Z 项排成一行,每个交点就是对应两项之和。这种“每个横轴项加上每个纵轴项”就是 outer sum。能拆分的是进入 Random 之前的坐标项,最终的史莱姆判定仍要逐格执行。
|
||||
|
||||
[`04_cuda_outer_sum.cuh`](04_cuda_outer_sum.cuh) 让一个 block 处理 16×16 个候选位置。每条轴需要的项数为“16 + 目标区域的边长 − 1”,这些项放入 shared memory,供整个 block 使用。
|
||||
|
||||
当目标为 5×5 时,每条轴需要 20 个项,总共 40 个轴项计算,就能供这块区域的线程复用。后续的轴项相加、Random 运算和图案检查仍然需要执行;节省的是重复的坐标多项式计算。
|
||||
|
||||
> **实测对照 C:线程私有轴项与 block 共享轴项。** 固定一个种子,扫描 8193×8193 个位置,目标仍为实心 5×5;重新测量 03,再与 04 比较:
|
||||
>
|
||||
> | 版本 | GPU kernel 时间中位数 |
|
||||
> | --- | ---: |
|
||||
> | 03,线程私有轴项 | 2.715 ms |
|
||||
> | 04,outer sum 共享轴项 | 1.598 ms |
|
||||
>
|
||||
> 约加速 1.698 倍,完整命中列表相同。这是 100 轮交替测量的中位数。该组只搜索一个种子,不能与 B 组跨任务计算加速比。
|
||||
|
||||
CPU、按种子分工、按坐标分工和 outer sum 这四个版本,都在加快同一类枚举检查。接下来转向另一个问题:利用 Random 的确定性,提前判断哪些种子不可能成功,并直接求出部分未知位。
|
||||
|
||||
outer sum 的共享轴项实现属于这段演进中的教学版本。当前 `bench` 仍逐格计算完整坐标项;后面的数学推导会利用 $T=F+G$ 的恒等式,但没有把这段 block 共享实现接入最终搜索入口。
|
||||
|
||||
## 5. Random 状态更新与 48 位等价关系
|
||||
|
||||
### Random 状态更新与首次输出
|
||||
|
||||
设 $w$ 是世界种子的低 48 位。合并史莱姆公式和 Random 初始化里的两次 XOR:
|
||||
|
||||
$$
|
||||
A=\mathtt{0x5DEECE66D},\qquad
|
||||
C=A\mathbin{\mathrm{xor}}987234911=\mathtt{0x5E434E432}.
|
||||
$$
|
||||
|
||||
对单个区块 $(x,z)$,首次输出由三步得到:
|
||||
|
||||
$$u=((w+T(x,z))\mathbin{\mathrm{xor}}C)\bmod2^{48},$$
|
||||
|
||||
$$v=(Au+11)\bmod2^{48},$$
|
||||
|
||||
$$q=v\gg17.$$
|
||||
|
||||
$u$ 是初始化状态,$v$ 是推进一次后的状态,$q$ 是取出状态高 31 位得到的整数,范围为 $0\le q<2^{31}$。右移 17 位就是丢掉低 17 位。
|
||||
|
||||
如果首次输出被接受,`nextInt(10)` 返回 $q\bmod10$;返回 0 表示史莱姆区块。因此首次输出分支的候选条件是 $q\equiv0\pmod{10}$。
|
||||
|
||||
### 世界种子的 48 位等价关系
|
||||
|
||||
Random 初始化只保留低 48 位。世界种子相差 $2^{48}$ 的整数倍,在相同坐标上就得到相同的完整 Random 序列,包括重抽。因此可以先搜索 $w\in[0,2^{48})$;一个 $w$ 对应 64 位世界种子的 $2^{16}$ 种高位取值。
|
||||
|
||||
这只是史莱姆判断等价,不能据此说整个 Minecraft 世界相同。
|
||||
|
||||
## 6. 有效种子归一化
|
||||
|
||||
### 从有效种子反推世界种子
|
||||
|
||||
先只看一个固定区块 $(x,z)$。上节的公式中,世界种子与坐标项总是以 $w+T(x,z)$ 的形式出现。把这个和记为**有效种子**:
|
||||
|
||||
$$s=(w+T(x,z))\bmod2^{48}.$$
|
||||
|
||||
于是初始化状态可以直接写成:
|
||||
|
||||
$$u=(s\mathbin{\mathrm{xor}}C)\bmod2^{48}.$$
|
||||
|
||||
对固定坐标,$T(x,z)$ 已知。我们可以先寻找满足输出条件的 $s$,再反推世界种子:
|
||||
|
||||
$$w=(s-T(x,z))\bmod2^{48}.$$
|
||||
|
||||
这是一一对应的换元,本身不删掉任何候选。它把搜索顺序改成了“先求有效种子,再减去坐标项恢复世界种子”。
|
||||
|
||||
### 从单个区块扩展到窗口
|
||||
|
||||
实际要找的是让一整片区块同时满足条件的世界种子。因此指定一个窗口,取最小 X、最小 Z 的格子为锚点,并把刚才定义 $s$ 时使用的 $(x,z)$ 选在这个锚点上。
|
||||
|
||||
> **贯穿案例:固定一个实心 5×5 窗口。**
|
||||
>
|
||||
> 全文都搜索锚点 $(51069,419124)$ 的窗口:$x=51069,\ldots,51073$,$z=419124,\ldots,419128$,要求全部 25 格都是史莱姆区块。按 X 方向先编号:$i=5\Delta z+\Delta x$,其中 $\Delta x,\Delta z\in\{0,1,2,3,4\}$。
|
||||
>
|
||||
> | $z\backslash x$ | 51069 | 51070 | 51071 | 51072 | 51073 |
|
||||
> | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
> | 419124 | 0(锚点) | 1 | 2 | 3 | 4 |
|
||||
> | 419125 | 5 | 6 | 7 | 8 | 9 |
|
||||
> | 419126 | 10 | 11 | 12 | 13 | 14 |
|
||||
> | 419127 | 15 | 16 | 17 | 18 | 19 |
|
||||
> | 419128 | 20 | 21 | 22 | 23 | 24 |
|
||||
>
|
||||
> 接下来计算各格相对锚点的坐标项差,再跟随一个低位根及其中一组中间位,最终恢复完整种子。局部展示两三格时,格子仍取自这张表;是否满足窗口要求,始终以全部 25 格为准。
|
||||
|
||||
### 各格相对锚点的坐标项
|
||||
|
||||
窗口内各格共享同一个 $w$,但坐标项不同。既然 $s$ 已经吸收了锚点的坐标项,计算其他格子时,只需再补上“该格坐标项减去锚点坐标项”的差。把这个差记为 $d_i$,称为**相对坐标项**。
|
||||
|
||||
为简写公式,将第 $i$ 格的坐标项记为 $T_i$,锚点编号为 0,坐标项就是 $T_0$。这样:
|
||||
|
||||
$$s=(w+T_0)\bmod2^{48},\qquad d_i=(T_i-T_0)\bmod2^{48}.$$
|
||||
|
||||
锚点满足 $d_0=0$,各格则满足:
|
||||
|
||||
$$w+T_i\equiv s+d_i\pmod{2^{48}}.$$
|
||||
|
||||
给状态和输出也加上格子下标后,每格统一按以下公式计算:
|
||||
|
||||
$$u_i=((s+d_i)\mathbin{\mathrm{xor}}C)\bmod2^{48},$$
|
||||
|
||||
$$v_i=(Au_i+11)\bmod2^{48},\qquad q_i=v_i\gg17.$$
|
||||
|
||||
搜索目标就是找到同一个 $s$,让窗口内所有格子都满足首次输出条件。求到后仍按原来的逆变换恢复:
|
||||
|
||||
$$w=(s-T_0)\bmod2^{48}.$$
|
||||
|
||||
这就是以锚点为基准的**有效种子归一化**。它也让不同位置的低位条件可以按坐标类复用,具体见第 8 节。
|
||||
|
||||
> **贯穿案例:锚点的整数回绕。** 锚点 $x=51069$ 的第一项在不考虑溢出时为 $4987142\times51069^2=13006679591179062$。按 Java 的 32 位整数计算,结果是 $J_{32}(13006679591179062)=-1799531722$。这个回绕值才参与后面的坐标项求和。
|
||||
|
||||
> **贯穿案例:前两格的坐标项。** 将锚点和右邻格坐标代入,得到:
|
||||
>
|
||||
> $$T_0=T(51069,419124)=-1883366190470623,$$
|
||||
>
|
||||
> $$T_1=T(51070,419124)=-1883363608967202.$$
|
||||
>
|
||||
> 后面所有相对坐标项都从同一组 25 个 $T_i$ 计算。
|
||||
|
||||
> **贯穿案例:把锚点吸收到有效种子里。** 由上面的坐标项:
|
||||
>
|
||||
> $$s=(w-1883366190470623)\bmod2^{48},$$
|
||||
>
|
||||
> $$d_0=0,\qquad d_1=T_1-T_0=2581503421.$$
|
||||
>
|
||||
> 因而右邻格统一从 $s+2581503421$ 初始化。现在 $w$、$s$ 都还未知,下一步先求 $s$ 的低 18 位。
|
||||
|
||||
## 7. 基于输出奇偶的低 18 位筛选
|
||||
|
||||
把待求的有效种子拆成:
|
||||
|
||||
```text
|
||||
高 13 位 h | 中间 17 位 m | 低 18 位 p
|
||||
```
|
||||
|
||||
$$s=p+2^{18}m+2^{35}h,$$
|
||||
|
||||
$$0\le p<2^{18},\quad0\le m<2^{17},\quad0\le h<2^{13}.$$
|
||||
|
||||
一个数能被 10 整除,必须先是偶数。$q_i=v_i\gg17$,所以 $q_i$ 的最低位就是 $v_i$ 的第 17 位,位编号从 0 开始。
|
||||
|
||||
加法、乘法和逐位 XOR 的低 18 位只依赖输入的低 18 位,高位不会反过来影响低位。因此只用 $p$ 就能判断所有格子的首次输出奇偶:计算状态的低 18 位,并要求每格都满足下面的条件:
|
||||
|
||||
$$
|
||||
[A((p+d_i)\mathbin{\mathrm{xor}}C)+11]\bmod2^{18}<2^{17}.
|
||||
$$
|
||||
|
||||
这里可以先用完整整数做加法、XOR 和乘法,最后统一取模:这些运算的低 18 位只依赖输入的低 18 位,所以不必对 $p+d_i$ 和 $C$ 分别取模。末尾的模 $2^{18}$ 仍需保留,条件检查的是状态低 18 位,而不是完整整数的大小。
|
||||
|
||||
小于 $2^{17}$ 就表示第 17 位为 0。某格不满足,该 $p$ 对应的全部中间位、高位都可以直接排除。
|
||||
|
||||
实际执行时枚举 $p=0,1,\ldots,262143$,通过所有格子的 $p$ 保存为低位根,也就是代码里的 `root`。单格恰好保留一半,因为加法、XOR、奇数乘法在模 $2^{18}$ 下都是一一对应的变换。
|
||||
|
||||
这一步保证首次输出都是偶数,还没有保证能被 5 整除。每根仍有 $2^{17}=131072$ 种中间位,随后继续筛选和求解。不同坐标的根数可能不同,不能假设各格条件互相独立、每加一格都恰好减半。
|
||||
|
||||
> **实测案例:5×5 窗口的低位候选数量。**
|
||||
>
|
||||
> 锚点 $(51069,419124)$ 的实心 5×5 窗口,最后保留 **84 个低位根**,约占原来 262144 个取值的 0.032%。归一化改变根的具体编号,但不改变根的数量。
|
||||
>
|
||||
> 每根补上全部中间位后,一共需要考虑:
|
||||
>
|
||||
> $$84\times131072=11010048$$
|
||||
>
|
||||
> 种低 35 位取值。84 和 11010048 是这个具体窗口的结果,不是所有位置的固定候选数量。
|
||||
>
|
||||
> 后面跟随其中的 $p=218767$。代入筛选式,前两格的状态低 18 位分别是 59524、80305,都小于 $2^{17}=131072$;其余 23 格也通过。先保留这个根,再搜索它对应的全部中间位;此时尚不知道哪个 $m,h$ 会成功。
|
||||
|
||||
## 8. 坐标类周期与低位根复用
|
||||
|
||||
对固定窗口形状,第 $i$ 格相对锚点的偏移记为 $(\alpha_i,\beta_i)$。由坐标项的可分离性:
|
||||
|
||||
$$d_i=F(x+\alpha_i)-F(x)+G(z+\beta_i)-G(z)\pmod{2^{48}}.$$
|
||||
|
||||
低位筛选只看 $d_i\bmod2^{18}$。Java 回绕与符号扩展不会改变低 18 位,所以推导这部分时可以直接在模 $2^{18}$ 下展开多项式:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
d_i\equiv{}&4987142(2x\alpha_i+\alpha_i^2)+5947611\alpha_i\\
|
||||
&+4392871(2z\beta_i+\beta_i^2)+389711\beta_i\pmod{2^{18}}.
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
将锚点 X 增加 $2^{16}=65536$,相对项的变化为:
|
||||
|
||||
$$2\cdot4987142\cdot2^{16}\alpha_i.$$
|
||||
|
||||
4987142 是偶数,所以变化是 $2^{18}$ 的整数倍。将 Z 增加 $2^{17}=131072$,变化为:
|
||||
|
||||
$$2\cdot4392871\cdot2^{17}\beta_i,$$
|
||||
|
||||
也必然是 $2^{18}$ 的整数倍。因此可用:
|
||||
|
||||
$$(x\bmod65536,\ z\bmod131072)$$
|
||||
|
||||
作为坐标类。同类位置所有 $d_i$ 的低 18 位相同,**有效种子的完整低位根列表也相同**。
|
||||
|
||||
程序为每个未缓存的类筛一次根,再把列表用于该类具体位置。无根类的具体位置直接跳过;有根位置才生成后续任务。缓存有大小上限。
|
||||
|
||||
共享的是低位根,不是最终种子。完整 $d_i$ 的高位仍可能不同,后面的中间位、高位条件需要为每个具体位置分别计算。这一步属于有效种子低位筛选的复用,没有另加一轮固定世界种子的坐标余数对过滤。
|
||||
|
||||
## 9. 高 13 位引起的输出共同平移
|
||||
|
||||
先固定 $p,m$。用 $q_i(h)$ 表示高位取 $h$ 时的首次输出,并定义基准输出:
|
||||
|
||||
$$\bar q_i=q_i(0).$$
|
||||
|
||||
横线表示固定低 35 位、取 $h=0$ 时的基准值,不表示平均值。再记:
|
||||
|
||||
$$\ell=p+2^{18}m,\qquad s=\ell+2^{35}h.$$
|
||||
|
||||
选择在第 35 位拆分,是因为 $C<2^{35}$,XOR 只翻转低 35 位,不碰高 13 位。
|
||||
|
||||
这里不能忽略坐标加法的进位。将 $h=0$ 时的输入写成:
|
||||
|
||||
$$(\ell+d_i)\bmod2^{48}=\lambda_i+2^{35}\kappa_i,$$
|
||||
|
||||
其中 $0\le \lambda_i<2^{35}$、$0\le \kappa_i<8192$。$\kappa_i$ 已包含坐标项及其进位。加上 $2^{35}h$ 后,$\lambda_i$ 不变,高位变成 $(\kappa_i+h)\bmod8192$。
|
||||
|
||||
因为 XOR 不动这段高位,初始化状态满足:
|
||||
|
||||
$$u_i(h)=(u_i(0)+2^{35}h)\bmod2^{48}.$$
|
||||
|
||||
乘 $A$ 并加 11 后,先把状态更新公式完整写出:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
v_i(h)
|
||||
&=(Au_i(h)+11)\bmod2^{48}\\
|
||||
&=(Au_i(0)+Ah\,2^{35}+11)\bmod2^{48}.
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
而 $h=0$ 时的基准状态已经是:
|
||||
|
||||
$$v_i(0)=(Au_i(0)+11)\bmod2^{48}.$$
|
||||
|
||||
所以 **11 已经包含在 $v_i(0)$ 里面**,代入后才简写成:
|
||||
|
||||
$$v_i(h)=(v_i(0)+Ah\,2^{35})\bmod2^{48}.$$
|
||||
|
||||
这里比较的是两种高位取值带来的变化:两边都加同一个 11,因此变化量只有 $Ah\,2^{35}$;状态本身仍包含加 11 的结果。
|
||||
|
||||
新增量是 $2^{35}$ 的整数倍,不会改变右移时丢掉的低 17 位,所以:
|
||||
|
||||
$$q_i(h)=(\bar q_i+Ah\,2^{18})\bmod2^{31}.$$
|
||||
|
||||
**所有格子的输出都加上同一个量,再在 $2^{31}$ 处回绕。** 这让我们既能提前检查格子之间的输出差,又能解析求高位。
|
||||
|
||||
> **贯穿案例:同一窗口的共同平移。** 后面会筛出 $p=218767,m=40302$。固定这两段时,前两格的基准输出为 $\bar q_0=227860608$、$\bar q_1=1920935636$。若高位取 $h=2995$,共同增量取模后为:
|
||||
>
|
||||
> $$Ah\,2^{18}\bmod2^{31}=886833152.$$
|
||||
>
|
||||
> 因而 $q_0=1114693760$,$q_1=(1920935636+886833152)\bmod2^{31}=660285140$。右邻格发生了回绕,锚点没有,但两格都遵循同一个平移公式。这里先展示这条关系;第 13 节再说明这些中间位和高位如何得到。
|
||||
|
||||
这条关系来自 XOR 常量与未知高位分处不同位域,不能理解为一般 XOR 具有加法线性。若切在第 34 位,$C$ 仍会翻转未知部分的一位,上面的平移推导就不成立。
|
||||
|
||||
## 10. 基于输出差的低 35 位过滤
|
||||
|
||||
共同平移会改变各格输出,却不改变它们的模差。固定位置、低位根 $p$ 和中间位 $m$ 后,模差与高位 $h$ 无关。定义:
|
||||
|
||||
$$\delta_i=(\bar q_i-\bar q_0)\bmod2^{31}.$$
|
||||
|
||||
如果存在某个高位 $h$,使两格最终输出都是 10 的倍数,那么它们的直接差也是 10 的倍数。若差非负,模差除以 10 余 0;若差为负,取模需要加上 $2^{31}$,而:
|
||||
|
||||
$$2^{31}\bmod10=8.$$
|
||||
|
||||
所以必要条件是:
|
||||
|
||||
$$\delta_i\bmod10\in\{0,8\}.$$
|
||||
|
||||
> **贯穿案例:右邻格的模输出差。** 沿用上一节的 $p,m,h$,两格的实际输出差为负:
|
||||
>
|
||||
> $$q_1-q_0=660285140-1114693760=-454408620.$$
|
||||
>
|
||||
> 加上 $2^{31}$ 后得到 $\delta_1=1693075028$,除以 10 余 8,因此通过差过滤。若反向相减,正差 $454408620$ 除以 10 余 0。
|
||||
>
|
||||
> 用基准输出计算也得到 $(1920935636-227860608)\bmod2^{31}=1693075028$。这正是“不必先知道 $h$,也能检查模差”的意义;其余 23 格仍要分别检查。
|
||||
|
||||
模差的余数不属于 $\{0,8\}$,就可以断定当前低 35 位无论怎样补高位都不会成功。
|
||||
|
||||
通过所有差条件仍不代表存在共同高位,因为这些条件没有决定每格的回绕情况是否能同时满足。还要做第 13 节的区间求交。
|
||||
|
||||
当前实现用第 12 节的组 mask 执行这个筛选,结果与逐个检查差条件完全相同。
|
||||
|
||||
> **实测案例:输出差过滤的幸存数量。**
|
||||
>
|
||||
> 锚点 $(51069,419124)$ 的实心 5×5 窗口,在 11010048 种低 35 位中,只有 **36 种通过全部输出差条件**。
|
||||
|
||||
## 11. 每个低位根的公共项预计算
|
||||
|
||||
先看单个中间位怎样快速算出 $\bar q_i$。固定位置和根 $p$,令:
|
||||
|
||||
$$\sigma_i=p+d_i,\qquad \eta_i=\sigma_i\bmod2^{18},\qquad \gamma_i=\lfloor \sigma_i/2^{18}\rfloor.$$
|
||||
|
||||
加入中间位后,输入就是:
|
||||
|
||||
$$\ell+d_i=\eta_i+2^{18}(\gamma_i+m).$$
|
||||
|
||||
令 $E=C\gg18=\mathtt{0x1790d}$。XOR 可按位拆开:
|
||||
|
||||
$$
|
||||
(\ell+d_i)\mathbin{\mathrm{xor}}C
|
||||
=(\eta_i\mathbin{\mathrm{xor}}(C\bmod2^{18}))
|
||||
+2^{18}((\gamma_i+m)\mathbin{\mathrm{xor}}E).
|
||||
$$
|
||||
|
||||
这里只是低 18 位与高位分别做 XOR,不是把 XOR 分配到加法上。超出 48 位的部分最终取模即可。
|
||||
|
||||
把低位推进带来的贡献记为:
|
||||
|
||||
$$\omega_i=\left\lfloor\frac{A(\eta_i\mathbin{\mathrm{xor}}(C\bmod2^{18}))+11}{2^{17}}\right\rfloor.$$
|
||||
|
||||
于是首次输出精确等于:
|
||||
|
||||
$$\bar q_i(m)=[\omega_i+2A((\gamma_i+m)\mathbin{\mathrm{xor}}E)]\bmod2^{31}.$$
|
||||
|
||||
$\gamma_i,\omega_i$ 都不依赖 $m$,每个 `(位置, 根)` 只需准备一次。后面重复使用它们,减少 131072 个中间位里的重复计算。
|
||||
|
||||
实现只需要最终结果的低 31 位,可以把公共项存为 32 位无符号整数,用 32 位回绕计算再保留低 31 位。这与公式中的模运算等价,也减少重复的 64 位乘法。
|
||||
|
||||
$\gamma_i$ 表示固定根下的输入高位,$\omega_i$ 表示低位计算产生的输出贡献;它们都会随位置和根改变,但不随中间位 $m$ 改变。
|
||||
|
||||
> **贯穿案例:为根 $p=218767$ 准备公共项。** 前两格的值为:
|
||||
>
|
||||
> | 格子 $i$ | $d_i$ | $\eta_i$ | $\gamma_i$ | $\omega_i$ |
|
||||
> | --- | ---: | ---: | ---: | ---: |
|
||||
> | 0 | 0 | 218767 | 0 | 46624833586 |
|
||||
> | 1 | 2581503421 | 128076 | 9848 | 13419656886 |
|
||||
>
|
||||
> 这些值算一次即可用于这个根的全部中间位。代入 $m=40302$,有 $(40302\mathbin{\mathrm{xor}}96525)=124003$、$((9848+40302)\mathbin{\mathrm{xor}}96525)=113387$,从而:
|
||||
>
|
||||
> $$\bar q_0=[46624833586+2A\times124003]\bmod2^{31}=227860608,$$
|
||||
>
|
||||
> $$\bar q_1=[13419656886+2A\times113387]\bmod2^{31}=1920935636.$$
|
||||
>
|
||||
> $\omega_i$ 是中间贡献项,可以超过 $2^{31}$;最终的 $\bar q_i$ 才取模到 31 位输出范围内。
|
||||
|
||||
## 12. 中间位的 16 候选成组筛选
|
||||
|
||||
### 中间位分组规则
|
||||
|
||||
$E=\mathtt{0x1790d}$ 的第 4~7 位恰好全是 0。让 $m$ 的这四位变化,其余位固定,就得到一组 16 个值:
|
||||
|
||||
$$m_j=\mathrm{base}+16j,\qquad j=0,\ldots,15.$$
|
||||
|
||||
base 的第 4~7 位必须是 0。全部中间位可以无遗漏、无重复地分成 8192 组:
|
||||
|
||||
$$\mathrm{base}=256\lfloor g/16\rfloor+(g\bmod16),\qquad g=0,\ldots,8191.$$
|
||||
|
||||
> **贯穿案例:包含 $m=40302$ 的那一组。** 固定根仍为 $p=218767$。取 $g=2526$,得到 $\mathrm{base}=40206$,整组为:
|
||||
>
|
||||
> $$m_j=40206+16j=40206,40222,\ldots,40446.$$
|
||||
>
|
||||
> 其中 $j=6$ 对应 $m=40302$。这一组的低四位都为 14,第 4~7 位依次取遍 0~15,其余位相同。
|
||||
|
||||
### 组内输出差的分段常量性质
|
||||
|
||||
这里的“一段”是针对某个格子 $i$ 而言:在一组候选中,$\gamma_i+m_j$ 没有跨过 256 边界的连续部分。不同格子的 $\gamma_i$ 不同,分界位置也可能不同。下面分步说明这一段中的输出差为什么不变。
|
||||
|
||||
**输入只在四个 bit 上变化。** 从候选 $j$ 换到 $j+1$,中间位增加 16:
|
||||
|
||||
$$m_{j+1}=m_j+16.$$
|
||||
|
||||
给一个整数加 16,不会改变它的低四位。如果没有跨过 256 的边界,第 8 位及以上也不变;变化只发生在第 4~7 位。这四位在 $E$ 中全是 0,因此 XOR 不会翻转它们。其余位经过 XOR 后虽然可能改变,但在同一段内仍是固定的。
|
||||
|
||||
所以,在两个候选同属这一段的前提下,XOR 后的整数也恰好增加 16:
|
||||
|
||||
$$
|
||||
((\gamma_i+m_{j+1})\mathbin{\mathrm{xor}}E)
|
||||
=((\gamma_i+m_j)\mathbin{\mathrm{xor}}E)+16.
|
||||
$$
|
||||
|
||||
这里的等式需要“不跨边界”这个前提;一般情况下,XOR 后的增量不一定等于 XOR 前的增量。
|
||||
|
||||
**该格的输出每次增加同一个量。** 将上式代入预计算后的输出公式:
|
||||
|
||||
$$\bar q_i(m_j)=[\omega_i+2A((\gamma_i+m_j)\mathbin{\mathrm{xor}}E)]\bmod2^{31}.$$
|
||||
|
||||
$\omega_i$ 不随 $j$ 改变,XOR 后的输入每次增加 16,所以输出满足:
|
||||
|
||||
$$\bar q_i(m_{j+1})\equiv \bar q_i(m_j)+32A \pmod{2^{31}}.$$
|
||||
|
||||
也就是输出每次都加 $2A\times16=32A$,再取模。
|
||||
|
||||
**锚点输出也增加同一个量。** 锚点 $d_0=0$,所以 $\gamma_0=0$,它的输入就是 $m_j$。base 的第 4~7 位为 0,整组只是将这四位依次取遍,因此锚点输入在整组内都不跨 256 边界。对锚点同样有:
|
||||
|
||||
$$\bar q_0(m_{j+1})\equiv \bar q_0(m_j)+32A \pmod{2^{31}}.$$
|
||||
|
||||
**相减时,两个增量抵消。** 回到我们实际要筛选的模输出差:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
\delta_i(m_{j+1})
|
||||
&\equiv \bar q_i(m_{j+1})-\bar q_0(m_{j+1})\\
|
||||
&\equiv [\bar q_i(m_j)+32A]-[\bar q_0(m_j)+32A]\\
|
||||
&\equiv \bar q_i(m_j)-\bar q_0(m_j)\\
|
||||
&\equiv \delta_i(m_j) \pmod{2^{31}}.
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
两边的 $\delta_i$ 都取 $[0,2^{31})$ 内的非负余数,因此同余也就意味着数值相等。**同一段里,各格输出都在变化,但该格与锚点的输出增加得一样快,所以两者的模差保持不变。** 即使其中一个输出先在 $2^{31}$ 处回绕,取模后的差仍然相同。
|
||||
|
||||
**跨边界时,差值可能切换。** 当 $\gamma_i+m_j$ 跨过 256 边界,第 8 位及以上发生进位,这些位可能被 XOR 翻转。跨界那一步的输出增量不再保证为 $32A$,因此模输出差可能改变。跨界之后,输入又只在第 4~7 位变化,差值便再次保持不变。
|
||||
|
||||
一组的跨度只有 $16\times15=240<256$,每格最多跨一次边界。因此,这一格的整组模输出差最多有两个值:边界前一个,边界后一个;也可能两个值相同,或者整组没有跨界。
|
||||
|
||||
> **贯穿案例:右邻格在同一组中的两段差值。** 仍用 $\mathrm{base}=40206$。由预计算表,右邻格 $\gamma_1=9848$,锚点 $\gamma_0=0$,且 $E=96525$:
|
||||
>
|
||||
> | $j$ | $m_j$ | $9848+m_j$ | 锚点 XOR 后 | 右邻格 XOR 后 | XOR 后两输入之差 |
|
||||
> | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
> | 0 | 40206 | 50054 | 123907 | 113291 | −10616 |
|
||||
> | 1 | 40222 | 50070 | 123923 | 113307 | −10616 |
|
||||
> | 7 | 40318 | 50166 | 124019 | 113403 | −10616 |
|
||||
> | 8 | 40334 | 50182 | 124035 | 113931 | −10104 |
|
||||
> | 15 | 40446 | 50294 | 124147 | 114043 | −10104 |
|
||||
>
|
||||
> $j=0,\ldots,7$ 时,两列 XOR 结果每步都增加 16,差固定为 $-10616$。从 $j=7$ 到 8,右邻格输入跨过 256 的整数倍 $50176$,该格 XOR 结果增加 528,锚点仍只增加 16,所以差切换为 $-10104$;之后又保持不变。
|
||||
>
|
||||
> 表格最后一列仍是输入差。代入输出公式,才得到真正要筛选的模输出差:
|
||||
>
|
||||
> $$\delta_1(m_j)=\begin{cases}
|
||||
> (13419656886-46624833586-2A\times10616)\bmod2^{31}=1693075028,&j=0,\ldots,7,\\
|
||||
> (13419656886-46624833586-2A\times10104)\bmod2^{31}=411302484,&j=8,\ldots,15.
|
||||
> \end{cases}$$
|
||||
>
|
||||
> 前段除以 10 余 8,通过;后段余 4,淘汰。仅检查右邻格,就能一次保留前 8 个、排除后 8 个中间位。
|
||||
|
||||
### 16 位掩码的构造与求交
|
||||
|
||||
只需算出组首、组尾与锚点的模差,并找到进位分界,就知道整组哪些 $j$ 满足余 0 或 8。
|
||||
|
||||
设 $\mathrm{offset}_i=(\mathrm{base}+\gamma_i)\bmod256$,第一个跨边界的索引是:
|
||||
|
||||
$$\mathrm{cut}_i=\left\lceil\frac{256-\mathrm{offset}_i}{16}\right\rceil.$$
|
||||
|
||||
$\mathrm{cut}_i$ 的范围为 1~16。$j<\mathrm{cut}_i$ 使用前段差值,$j\ge \mathrm{cut}_i$ 使用后段差值;$\mathrm{cut}_i=16$ 表示组内没有跨边界。
|
||||
|
||||
程序用一个 16 位 mask 表示通过情况:第 $j$ 位为 1,表示 $m_j$ 还可能成功。每格构造允许 mask,再逐格按位 AND;变成 0 就整组退出。
|
||||
|
||||
> **贯穿案例:同一组的掩码逐步求交。** 右邻格允许 $j=0,\ldots,7$,掩码为 `0x00ff`。继续检查窗口内各格,挑出会进一步缩小结果的几步:
|
||||
>
|
||||
> | 检查的格子 | 该格允许的 $j$ | 该格掩码 | 累计交集 |
|
||||
> | --- | --- | --- | --- |
|
||||
> | $i=1$ | 0~7 | `0x00ff` | 0~7 |
|
||||
> | $i=3$ | 2~15 | `0xfffc` | 2~7 |
|
||||
> | $i=4$ | 3~15 | `0xfff8` | 3~7 |
|
||||
> | $i=8$ | 4~15 | `0xfff0` | 4~7 |
|
||||
> | $i=9$ | 6~15 | `0xffc0` | 6、7 |
|
||||
>
|
||||
> 表中略去的格子也都参与检查,但没有继续缩小累计交集。检查完全部 25 格,最终 mask 是 `0x00c0`,只剩 $m_6=40302$、$m_7=40318$。下一节先沿 $m=40302$ 求高位;$40318$ 也会走相同流程。
|
||||
|
||||
最后仅对 mask 中剩余的 $m_j$ 求解高位。
|
||||
|
||||
这一步仍覆盖全部 131072 个中间位。它把逐候选差检查改成每根 8192 个组任务,每组工作量更大,不能直接说速度提升了 16 倍。
|
||||
|
||||
## 13. 高 13 位的区间求解
|
||||
|
||||
对组筛选留下的一个 $m$,固定 $\ell=p+2^{18}m$,计算各格 $h=0$ 时的输出并拆分:
|
||||
|
||||
$$\bar q_i=r_i+2^{18}b_i,\qquad 0\le r_i<2^{18},\quad0\le b_i<8192.$$
|
||||
|
||||
$r_i$ 是输出的低 18 位,$b_i$ 是输出的高 13 位;它们都是已知数。种子的高位 $h$ 仍是未知数。由共同平移关系:
|
||||
|
||||
$$q_i(h)=r_i+2^{18}[(b_i+Ah)\bmod8192].$$
|
||||
|
||||
### 高位换元与模逆恢复
|
||||
|
||||
令:
|
||||
|
||||
$$\theta=Ah\bmod8192.$$
|
||||
|
||||
$A$ 是奇数,与 $8192=2^{13}$ 互质,所以乘 $A$ 只是重新排列 8192 个取值,既不重复也不遗漏。具体的逆元是:
|
||||
|
||||
$$A\bmod8192=1645,\qquad1645\times4965\equiv1\pmod{8192}.$$
|
||||
|
||||
因此求到 $\theta$ 后,可以恢复:
|
||||
|
||||
$$h=4965\theta\bmod8192.$$
|
||||
|
||||
### 单格的区间与同余条件
|
||||
|
||||
记 $\phi_i=(b_i+\theta)\bmod8192$。低位筛选已保证 $r_i$ 为偶数,只需再满足模 5 条件。因为 $2^{18}\equiv-1\pmod5$:
|
||||
|
||||
$$q_i=r_i+2^{18}\phi_i\equiv r_i-\phi_i\pmod5.$$
|
||||
|
||||
所以要 $\phi_i\equiv r_i\pmod5$。$b_i+\theta$ 最多回绕一次,回绕点为 $\tau_i=8192-b_i$:
|
||||
|
||||
| $\theta$ 的范围 | $\phi_i$ 的值 | 要求 |
|
||||
| --- | --- | --- |
|
||||
| $[0,\tau_i)$ | $b_i+\theta$ | $\theta\equiv r_i-b_i\pmod5$ |
|
||||
| $[\tau_i,8192)$ | $b_i+\theta-8192$ | $\theta\equiv r_i-b_i+2\pmod5$ |
|
||||
|
||||
第二条多出 2,是因为 $8192\bmod5=2$。区间 $[\mathrm{lo},\mathrm{hi})$ 包含下界、不包含上界;若 $b_i=0$,第二条为空。
|
||||
|
||||
### 多格条件的区间求交
|
||||
|
||||
从锚点的两条条件开始。对后续格子,余数相同的区间可以求交:
|
||||
|
||||
$$[\mathrm{lo},\mathrm{hi})\cap[\mathrm{lo}',\mathrm{hi}')=[\max(\mathrm{lo},\mathrm{lo}'),\min(\mathrm{hi},\mathrm{hi}')).$$
|
||||
|
||||
余数不匹配就删除。每格的两个余数相差 2,对一个既定余数最多只有一段能匹配,所以求交不会继续分裂,始终最多保留两条。
|
||||
|
||||
区间非空还不够,里面必须有满足模 5 条件的整数。
|
||||
|
||||
对于余数 $\rho\in\{0,1,2,3,4\}$,第一个满足条件的数是:
|
||||
|
||||
$$\theta_{\min}=\mathrm{lo}+((\rho-\mathrm{lo})\bmod5).$$
|
||||
|
||||
若 $\theta_{\min}\ge \mathrm{hi}$,也删除这一条。两条都被删掉,就立即判定这组低 35 位没有解。
|
||||
|
||||
最后按 $\theta_{\min},\theta_{\min}+5,\ldots<\mathrm{hi}$ 枚举实际解,换回 $h$。整个过程无需先收集并排序所有格子的回绕点,也无需逐个尝试 8192 种高位。
|
||||
|
||||
### 贯穿案例的高位求解与种子恢复
|
||||
|
||||
> **贯穿案例:从 $m=40302$ 求出高位。** 固定 $p=218767,m=40302$,前两格的输出拆分为:
|
||||
>
|
||||
> | 格子 | $\bar q_i$ | $r_i$ | $b_i$ | 回绕点 $\tau_i$ |
|
||||
> | --- | ---: | ---: | ---: | ---: |
|
||||
> | 0 | 227860608 | 57472 | 869 | 7323 |
|
||||
> | 1 | 1920935636 | 206548 | 7327 | 865 |
|
||||
>
|
||||
> 代入单格条件,锚点要求:$[0,7323)$ 中 $\theta\bmod5=3$,或 $[7323,8192)$ 中 $\theta\bmod5=0$。右邻格要求:$[0,865)$ 中 $\theta\bmod5=1$,或 $[865,8192)$ 中 $\theta\bmod5=3$。
|
||||
>
|
||||
> 只有余数相同的两段能求交,因此前两格共同留下 $[865,7323)$,余数为 3。继续加入窗口内其余格子,区间逐步缩小:
|
||||
>
|
||||
> | 已检查到的格子 | 保留区间 | $\theta\bmod5$ |
|
||||
> | --- | --- | ---: |
|
||||
> | $i=1$ | $[865,7323)$ | 3 |
|
||||
> | $i=2$ | $[865,3544)$ | 3 |
|
||||
> | $i=3$ | $[3202,3544)$ | 3 |
|
||||
> | $i=13$ | $[3202,3506)$ | 3 |
|
||||
> | $i=19$ | $[3382,3506)$ | 3 |
|
||||
> | $i=22$ | $[3382,3440)$ | 3 |
|
||||
> | 全部 25 格 | $[3382,3440)$ | 3 |
|
||||
>
|
||||
> 其余步骤不再缩小区间。第一个同余解为 $\theta_{\min}=3382+((3-3382)\bmod5)=3383$,所以这一条低 35 位候选共有 12 个高位解:
|
||||
>
|
||||
> $$\theta=3383,3388,\ldots,3438.$$
|
||||
>
|
||||
> 取第一个 $\theta=3383$,得到:
|
||||
>
|
||||
> $$h=4965\times3383\bmod8192=2995,$$
|
||||
>
|
||||
> $$s=218767+2^{18}\times40302+2^{35}\times2995=102917981558415,$$
|
||||
>
|
||||
> $$w=(s-T_0)\bmod2^{48}=15959335054446.$$
|
||||
>
|
||||
> 前两格此时 $\phi_0=4252$、$\phi_1=2518$,输出为 $q_0=1114693760$、$q_1=660285140$,都能被 10 整除。这与第 9、10 节预先展示的数值一致;下一节再核验整个 5×5。
|
||||
|
||||
## 14. 种子恢复、精确验证与覆盖边界
|
||||
|
||||
对每个高位解拼出:
|
||||
|
||||
$$s=p+2^{18}m+2^{35}h.$$
|
||||
|
||||
先用 $s$ 和相对坐标项 $d_i$ 逐格执行完整 `nextInt(10)` 判断。通过后再恢复世界种子:
|
||||
|
||||
$$w=(s-T_0)\bmod2^{48}.$$
|
||||
|
||||
完整判断与单次取模有一个差别:Java 为避免取模偏差,会拒绝首次输出的最后 8 个值,即:
|
||||
|
||||
$$q\ge2^{31}-8=2147483640,$$
|
||||
|
||||
并继续推进状态、重新取输出,可能不止重抽一次。
|
||||
|
||||
因此,首次输出虽能被 10 整除,也可能被拒绝后重抽失败;准确验证会剔除它。反过来,首次输出不满足模 10 条件,却可能在拒绝后重抽成功;这类种子可能在候选阶段已被淘汰,验证不能补回。
|
||||
|
||||
所以当前算法**保证记录的种子正确,但不保证找齐完整 Java 语义下的所有解**。独立拒绝补扫尚未接入 `bench`,不属于本文的算法步骤。
|
||||
|
||||
> **贯穿案例:核验完整 5×5。** 将刚恢复的 $w=15959335054446$ 代入第 0 节代码,25 格首次原始输出 $q_i$ 如下:
|
||||
>
|
||||
> | $z\backslash x$ | 51069 | 51070 | 51071 | 51072 | 51073 |
|
||||
> | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
> | 419124 | 1114693760 | 660285140 | 2105382940 | 47575410 | 1796905160 |
|
||||
> | 419125 | 2074383590 | 1410817530 | 1734889590 | 2009625200 | 440710690 |
|
||||
> | 419126 | 1664890290 | 537178720 | 1699969350 | 2115254790 | 688097620 |
|
||||
> | 419127 | 1842255790 | 1387847170 | 1302453480 | 1264531310 | 344660 |
|
||||
> | 419128 | 1757217670 | 204531470 | 2132545510 | 1606583470 | 569937090 |
|
||||
>
|
||||
> 每个数都能被 10 整除,并且都小于拒绝区间起点 $2147483640$,因此全部被接受,`nextInt(10)` 都返回 0。这个世界种子确实让所选窗口的 25 格全是史莱姆区块。
|
||||
|
||||
## 15. 完整执行流程与 GPU 任务划分
|
||||
|
||||
前文为了先说明成组筛选的依据,提前推导了高位平移;程序并不需要先枚举高位。实际执行顺序如下:
|
||||
|
||||
1. 遍历给定范围内的锚点,计算坐标类键。
|
||||
2. 对未缓存的类生成全部低 18 位根;同类位置复用列表,无根位置跳过。
|
||||
3. 对有根的具体位置计算完整相对项 $d_i$,展开 `(位置, 根)` 任务。
|
||||
4. 每个任务预计算 $\gamma_i,\omega_i$,供所有中间位使用。
|
||||
5. 将中间位分为 8192 组,用输出差的 16 位 mask 筛选;只保留幸存的 $m$。
|
||||
6. 每个幸存 $m$ 用区间求交求 $\theta$,再恢复高位 $h$。
|
||||
7. 拼出 $s$,完整验证,恢复并记录 $w$。
|
||||
|
||||
GPU 的搜索线程对应一个 `(位置, 根, 中间位组)`,线程内处理 mask 中的幸存者。多卡分配锚点批次,各自维护有限的类根缓存。这些并行安排不改变数学条件。
|
||||
|
||||
各步骤省掉的工作不同:归一化统一输入;low18 排除整片种子;类缓存避免重复筛根;公共项避免重复算术;组 mask 共用差过滤;区间求交代替高位穷举;最终验证保证输出符合原始判断。
|
||||
|
||||
## 16. 候选数量与优化效果
|
||||
|
||||
> **实测案例:已知窗口的候选数量链。**
|
||||
>
|
||||
> 已知锚点 $(51069,419124)$、实心 5×5 的候选数量链是:
|
||||
>
|
||||
> $$262144\text{ 个低位取值}\longrightarrow84\text{ 个根}$$
|
||||
>
|
||||
> $$\longrightarrow11010048\text{ 种低 35 位}\longrightarrow36\text{ 个差过滤幸存者}$$
|
||||
>
|
||||
> $$\longrightarrow24\text{ 个最终世界种子低 48 位结果。}$$
|
||||
>
|
||||
> 36 统计的是低 35 位候选,24 统计的是最终完整种子。本案例中,36 个差过滤幸存者的低位根都是 $p=218767$;只有 $m=40302$、$40318$ 能继续求出高位,各产生 12 个解,共 24 个。前文沿第一条分支的第一个解展开,其余 23 个结果也经过相同验证。
|
||||
|
||||
> **实测案例:同一个 5×5 窗口的性能对照。** 以下只列已有报告中锚点 $(51069,419124)$ 的测量,本次没有重跑 GPU 性能基准:
|
||||
>
|
||||
> | 优化 | 同窗口搜索时间 | 对照效果 |
|
||||
> | --- | --- | --- |
|
||||
> | 输出差过滤+区间求交 | 63.750 → 约 0.324 ms | 相对旧回绕事件排序版本约 197 倍 |
|
||||
> | 公共项预计算 | 0.323584 → 0.268288 ms | 约 1.21 倍 |
|
||||
> | 在预计算基础上增加组 mask | 0.268288 → 0.156672 ms | 再约 1.71 倍;后两项合计约 2.07 倍 |
|
||||
>
|
||||
> 这里统计搜索阶段,不含筛根、初始化和传输;后两行包括每轮公共项准备。首行与后两行来自不同轮次的对照实验,不能把倍数连乘成实测整体加速。
|
||||
|
||||
类根复用优化的是跨位置的批次任务,单个固定窗口不足以测量其复用收益,因此这里不混入另一批坐标的数值。批次性能及适用条件见[批次流程报告](../research/experiments/normal_search/PIPELINE_REPORT.md)。
|
||||
|
||||
代码对应关系:原始算术与验证在 [`solver_core.hpp`](../include/slime/solver_core.hpp);区间求交在 [`upper_interval.hpp`](../include/slime/upper_interval.hpp);类缓存、公共项准备、组 mask 和 GPU 调度在 [`slime_bench.cu`](../bench/slime_bench.cu)。
|
||||
|
||||
数量与实测来源:[成组筛选报告](../research/experiments/normal_search/REPORT.md)、[批次流程报告](../research/experiments/normal_search/PIPELINE_REPORT.md)、[输出差与区间对照报告](../research/experiments/full_search_feasibility/REPORT.md)。历史 CPU/GPU 与 outer sum 演进仍见[视频工程](video/README.md),不作为当前算法的额外步骤。
|
||||
|
||||
## 17. 符号与常量总表
|
||||
|
||||
全文每个符号只承担一种含义,区间端点统一写成 $\mathrm{lo},\mathrm{hi}$。下标 $i$ 表示格子,$i=0$ 表示锚点;下标 $j$ 表示组内候选。以下列出一般推导使用的符号,贯穿案例中的具体赋值不属于固定定义。
|
||||
|
||||
### 种子、坐标与随机输出
|
||||
|
||||
| 符号 | 含义与定义 | 取值范围或说明 |
|
||||
| --- | --- | --- |
|
||||
| $x,z$ | 区块坐标;相对坐标推导中为锚点坐标 | Java 有符号 32 位整数 |
|
||||
| $i$ | 窗口内格子编号 | 0 为锚点 |
|
||||
| $\alpha_i,\beta_i$ | 第 $i$ 格相对锚点的 X、Z 偏移 | 由窗口形状确定 |
|
||||
| $J_{32}(\xi)$ | 将整数 $\xi$ 回绕为有符号 32 位整数 | $[-2^{31},2^{31})$;$\xi$ 仅表示该函数的整数输入 |
|
||||
| $F(x),G(z)$ | 坐标项的横轴部分、纵轴部分 | 严格保留 Java 回绕规则 |
|
||||
| $T(x,z),T_i$ | 完整坐标项;$T_i$ 是第 $i$ 格的坐标项 | $T=F+G$;$T_0$ 为锚点坐标项 |
|
||||
| $d_i$ | 相对坐标项,$(T_i-T_0)\bmod2^{48}$ | $[0,2^{48})$,$d_0=0$ |
|
||||
| $w$ | 世界种子的低 48 位,最终输出 | $[0,2^{48})$ |
|
||||
| $s$ | 归一化有效种子,$(w+T_0)\bmod2^{48}$ | $[0,2^{48})$;搜索对象 |
|
||||
| $p$ | 有效种子的低 18 位;通过筛选后称为根 `root` | $[0,2^{18})$ |
|
||||
| $m$ | 有效种子的中间 17 位 | $[0,2^{17})$ |
|
||||
| $h$ | 有效种子的高 13 位 | $[0,2^{13})$;$s=p+2^{18}m+2^{35}h$ |
|
||||
| $\ell$ | 有效种子的低 35 位,$p+2^{18}m$ | $[0,2^{35})$ |
|
||||
| $u,v$;$u_i,v_i$ | Random 初始化状态、推进一次后的状态 | $[0,2^{48})$;下标 $i$ 指定窗口格子,参数 $(h)$ 表示随高位变化 |
|
||||
| $q$;$q_i$ | 首次 31 位输出,$v\gg17$;带下标时为 $v_i\gg17$ | $[0,2^{31})$;$q_i(h)$ 表示高位为 $h$ 时的输出 |
|
||||
| $\bar q_i$ | 固定低 35 位、取 $h=0$ 时的首次输出 | $\bar q_i=q_i(0)$;横线表示基准值,不表示平均值 |
|
||||
| $\bar q_i(m)$ | 固定位置与根、随中间位 $m$ 变化的首次输出 | 同样取 $h=0$ |
|
||||
| $\delta_i$、$\delta_i(m_j)$ | 第 $i$ 格与锚点的模输出差;带参数时明确指定中间位候选 | $(\bar q_i-\bar q_0)\bmod2^{31}$;随位置、$p,m$ 变化,与 $h$ 无关 |
|
||||
|
||||
### 公共项、分组与高位求解
|
||||
|
||||
| 符号 | 含义与定义 | 取值范围或说明 |
|
||||
| --- | --- | --- |
|
||||
| $\lambda_i,\kappa_i$ | $(\ell+d_i)\bmod2^{48}$ 的低 35 位、高 13 位 | 输入拆分为 $\lambda_i+2^{35}\kappa_i$;包含坐标加法进位 |
|
||||
| $\sigma_i$ | 固定根后,$p+d_i$ 的完整整数和 | 用于预计算 |
|
||||
| $\eta_i$ | $\sigma_i$ 的低 18 位,$\sigma_i\bmod2^{18}$ | $[0,2^{18})$;固定根后用于公共项预计算 |
|
||||
| $\gamma_i$ | $\sigma_i$ 除以 $2^{18}$ 的商 | $\lfloor \sigma_i/2^{18}\rfloor$;每根固定 |
|
||||
| $\omega_i$ | 低 18 位输入经状态更新、右移后的公共贡献 | 第 11 节定义;位置和根固定后不随 $m$ 变化 |
|
||||
| $g$ | 中间位组编号 | $0,\ldots,8191$ |
|
||||
| $\mathrm{base}$ | 一组中间位的起始值 | $256\lfloor g/16\rfloor+(g\bmod16)$ |
|
||||
| $j$ | 组内候选编号,也是 mask 的 bit 编号 | $0,\ldots,15$ |
|
||||
| $m_j$ | 组内第 $j$ 个中间位候选 | $\mathrm{base}+16j$ |
|
||||
| $\mathrm{offset}_i$ | 某格组首输入的低 8 位 | $(\mathrm{base}+\gamma_i)\bmod256$ |
|
||||
| $\mathrm{cut}_i$ | 某格组内首次跨越 256 边界的索引 | $\lceil(256-\mathrm{offset}_i)/16\rceil$;1~16,16 表示组内不跨界 |
|
||||
| mask | 组内 16 个候选的允许掩码 | 第 $j$ 位为 1 表示 $m_j$ 尚未被淘汰 |
|
||||
| $r_i,b_i$ | $\bar q_i$ 的低 18 位、高 13 位 | $\bar q_i=r_i+2^{18}b_i$;都由当前低 35 位确定 |
|
||||
| $\theta$ | 高位换元,$Ah\bmod8192$ | $[0,8192)$;由 $h=4965\theta\bmod8192$ 恢复 |
|
||||
| $\phi_i$ | 换元后的输出高 13 位 | $(b_i+\theta)\bmod8192$ |
|
||||
| $\tau_i$ | 第 $i$ 格输出高位的回绕点 | $8192-b_i$,范围 1~8192 |
|
||||
| $\mathrm{lo},\mathrm{hi}$ | 当前保留区间的下界、上界 | $[\mathrm{lo},\mathrm{hi})$;仅用于高位换元 $\theta$ 的区间 |
|
||||
| $\mathrm{lo}',\mathrm{hi}'$ | 与当前区间求交的另一段区间端点 | 撇号表示另一段区间;仍然只表示端点 |
|
||||
| $\rho$ | 区间要求的模 5 余数 | $0,1,2,3,4$ |
|
||||
| $\theta_{\min}$ | 区间中第一个满足余数要求的整数 | $\mathrm{lo}+((\rho-\mathrm{lo})\bmod5)$;若不小于 $\mathrm{hi}$,则无解 |
|
||||
|
||||
### 算法固定常量与记号
|
||||
|
||||
| 常量或记号 | 数值或含义 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| $A$ | $\mathtt{0x5DEECE66D}$ | Java Random 状态更新的乘数 |
|
||||
| 加数 | 11 | Java Random 状态更新的固定加数 |
|
||||
| 史莱姆 XOR 常量 | 987234911 | 原始史莱姆公式的 XOR 常量 |
|
||||
| $C$ | $A\mathbin{\mathrm{xor}}987234911=\mathtt{0x5E434E432}$ | 合并后的初始化 XOR 常量;只占低 35 位 |
|
||||
| $E$ | $C\gg18=\mathtt{0x1790d}$ | 公共项公式中的 XOR 常量;第 4~7 位为 0 |
|
||||
| 坐标系数 | 4987142、5947611、4392871、389711 | 分别对应 X 平方、X 一次、Z 平方、Z 一次项 |
|
||||
| 1645、4965 | $A\bmod8192$ 及其模 8192 逆元 | 两者乘积模 8192 为 1 |
|
||||
| $2^{18}=262144$ | 低位取值总数;也是输出低位拆分的模数 | 由算法位数决定,与具体窗口无关 |
|
||||
| $2^{17}=131072$ | 每个根对应的中间位取值总数 | 分为 8192 组,每组 16 个 |
|
||||
| $2^{13}=8192$ | 高位取值总数;也是换元 $\theta$ 的模数 | 与具体窗口无关 |
|
||||
| $2^{31}-8=2147483640$ | `nextInt(10)` 拒绝区间的起点 | 精确验证处理重抽 |
|
||||
| $n$ | 模 $2^n$ 运算保留的位数 | 一般性位数记号,不是组内分界 |
|
||||
| $\bmod$、$\equiv$ | 非负取余、同余关系 | 模 $2^n$ 等价于保留低 $n$ 位 |
|
||||
| $\mathrm{xor}$、$\gg$ | 按位异或、右移 | 本文随机状态和输出均按非负整数解释 |
|
||||
| $[\mathrm{lo},\mathrm{hi})$ | 含下界、不含上界的区间 | 枚举整数时要求 $\mathrm{lo}\le \theta<\mathrm{hi}$ |
|
||||
Loading…
Add table
Add a link
Reference in a new issue