乍听之下,不无道理;仔细揣摩,胡说八道

0%

大学的时候挺喜欢解Project Euler上的题目的,尤其是它不在乎答题者使用哪一门编程语言,甚至还有很多参与者是使用pen&paper来解题的。去年开始重新开始做Project Euler上的题目,而第69题则是最近刚刚解决的一题。惭愧的是,因为不晓得欧拉函数的计算公式(甚至都没有想过欧拉函数有没有可以用来计算的公式),所以这一题我是用暴力计算的方法来解决的。尽管花了40分钟左右才找出了问题的答案,但欧拉函数的计算方法本身还是让我觉得挺有意思的,下面我就来讲讲我在计算欧拉函数方面做的一些尝试。

69题本身很容易读懂,就是要找到一个不大于一百万的正整数n,这个n与以它为参的欧拉函数值的比值能够达到最大。欧拉函数的介绍可以看维基百科,简而言之,就是在不大于n的正整数中与n互质的数的个数,具体的例子可以参见69题描述中给出的表格。

网上可以找到别人的解法,基本的思路是:按从小到大的顺序,对于一百万以内的每个素数,都计算出它们的倍数的欧拉函数值的一部分——即对于素数p计算出1-1/p并与这个位置上原来的值相乘。当遍历完一百万以内的所有素数后,也就计算出每一个位置上的欧拉函数值,再遍历一次就可以计算出比值最大的数字了。

但我今天要讲的是笨方法。

笨方法的关键就是乖巧地计算出每一个数字的欧拉函数值。而其中最笨的,当属挑出每一个不大于n的因数,计算它们与n的最大公约数,并根据这个最大公约数是否为1,来决定是否给某一个计数器加一。示例代码如下

1
2
3
4
5
(defun phi (n)
(let ((count 0))
(dotimes (i n count)
(when (= (gcd (1+ i) n) 1)
(incf count)))))

