constloop = (n) => { let a = 1 let pool = newSet() while (true) { let digi = (a / n) | 0 a = a % n * 10 if (a == 0) { return"" } // 除尽没有循环节 if (pool.has(a)) { // 当余数作为被除数曾出现过,新的循环节开始。 let loop = [...pool] let index = loop.findIndex(i => i == ele) let str = loop.slice(index).map(e => e % 10).join('') return str } else { pool.add(ele) } } }
let start = 7 while (start < 1000) { let str = loop(start) if (str.length == start - 1) { console.log(start, str) } start++ }
constn_devide_len = (n) => { let a = 1, pool = newSet() while (true) { a = a % n * 10 if (a == 0) { return0 } if (pool.has(a)) { return pool.size } else { pool.add(a) } } }
let primes = []; let eularboard = newArray(n + 1).fill(true); let carousels = []
constisCarousel = (p, radix = 10) => { let a = 1, pool = newSet(), end = (p + 1) / 2 while (pool.size < end) { a = a % p * radix if (a == 0) { returnfalse } if (pool.has(a)) { returnfalse } else { pool.add(a) } } returntrue }
for (let i = 2; i <= n; i++) { if (eularboard[i]) { primes.push(i); if (isCarousel(i)) { carousels.push(i) } } for (let j = 0; j < primes.length; j++) { if (i * primes[j] > n) break; // 上限判断 eularboard[i * primes[j]] = false; if (i % primes[j] === 0) break; // 跳过本轮后续筛选 } }
质数,也叫素数,是对 Prime Number 的不同翻译。素数的叫法大概率来自日语,素在日语里引申出了根本,源头以及不可再分割的意思,这一点古汉语里是没有的。因此诞生出了一大批和制词汇,比如元素,素材,要素等等,其中最有名的要数那个味之素了。而 Prime Number正好符合这个特性,叫素数再合适不过了。现代汉语由于引入了上面这些和制词汇,所以素也开始有了上述意思。至于港台地区保留了这个叫法也是因为延袭了民国时期的翻译习惯,没有做变更而已。
当划到 i > sqrt(N) 时,则剩余未标记数也都是素数。假设有任意合数 m 满足 sqrt(N) < m < N,则 m 的素因数 p1,p2 ... 中最小的一个 p 也必然小于 sqrt(N)。而我们已经找到所有小时 sqrt(N) 的素数,并划掉其倍数了,所以找到 p 时必定已经划掉 m。
let primes = []; let numboard = newArray(n + 1).fill(true); let sqrt_n = Math.sqrt(n);
for (let i = 2; i <= n; i++) { if (numboard[i]) { if (i > sqrt_n) break// 根据上文证明,剩余未标记数也都是素数 for (let j = i * i; j < n; j += i) { // j = i * 2 -> j = i * i numboard[j] = false; } } }
for (let j = 2; j <= n; j++) { if (numboard[j]) { primes.push(j) } // 清点未标记数 }
functionbitsToNums(arr,n) { let primes = []; for (let i = 0; i < arr.length; i++) { for (let j = 0; j < 32; j++) { if (arr[i] & (1 << j)) { primes.push(i * 32 + j); } } }
let real_primes = []; for (let j = 2; j <= n; j++) { real_primes.push(primes[j]) } // 因为是字节位长的整倍数,位运算可能会多一些数字,因此要二次筛选一下 let real_primes = []; for (let j = 2; j <= n; j++) { if (primes[j] <= n) real_primes.push(primes[j]) elsebreak; }
return real_primes; }
将几个函数代入原程序中的对应位置,我们可以得到一个位运算版本的埃拉托斯特尼筛选算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
functioncalcPrimes(n) { let bitboard = newArray((n >> 5)+1).fill(-1); // 位运算数组 let sqrt_n = Math.sqrt(n);
for (let i = 2; i <= n; i++) { if ( isBitTrue(bitboard, i) ) { if (i > sqrt_n) break; for (let j = i * i; j <= n; j += i) { setBitFalse(bitboard, j); } } }
functioncleanBoard(s, e) { // 分块筛选区间 [s, e] 素数的子函数 let board = newArray(e - s + 1).fill(true); let sqrt_e = Math.sqrt(e);
for (let i = 0; i < primes.length; i++) { // 用先前得到的素数筛选本区间 let p = primes[i]; let start = Math.ceil(s / p) * p for (let j = start; j <= e; j += p) { board[j - s] = false; } }
for (let i = 0; i < board.length; i++) { // 本区间埃筛 if (board[i]) { let p = i + s; if (p > sqrt_e) break; for (let j = p * p; j <= e; j += p) { board[j - s] = false; } } }
for (let i = 0; i < board.length; i++) { // 清点本区间的素数 if (board[i]) { primes.push(i + s); } }
进一步用力注意到,筛法的本质是从『验证素数』变成了『排除合数』,而且是有规律的排除合数。考察一个合数 6 = 2 x 3,我们希望在算法中,2 x 3 只发生一次,而不是在 2 与 3 的循环中各发生一次。对于 12 也一样,尽管它有 2 x 2 x 3, 4 x 3, 6 x 2 这三种分解方式,但我们希望只有其中一种发生,其它的被跳过。
仿佛一头雾水但哪里又有一些灵感,这好像是可能的,只要足够聪明合理地安排合数生成,好像可以做到?
事实确实如此。欧拉筛法就是为了解决这个问题而生的。具体操作是这样的:
有一个自增的数字 i,从 2 开始。
有一个数组 primes,存放已经找到的素数。
如果筛板上 i 还没被划掉,则 i 是素数,将其加入 primes 数组。
用每一个 i 乘以当前所有 primes 中的素数,得到的数都是合数,在筛板将它们标记为 false 划掉。
如果 i 是 primes 中某个素数的位数,那么 i * p 这个数会被标记 false,但后续直接跳过,进到 i+1 轮。
一个随机性的否定性算法。当测试结果为 false 则一定是合数,测试结果为 true 可能为素数。算法基于费马小定理和二次探测定理,在进行 n 次测试后,错误概率为 $1/4^n$。
费马小定理:如果 p 是一个素数,而 a 是任意不是 p 的倍数的整数,则 $a^{p-1} ≡ 1 (mod\ p)$。这意味着对于任意素数 p,选择一个不是 p 的倍数的整数 a,计算 $a^{p-1}\ \% \ p$,如果不等于 1,则 p 一定不是素数;等于 1,则 p 可能是素数 。 二次探测定理:假设 p 是一个素数,我们可以将 p-1 写为 $2^s * d$ 的形式,其中 d 是奇数。则对于任意整数 a,如果存在整数 x 满足:
$a^d ≡ 1 (mod\ p)$
存在一个 i 满足 0 ≤ i < s,使得 $a^{2^i * d} ≡ -1 (mod\ p)$
以上两个条件之一,则 a 是一个模 p 的非平凡平方根,即 $a^2 ≡ x (mod\ p)$:
假设我们要测试数字 p = 17 是否为素数。
选择不是 p 的倍数的整数 a,假设选择 a = 3。计算 $a^{p-1}\ \%\ p = 3^{16} \% 17 = 1$ 结果等于 1。根据费马小定理,17 可能是素数。
根据二次探测定理,当 p = 17,有 $p-1=16=2^4*1$,因此 s = 4,d = 1。我们需验证任意整数 a,可以满足以下两个条件之一:
在 CPU 提升为 N200 后,Surface Go 4 似乎成为了一款不错的 Galgame 设备。当然现在还有另一个选择 Steamdeck,但是 Steamdeck 也有屏幕、风扇、系统兼容性等方面的劣势。尤其是在屏幕方面,差别非常大。总之先列举一下配置作参考。
Surface 系列
名称
配置
重量
厚度
CPU分数
Surface Go
奔腾 G4415Y HD615 8G 128/256G 1800*1200
522g
8.3mm
1591
Surface Go 2
奔腾 G4425Y HD615 8G 128G 1920*1280
544g
8.3mm
1696
Surface Go 2
酷睿 M3 8100Y HD615 8G 128/256G 1920*1280
544g
8.3mm
2878
Surface Go 3
奔腾 G6500Y HD615-23EU@900M 8G 128G 1920*1280
544g
8.3mm
3022
Surface Go 3
酷睿 i3 10100Y HD615-24EU@1G 8G 128/256G 1920*1280
544g
8.3mm
2935
Go 4 Business
酷睿 N200 UHD 8G 64/128/256G UFS 1920*1280
521g
8.3mm
5455
Steamdeck 系列
名称
配置
重量
厚度
屏幕
Steamdeck
7nm Zen2 8CU 16G 64G-1T
669g
49mm
7寸 1280 × 800
Steamdeck OLED
6nm Zen2 8CU 16G 512G-1T
640g
49mm
7.4寸 1280 × 800@90 HDR
对比
两者还是有挺大区别的,前几代 Surface Go 的 CPU 性能基本已经脱离主流了,毕竟被动散热,上架就是同时代低端性能,跑 E-mote/Live2D 都存在一定性能问题。手头的 Surface Go 一代跑 Steam 客户端本身都显得很勉强。4 代的 N200 CPU 性能其实也是本代低端,但毕竟刚上市还过得去。