本帖最后由 Kagamia 于 2023-5-13 01:07 编辑
窝是一个高产似那啥的新人
前情提要
最原始的题目出处在这里,是一个QQ好友问题。
好吧,就是我自己!我今天要做自己的破壁者!
题目如下:jianshu/p/831f57c7458b
挺无厘头的一个字符串,但是很显然(教科书参考答案的常见用语),把这个字符串补全,你应该获得这样一个网址:
https://www.jianshu.com/p/831f57c7458b
完整的题目就在那里。
如今我是永远不会登录简书了,首先我不理解为什么它突然强制让我绑定手机号微信号;其次它的弹窗广告太多了,用户阅读体验极差。
但是我也不想把题目复制过来,篇幅太长了,烦劳您点击一下吧。
这道题目来自力扣https://leetcode.com/problems/count-and-say/,它的原始题目是求每一行的字符串本身,而我简化了问题,你只需要告诉我长度即可。
但是这个长度需要到2^35,大于32GiB。
想象中,如果生成第n行,你需要知道第n-1行,如果第n行要占32GB,那第n-1行大约也要32GB。理想地,这道题目可以在一台拥有64GB内存的机器上轻松计算出来。
放在现在,内存已经十分廉价,论坛中很多小伙伴的个人工作站都可以满足这个要求。
不过这道题目是5年前出的,当然5年前你也可以为了解这道题目,使用虚拟内存替代内存,使用外存替代内存,使用公家的内存,使用云主机搞这么多内存,whatever,不用思考地硬算,总有一天可以算出来。
目前真正算出来的群友大概少于5名,而我们亲爱的A5因为懒得计算,通过114514个宅币把我骗进论坛作为交易,成功by-pass了我的好友验证。
此时此刻,你算出来也不好使了。我已经准备把这道题目从QQ好友问答中清除掉了。
当下一个人成功计算出来的时候,我就会设计思索下一道题目。
如果你遇到了诸如算力不够、内存不够、复杂度太高等困难,这里我会给你一些非常非常概括性的提示。
我曾经实现过它(要不然我自己是怎么设置答案的),但是代码丢了,所以我就不放代码了。
或者,等我病好了在重新写一份实现,以飨诸君。
解题提示
使用任何你熟悉的语言,在最新的CPU上,你应该很容易瞬间计算S(78)之前的结果。
即使你使用vb,或是python,算个S(50) ~ S(70)应该不是什么难事
能够正确地编程实现,就算过了第一关。
但是到了后面,你会体验到这么几个问题:
- 每一行长度明显呈现指数级递增趋势
- 所以计算时间也是指数
- 所以内存占用也是指数
到了一个阈值,你的计算机程序或耐心至少有一个会崩溃。
即便如此,收集前面的每一行长度,你能获得这个指数底数的一个非常精确的解,甚至能精确到5位有效数字。这是hint1。
这件事很重要。为什么呢?因为它可以用来估算目标的n大致是多少,以便于你可以选择及时存档、分布式计算等策略。
在这个精度下,你估算出n的误差不会超过1。能够想到这一层,就算是过了第二关。
经过了长时间的鼓捣,你会发现每一行只会出现123三种数字,0和4都不会出现。你会想到使用位域压缩来解决内存问题。
又经过长时间的鼓捣,你会发现123这几种数字产生的组合序列也是有限种,你会根据已有数据构建一个状态机,遇到固定序列查表跳转,来解决算力不足问题。
但这都只压缩了常量系数,你依然可能卡在指数成长的复杂度上,有没有“质的飞跃”般的优化方法呢?
下面一件事就是我思路的重点:
你一定要一层层计算吗?
其实仔细想想,这件事大可不必。
我打个比方,比如说,你想写一个杨辉三角,我们使用左对齐方式,写出来大概这样
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
...
似乎按行写出来非常顺畅。
但是你有没有想过,如果指定让你写6行就OK,你是不是,其实,可以竖着写?
而Count and say这道题目,其实你也可以竖着写。和杨辉三角不同的是,因为指数级长度的出现,竖着写会越写越长。
比如,我们从第5行“111221”开始竖着写,写到第8行,大概顺序是这样的。
这张图画的比较草率,箭头的源是单元格,目的是红色方块,excel自带单元格用于相同数字组合。
按你胃(anyway),如果看不懂就自己用自己喜欢的格式推导一遍,你立刻就理解为什么它可以竖着来。
为什么“可以纵向推导”这件事很重要呢?
- 这个算法可以适当并行,第二列底部没推导完,第三列顶部其实可以并行开工了,只要斜向箭头已构造出来就可以不停地链式计算。
但你会说按行计算也可以并行吖,就是越往后越费内存,所以做不了并行。
那么请看下一个理由。 - 纵向推导意味着,当你推导完一列后,黄色已完成区域,就可以从内存中丢掉了,你只要记录一个累计长度就够了。
这样每一行只剩下“已处理长度”和“待处理尾部数据”是有意义的,而这部分数据,很短,甚至也就占10字节,连6502的寻址空间都足够把它跑完了。
最后,这种实现的前提是,你需要提前指定目标层数,才能放心大胆地舍弃数据,如此一来,前面的思考就全都没有白费。
参考实现
比较接近真实的场合,我没有使用刚才手算的这么极端的纵向计算方法。
因为在多线程下,运算单元太小而锁太多,导致线程调度成本过高。
对应地,我其实使用“块”作为运算单元,使用“协程”让每一层计算可被随时中断,使用“队列”向下一层传递数据,使用“阻塞管道”传递数据就自动实现了中断。
噢,这听上去是不是适合用go实现?
是的,我就是用c#实现的。c#有自动编译成协程的yield和await,有从go那里inspire来的channel,有轻量级工作单元Task,零件齐全。
我只要实现这样一系列函数就够了:
- struct Worker {
- public int LayerID;
- public long ProcessedLength;
- public byte LastChar;
- public byte LastCharLength;
-
- void HandleBlock(Span<byte> b);
- void HandleFinalBlock(Span<byte> b);
- }
复制代码
最后用channel把全部worker串连起来并全部启动,剩下的就是找点游戏消磨时间了。
在5年前的配置上,我大概使用400MB左右的内存,使用4c8t的4代酷睿cpu,10分钟左右完成了答题任务。
放到现在的配置上,结合块匹配、查表修改状态等优化方式,可能一杯茶功夫计算S(100)也不在话下。
如果你有更好的方案,欢迎留言回复,交流使我成长。 |