无忧启动论坛

 找回密码
 注册
搜索
系统gho:最纯净好用系统下载站投放广告、加入VIP会员,请联系 微信:wuyouceo
楼主: 不点
打印 上一主题 下一主题

js (浏览器版) 中国象棋

    [复制链接]
31#
 楼主| 发表于 2022-9-6 11:25:00 | 只看该作者
本帖最后由 不点 于 2022-9-8 06:56 编辑

接着探讨对相的不同位置组合进行排序的方案。

先看帅在中相位的情况。此时,相只有 6 种位置可以放置(因为中相位已经被帅占据):

0号相位——一路边相
1号相位——三路底相
2号相位——三路河口相
3号相位——七路底相
4号相位——七路河口相
5号相位——九路边相

双相位置序号0:双相都不存在。
双相位置序号1:只存在一个相,位于0号相位。
双相位置序号2:只存在一个相,位于1号相位。
双相位置序号3:只存在一个相,位于2号相位。
双相位置序号4:只存在一个相,位于3号相位。
双相位置序号5:只存在一个相,位于4号相位。
双相位置序号6:只存在一个相,位于5号相位。
双相位置序号07:双相都在,一相位于0号相位,另一相位于1号相位。
双相位置序号08:双相都在,一相位于0号相位,另一相位于2号相位。
双相位置序号09:双相都在,一相位于0号相位,另一相位于3号相位。
双相位置序号10:双相都在,一相位于0号相位,另一相位于4号相位。
双相位置序号11:双相都在,一相位于0号相位,另一相位于5号相位。
双相位置序号12:双相都在,一相位于1号相位,另一相位于2号相位。
双相位置序号13:双相都在,一相位于1号相位,另一相位于3号相位。
双相位置序号14:双相都在,一相位于1号相位,另一相位于4号相位。
双相位置序号15:双相都在,一相位于1号相位,另一相位于5号相位。
双相位置序号16:双相都在,一相位于2号相位,另一相位于3号相位。
双相位置序号17:双相都在,一相位于2号相位,另一相位于4号相位。
双相位置序号18:双相都在,一相位于2号相位,另一相位于5号相位。
双相位置序号19:双相都在,一相位于3号相位,另一相位于4号相位。
双相位置序号20:双相都在,一相位于3号相位,另一相位于5号相位。
双相位置序号21:双相都在,一相位于4号相位,另一相位于5号相位。

有没有什么公式来把序号与相位进行对应?难点在于双相的情况,下面只讨论双相的情况。

上述的对应列表,有关双相的部分,可以排列成倒金字塔形状:

07(0,1) 08(0,2) 09(0,3) 10(0,4) 11(0,5)

   12(1,2) 13(1,3) 14(1,4) 15(1,5)

      16(2,3) 17(2,4) 18(2,5)

         19(3,4) 20(3,5)

            21(4,5)

这个堆积木(倒三角)的形状,有如下一些规律:
1、每一行,小相的序号是相同的,大相的序号是递增的。
2、每一行,第一个项目的小相与大相的序号相差1,只要能确定第一项的小相序号,此行的其他情况也都确定了。
3、各行小相的序号是递增的。

现在我们的目的是确定,括号外的总序号与括号内的双相位置序号有什么能够互相推导的关系。

先来看看如下堆积木问题:

    1
   2 3
  4 5 6
 7 8 9 10

它是连续正整数排成积木形状,问正整数 m 排在第几行?就是求相应的行号 n。

注意到,如果 m 碰巧是三角形右下角的数(它是积木的总数),那么,


(1 + n) * n / 2 = m

已知 m 可以求出 n。

两边乘以 2,得:n² + n = 2 * m

配方,得 n² + n + 1/4 = 2 * m + 1/4

(n + 0.5)² = 2 * m + 0.25


n = (2 * m + 0.25)𑁦 − 0.5


【此处 0.5 次方代表 “开平方”;也可以用 sqrt 函数来表示】

如果 m 比右下角的数小(但比上一行的数都大),那么,计算出来的 n 值是带有小数部分的,不是一个整数。如果把结果向上补足到整数(就像 ceil 函数所做的那样),那就得到正确的行号了。

所以,无论 m 是否位于右下角,它的行号 n 是可以用公式计算出来的:

n = Math.ceil(Math.sqrt(2 * m + 0.25) − 0.5);

行号知道了,但一行中有很多数,究竟 m 在哪个位置呢?

这容易知道。因为此行中最右侧的数是 (行号)*(行号 + 1)/ 2,它比 m 大(至少等于 m),

所以,m 距离此行最右侧的距离就是 (行号)*(行号 + 1)/ 2 − m。

这就是说,已知 m,可以完全确定它在哪一行的哪个具体位置了。

反过来,如果知道某个数 m 位于第 n 行的最右侧,n 是已知的,m 是未知的,如何求出 m 呢?

很容易。因为前面说了,m = n * (n + 1) / 2,特别简单。

如果 m 不在一行的最右侧呢?那得告诉它距离最右侧有多远。假定距离是 d,那么

m = n * (n + 1) / 2 − d;

也很简单。

这就是说,如果知道了某个数(未知的,m)在某行(已知的行号)的某个位置(已知的位置),也就能够算出这个数的值了。

好的,这个数学问题解决了之后,我们前面的序号与相的位置的对应关系问题,就容易解决了。

回复

使用道具 举报

32#
发表于 2022-9-6 11:36:26 | 只看该作者
我以为我下不过程序  原来我连网页都打不过
回复

使用道具 举报

33#
 楼主| 发表于 2022-9-7 07:23:33 | 只看该作者
前面我们在排序的时候,排成了倒三角形。现在试试重新排列成正三角形。



        07(0,1)

      08(0,2) 09(1,2)

    10(0,3) 11(1,3) 12(2,3)


  13(0,4) 14(1,4) 15(2,4) 16(3,4)

17(0,5) 18(1,5) 19(2,5) 20(3,5) 21(4,5)



把括号外的数字也改小一点,便于我们观察:


        01(0,1)

      02(0,2) 03(1,2)

    04(0,3) 05(1,3) 06(2,3)


  07(0,4) 08(1,4) 09(2,4) 10(3,4)

11(0,5) 12(1,5) 13(2,5) 14(3,5) 15(4,5)



看看!规律是不是出来啦!


单看括号外的数字,那就是我们前面堆积木的格式。
括号内的,右侧那个数,就是行号。
而括号内左侧那个数呢,在一行中是递增的,它代表的是在本行的序号。


前面我们已经研究过了,只要知道括号外的数,就能算出它所处的行号以及它在行内的序号。反过来也行:只要知道行号和行内的位置(就是括号内的两个数),也能算出它的序号(就是括号外的那个数)。


这样,我们的问题就彻底解决了。



解决了双相的位置表示问题,也就顺便解决了双马、双车、双炮的位置表示问题。其实,双相的位置变化很少,每个相最多只有 7 种不同的位置。即便我们找不出公式,也能用穷举(列表)的方式,把位置与序号的对应关系建立起来。然而,车、马、炮的情况就不同了。每个车、马、炮都有 90 个可能的位置,双车、双马、双炮的组合,各自都超过 4000,合起来要超过 12000,这么多的数据,用列表来穷举,工作量太大,难以承受。


前面说了,双马如果不揉合在一块儿,而是各自独立处理,每个马有 90 个位置,需要 7 位二进制数来表示。这样双马就需要 14 位。而搅合在一块儿,只需 12 位即可表示双马的全部 4096 种位置组合。也就是说,我们节约了两个二进制位。


有时候,从另外的视角来看的话,也许有人觉得不值得如此抠门。费这么大劲,只是节约了 2 个二进制位。即便是浪费了一点,如果能够带来别的好处,那不也是很值吗?当然是这样的。具体该怎么做,那是一个权衡。觉得怎么合适,就应该怎么去做。


但是,我们现在只是研究嘛。我们研究的是技术的极限情况。究竟能否达到这种极限?我们研究的是实在可行性,能够具体实施,而不是只有一个空洞的、笼统的理论框架。万一有人需要这种技术呢?如果我们的结论是能做到,但比较辛苦。那人家不怕辛苦,就愿意这么干呢?那么,这个研究,对他来说,就是有用的。我不知道有没有人能够跟随我的帖子,一口气看到这里。如果有的话,那很有可能是同意我的观点和做法的,至少是部分同意的。那么,我也就是在和您一起研究和探讨了,我也就不是孤立的了。



