每日简讯:保存象棋棋盘信息,需要多少比特?我只用139-167位二进制
如何存储当前棋局
方案有3种:
象棋一共32个棋子,每个棋子有91种状态:死亡或位于0-89中任一位置。所以用长度为32的列表即可,每个数的值域是0-90,其中90代表死亡。死亡的棋子不再占用空间,使用类似map的结构,key是棋子id,value是棋子位置(0-89)。压缩空间的方案:将帅个子有9个可能在的位置,只需要0-9即可表示,需要至多5位二进制。士有5种位置,每个士只需要至多3位二进制。以此类推……占用空间最少。分析方案一:数组长度为32,每个数组项目是个uint8,总共8 * 32 = 256 位。
(资料图片)
分析方案二:在棋子多的时候,占用空间较多,所以存储空间的大小不太稳定。
方案三占用空间少,但是开发成本也较高,需要开发者去拼接二进制位。
今天我们探讨方案三。
前缀码
引入一个概念:Prefix-Free Code
,也可以叫 Prefix Code
,它来源于信息论学科,维基百科:en.wikipedia.org/wiki/Prefix… 描述如下:
A prefix codeis a type of code system distinguished by its possession of the "prefix property", which requires that there is no whole code word in the system that is a prefix (initial segment) of any other code word in the system. It is trivially true for fixed-length code, so only a point of consideration in variable-length code.
For example, a code with code words {9, 55} has the prefix property; a code consisting of {9, 5, 59, 55} does not, because "5" is a prefix of "59" and also of "55". A prefix code is a uniquely decodable code: given a complete and accurate sequence, a receiver can identify each word without requiring a special marker between words. However, there are uniquely decodable codes that are not prefix codes; for instance, the reverse of a prefix code is still uniquely decodable (it is a suffix code), but it is not necessarily a prefix code.
它举了个例子,针对集合{9, 5, 59, 55}就不是 prefix code,因为「5」有二义性,遇到5后,不知道该结束流程,还是继续读取后面的9或5。
哈夫曼编码 Huffman Coding
信息论中有个经典问题:给定一篇文章,如何用最短的二进制编码它。
解决方案就是:找出出现的所有单词集合(例如:I am good good good,出现了3个单词),计算每个单词出现频率,以某种方式,构造每个单词对应的二进制编码,满足条件:基于前缀就能知道它代表哪个单词。然后我们把这些前缀拼在一起,就成功编码了(并且是可以解码的)。
例如这种编码 good = 0, I = 10, am = 11,文章就表示为1011000。
这是最简短的编码了。构造方法就是通过构造一颗哈夫曼树,算法如下:
针对每一个单词(或组合),都有一个对应的频数,作为频数表。如果当前只有1个,就进入4,否则进入2。找到频数最低的2个,作为表示一个组合,他们对应的频数就是两个单词(或组合)的频数之和,加入频数表(同时删除这2个单词或组合各自的频数)。选取的2个单词(或组合),分别作为左子树和右子树,组成一个树。进入1。现在得到了一个二叉树(叫做哈夫曼树),每个叶子结点代表一个单词。规定左分叉为0,右分叉为1,这个单词对应的Prefix Code
就是根节点到它的路径。例如上述编码对应的哈夫曼树就是:
对于象棋的启发
回到象棋棋盘状态的问题:
将帅有10个位置(包括死亡状态)。士有6个位置(包括死亡状态)。象有8个位置(包括死亡状态)。马有91个位置(包括死亡状态)。车有91个位置(包括死亡状态)。炮有91个位置(包括死亡状态)。兵有48个位置(包括死亡状态)。不妨假设他们出现在各个位置的频率都一致,不难构造出对应的编码。(这样的编码是比较稳定的,无论棋局变成什么样子,存储占用空间都不会太大)
10个位置,需要3-4位。6个位置,需要2-3位。8个位置,需要3位。48个位置,需要4-5位。91个位置,需要6-7位。这样算下来,保存一个象棋的棋子位置信息,最少需要:
(3+2*2+3*2+6*6+4*5)*2=138
位,再用1位保存该谁下棋了,总共至少需要139位。至多需要(4+3*2+3*2+7*6+5*5)*2=166
位,再用1位保存该谁下棋了,总共至多需要167位。
有办法实现吗?
上面说的很理想,如何实现呢?
我们以10个位置的情况,来探讨通用的编码生成方法。首先根据哈夫曼树,可以构造这样的编码:
000代表0001代表1010代表2011代表3100代表4101代表51100代表61110代表71101代表81111代表9随后容易发现这样的规律:
至于0-5,用3位二进制编码即可。至于6-7,我们需要在3位的6(110)
和7(111)
末尾新增0。至于8-9我们需要在3位的6
和7
末尾新增1。可以利用数学归纳法,归纳总结出这样的算法:
针对X个位置的情况,计算Log2(X),分别向下取整和向上取整,得到A和B。如果A=B,则用A位二进制表示这X个数即可,直接转换进制。如果A0至2^A-1-(X-2^A)
;用B位二进制表示其它位置;针对位置2^A-(X-2^A)
至2^A-1
,编码为A位的进制转换,并在末尾拼接一位0
(共计B位);针对其它位置,编码为位置减去(X-2^A)
再转换二进制,并在末尾拼接一位1
(共计B位)。可以发现,这种算法,位置编号小的比位置编号大的少了一位。也就是说,我们应该尽量把出现频率较高的位置放在前面。
生成各棋子的位置列表
const RedAllCandidates = new Array(90).fill(0).map((a, i) => 89 - i);const BlackAllCandidates = new Array(90).fill(0).map((a, i) => i);const RedSoliderCandidates = new Array(45).fill(0).map((a, i) => 44 - i);const BlackSoliderCandidates = new Array(45).fill(0).map((a, i) => 45 + i);// 分别是将、士、士、……const PieceCandidates = [ [85, 86, 84, 76, 77, 75, 67, 68, 66, 127], [127, 86, 84, 76, 68, 66], [127, 84, 86, 76, 66, 68], [127, 87, 67, 71, 51, 83, 47, 63], [127, 83, 67, 63, 47, 87, 51, 71], [127, ...RedAllCandidates], [127, ...RedAllCandidates], [127, ...RedAllCandidates], [127, ...RedAllCandidates], [127, ...RedAllCandidates], [127, ...RedAllCandidates], [127, 62, 53, ...RedSoliderCandidates], [127, 60, 51, ...RedSoliderCandidates], [127, 58, 49, ...RedSoliderCandidates], [127, 56, 47, ...RedSoliderCandidates], [127, 54, 45, ...RedSoliderCandidates], [4, 3, 5, 13, 12, 14, 22, 21, 23, 127], [127, 3, 5, 13, 21, 23], [127, 5, 3, 13, 23, 21], [127, 2, 22, 18, 38, 6, 42, 26], [127, 6, 22, 26, 42, 2, 38, 18], [127, ...BlackAllCandidates], [127, ...BlackAllCandidates], [127, ...BlackAllCandidates], [127, ...BlackAllCandidates], [127, ...BlackAllCandidates], [127, ...BlackAllCandidates], [127, 27, 36, ...BlackSoliderCandidates], [127, 29, 38, ...BlackSoliderCandidates], [127, 31, 40, ...BlackSoliderCandidates], [127, 33, 42, ...BlackSoliderCandidates], [127, 35, 44, ...BlackSoliderCandidates],];
解释:
我可以把将帅的「死亡」(127)调整到了最后一位,因为他们死亡是非常罕见的,这样可以节约2bit空间。我刻意把棋子常见位置放在了数组前几位,尤其是将帅、士、兵,这样可以节约几bit空间。兵的位置,红色和黑色不同,刚过河的一排放在前面,离河远的位置放在后面,可以节约几bit空间。提前计算log
为了提高效率,我应该避免在JS中计算Math.log2,而要提前定义好运算结果。
const ceilLog2Map = new Map([ [1, 0], [2, 1], [3, 2], [4, 2], [6, 3], [8, 3], [10, 4], [17, 5], [48, 6], [91, 7],]);const floorLog2Map = new Map([ [1, 0], [2, 1], [3, 1], [4, 2], [6, 2], [8, 3], [10, 3], [17, 4], [48, 5], [91, 6],]);
按照编码规则encode
基于文章《JS 按自定义格式 拼接二进制串 解析二进制串》提到的concatBits
函数,我写了concatFlexibleBits
函数:
function concatFlexibleBits(current: number, offset: number, candidateIndex: number, candidateLength: number): [number, number, number[]] { const floorLog = floorLog2Map.get(candidateLength)!; const ceilLog = ceilLog2Map.get(candidateLength)!; const last = 2 ** floorLog; const beyond = candidateLength - last; if (floorLog === ceilLog || candidateIndex < last - beyond) { return concatBits(current, offset, candidateIndex, floorLog); } let newCurrent = current; let newOffset = offset; const array: number[] = []; let newUint8: number[]; if (candidateIndex < last) { [newCurrent, newOffset, newUint8] = concatBits(newCurrent, newOffset, candidateIndex, floorLog); array.push(...newUint8); [newCurrent, newOffset, newUint8] = concatBits(newCurrent, newOffset, 0, 1); array.push(...newUint8); } else { [newCurrent, newOffset, newUint8] = concatBits(newCurrent, newOffset, candidateIndex - beyond, floorLog); array.push(...newUint8); [newCurrent, newOffset, newUint8] = concatBits(newCurrent, newOffset, 1, 1); array.push(...newUint8); } return [newCurrent, newOffset, array];}
这里encode规则,就是按照上面提到的算法实现的。不过多解释了。
按照编码规则decode
基于文章《JS 按自定义格式 拼接二进制串 解析二进制串》的readBits
函数,我写了readFlexibleBits
函数:
function readFlexibleBits(array: Uint8Array, bitsOffset: number, candidateLength: number) { const floorLog = floorLog2Map.get(candidateLength)!; const ceilLog = ceilLog2Map.get(candidateLength)!; const last = 2 ** floorLog; const beyond = candidateLength - last; const [number, offset] = readBits(array, bitsOffset, floorLog); if (floorLog === ceilLog || number < last - beyond) { return [number, offset]; } const [current, offset2] = readBits(array, offset, 1); if (current) { return [number + beyond, offset2]; } return [number, offset2];}
这里decode规则,是按照上面算法解析的。先读取floorLog
位,如果总位置数就是2的次方,则结束。如果读取到的数比较小,也结束。如果读取到的数超过某个临界值,就需要多读取一位,决定它代表谁。
结论
方案三可以实现,并且比方案一节约了35%-45%的空间。
关于性能:编码、解码逻辑全都位于用户浏览器中执行,对服务器无影响,浏览器也会在人感知不到的耗时内运算完。
有什么用?
我在开发《象棋》时,期望通过URL来分享棋局。我希望分享的URL能永久有效,而且不喜欢给服务器太多债务(不采用token+数据库存储棋盘信息)。那么URL中必须包含完整的棋盘信息。
如果把棋盘信息存到URL中,那么URL越短,越好。
例如:game.hullqin.cn/xq?p=gSQCL5P5oIDhCFJCIBJ4eQCAkX8&r=86pU6-4nbSh38OCojLarcupWOb1rXw&s=1 ,点开后就能立马展现某场对局。
这个URL里,保存了棋盘所有棋子信息、所有历史记录(10个回合即20步)。方便大家保存、分享。
保存历史记录,也是通过类似的手段实现的,占用空间非常小(长度两百的字符串,足够存储大部分常规对局的历史记录)。
欢迎观看 【可以「旋转棋盘」的联机象棋】 - 哔哩哔哩
写在最后
我是HullQin,公众号线下聚会游戏的作者(欢迎关注我,交个朋友)。转发本文前需获得作者HullQin授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩UNO、飞行棋、斗地主、五子棋、一夜狼、狼人杀、象棋、德国心脏病、达芬奇密码等游戏,不收费无广告。还开发了《Dice Crush》参加Game Jam 2022。喜欢可以关注我噢~我有空了会分享做游戏的相关技术,会在这个专栏里分享:《教你做小游戏》。
标签:
精彩推送
环球今头条!周润发电视剧大全列表池女_周润发电视剧大全
1、1980年:《上海滩》饰许文强1981年:《苏乞儿》饰苏灿1982年:《播音人》饰韦业昌1982年:《孤城客》饰
湖滨街道招商与建设同步抓 一站式采购生活综合体力争今年迎客|天天日报
4月18日上午,记者在坐落于长福商城A区的湖滨街道重点项目——海润家居生活广场看到,商场装修正在紧锣...
亚通精工:公司严格遵守上市规则,公司股价波动受市场资金供求、行业整体情况和投资者情绪等多种因素影响
亚通精工(603190)04月21日在投资者关系平台上答复了投资者关心的问题。
新闻快讯
X 关闭
X 关闭
新闻快讯
- 每日简讯:保存象棋棋盘信息,需要多少比特?我只用139-167位二进制
- 焦点快看:2023年郑州市精品剧目演出活动参与指南(持续更新)
- 2023年全国青少年网球积分排名赛暨中国青少年网球巡回赛贵阳站开赛
- 优科豪马ADVAN Sport V107轮胎为BMW M系新款M3 Sedan和M4 Coupe提供新车配套 关注
- 苹果a12处理器参数_苹果a12处理器_天天热门
- 零点有数所投千匠网络获“数字科技优秀企业”奖_全球关注
- 8进4生死战!媒体人:广厦前锋朱俊龙确认今晚战广东复出_时讯
- 环球热头条丨七夕送女朋友什么礼物好呢_七夕送女朋友什么
- 当前关注:“天才医生”陶勇返赣谈人生
- 迪士尼公司计划本周裁员数千人 包括娱乐部门约15%的员工-环球最资讯
- 环球即时:违规收受礼品礼金等问题 蚌埠三名干部被通报处分
- 星环+素皮,realme11真机曝光-世界快看
- 当前观察:魅族与Polestar合作车机系统,为SUV极星4提供Flyme Auto Core
- 全球观速讯丨在骑行中领略风土人情 2023年“凤凰杯”上海湾区自行车定向赛举行
- 4月楼市原形毕露,住建部长喊话“房地产是顶梁柱”,什么信号?_天天讯息
- 孔集卤鸡
- 热血传奇最高等级多少_热血传奇最高等级-焦点热闻
- 唐山这些老旧小区要改造!另华北理工大学附属医院要建花海院区!_天天要闻
- 定了!通州21个老旧小区将改造,这2个小区弯道超车杀进名单
- 警方回应公开色狼姓名:合规 满满干货-世界资讯
- 送呈台启是什么意思 格式(送呈台启是什么意思)-视讯
- 《最后的生还者》PC版1.0.4版升级补丁将于下周发布
- 5500元游戏电脑配置推荐,R5 7600和i5 13490F电脑配置二选一
- 笑果落户北京隆福寺,打造多元化演艺新空间|最新快讯
- 中国驻英国使馆回应所谓“海外警察站”_当前头条
- adp+cp_ADP数据是什么意思_天天热点
- 甲基丙烯酸叔丁酯商品报价动态(2023-04-22)_每日热门
- 司法房产拍卖价是否等于市价_司法房产拍卖流程 全球快看
- 广西多地今晚将迎较强降雨 局地伴有强对流天气
- 短消息网关
- iphone自动关机怎么调_iphone自动关机
- “以赛促学”提升青年职工技能水平-今日看点
- 天天速读:厚植鱼米之乡文化底蕴,挖掘保护农业文化遗产,“农遗良品”优选计划名单发布
- 全球信息:花开等你 闻着花香游鸡西(下)
- 康美镇美山村志愿服务队
- 神农谷第一届民俗文化节举行
- 【当前独家】五一游火爆,多地中心区域难寻500元以下快捷酒店
- 以青春之名,向亚运出发!杭城青年毅行大会今日热闹开走 全球资讯
- 长盈通:营收同比增长20%,一体化产业链驱动业绩全面提升
- 达嘉维康(301126)3月31日股东户数1.69万户,较上期减少0.91%
- dunhill香烟价格表爆珠_dunhill香烟价格表_全球播报
- 股东户数最新变动:首都在线(300846)股东户数5.2万户,较上期增加38.18%-当前关注
- 爱奇艺泡泡圈断签了怎么办_爱奇艺泡泡圈官网 天天时讯
- 记得忘记歌词
- 第五届“中国华服日”活动多地联动举办 每日看点
- 标普将英国主权信用评级展望从负面上调至稳定 速读
- 杭甬高速复线宁波一期项目东段主线全幅贯通
- 转发 世界动态
- 全球减持美元 加快人币国际化
- “跳出”书本,苏州太平金澄社区让小蝌蚪“游”进科普小课堂 全球今热点