这个phi可以稍微改进一下。例如,如果一个数a与n不是互质的,那么a的倍数(小于n的那一些)也一定不会与n互质。因此,每当遇到这么一个因数a,就知道后续的2a3a等等,都不再需要计算其与n的最大公约数了。改进后的算法代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(defun phi (n)
"通过将不互质的比特设置为1并计算为0的比特的个数来计算phi函数"
(let ((bits (make-array n :element-type 'bit :initial-element 0))
(count 0))
(dotimes (i n)
(cond ((= (bit bits i) 1)
;; 该比特已经为1,说明已经在比它小的倍数被处理时一并被标记了
)
((= i (1- n))
;; 只处理比上界要小的数字
)
((/= (gcd (1+ i) n) 1)
;; 除了当前这个不互质的数字之外,还需要将这个数字的倍数也一并处理
(dotimes (j (floor n (1+ i)))
(let* ((j (1+ j))
(m (* (1+ i) j)))
(setf (bit bits (1- m)) 1))))
(t (incf count))))
count))

为了节省内存空间,这里用了一个bitmap来标记小于n的每一个数字是否与n互质——1表示不互质,0表示互质。

其实并不需要遍历所有比n小的数字,只要遍历所有n的素因子即可。比如,将8分解为素因子,就是3个2,那么对于小于8的所有2的倍数(4和6)都是与8不互质的。基于这个方法,将所有的素因子的倍数所对应的位置为1,再数一下总共有多少个0比特即可。

对每个n都进行质因数分解效率不高,先生成一个一百万以内的素数表吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(defun primep (n)
(cond ((< n 2) nil)
((= 2 n) t)
(t
(let ((bnd (truncate (sqrt n))))
(labels ((rec (test)
(cond ((> test bnd) t)
((= 0 (rem n test)) nil)
(t (rec (1+ test))))))
(rec 2))))))

(defvar *primes-in-1000000* nil)
(defun generate-primes-in-1000000 ()
(dotimes (i 1000000)
(when (primep (1+ i))
(push (1+ i) *primes-in-1000000*)))
(setf *primes-in-1000000* (nreverse *primes-in-1000000*)))

然后对于一个给定的n,遍历所有小于它的素数并对相应的倍数所在的比特置一就可以了,示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(defun phi3 (n)
"直接用素数表来做筛法"
(prog
((bits (make-array n :element-type 'bit :initial-element 0)))
(dolist (num *primes-in-1000000*)
(cond ((> num n)
(go :return))
((zerop (mod n num))
(labels ((aux (i)
(when (< i n)
(setf (bit bits i) 1)
(aux (+ i num)))))
(aux (1- num))))))
:return
(return-from phi3
(count-if (lambda (bit) (zerop bit)) bits))))

PS:写这个phi3的时候发现Common Lisp提供了一个prog宏,这个宏倒是真的挺好用。

改进了两轮,其实这仍然是笨方法。即便是用phi3,用来计算题目的答案也花了40多分钟。

全文完

在Emacs中写代码的时候,常常需要查找一个函数、方法,或者变量的定义。如果是正在写Common Lisp,那么SLIME已经配置好了相应的快捷键M-.,只需要将光标移动到要查看的函数、方法,或者变量的名字上,按下M-.便可以跳转过去——再按一下M-,还能回到原来的位置。

如果是写其它语言的代码,很多时候都没办法方便地跳转过去,这时候就需要依赖于文本搜索了,这也是本篇所要讲述的主题。

通常情况下,用C-sC-r就足够了——一个负责“往下”搜索一个负责“往上”搜索。尤其在安装了Emacs的插件swiper之后,只需使用C-s便可以同时查看到上下两个方向的匹配文本。

C-s也有其局限性。例如,它不能跨文件搜索,如果要查看的函数、方法,或者变量的定义不在当前buffer中,就不得不手动在多个buffer间切换并频繁按下C-s了。

有多种办法可以解决上面这种问题。例如,可以用Emacs的projectile-ag。通常,如果代码散布在多个源文件中,那么它们多半是放在一个项目中——比如一个Git仓库。打开位于项目中的文件时,Emacs的projectile-mode就会启动。此时,按下C-c C-p s s这套组合键,会调用projectile-ag函数。projectile-ag会在minibuffer中等候输入要搜索的内容,按下回车后,Emacs会调用命令行工具ag来搜索这个项目下的所有文件,找出匹配关键字的行并显示。

projectile-ag函数会打开另一个buffer来展示搜索结果,一个示例如下

1
2
3
4
5
6
ag --literal --group --line-number --column --color --color-match 30\;43 --color-path 1\;32 --smart-case --stats -- emacs .
0 matches
0 files contained matches
36 files searched
111365 bytes searched
0.007795 seconds

使用projectile-ag的前提是要搜索的文件都在同一个一个项目中,但并非所有时候都满足这个要求。这时,可以用Emacs的find-grep函数。

find-grep函数调起后同样要求使用者在minibuffer输入内容,但它更原始一点

光标会定位在-e选项之后,需要填补交给grep的正则表达式。由于minibuffer中给出的是完整的、将会被运行的命令,因此可以也给find命令添加一些选项和参数,来改变搜索行为。

如果是在一个Node.js项目中搜索,一般还要让find忽略一些文件,如node_modules目录下的大量依赖,或者构建产生出来的.css和.js文件。这些文件中的行不仅很可能会命中输入的正则表达式,还极可能成片地出现,占据搜索结果中的半壁江山。

除了grep之外,还有许多命令行的文本搜索工具,例如ackrg,并且它们都称自己更快。要在Emacs中使用它们也很简单,尤其是后者还有相应的插件rg.el可以方便调起。

如果经常要控制find来忽略node_modules,可以考虑用git-grepman git-grep中说到,它只会搜索tracked的文件

git-grep的man文档

node_modules一般都不会被git跟踪,自然也就不会被搜索。

全文完

缘由

org-mode是一个Emacs内置的major mode,当打开一个后缀为.org的文件时就会被启用。在官网的介绍中提到,它可以用于管理待办事项,而这也正是我目前使用org-mode最多的场合。比如,我用它来记录漫画的阅读进度,每一话或每一章就是一个标记了TODO关键字的条目,读完那一话或那一章后就会将对应的条目标记为DONE。一般我会一周一次地归档自己标记为DONE的条目,但由于一次要处理的条目可能很多,逐一将它们归档比较繁琐,因此,便打算二次开发实现一个自动归档的功能。

需要事先声明的是,本文不是org-mode的入门教程,也不会讲解如何配置org-mode。对这方面有兴趣的读者,可以自己搜索一番,资料还是相当丰富的。

菜谱

对于每一个被完成的单独的阅读任务,我的做法是将其internal archive。等到一整本漫画都读完之后,再将整个以漫画名命名的条目归档到别的文件中去。要实现自动的internal archive,最简单直接的办法是借助于org-mode提供的各种hook。

org-mode提供了许多的hook,在官方的文档中有一一列举 。其中,名为org-after-todo-state-change-hook的便是我所需要的钩子。只需往这个变量所绑定的列表中添加一个函数,那么这个函数便会在条目切换状态时(比如从TODO切换到DONE)被org-mode调用。

最终的ELisp代码如下

1
2
3
4
5
6
7
8
(defun lt-archive-if-manga ()
(let ((state org-state))
(when (string= state "DONE")
(let ((tags (org-get-tags-at)))
(when (member "漫画" tags)
(org-toggle-archive-tag))))))

(add-to-list 'org-after-todo-state-change-hook 'lt-archive-if-manga t)

稍微解释一下。从C-h v org-after-todo-state-change-hook RET的文档可以得知,条目的新状态可以通过变量org-state获取。取得新状态(是个字符串)后,首先检查其是否为"DONE"。如果是,再检查这个条目是否为一个阅读漫画的任务。

在我的用法中,凡是漫画条目,都打上了名为"漫画"的标签。因此,使用函数org-get-tags-at取得一个条目的所有标签(包括从父级条目继承下来的),再用member函数判断这些标签中是否包含字符串"漫画"。如果有,就调用org-toggle-archive-tag将该条目internal archive。

传给函数add-to-list的第三个参数t的作用,是让这个新加入钩子的函数最后被调用。

全文完

转眼间2018年只剩下最后的几天了,赶紧趁热写篇年度总结,毕竟据说元旦会变冷。

入手Mac

参加工作的第五个年头,终于买了一台自己的MacBook Pro。其实我从高中时起,就对Mac有一种憧憬。那时候每到周末,就常常往苹果的实体店跑,就为了去看看那些精致的笔记本,试着在触控板上滑动一下手指,点开几个自己只在苹果官网上看过icon的陌生应用。

依稀记得下决心买这台电脑的那天晚上,回到家掏出之前的联○笔记本,发现转动显示屏盖子的地方坏掉了,导致笔记本的盖子翻不起来。在家里要接一个外置的显示器来用实在是太麻烦了,立即就萌生了买一台新的来应急的想法。再三思索后,决定尝试一下Mac,便立即在官网下单了。令人哭笑不得的是,明明是要应急用的,结果还是过了三天才到手。庆幸的是,在公司是用外置显示器来办公的。

用上Mac之后有挺多的感触,有兴趣的读者可以移步这里阅读,这里就不再赘述了。令我自己也感到惊讶的,是我在用了Mac之后还购买了几款软件——我并不是一个很舍得花钱买软件的人,多数情况下,都是用一些免费的开源软件的。在Mac上买的这几款软件,大概是因为它们真的挺好用吧。最早入手的是Alfred,买了它的Powerpack。后来买了BetterTouchTool,自定义了很多touchbar上的按钮,用得挺欢的

BetterTouchTool的使用现状

再后来,遇到了堪称神器的Contexts,现在在macOS中切换窗口就像是牛奶巧克力那般的丝滑。最近买的,则是Bartender,是在淘宝上的数码荔枝那里买的,趁着双十一的时候有折扣赶紧入了手。它们都很实用,使用频率也非常地高。当然了,像Default Folder X虽然也非常好用,但因为它可以无限期地免费使用(只是会偶尔弹个窗提醒购买),所以我就没有急着花钱了。一些比较有意思的应用,比如Little Snitch,虽然很酷炫(看着世界地图上的各种连线),但对我而言用处不大,最后也就卸载了。

写博客

越是写博客就越发现,博客的力量是有限的,除非超越博客。我不做程序员啦JOJO比起写给自己查阅的笔记,写公开发表的文章是大不同的。笔记可以写得像铜墙铁壁那么规整,可以一层一层地嵌进去。但是发表在博客上的文章就像代码,是写给自己之外的人看的,要讲究阅读体验。偶尔要用段子活跃一下气氛给读者提提神,字里行间也要注意正确地使用行话。尤其是写一些教程一般的文章时,要循序渐进地讲述自己的操作过程,还要战战兢兢地担心别人无法复现自己的结果(人类的本质是复读机)。

重新开始写作后才发现,简书上的最后文章已经是2017年七月份的了。重开的博客,打算继续发表在GitHub Pages上。本来GitHub Pages上的博客的页面,是我用自己写的一个工具来生成的。结果这个半成品在Mac上因为cl-mysql安装失败跑不起来,我也一时不想折腾,于是决定换个成熟的工具来用。目前用的是Hexo。一个惊喜是,Hexo默认支持Google Analytics——尽管并没有多少人会去看我的博客。

除了GitHub Pages之外,我也把文章发表到了SegmentFault的专栏上。感谢SegmentFault极其不友好的插入图片的方式,迫使我写了一个Alfred的Workflow,用来快速地把截图的图片上传到GitHub的一个仓库里(拿GitHub的仓库当图床)。现在的写作流程,是在电脑上用Typora先写好,然后hexo new一下生成源文件,把写好的内容粘贴进去,再发布,最后把文章内容再到SegmentFault上创建篇新文章再贴一次,发表出去。

布谷,布谷

以前用(坏掉现在又修好了的)联○笔记本的时候,我用Windows 10自带的Alarm设置了很多提醒——叫外卖的、喝水的,以及起来走走的(久坐是不好的哟),大量的定时提醒让我有一种生活井井有条的感觉——写作感觉读作错觉。Mac在这方面可以做得更好,因为它自带crontab。于是我便用crontab和alerter(刚开始的时候用的是terminal-notifier)给自己设定了不少定时提醒。等到crontab -l的输出开始泛滥后,便萌生了自己写一个管理工具的想法。

一开始还在Boostnote上煞有介事地写了一篇需求文档和设计文档(已经都是废稿了),想着用Common Lisp来开发。但同样因为cl-mysql安装不成功,我又不希望把时间都花在了折腾环境上,便改用了Node.js来编写这个管理工具。框架选择了egg-js,在操作MySQL和Redis方面都有相应的插件,此外还内置支持定时任务,上手很方便——真要是用Common Lisp的话,也许还在纠结某个功能是用某个半残的第三方库还是自己费劲从零写起。

用Redis的ZADD、ZRANGEBYSCORE、ZREM,以及ZSCORE指令做了一个简陋但够用的消息队列——用Z*系列的指令是为了可以模拟出延时消息的效果(beanstalkd阿里云MNS都支持这种特性)。配合egg-js的定时任务功能,就可以实现定时提醒了——弹出提醒仍然是用alerter。目前这套系统运作得还不错,大部分原本录入在crontab中的定时提醒已经交由它来处理了。尽管还有不少的小问题,不过相信都是可以解决的。

对了,这玩意儿的名字叫做cuckoo,即布谷鸟。

GTD?

Mac跟“效率”这个词似乎特别有缘,常常被人换做生产力工具,仿佛一拿起Mac,便自动屏蔽了外界的干扰。开始用Mac的几天后,我便开始把玩macOS上各款大名鼎鼎的TODO list应用了。关于这个话题之前也写了一篇吐槽文,有兴趣的可以移步这里阅读。世间的TODO list应用是真的多,不过可能是我的口味实在是太刁钻了,我竟然没有一款是特别满意的。在把玩的期间最让我产生好感的,要属My Life Organized,然而这货没有Mac版,不然我真的很可能会喜加一。

每过一段时间,我就会想要把自己对TODO list类应用的一些想法付诸实践,自己动手开发一个给自己用。不过到目前为止,这些想法仍然处于被封存的状态,被遗忘在了磁盘上哪个角落里的文件中。目前Emacs的org-mode还算够用,它兼顾了我使用上的凌乱与规整,尤其是当我需要在某个任务下写一些包含代码的笔记或者想法的时候,org-mode几乎就是所有TODO list类应用中的唯一选择了。但工具只是用来管理任务,当夜深人静坐下来,想要自己第二天给安排得明明白白的时候,就会发现,即便有最好的工具(我并不是说org-mode),也仍然需要方法论来指导这个安排的过程。尤其是,这个过程应当是“object-oriented”的——不是面向对象,而是“目标导向”。如果不事先制定一些目标——不管是像人生规划这般空泛的目标,还是像租一辆共享汽车开车上路这样具体的短期目标,如果缺乏目标,那么很快就会陷入了“随便找一些任务来填充第二天的空闲时间”这样的状态,久而久之GTD也就实践不起来了。

规划不等于目标。

Note-taking

无法高亮编程语言代码的Evernote、OneNote,使用不通用的存储格式的Boostnote、Quiver,还有收费的为知笔记,都没能够取代Emacs的org-mode成为我做笔记的工具。org-mode最弱的地方,就在于使用起来不够随意,不像其它的几款笔记软件那样,截图之后的图片没有办法一键粘贴到.org文件中去。但恰恰我个人不太喜欢截个图配一段话的笔记形态,所以这个缺点可以视若无睹。我现在的笔记都是QA形式,一个一级headline就是一个问题,headline下的文本就是答案,而org-mode又支持嵌入代码(虽说Markdown也支持),很适合我的习惯

Emacs中的笔记示例

最近我觉得,与记笔记同样重要的,是能够方便并且准确地查找自己的笔记。笔记如果只是记而没有翻阅出来利用,那还不如每次都打开搜索引擎当场查找算了。我打算把笔记的导入到ElasticSearch中去,然后依托它的全文搜索功能来查找。感谢org-mode,是纯文本的存储格式。要写一个工具,把.org文件中的每个问题和对应的答案组装成一个JSON喂给ElasticSearch真是太简单了。现在缺的是一个方便的入口,以及一个美观大方的结果显示方式。

不过这个新想法的项目名还没想好

Web后端的固有结界

年初开始渐渐负责起了面试的工作。为了可以比较系统地面试,便整理了一份Web后端工程师需要掌握的知识的清单。目前这份清单还在绝赞完善中——想必这个完善的过程是不会停止下来的,而且目前积累的面试题也不足。

原本还有另一份清单,是自我提升用的指引。但渐渐地我发现要求面试者所具备的知识,和充电用的技能树指引,其实是应当合二为一的,于是乎便诞生了一个叫做charging的项目。在其中的一个叫做knowledge.org的文件中(又是org-mode),我以自己的理解自上而下地给Web后端的软件工程师所需要的知识做了一下划分,并逐级细分,到了合适的粒度的headline,便添加这个分类下的相关面试题。除了在这些叶子节点上挂上面试题之外,我还依照这些合适粒度的headline给自己安排学习的内容,一般是相关主题的电子书或者PDF。经过最近一次的梳理后,接下来可能会学习一下Erlang(都不记得是第几次了),读一下《重构》,以及《Redis实战》。当然了,这些只是最近一次整理增加的内容,仅仅是完整学习内容的冰山一角XD

我本来不喜欢听网课的,认为视频和语音方式的教学,接收信息的效率比用眼看的方式要来得低效,毕竟不管是视频还是音频,总是要收完前一段内容才能继续收下一段内容(真香警告)。大约在两周前,买了极○时间的专栏《MySQL实战45讲》,听下来发觉其实还挺有意思,尤其适合在通勤和夜晚慢跑时听,算是2018年新增的一种学习方式吧。

后记

尽管有年月日的划分,但日子毕竟是连在一起过的,所以今年未完成的学习安排并不会在2019年到来的那一刻戛然而止。Org Agenda中还有很多标记为TODO的条目,Pocket中还有很多未读的文章,还有很多没看完的PDF,LeetCode和Project Euler上也还有很多的题目没做。2019年,想必会是忙碌的一年。

全文完

背景

由于个人喜好的因素,选择了用Emacs的org-mode来实践GTD,管理自己的任务和安排日程。但也因为个人喜好的因素,导致在安排第二天的计划,从积累的TODO列表中挑选要做的事情时,总会下意识地跳过一些一看就很麻烦的任务。久而久之,在列表的顶部,便堆积着一些好久前就创建的TODO。而因为总是从上往下挑选任务,列表的底部则是堆积着好久没有露脸的TODO。有不见者,三十六年。

迫切需要一个办法来解决这个难题,但若真的一丝不苟地从上往下处理每一条TODO又觉得没意思,该怎么办?不如从中随机地挑选TODO来安排到第二天的日程中?Sounds good。

如何实现

一开始,我是打算找找现成的类似功能的,不过放狗搜了一番后并没有什么收获。之后的某天我忽然意识到,.org文件不过就是普通的文本文件而已,直接用命令行工具处理就好了呀。摸索一番之后才知道并不难,成果就是下面这段简单的shell命令

1
find . -name '*.org' ! -name 'trash.org' ! -name 'work.org' -exec grep -Hn '\*\* TODO' {} \; | sort -R | head -3

稍微解释一下。首先登场的是find,它用来遍历目录下的所有.org文件——因为我把TODO按照不同的领域放到了不同的.org文件下。传递给find的参数的意思,是“匹配所有文件名以.org结尾、但既不叫trash.org、也不叫work.org的文件”

1
-name '*.org' ! -name 'trash.org' ! -name 'work.org'

trash.org是垃圾箱,work.org存放的是工作相关的任务——我可不喜欢把工作安排到自己的闲暇时光里。

通过-execfind调用grep从.org文件中过滤出符合条件的带有TODO关键字的行——在我的.org文件中,有很多行是没有TODO关键字的非任务型的内容,它们可能是一个目标、一个分类,甚至可以是某个TODO条目下的“笔记”。用下面的正则即可筛选出想要的内容

1
'\*\* TODO'

总和运用findgrep后,便到了从中挑选的环节了。虽然开始的时候提到的是“随机地挑选”,但可以参考音乐播放器的“随机播放”功能的做法,即先将所有的TODO条目随机排序,然后从头开始按顺序取出前几个。sort命令的-R选项已经实现了随机排序,再用head选取前3个即可。

全文完