前面说的东西,其实都不难,实施起来,也没有实质障碍。接下来要说的,恐怕还真是有些难度了。因为,5 个兵的情况,发生了跳跃性的变化,如果要揉合在一起来处理的话,比 2 个相、2 个马、2 个车、2 个炮的情况复杂了很多。光是数学,都够吓人的。我们前面解方程的时候,解的是一元二次方程,有成熟的求根方法和求根公式。想象一下,假如我们在处理兵的时候,碰上 3 次、4 次、5 次方程,那还能解吗?当然了,如果我们畏惧的话,总是可以退缩的:我们把每个兵进行独立处理,每个兵占用 6 位二进制数,5 个兵就占用 30 位。前面我们已经知道,如果 5 个兵揉合在一起,用排列组合的方法,知道它的变化可以用 22 位二进制数来表示。这样,一下子就节约了 8 个位。前面在处理相、车、马、炮的时候,一两个位,我们都很抠门,现在 8 个位,如果我们放过了、无视了、认栽了,应该也会 “心有不甘” 吧?



回复

使用道具 举报

34#
 楼主| 发表于 2022-9-8 09:15:57 | 只看该作者
本帖最后由 不点 于 2022-9-8 09:22 编辑

前面我们知道,在对兵的处理 “不节约” 的情况下,整个局面的表示,需要 156 位二进制数,或者说,需要 20 个字节(精确地说是 19.5 个字节)。而在对兵的表示法进行极限优化的情况下,要表示一个局面,只需 17.5 个字节(即 140 位二进制数)。

现在,无论是花费 20 个字节,还是 18 个字节,我们大致知道了一个局面需要的空间的大小。库中不止有局面,还得有着法。对于给定的一个局面,要给出各种最好的着法,这就是库的用处。最好的着法,可能有很多,而不是只有 1 个。所以,库应该给出那些最好的着法。

一个局面,最多能有多少种着法呢?当然了,一个局面,都是轮到 “我” 走棋了,才需要知道着法是啥。所以,库都是假定,自己就是该走棋的一方。库要给出一个局面的各种最优的走法,供 “我” 选择。这个 “我”,肯定是电脑,因为人是无法利用库的。人只能用自己的大脑。此处所说的库,不是像 中国象棋 in html5 中采用的那样,仅仅用于 “开局”,而是指一般性的 “库”,没有 “开局”、“终局”、“残局” 之分。这个库,用20 个字节或 18 个字节来表示局面,然后,紧跟着就是配套的着法了。

“我” 方要走棋了,那肯定只能移动我方的棋子。对方的棋子可以被吃,但我方无权移动对方的棋子。

所以,走一着棋,有多少种可能性呢?那就只需要考虑 “我” 方的棋子了。用穷举法列出来看看吧。

先说说如何区分两个相、两个马、两个车、两个炮、5 个兵。按照它们在当前棋盘上的位置,总能分出先后。一路的棋子在最先,其次是二路、三路,……,九路的棋子在最后。同一路的棋子,靠近己方底线的在先。

0、保留,暂不使用


1、兵——向右移动一格
2、兵——向上移动一格
3、兵——向左移动一格
4、第二兵——向右移动一格
5、第二兵——向上移动一格
6、第二兵——向左移动一格

7、第三兵——向右移动一格
8、第三兵——向上移动一格
9、第三兵——向左移动一格
10、第四兵——向右移动一格
11、第四兵——向上移动一格
12、第四兵——向左移动一格
13、第五兵——向右移动一格
14、第五兵——向上移动一格
15、第五兵——向左移动一格
16、马——先向右走两格,再向下走一格
17、马——先向右走两格,再向上走一格
18、马——先向下走两格,再向右走一格
19、马——先向上走两格,再向右走一格
20、马——先向下走两格,再向左走一格
21、马——先向上走两格,再向左走一格
22、马——先向左走两格,再向下走一格
23、马——先向左走两格,再向上走一格
24、第二马——先向右走两格,再向下走一格
25、第二马——先向右走两格,再向上走一格
26、第二马——先向下走两格,再向右走一格
27、第二马——先向上走两格,再向右走一格
28、第二马——先向下走两格,再向左走一格
29、第二马——先向上走两格,再向左走一格
30、第二马——先向左走两格,再向下走一格
31、第二马——先向左走两格,再向上走一格
32、帅——向右移动一格
33、帅——向下移动一格
34、帅——向上移动一格
35、帅——向左移动一格

36、士——向右下滑动,即使有两个士,也只有其中一个士能够这样走,不可能同时有两个士都这么走。所以,不需要明确指出究竟是哪个士要走这步棋,因为双士会互相堵塞,其结果是,无论如何摆放,最多都只能有一个士来走这步棋。

37、士——向右上滑动,参考上述说明,无需指出是哪个士走这步棋,因为只有一个士能走这步棋。

38、士——向左下滑动,参考上述说明,无需指出是哪个士走这步棋,因为只有一个士能走这步棋。
39、士——向左上滑动,参考上述说明,无需指出是哪个士走这步棋,因为只有一个士能走这步棋。

40、相——向右下走
41、相——向右上走
42、相——向左下走
43、相——向左上走
44、第二相——向右下走
45、第二相——向右上走
46、第二相——向左下走
47、第二相——向左上走

48、车——向右走 8 格,或向左走 1 格
49、车——向右走 7 格,或向左走 2 格
50、车——向右走 6 格,或向左走 3 格
51、车——向右走 5 格,或向左走 4 格
52、车——向右走 4 格,或向左走 5 格
53、车——向右走 3 格,或向左走 6 格
54、车——向右走 2 格,或向左走 7 格
55、车——向右走 1 格,或向左走 8 格
56、车——向上走 1 格,或向下走 9 格
57、车——向上走 2 格,或向下走 8 格
58、车——向上走 3 格,或向下走 7 格
59、车——向上走 4 格,或向下走 6 格
60、车——向上走 5 格,或向下走 5 格
61、车——向上走 6 格,或向下走 4 格
62、车——向上走 7 格,或向下走 3 格
63、车——向上走 8 格,或向下走 2 格
64、车——向上走 9 格,或向下走 1 格
65、第二车——向右走 8 格,或向左走 1 格
66、第二车——向右走 7 格,或向左走 2 格
67、第二车——向右走 6 格,或向左走 3 格
68、第二车——向右走 5 格,或向左走 4 格
69、第二车——向右走 4 格,或向左走 5 格
70、第二车——向右走 3 格,或向左走 6 格
71、第二车——向右走 2 格,或向左走 7 格
72、第二车——向右走 1 格,或向左走 8 格
73、第二车——向上走 1 格,或向下走 9 格
74、第二车——向上走 2 格,或向下走 8 格
75、第二车——向上走 3 格,或向下走 7 格
76、第二车——向上走 4 格,或向下走 6 格
77、第二车——向上走 5 格,或向下走 5 格
78、第二车——向上走 6 格,或向下走 4 格
79、第二车——向上走 7 格,或向下走 3 格
80、第二车——向上走 8 格,或向下走 2 格
81、第二车——向上走 9 格,或向下走 1 格
82、炮——向右走 8 格,或向左走 1 格
83、炮——向右走 7 格,或向左走 2 格
84、炮——向右走 6 格,或向左走 3 格
85、炮——向右走 5 格,或向左走 4 格
86、炮——向右走 4 格,或向左走 5 格
87、炮——向右走 3 格,或向左走 6 格
88、炮——向右走 2 格,或向左走 7 格
89、炮——向右走 1 格,或向左走 8 格
90、炮——向上走 1 格,或向下走 9 格
91、炮——向上走 2 格,或向下走 8 格
92、炮——向上走 3 格,或向下走 7 格
93、炮——向上走 4 格,或向下走 6 格
94、炮——向上走 5 格,或向下走 5 格
95、炮——向上走 6 格,或向下走 4 格
96、炮——向上走 7 格,或向下走 3 格
97、炮——向上走 8 格,或向下走 2 格
98、炮——向上走 9 格,或向下走 1 格
99、第二炮——向右走 8 格,或向左走 1 格
100、第二炮——向右走 7 格,或向左走 2 格
101、第二炮——向右走 6 格,或向左走 3 格
102、第二炮——向右走 5 格,或向左走 4 格
103、第二炮——向右走 4 格,或向左走 5 格
104、第二炮——向右走 3 格,或向左走 6 格
105、第二炮——向右走 2 格,或向左走 7 格
106、第二炮——向右走 1 格,或向左走 8 格
107、第二炮——向上走 1 格,或向下走 9 格
108、第二炮——向上走 2 格,或向下走 8 格
109、第二炮——向上走 3 格,或向下走 7 格
110、第二炮——向上走 4 格,或向下走 6 格
111、第二炮——向上走 5 格,或向下走 5 格
112、第二炮——向上走 6 格,或向下走 4 格
113、第二炮——向上走 7 格,或向下走 3 格
114、第二炮——向上走 8 格,或向下走 2 格
115、第二炮——向上走 9 格,或向下走 1 格

用 1 个字节(实际上只需要 7 个二进制位),足以表示这 115 个着法。



回复

使用道具 举报

35#
 楼主| 发表于 2022-9-8 19:26:15 | 只看该作者
本帖最后由 不点 于 2022-9-8 20:55 编辑

前面我们有了堆积木的公式:

(n + 1) * n / 2 = m

这是一元二次方程,可以求出 n:

n = ceil(sqrt(2 * m + 0.25) − 0.5);


这个结果很重要,如果没有它,我们就无法根据序号 m 来反求双相的位置。

现在,我们不再用解方程的方法了,而是使用估算的方法,来得出 n 的值。

难道说,上面得出的这个公式还不够好吗?

不是 “不好”,而是 “太好了”,以至于它难以推广到高次方程当中。

一元二次方程,求解的结果太准确了。我们要让求解的过程粗糙一些,但结果当然必须仍是准确的。

我们现在就开始估算了。我们假装 n 是难以求出来的,只能采用估算的方法。

方程两边同乘以 2,得

(n + 1) * n = 2 * m

左边是两个连续自然数相乘,我们心中暗想:唉!假如左边直接是两个相同的数相乘,那不就简单了吗?

哪个数的平方与 (n + 1) * n 最接近呢?很容易想到,(n + 0.5)²(n + 1) * n 最接近。

下一个问题,究竟 (n + 1)* n 与 (n + 0.5)² 谁大呢?

肯定 (n + 0.5)² 大呀!

在两数的总和是固定的情况下,两数相乘要想取得最大值,只有当这两个数完全相等时才行。

这条规律在多个数相乘时,也是成立的。

也就是说,这方法能够用于  (n + 2) * (n + 1) * n 的情况。

我们将来在求解三个兵的组合问题时,就要遇到这个情况。

好的,回到正题。我们已经知道

(n + 0.5)²  > (n + 1) * n

也就是

(n + 0.5)² > 2 * m

因此,我们得到了第一个估算的式子:

n > sqrt(2 * m) 0.5

这是 n 大于某个数的形式。

此时我们心中暗想,假如还能有个 n 小于某个数的形式,那就齐全了。而最理想的呢,就是这个:

n < sqrt(2 * m) + 0.5

试试看,有没有好运气?

2 * m = (n + 1) * n > n²

所以

n < sqrt(2 * m) 【<------------- 这个运气超好!】

综合两个不等式,现在我们有了

sqrt(2 * m) 0.5 < n < sqrt(2 * m)

简直太好了!我们来验证一下,看看对不对:


【记住 m 一定是一行中最大的那个数,否则 (n + 1) * n / 2 = m 不成立】

当 m = 1 时,得到 1.414 0.5 < n < 1.414,此时,n 只能等于 1,嗯,没错。
当 m = 3 时,得到 2.449 0.5 < n < 2.449,此时,n 只能等于 2,嗯,没错。
当 m = 6 时,得到 3.464 0.5 < n < 3.464,此时,n 只能等于 3,嗯,没错。
当 m =10 时,得到 4.472 0.5 < n < 4.472,此时,n 只能等于 4,嗯,没错。
当 m =15 时,得到 5.477 0.5 < n < 5.477,此时,n 只能等于 5,嗯,没错。
当 m =21 时,得到 6.481 0.5 < n < 6.481,此时,n 只能等于 6,嗯,没错。
当 m =28 时,得到 7.483 0.5 < n < 7.483,此时,n 只能等于 7,嗯,没错。
……………………………………
弄个大一点的数试试,因为如果小数部分与 0.5 逐渐逼近,电脑的误差就可能影响程序的执行结果了!

当 m =200010000 时,得到
20000.49999375 0.5 < n < 20000.49999375,此时,n 只能等于 20000,嗯,没错。

果然!小数部分无限接近 0.5,电脑有可能把它弄成 0.5,从而造成错误!我们今后还得防止此类问题的发生。

回到正题。既然有了

sqrt(2 * m) 0.5 < n < sqrt(2 * m)


那我们就可以肯定

n = ceil(sqrt(2 * m) 0.5)

了。

跟我们先前得到的结果对比一下。我们原先得到的是

n = ceil(sqrt(2 * m + 0.25) − 0.5)

其实两个结果都是正确的,只不过计算公式有一点点差别。

必须说明,我们求得的表达式,也适用于当 m 不是一行中最大值的情况。这是因为,这一行的最小一个数,也比上一行最大的数 m’ 大。我们只需要证明,只要 m = m’ + 1,即,m 只比 m’ 增加了 1,相应的 n 就会比 n’ 增加 1。

因为 m’ 是一行中最大的那个数,所以,

m’ = (n’ + 1) * n’ / 2

sqrt(2 * m) 0.5 = sqrt(2 * m’ + 2) 0.5 = sqrt((n’ + 1) * n’ + 2) 0.5

我们现在来证明它是大于 n’ 的。也就是要证明:

sqrt((n’ + 1) * n’ + 2) 0.5 > n’

把 0.5 移到右边,就是要证明

sqrt((n’ + 1) * n’ + 2) > n’ + 0.5

两边再平方,就等价于要证明

(n’ + 1) * n’ + 2 > (n’ + 0.5)²

展开以后,就是

n’ ² + n’ + 2 > n’ ² + n’ + 0.25

这显然是成立的。因此,倒推回去,就证明了

sqrt((n’ + 1) * n’ + 2) 0.5 > n’

也即

sqrt(2 * m) 0.5 > n’

因此,

ceil(sqrt(2 * m) 0.5) > n’

另一方面,此时 m 是新行中最小的数,而新行的行号 n 肯定不小于 ceil(sqrt(2 * m) 0.5),即

n >= ceil(sqrt(2 * m) 0.5)

这是因为 n = ceil(sqrt(2 * M) 0.5),M 是此行中的最大数,而 m 却是此行中的最小数。

另外,注意到 n = n’ + 1,所以

n’ < ceil(sqrt(2 * m) 0.5) <= n’ + 1

因此肯定有

ceil(sqrt(2 * m) 0.5) = n’ + 1 = n

到此为止,我们证明了,即使 m 是一行中最小的那个数,m 所在的行号 n 仍然可以表示为

n = ceil(sqrt(2 * m) 0.5)

大功告成,证明完毕。


上述求解(估算)的过程,比前面直接求解一元二次方程繁琐多了!然而,这个办法推广到高次方程的情况,也基本就是同样的套路,不会更加繁琐了。可是,前面一元二次方程的求解过程,却无法套到高次方程上。所以,尽管这是一个繁琐的方法,但却比简单的方法更有优势。




回复

使用道具 举报

36#
 楼主| 发表于 2022-9-8 22:28:37 | 只看该作者
本帖最后由 不点 于 2022-9-9 06:44 编辑

我们不着急去处理兵的高次方程问题。我们先把二次方程的简单问题彻底搞透了,再去处理那些复杂问题也不迟。相的数据量比较小,还是拿相来说事。而且,我们让帅占据中相位,这样,每个相只有 0,1,2,3,4,5 这六个位置可以活动(前面已经定义了这六个位置,不再重复),这样我们勾画出映射的框架,易于观察和理解。

下面就是这个对应关系了。

括号外是整体可能性的序号。括号内是两个相的位置编号。

双相都不存在的情况,是特殊情况。这种可能性也不能被漏掉,所以我们要为它编号,把它记录下来。这种情况,整体就编为 0 号吧:

        0()

只存在一个相的情况,整体编号 1~6:

1(0, ) 2(1, ) 3(2, ) 4(3, ) 5(4, ) 6(5, )

双相都存在的情况,其中较小的相位编号总是排在左侧。整体编号 7~21:

        07(0,1)

      08(0,2)  09(1,2)

    10(0,3) 11(1,3) 12(2,3)


  13(0,4) 14(1,4) 15(2,4) 16(3,4)

17(0,5) 18(1,5) 19(2,5) 20(3,5) 21(4,5)




第一个问题:已知括号外的整体编号 SN,如何确定双相的位置(A,B)呢?

function get_相位置_from_整体编号 (SN) {
    if (SN == 0) {
        return [null, null]; // 双相都不存在
    }
    if (SN < 7) {
        return [SN - 1, null]; // 只存在一个相
    }
    //双相都在
    let m = SN - 6; // m 是从 1 开头的自然数
    let B = Math.ceil(Math.sqrt(2 * m) - 0.5); // m 所在的行号
    //此行的前一行中最大的数是多少?
    let M = B * (B - 1) / 2;
    let A = m - M - 1;
    return [A, B]; // 双相的位置
}


此处调用 sqrt 和 ceil 函数,就可能产生精度错误了。由于 m 是正整数,因此


sqrt(2*m) - 0.5


就不可能是整数了。我们前面已经知道,它总是小于我们想要得到的那个行号,所以,我们再添上一个 ceil 函数,就得到了行号。比如说,它如果是 7.999999,那我们用 ceil 函数:ceil(7.999999) 就得到了 8。这没问题。如果再狠一点,假如它是 700000000000.9999999999999999999999999999999999999999999999
,我们还是要用 ceil 函数。假如在调用 ceil 之前,系统已经有误差了,已经把它弄成  700000000001 了,那么,ceil(700000000001) 仍然得到 700000000001,这仍然很幸运,没出现问题。不过,假如系统已经把它弄成 700000000001.00001 了,此时若执行 ceil 函数,会得到一个错误的结果:700000000002,出错了!这对于我们的程序来说可是致命的!如果只有一次 sqrt 函数调用,出现的误差可能是不大的。假如反复调用一些会产生误差的函数,误差积累时,就可能出现超出预期的偏差了。我们这里没有误差积累(调用 sqrt 之后马上就调用 ceil 函数,没有别的步骤来使误差产生积累),而且参数的值也不会达到 700000000000 这样的数量级。所以,基本上可以说,不会出问题。因此也就不需要使用额外的手段来加强防护了。


第二个问题:已知双相位置 pos,如何获得整体编号呢?

function get_整体编号_from_相位置 (pos) {
    if (typeof pos[0] !== 'number') {
        return 0; // 双相都不存在
    }
    if (typeof pos[1] !== 'number') {
        return pos[0] + 1; // 只存在一个相位于 pos[0]
    }
    //双相都在
    let B = pos[1]; // 行号
    let M = B * (B - 1) / 2; // 前一行的最大数
    return M + pos[0] + 1 + 6;
}


回复

使用道具 举报

37#
 楼主| 发表于 2022-9-9 07:55:51 | 只看该作者
本帖最后由 不点 于 2022-9-9 09:36 编辑

现在看看 3 个兵的情况。兵本来有 55 个可能的位置,但是,我们先探讨规律,所以,先只观察 8 个位置的情况。假定兵的位置编号是 0, 1, 2, 3, 4, 5, 6, 7。以下先穷举出那些同时出现 3 个兵的所有可能的组合:

共有 8*7*6/(3*2) = 56 种:

(0 1 2) (0 1 3) (0 1 4) (0 1 5) (0 1 6) (0 1 7)
(0 2 3) (0 2 4) (0 2 5) (0 2 6) (0 2 7)
(0 3 4) (0 3 5) (0 3 6) (0 3 7)
(0 4 5) (0 4 6) (0 4 7)
(0 5 6) (0 5 7)
(0 6 7)


(1 2 3) (1 2 4) (1 2 5) (1 2 6) (1 2 7)
(1 3 4) (1 3 5) (1 3 6) (1 3 7)
(1 4 5) (1 4 6) (1 4 7)
(1 5 6) (1 5 7)
(1 6 7)


(2 3 4) (2 3 5) (2 3 6) (2 3 7)
(2 4 5) (2 4 6) (2 4 7)
(2 5 6) (2 5 7)
(2 6 7)


(3 4 5) (3 4 6) (3 4 7)
(3 5 6) (3 5 7)
(3 6 7)


(4 5 6) (4 5 7)
(4 6 7)


(5 6 7)

我们在双相的时候,同时出现两个相的情况,是排列成(平面)三角形了。


现在,三个兵的情况,已经上升到 “立体” 了,就跟立方体用刀切掉一个角是一样的。


不过,我们要的,正是被切掉的角部四面体,而不是留下的多面体。


这是一个直角的四面体,或者说成三角锥体也行。


如果我们把砖块堆到墙角,堆成一个斜坡形状,那就是锥形了。


刚才在上面列出的数据,分成了几个三角形。


最上面的那个三角形最大,我们可以把它放在墙角的底部。


第二个三角形比它稍小,就摞在它上面。


第三个更小一点,就再摞起来。


然后摞上第四个小三角形。


到第五个的时候,这个三角形就只有三个元素了,也把它摞上。


第六个,是最后一个三角形。它已经不是三角形了,它退化成一个元素了,把它也摞上,它就位于锥形的顶尖了。


我们看到,在二维平面的情况,我们还可以画出框架。现在是三维立体了,我们已经没法画出来了,只能用文字来描述。


设想一下,将来如果碰上四维、五维的超体,那岂不更糟糕?


所以,我们该知足了。目前的情况还是令人满意的。


回复

使用道具 举报

38#
 楼主| 发表于 2022-9-9 11:53:51 | 只看该作者
本帖最后由 不点 于 2022-9-9 17:17 编辑

把数据重新排列一下,貌似这样,才可以有简单的运算规律。

(0 1 2)

(0 1 3)
(0 2 3) (1 2 3)

(0 1 4)
(0 2 4) (1 2 4)
(0 3 4) (1 3 4) (2 3 4)

(0 1 5)
(0 2 5) (1 2 5)
(0 3 5) (1 3 5) (2 3 5)
(0 4 5) (1 4 5) (2 4 5) (3 4 5)

(0 1 6)
(0 2 6) (1 2 6)
(0 3 6) (1 3 6) (2 3 6)
(0 4 6) (1 4 6) (2 4 6) (3 4 6)
(0 5 6) (1 5 6) (2 5 6) (3 5 6) (4 5 6)

(0 1 7)
(0 2 7) (1 2 7)
(0 3 7) (1 3 7) (2 3 7)
(0 4 7) (1 4 7) (2 4 7) (3 4 7)
(0 5 7) (1 5 7) (2 5 7) (3 5 7) (4 5 7)
(0 6 7) (1 6 7) (2 6 7) (3 6 7) (4 6 7) (5 6 7)

在上述排列中,这个立体是分层的。括号外面的自然数省略了,没有标注。从上到下,自然数是递增的。

自然数 m 和层号 n 的关系是(此处,m 是某一层中最大的那个自然数,否则不成立):

m = (n + 2) * (n + 1) * n / 6

比如,第一层,n = 1,计算的结果,m = 1。

第二层,n = 2,计算结果,m = 4,它就是第二层中最大的那个自然数(序号)。

就是说,已知层数 n,求层内最大的数 m,很容易。

但是,反过来,已知 m 来求 n,则需要解三次方程,就不容易了。

我们不去解方程,而是采用估算的方法。

把方程变成这样:

(n + 2) * (n + 1) * n = 6 * m

这三个连续自然数相乘,要与某个数的立方进行对比,很容易想到,应该与 (n + 1)³ 进行对比。

(n + 1)³ > (n + 2) * (n + 1) * n = 6 * m

所以,

n > (6 * m) ** (1 / 3) - 1

再用

n³ < (n + 2) * (n + 1) * n = 6 * m

得到

n < (6 * m) ** (1 / 3)

综合以上两个不等式,可以肯定

n = ceil((6 * m) ** (1 / 3) - 1)

这个式子,是当 m 是一层中最大的数时,才可以保证成立的。需要进一步证明,当 m 不是一层中的最大数时,式子也能成立。只需要证明,当 m 是一层中最小的数时,式子照样成立便可。


假定 m 是一行中最小的数,而 m’ 是上一行最大的数。显然,m = m’ + 1。由于 m’ 是一行里面最大的数,所以

n’ = ceil((6 * m’) ** (1 / 3) - 1)

m’ = (n’ + 2) * (n’ + 1) * n’ / 6

都是成立的。我们考察 (6 * m) ** (1 / 3) - 1,希望能证明它大于 n’。因为

(6 * m) ** (1 / 3) - 1 = (6 * m’ + 6) ** (1 / 3) - 1

所以,也就是说,我们想要证明

(6 * m’ + 6) ** (1 / 3) - 1 > n’

变换一下,等价于要证明

(6 * m’ + 6) ** (1 / 3) > n’ + 1


也即



(6 * m’ + 6) > (n’ + 1)³

也即


(n’ + 2) * (n’ + 1) * n’ + 6  > (n’ + 1)³



两边展开后是


n ³ + 3 * n’ ² + 2 * n’ + 6 > n ³ + 3 * n’ ² + 3 * n’ + 1



糟糕!这式子无法成立!当 n’ 增大时,右边将变大!


我们要失败了!


这说明,我们估计出来的表达式


n = ceil((6 * m) ** (1 / 3) - 1)



不够精确,需要更精细的估计才行。


回复

使用道具 举报

39#
 楼主| 发表于 2022-9-9 19:49:35 | 只看该作者
前面用估算的方法求解方程

(n + 2) * (n + 1) * n = 6 * m

得到的表达式

n = ceil((6 * m) ** (1 / 3) - 1)

虽然是正确的,但却不能适用于当 m 不是一层中的最大值的情况。所以,这仍然算是一个失败的表达式。

本帖要尝试一种更精细的估计。

我们看

(n + 2) * (n + 1) * n = 6 * m

变换左边,得

(n + 1) ³ - (n + 1)  = 6 * m

再变换

(n + 1) ³ = 6 * m + (n + 1)

令 x = n + 1,有

x³ = 6 * m + x

x = (6 * m + x) ** (1 / 3)

由于 x = n + 1 > 0,因此

x = (6 * m + x) ** (1 / 3) > (6 * m + 0) ** (1 / 3)

因此

x > (6 * m) ** (1 / 3)

这其实就是我们以前给出的估计值。

现在我们粗略认为这个估计仍然可以初步使用,即

x ≈ (6 * m) ** (1 / 3)

好了,我们把它代入前面曾经得到的式子

x = (6 * m + x) ** (1 / 3)

的右端,得

x ≈ (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)

这个估计值,显然比前面的估计值更大一些了。

我们知道,

x = (6 * m + x) ** (1 / 3)     ……………………(1)

是真正成立的。我们又知道

x > (6 * m) ** (1 / 3)

因此,当我们把(1)式右边的 x 换成 (6 * m) ** (1 / 3) 以后,

(1)的右边就变小了,而(1)的左边还是 x,所以,左边大,右边小:

x >  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)

注意到 x = n + 1,所以

n > -1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)

这就是对 n 的最新一次估计。当然,为了确定 n,我们还需要证明

n <  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)

这是容易的,因为我们在前一帖中已经知道

n < (6 * m) ** (1 / 3)



(6 * m) ** (1 / 3) < (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)

所以

n <  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)

也是没问题的。综合以上两个不等式,就可以肯定

n = ceil( -1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3))

好了,别忘了,这是假定 m 为一层中的最大值才获得的结果。接下来需要证明,这个结果对于别的 m 也成立,其实只需证明,对于一层中最小的那个 m 成立即可。

假定 m 是一层中最小的值。那么,m’ = m - 1 就是上一层中最大的值了。所以,

n’ = ceil( -1 +  (6 * m’ + (6 * m’) ** (1 / 3)) ** (1 / 3))
m’ = (n’ + 2) * (n’ + 1) * n’ / 6

都成立。我们考察

-1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)

希望能证明它大于 n’。因为

-1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3)
= -1 +  (6 * m’ + 6 + (6 * m’ + 6) ** (1 / 3)) ** (1 / 3)

所以,也就是说,我们想要证明

-1 +  (6 * m’ + 6 + (6 * m’ + 6) ** (1 / 3)) ** (1 / 3) > n’

把它进行等价变换,就是想要证明

(6 * m’ + 6 + (6 * m’ + 6) ** (1 / 3)) ** (1 / 3) > n’ + 1

也即

(6 * m’ + 6 + (6 * m’ + 6) ** (1 / 3)) > (n’ + 1) ³


也即


((n’ + 2) * (n’ + 1) * n’ + 6 + ((n’ + 2) * (n’ + 1) * n’ + 6)  ** (1 / 3)) > (n’ + 1) ³


两边展开一下,得


n’  ³ + 3 * n’ ² + 2 * n’ + 6 + (n’  ³ + 3 * n’ ² + 2 * n’ + 6)  ** (1 / 3) > n’  ³ + 3 * n’ ² + 3 * n’ + 1


化简,得



(n’  ³ + 3 * n’ ² + 2 * n’ + 6)  ** (1 / 3) > n’ - 5


这显然是成立的,因为左边大于 n’, 而右边小于 n’。



反推回去,就证明了


-1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3) > n’


终于成功啦!这样,就得到


ceil( -1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3) ) > n’

假定 m 所在层中的最大数是 M,我们有


n = ceil( -1 +  (6 * M + (6 * M) ** (1 / 3)) ** (1 / 3) )


  >= ceil( -1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3) )


注意到 n = n’ + 1,综合上述两个不等式,肯定有


n = ceil( -1 +  (6 * m + (6 * m) ** (1 / 3)) ** (1 / 3) )


到此为止,我们证明了,当 m 并非一层中的最大值时,上述这个求层号的表达式仍然成立。只有像这样证明了其普适性以后,我们才能使用这个表达式,否则,我们在程序中就不敢使用这个表达式。

回复

使用道具 举报

40#
 楼主| 发表于 2022-9-9 23:42:35 | 只看该作者
本帖最后由 不点 于 2022-9-10 06:01 编辑

现在给出已知整体编号 SN 求三兵位置的函数


function get_三兵位置_from_整体编号 (SN) {
    if (SN == 0) {
        return [null, null, null]; // 三兵都不存在
    }
    if (SN < 9) {
        return [SN - 1, null, null]; // 只存在一个兵
    }
    if (SN < 37) {    //存在两个兵
        let m = SN - 8; // m 是从 1 开头的自然数
        let B = Math.ceil(Math.sqrt(2 * m) - 0.5); // m 所在的行号
        //此行的前一行中最大的数是多少?
        let M = B * (B - 1) / 2;
        let A = m - M - 1;
        return [A, B, null]; // 双兵的位置
    }
        //三兵都在
        let m = SN - 36; // m 是从 1 开头的自然数
        let C = Math.ceil((6 * m+(6*m)**(1/3))**(1/3) - 1); // m 所在的层号
        //此层的上一层中最大的数是多少?
        let M = (C + 1) * C * (C - 1) / 6;
        // 让此层的第一个数拥有临时序号 1
        let t = SN - M;
        // 从 t 可以求出 A, B
        let B = Math.ceil(Math.sqrt(2 * t) - 0.5); // t 所在的行号
        //此行的前一行中最大的数是多少?
        let W = B * (B - 1) / 2;
        let A = t - W - 1;

        return [A, B, C+1]; // 三兵的位置

}

下面的函数是, 已知三兵位置,算出整体编号。


function get_整体编号_from_三兵位置 (pos) {
    if (typeof pos[0] !== 'number') {
        return 0; // 三兵都不存在
    }
    if (typeof pos[1] !== 'number') {
        return pos[0] + 1; // 只存在一个兵位于 pos[0]
    }
    if (typeof pos[2] !== 'number') {
        // 只存在两个兵位于 pos[0], pos[1]
        let B = pos[1]; // 行号
        let M = B * (B - 1) / 2; // 前一行的最大数
        return M + pos[0] + 1 + 8;
    }

        //三兵都在

        let C = pos[2] - 1; // 层号

        let W = (C + 1) * C * (C - 1) / 6; // 前一层的最大数
        let B = pos[1]; // 在本层中的行号
        let M = B * (B - 1) / 2; // 前一行的最大数
    return M + pos[0] + 1 + W + 37;
}


回复

使用道具 举报

41#
 楼主| 发表于 2022-9-10 06:51:18 | 只看该作者
三个兵的情况,堆成了一个三角锥体,放在墙角。四个兵的情况,就没法再这样描述了。

在三个兵的锥体中,我们用层号来区分不同的层。

在四个兵的情况,它不是按层来分的,而是按 “锥体” 来分的。第一个锥体,就退化为一个元素。第二个锥体,是一个由 4 个元素组成的小锥体。第三个锥体,是一个由 10 个元素组成的一个边长为 3 个元素的小锥体。依次类推。这些锥体合起来,构成四维的超锥体。

从前面三维锥体的排列中,我们已经大致知道了其排列的规律。四维的情况,我们就不再罗列出每个情况了。因为我们已经明白,最关键的地方,三维的情况是要把层号求出来,四维的情况,就是把锥体号求出来。

现在进入正题。

跟三维的情况类似,四维超体中元素的最大编号 m 与锥体号 n 有如下关系:

m = (n+3)(n+2)(n+1)n/24

现在又变成一个数学问题,即,如何求解 n。
回复

使用道具 举报

42#
 楼主| 发表于 2022-9-10 07:56:08 | 只看该作者
上述四次方程碰巧能够求得精确解:

n = -1.5 + sqrt(1.25 + sqrt(1 + 24*m))

既然能得到精确解,那就不用再去估算了。而且,对于 m 不是某个 “锥体” 号中最大数的情形,只需对右边做 ceil 即可:

n = ceil ( -1.5 + sqrt(1.25 + sqrt(1 + 24*m)) )

然而这个地方就需要注意了。当我们手工计算能够得到准确的平方根的时候,我们很高兴,然而此时电脑反而有可能出现近似值。比如说,我们手工开平方得到了 3,而电脑却得到的是 3.00001,这可咋办?此时该不该用 ceil 函数得到 4 的最终结果?电脑就一根筋啊,它不知道 4 是错的呀。为了防止这种情况发生,我们可以稍稍改动一下上述表达式,让它稍微小一点:

n = ceil ( -1.5 + sqrt(1.25 + sqrt(24*m)) )

取 ceil 之后,并不影响最终结果。这样就比较安全了。
回复

使用道具 举报

43#
 楼主| 发表于 2022-9-10 09:20:38 | 只看该作者
前面的三次方程,其实也有精确解:

n = -1 + cbrt(3m + sqrt(9m² - 1/27)) + cbrt(3m - sqrt(9m² - 1/27))

此处 cbrt 是立方根的意思。

虽然有精确解,但是,它的计算量反而比较大,因为至少需要 3 次调用求根函数。我们前面采用的估计方法,反倒是更优的算法。

到目前为止,求解三次方程,是我们碰到的最难处理的一个环节了。不知道接下来的 5 次方程,情况会是怎样的。
回复

使用道具 举报

44#
 楼主| 发表于 2022-9-11 21:11:44 | 只看该作者
为了处理五次方程,查了很多资料。果然是难办的。


化简以后,我们其实需要处理的五次方程就是这个样子的:


x⁵ - 5x³ + 4x - 120m = 0


然而不幸的是,我未能找到这个方程的精确解。如果系数是这样的:


x⁵ - 5x³ + 5x - 120m = 0

那就有根式解了。可偏偏一次项系数不是 5,而是 4,那就没辙了。



我其实挣扎了一番,试图寻找求解方法,但最终还是失败了。


精确解没找到,能用估计的方法,求得一个近似解也行啊?


可是,估算的方法也不太顺利。


这个 5 次方程比 3 次方程复杂了不少,用估算的办法,也得消耗大量的时间去尝试。


今天我利用了一个在线画函数曲线的网站,来尝试画各种曲线图,以便帮助我判断,确定究竟啥样的曲线是合适的。终于有了一点希望。以下这个函数很关键,我迫不及待地把它记录下来:


-5*x^3+4*x+120+5*(x^5-5*x^3+4*x+120+5*(x^5-5*x^3+4*x+120)^(3/5))^(3/5) - 4*(x^5-5*x^3+4*x+120+5*(x^5-5*x^3+4*x+120)^(3/5))^(1/5)


这个函数有可能帮助我走向成功。还有下面这个函数,也是很奇妙的:

-5*x^3+4*x+120+5*(x^5-5*x^3+4*x+120+5*(x^5-5*x^3+4*x+120)^(3/5))^(3/5) - 4*(x^5-5*x^3+4*x+120)^(1/5)


我把它们放在这里,是不想让它们丢失,防止以后找不到它们了。




回复

使用道具 举报

45#
 楼主| 发表于 2022-9-11 21:48:39 | 只看该作者
通过初步的试验,这样一个表达式:

n = ceil(-2+(120*m+5*(120*m+5*(120*m)**0.6)**0.6-4*(120*m)**0.2)**0.2));

貌似是成功的。现在还没有从数学上严格证明。慢慢来吧,只要确实是成功的,应该也能从数学上证明。如果证明不了,就算能用,也不完美,而且,也放心不下。
回复

使用道具 举报

46#
发表于 2022-9-12 13:42:17 | 只看该作者
我喜欢象棋,上网下一局
回复

使用道具 举报

47#
 楼主| 发表于 2022-9-12 14:21:38 | 只看该作者
本帖最后由 不点 于 2022-9-12 14:23 编辑

上述 5 维锥体中元素号 m 与其所处 4 维锥体号 n 的关系为

n = ceil(-2+(120*m+5*(120*m+5*(120*m)**0.6)**0.6-4*(120*m)**0.2)**0.2));

本来想给出一个严格的证明,但是,当我尝试去证明的时候,发现这证明竟然异常困难。

尽管证明起来不容易,但我还是相信这个式子是对的。

下面考虑另一个问题:电脑计算时的误差。我验证了,当 n 值到达 500 左右那么大的时候,电脑计算产生的误差就可能导致 n 值被多算了 1 个。

不过,我们在解决 5 个兵的问题时,最大也只能碰上 n = 55 的情况(实际上只能碰上 n = 51 的情况)。因此,在我们的应用范围内,这个公式是安全的。公式不仅(针对电脑的计算误差而言)是安全的,而且(在我们的应用范围内)是正确的,这是因为对 400 以内的 n 值,我都是人工逐一验证过的,没问题的。也就是说,不管这个公式在理论上是否 100% 成立,它在我们具体的(较小的)应用范围内,是 100% 可靠的,其可靠性是由人工(穷举)验证来保证的。


下面是我验证时编写的程序,这是只有一行的程序,程序代码之中没有回车(或换行)符。这行代码可以复制粘贴到浏览器的控制台中直接(调试)执行。计算得到的四维锥号是未经 ceil 运算的值,便于观察它的变化规律。


for (let n = 1; n < 1000; n++) { let m = (n+4)*(n+3)*(n+2)*(n+1)*n; let p=m+120; console.log('真实四维锥号 n = ' + n + ', 计算得到的本锥最大元素的锥号: ' + (-2+(m+5*(m+5*m**0.6)**0.6-4*m**0.2)**0.2) + ', 计算得到的下一锥最小元素的锥号: ' + (-2+(p+5*(p+5*p**0.6)**0.6-4*p**0.2)**0.2)) ;}



计算的结果有 1000 行之多,就不适合贴在这里了。
回复

使用道具 举报

48#
 楼主| 发表于 2022-9-12 22:00:35 | 只看该作者
本帖最后由 不点 于 2022-9-13 07:53 编辑

现在给出五兵位置与整体编号互映射的两个函数,都已经过调试验证。

在两个函数之后的结尾处,附有简短的测试代码。



  1. <!DOCTYPE html>
  2. <html>
  3. <head> <meta charset="UTF-8">
  4. <script>
  5. //现在给出已知整体编号 SN 求五兵位置的函数

  6. //每个兵的位置有 55 种可能性,这 55 个位置的编号为 0~54。


  7. function get_五兵位置_from_整体编号 (SN) {
  8.     var A, B, C, D, E, t, m;

  9.     if (SN == 0) {
  10.         return [null, null, null, null, null]; // 五个兵都不存在
  11.     }
  12.     if (SN < 56) { // 56 = 1 + 55
  13.         return [SN - 1, null, null, null, null]; // 只存在一个兵
  14.     }
  15.     if (SN < 1541) {    // 1+55+55*54/2
  16.         //只存在两个兵
  17.         A = SN - 55; // 当前元素在当前层内部的编号, 从 1 开始
  18.         // 计算当前元素在当前层中的行号 B

  19.         t = 2 * A;

  20.         B = Math.ceil(Math.sqrt(t) - 0.5);

  21.         // 用 B 计算前一行最大元素在当前层内部的编号 m

  22.         m = B * (B - 1) / 2;
  23.         A -= m; // 当前元素在当前行内部的编号,从 1 开始

  24.         return [A-1, B, null, null, null]; // 双兵的位置
  25.     }
  26.     if (SN < 27776) {    // 1+55+55*54/2+55*54*53/6
  27.         //只存在三个兵
  28.         A = SN - 1540; // 当前元素在当前(三维)锥内部的编号, 从 1 开始
  29.         // 计算当前元素在当前(三维)锥中的层号 C

  30.         t = 6 * A;

  31.         C = Math.ceil((t + t**(1/3))**(1/3) - 1);
  32.         // 用 C 计算上一层最大元素在当前(三维)锥内部的编号 m
  33.         m = (C + 1) * C * (C - 1) / 6;
  34.         A -= m; // 当前元素在当前层内部的编号,从 1 开始
  35.         // 计算当前元素在当前层中的行号 B
  36.         t = 2 * A;

  37.         B = Math.ceil(Math.sqrt(t) - 0.5);

  38.         // 用 B 计算前一行最大元素在当前层内部的编号 m
  39.         m = B * (B - 1) / 2;
  40.         A -= m; // 当前元素在当前行内部的编号,从 1 开始

  41.         return [A-1, B, C+1, null, null]; // 三个兵的位置
  42.     }


  43.     if (SN < 368831) {  // 1+55+55*54/2+55*54*53/6+55*54*53*52/24
  44.         //只存在四个兵
  45.         A = SN - 27775; // 当前元素在四维超锥内部的编号, 从 1 开始

  46.         // 计算当前元素所在(三维)锥号 D

  47.         t = 24 * A;

  48.         D = Math.ceil ( -1.5 + Math.sqrt(1.25 + Math.sqrt(t)) );

  49.         // 用 D 计算上一锥最大元素在四维超锥内部的编号 m
  50.         m = (D + 2) *(D + 1) * D * (D - 1) / 24;
  51.         A -= m; // 当前元素在当前(三维)锥内部的编号,从 1 开始
  52.         // 计算当前元素在当前(三维)锥中的层号 C
  53.         t = 6 * A;

  54.         C = Math.ceil((t + t**(1/3))**(1/3) - 1);
  55.         // 用 C 计算上一层最大元素在当前(三维)锥内部的编号 m
  56.         m = (C + 1) * C * (C - 1) / 6;
  57.         A -= m; // 当前元素在当前层内部的编号,从 1 开始
  58.         // 计算当前元素在当前层中的行号 B

  59.         t = 2 * A;

  60.         B = Math.ceil(Math.sqrt(t) - 0.5);
  61.         // 用 B 计算前一行最大元素在当前层内部的编号 m
  62.         m = B * (B - 1) / 2;
  63.         A -= m; // 当前元素在当前行内部的编号,从 1 开始


  64.         return [A-1, B, C+1, D+2, null]; // 四个兵的位置

  65.     }



  66.         //五个兵都在
  67.         A = SN - 368830; // 当前元素在五维超锥内部的编号, 从 1 开始
  68.         // 用 A 计算当前元素所在(四维)超锥号 E
  69.         t = 120 * A;

  70.         E = Math.ceil ( -2 + (t + 5*(t+5*t**0.6)**0.6 - 4 * t**0.2)**0.2 );
  71.         // 用 E 计算上一(四维)超锥最大元素在五维超锥内部的编号 m
  72.         m = (E + 3)* (E + 2) *(E + 1) * E * (E - 1) / 120;


  73.         A -= m; // 当前元素在当前(四维)超锥内部的编号, 从 1 开始
  74.         // 用 A 计算当前元素所在(三维)锥号 D
  75.         t = 24 * A;

  76.         D = Math.ceil ( -1.5 + Math.sqrt(1.25 + Math.sqrt(t)) );

  77.         // 用 D 计算上一锥最大元素在四维超锥内部的编号 m
  78.         m = (D + 2) *(D + 1) * D * (D - 1) / 24;
  79.         A -= m; // 当前元素在当前(三维)锥内部的编号,从 1 开始
  80.         // 计算当前元素在当前(三维)锥中的层号 C

  81.         t = 6 * A;

  82.         C = Math.ceil((t + t**(1/3))**(1/3) - 1);
  83.         // 用 C 计算上一层最大元素在当前(三维)锥内部的编号 m
  84.         m = (C + 1) * C * (C - 1) / 6;
  85.         A -= m; // 当前元素在当前层内部的编号,从 1 开始
  86.         // 计算当前元素在当前层中的行号 B

  87.         t = 2 * A;

  88.         B = Math.ceil(Math.sqrt(t) - 0.5);
  89.         // 用 B 计算前一行最大元素在当前层内部的编号 m
  90.         m = B * (B - 1) / 2;
  91.         A -= m; // 当前元素在当前行内部的编号,从 1 开始


  92.         return [A-1, B, C+1, D+2, E+3]; // 五个兵的位置
  93. }



  94. //下面的函数是, 已知五兵位置,算出整体编号。

  95. function get_整体编号_from_五兵位置 (pos) {
  96.     var B, C, D, E, m;

  97.     if (typeof pos[0] !== 'number') {
  98.         return 0; // 五个兵都不存在
  99.     }
  100.     if (typeof pos[1] !== 'number') {
  101.         return pos[0] + 1; // 只存在一个兵位于 pos[0]
  102.     }
  103.     if (typeof pos[2] !== 'number') {
  104.         // 只存在两个兵位于 pos[0], pos[1]
  105.         m = 1 + 55;

  106.         B = pos[1]; // 在本层中的行号
  107.         //前一行的最大元素在当前层中的序号
  108.         m += B * (B - 1) / 2;

  109.         return m + pos[0];
  110.     }
  111.     if (typeof pos[3] !== 'number') {
  112.         // 只存在三个兵位于 pos[0], pos[1], pos[2]
  113.         m = 1 + 55 + 55 * 54 / 2;
  114.         C = pos[2] - 1; // 层号

  115.         //前一层的最大元素在当前(三维)锥中的序号

  116.         m += (C + 1) * C * (C - 1) / 6;
  117.         B = pos[1]; // 在本层中的行号

  118.         //前一行的最大元素在当前层中的序号
  119.         m += B * (B - 1) / 2;

  120.         return m + pos[0];
  121.     }
  122.     if (typeof pos[4] !== 'number') {
  123.         // 只存在四个兵位于 pos[0], pos[1], pos[2], pos[3]
  124.         m = 1 + 55 + 55 * 54 / 2 + 55 * 54 * 53 / 6;

  125.         D = pos[3] - 2; // 锥号
  126.         //前一(三维)锥中的最大元素在当前(四维)超锥中的序号
  127.         m += (D + 2) * (D + 1) * D * (D - 1) / 24;
  128.         C = pos[2] - 1; // 层号

  129.         //前一层的最大元素在当前(三维)锥中的序号
  130.         m += (C + 1) * C * (C - 1) / 6;
  131.         B = pos[1]; // 在本层中的行号

  132.         //前一行的最大元素在当前层中的序号

  133.         m += B * (B - 1) / 2;

  134.         return m + pos[0];
  135.     }
  136.         // 五个兵都在
  137.         m = 1 + 55 + 55 * 54 / 2 + 55 * 54 * 53 / 6 + 55 * 54 * 53 * 52 / 24;
  138.         E = pos[4] - 3; // 超锥号
  139.         //前一(四维)超锥中的最大元素在当前五维超锥中的序号
  140.         m += (E + 3) * (E + 2) * (E + 1) * E * (E - 1) / 120;
  141.         D = pos[3] - 2; // 锥号
  142.         //前一(三维)锥中的最大元素在当前(四维)超锥中的序号
  143.         m += (D + 2) * (D + 1) * D * (D - 1) / 24;
  144.         C = pos[2] - 1; // 层号
  145.         //前一层的最大元素在当前(三维)锥中的序号
  146.         m += (C + 1) * C * (C - 1) / 6;
  147.         B = pos[1]; // 在本层中的行号
  148.         //前一行的最大元素在当前层中的序号
  149.         m += B * (B - 1) / 2;


  150.         return m + pos[0];
  151. }


  152. // 以下是测试代码

  153. for (var i = 0; i < 3847592; i++) {

  154.         var pos = get_五兵位置_from_整体编号 (i);
  155.         var n = get_整体编号_from_五兵位置 (pos);
  156.         if (n != i) {
  157.                 console.log('出错! ' + i + ' ==== ' + pos + ' ==== ' + n);
  158.         } else
  159.         if ((i & 4095) == 0) {
  160.                 console.log('------ ' + i + ' ==== ' + pos + ' ==== ' + n);
  161.         }
  162.         //if (i > 30000) break;
  163. }
  164. console.log('--OK-- ' + '--last--' + ' ==== ' + pos + ' ==== ' + n);

  165. </script>

  166. </head>
  167. <body>

  168. </body>
  169. </html>

复制代码
回复

使用道具 举报

49#
 楼主| 发表于 2022-9-17 10:14:03 | 只看该作者
局面用 140 位二进制数表示,在 js 中可以用 bigint 来表示。那么,库的结构,也就容易设计了。为了方便使用,库用数组来表示。数组中,每个元素也是一个数组,这个数组的第一个元素就是局面的 bigint 表示,第二个元素,就是着法的字符串表示。库的数组元素按照局面 bigint 的大小排序。
回复

使用道具 举报

50#
发表于 2022-9-17 11:10:19 | 只看该作者
看完知道LZ为什么要浏览器js读写本地文件的权限了。
原来是在写js程序

点评

感谢,多谢理解。  详情 回复 发表于 2022-9-17 11:43
回复

使用道具 举报

51#
 楼主| 发表于 2022-9-17 11:43:38 | 只看该作者
本帖最后由 不点 于 2022-9-17 11:59 编辑
2010techon 发表于 2022-9-17 11:10
看完知道LZ为什么要浏览器js读写本地文件的权限了。
原来是在写js程序

感谢,多谢理解。


碰上一个能明白这个问题缘由的人,不容易。我似乎得多说几句,再解释一下。此处的 js,貌似是读取、写入本地文件,其实不然。其实是要读取和写入与 js 文件处于同一电脑上的文件。也就是说,电脑本身是处于 “服务器” 的地位,index.html 相当于是服务器上的网页。此时,js 和 html 都相当于是在服务器上,而不是所谓的“本地”。此时,js 本应有权读取和写入本机上的文件。如果 js 和 html 是在别的网站上(而不是在本机上),那自然是应该予以禁止访问的。但现在不是那样的情况,所以不应该禁止访问。现在 js 和 html 都是由本机提供的,不存在 http 访问互联网的问题,所以根本不存在所谓 “跨域” 访问的问题。以“跨域”访问作为理由,来禁止 js 应用程序的正常运作,其性质是“封杀”,这种目的,我认为属于流氓行为。





点评

大多数浏览器是都是属于BS应用,你的需求是要一个本地浏览器。 我记得chrome和ff是可以通过参数或者扩产来支持跨域js访问的  详情 回复 发表于 2022-9-17 12:10
回复

使用道具 举报

52#
发表于 2022-9-17 12:10:57 | 只看该作者
不点 发表于 2022-9-17 11:43
感谢,多谢理解。

大多数浏览器是都是属于BS应用,你的需求是要一个本地浏览器。
我记得chrome和ff是可以通过参数或者扩产来支持跨域js访问的

点评

你的记忆貌似停留在所谓 “旧时代”。新版已经完全封杀了,不再给你任何机会了。而且,封杀行为是统一的,而不是某个浏览器单独行动。 如果某个浏览器单独行动,那还达不到封杀的目的。因为只要有一个浏览器不封  详情 回复 发表于 2022-9-17 12:29
回复

使用道具 举报

53#
 楼主| 发表于 2022-9-17 12:29:46 | 只看该作者
2010techon 发表于 2022-9-17 12:10
大多数浏览器是都是属于BS应用,你的需求是要一个本地浏览器。
我记得chrome和ff是可以通过参数或者扩产 ...

你的记忆貌似停留在所谓 “旧时代”。新版已经完全封杀了,不再给你任何机会了。而且,封杀行为是统一的,而不是某个浏览器单独行动。

如果某个浏览器单独行动,那还达不到封杀的目的。因为只要有一个浏览器不封杀,就相当于开了 “漏子”(后门),就不能形成有效封杀之势。 只有统一封杀,才是真正的封杀。现在,我粗略感觉,貌似是已经进行了大面积的封杀了,至于说有没有漏网的,我也没有时间和精力去逐一验证。

点评

还在用XP从不追新的我就没这么多烦恼  详情 回复 发表于 2022-9-17 13:38
回复

使用道具 举报

54#
发表于 2022-9-17 13:38:42 | 只看该作者
不点 发表于 2022-9-17 12:29
你的记忆貌似停留在所谓 “旧时代”。新版已经完全封杀了,不再给你任何机会了。而且,封杀行为是统一的 ...

还在用XP从不追新的我就没这么多烦恼

点评

随着旧电脑坏掉、老去,XP 也就灭亡了。同理,win7、8、10 都会灭亡的,只剩下 11 可以活。据说 12 也出来了,我还没赶上,现在在用的是 11。 11 有很多毛病,正准备退回 7。 XP 的优势是安装后体积占用比较小  详情 回复 发表于 2022-9-17 14:43
回复

使用道具 举报

55#
 楼主| 发表于 2022-9-17 14:43:40 | 只看该作者
2010techon 发表于 2022-9-17 13:38
还在用XP从不追新的我就没这么多烦恼

随着旧电脑坏掉、老去,XP 也就灭亡了。同理,win7、8、10 都会灭亡的,只剩下 11 可以活。据说 12 也出来了,我还没赶上,现在在用的是 11。

11 有很多毛病,正准备退回 7。

XP 的优势是安装后体积占用比较小。还有一个优势,就是你说的情况,死不更新,不再有软件的恶意更新带来的烦恼了。


我已经不再使用 XP 了,已经适应了 7 带来的各种不自在。寄望将来有个好用的 Linux、BSD 之类的系统能够在办公环境普及开来。


回复

使用道具 举报

56#
 楼主| 发表于 2022-9-17 20:14:45 | 只看该作者
中国象棋 in html5 的 基础算法逻辑仍然含有漏洞。


这个图片,红方跳马将军,黑方认为将死了,跳出 “你赢了” 的提示。其实黑方还能垫车挣扎。

可以垫车.png (450.76 KB, 下载次数: 21)

AI不知道垫车可以解杀

AI不知道垫车可以解杀
回复

使用道具 举报

57#
 楼主| 发表于 2022-9-27 07:42:07 | 只看该作者
一个程序写得 “好” 还是 “坏”,那要看这个程序是给谁看了。评判 “好” 与 “坏” 的标准,就掌握在这个 “来看和研究这个代码” 的人手上。

好吧,各位猜到了,是我想发表评判结果了。首先,中国象棋 in html5,写得好。为什么好?因为我能看懂。如果看不懂的话,那怎么可能说 “好” 呢?

你是你们公司的一个领导,基层有一个技术项目的研究者向你汇报。他为了让项目通过验收,就设法用所谓 “新技术”、“新概念”,把项目吹成 “高、大、上”。一句话,他忽悠你,把你带到阴沟里,让你变成傻子,彻底不懂了,那么他就达到目的了。

回到正题。

中国象棋 in html5 的代码,我能看懂,至少是初步可以看懂,而有些暂时没看懂的,还可以继续琢磨。所以,我说,代码很棒!

看懂以后,发现程序中有一些毛病。有些是明显的毛病,比如,相的价值,对方边象的价值为零,己方边相价值为 18,这显然不对。我就把它更改为:都是 18。

己方马在己方车原位的时候,竟然价值为 88,超过在马原位的 85,这不像是手误,而可能是有某种道理的。但我目前想不出是啥道理,所以,我就改小这个数,变成 82。

另外,AI 走棋的时候,总喜欢弃掉自己未过河的兵,这在大多数情况下都是错的。究其原因,我觉得是兵的价值给的太少了,只有 10 左右。如果加大兵的份量,AI 就不会轻易弃掉自己的兵了。
回复

使用道具 举报

58#
 楼主| 发表于 2022-9-28 14:18:04 | 只看该作者
这个图片,暴露出代码的一个 bug。


跟踪执行后发现,刚过河的卒不能横走,只能向前。这是个 bug。究其原因,卒的行棋规则里面,y >= 6 应为 y>= 5 才是正确的。


可以垫卒.png (388.41 KB, 下载次数: 10)

可以垫卒挣扎,却宣布“你赢了”

可以垫卒挣扎,却宣布“你赢了”
回复

使用道具 举报

59#
 楼主| 发表于 2022-10-1 08:22:56 | 只看该作者
这个图,我跳左马去将军,对方竟然用黑马吃掉我的马!AI 不知道黑马的任务是蹩另一个红马的脚,不可以离开。


这个 bug 也跟踪到了,发现了代码的错误:在马的走棋规则中,play.map 应为 map 才对!


蹩脚马还敢走开.png (452.38 KB, 下载次数: 7)

红方跳左马将军,黑方用黑马吃掉红马,不知道此黑马需要蹩另一个红马的脚

红方跳左马将军,黑方用黑马吃掉红马,不知道此黑马需要蹩另一个红马的脚
回复

使用道具 举报

60#
发表于 2022-10-2 11:28:14 | 只看该作者
漏洞肯定有的吧
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

小黑屋|手机版|Archiver|捐助支持|无忧启动 ( 闽ICP备05002490号-1 )

闽公网安备 35020302032614号

GMT+8, 2025-12-12 02:43

Powered by Discuz! X3.3

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表