[{"content":"今天看爱因斯坦传又发现一些趣事。很多人都知道，爱因斯坦获得诺贝尔奖的官方理由并不是相对论，而是“光电效应定律”，但是这背后还隐藏着一段富有黑色幽默的故事。\n自从1905年爱因斯坦颠覆性地提出了狭义相对论（以及光电效应）之后，反对相对论的声音便一直存在着，而他在1916年提出的广义相对论更是遭到了很多人的激烈反对。这其中有一部分是科学层面的反对，但也有政治层面的排斥。\n科学层面的反对集中在实验证据的缺乏上。虽然相对论已经获得了很多顶尖的理论物理学家的认可，但瑞典化学家阿雷尼乌斯在一次诺奖评审的内部报告中指出，相对论成功预言的几种物理现象也能用其他理论解释，因此不能算作被完全证实了。\n瑞典的诺奖委员会一直对诺贝尔的遗愿念念不忘，牢记该奖项应当奖励那些“最重要的发现或发明”。它感觉这两项要求相对论都不符合，于是委员会的报告说，“在人们可以接受这一原理，特别是授予诺贝尔奖之前”，相对论有待于获得更多的实验证据。\n政治层面的反对则与当时不断上涨的反犹主义有关。虽然现在我们常说“科学无国界”，但就在一百年前，一些顶尖的科学家（例如发现了光电效应的勒纳德）还在把科学与种族挂钩，以至于造出诸如“犹太科学”、“德意志物理学”这样的怪异词语。\n勒纳德批评这种物理学不是基于实验和具体发现。但勒纳德的报告中夹杂着对那种“哲学臆想”的强烈憎恨，他经常斥之为“犹太科学”的特征。\n在巴特瑙海姆的会议之后，他（勒纳德）愈发激烈地攻击爱因斯坦和“犹太科学”。他鼓吹创建一种“德意志物理学”，将德国物理学中的犹太流毒肃清。在他看来，首当其冲的就是爱因斯坦的相对论及其理论的、非实验的抽象方法，以及它拒斥绝对、秩序和必然性的（至少在他看来的）相对主义味道。\n一些别有用心的政治活动家，也想通过批驳相对论来巩固自己的反犹立场。\n臭名昭著的德国民族主义者保罗·魏兰德，成了一个有政治野心的辩论家。他是一个右翼民族主义政党的激进分子，该党在1920年的正式纲领中誓言“削弱犹太人在政府和公众中日益显著的影响”…………魏兰德发表文章谴责相对论是“一场大骗局”，还成立了一个由乌合之众组成的团体（然而资金却很充足）并给它起了一个冠冕堂皇的名字——“德国科学家维护科学纯洁研究小组”。\n由于这些反对声音，诺奖委员会一直在拖延爱因斯坦的这份诺奖。早在1910年爱因斯坦就因为狭义相对论被提名过，此后又获得了来自各种大佬的各种提名，但委员会就是一直拖，一拖就是十年。\n这十年间，爱因斯坦的知名度与日俱增。一开始，相对论还只是理论物理学界的一种新颖理论，讨论范围仅限于他们的小圈子。但是1919年的一次日食观测成功验证了广义相对论的预言，一些媒体嗅到了大新闻的味道，便开始大肆宣传这项连他们自己都弄不懂的崭新理论，爱因斯坦也因此成了家喻户晓的人物。也难怪有人说他“忽然一跃成为国际上最著名的科学家和超级明星，恰恰证明他善于自我推销，不配获得诺贝尔奖”（这饭圈味也太冲了）。\n然而1920年的诺贝尔物理学奖还是没有颁发给爱因斯坦，而是给了国际计量局局长纪尧姆，以表彰他“发现镍钢合金的反常性及其在精密物理中的重要性”。换句话说：他发现了一种新的合金材料，能让测量变得更准。这一颁奖结果，即便是反对相对论的人也觉得莫名其妙。\n到了1921年，爱因斯坦和他的相对论已经成了每个人茶余饭后的话题，支撑相对论的证据也越来越多。但反对者依旧有足够的力量与之抗衡，于是这一年的诺奖干脆被推迟了。评委会面临的压力越来越大。现在的局面是，诺奖已经需要爱因斯坦来证明自己的权威性了，而不是反过来。\n这一结局弄不好就会变得无法收场。爱因斯坦不获奖对诺贝尔奖的影响要比对他本人更坏。“试想一下，如果50年后爱因斯坦的名字没有出现在诺贝尔奖获得者的名单中，舆论会怎么想。”法国物理学家马塞尔·布里渊在1922年的提名信中写道。\n这时，刚进入评委会的理论物理学家奥森前来“救驾”了。他提出了一个非常聪明的解决方案：让爱因斯坦因“发现光电效应定律”而获奖。这条定律已经完全被实验所证实，不像相对论那样富有争议。更绝的是，这刚好给了同时授予玻尔1922年诺贝尔奖的理由，因为玻尔的原子模型建立在对光电效应定律的解释上。不得不说，奥森这个和事佬做得是真的好。评委会自然是喜笑颜开地采纳了，爱因斯坦也终于获得了这份迟来的认可。\n最具黑色幽默的部分来了。爱因斯坦得了奖，评委会也没有得罪反相对论的势力，本来应该是皆大欢喜，但有一个人却破防了，这人就是勒纳德。他反对爱因斯坦获奖的积极分子，但他同时也对光电效应有重大贡献。换句话说，爱因斯坦之所以能提出光电效应定律，主要都是基于勒纳德的工作！现在评委会为了和稀泥强行把奖颁给光电效应，勒纳德却啥也没捞到，可不得破防吗？\n在1905年的论文中，爱因斯坦曾经感谢过勒纳德的“先驱性”工作。但在1920年柏林的反犹集会之后，他们变成了仇敌。现在，爱因斯坦不仅在勒纳德反对的情况下获了奖，而且还是在一个以勒纳德为先驱的领域，这使勒纳德异常愤怒。他气急败坏地给科学院写了封信（这也是科学院收到的唯一一封正式抗议信），说爱因斯坦误解了光的真正本性，而且他还是一个追名逐利的犹太人，其做法与德国物理学的真正精神背道而驰。\n虽然科学家的本职是追求真理，但他们也是凡人，也不得不参与名利斗争。看着一向被视为纯粹、高贵、权威的诺贝尔奖，因为相对论引发的骚动被搅得一团糟，真是太有意思了。\n","permalink":"https://daichao1997.github.io/posts/life/%E6%8E%A8%E9%80%81%E7%88%B1%E5%9B%A0%E6%96%AF%E5%9D%A6%E4%B8%8E%E8%AF%BA%E8%B4%9D%E5%B0%94%E5%A5%96/","summary":"\u003cp\u003e今天看爱因斯坦传又发现一些趣事。很多人都知道，爱因斯坦获得诺贝尔奖的官方理由并不是相对论，而是“光电效应定律”，但是这背后还隐藏着一段富有黑色幽默的故事。\u003c/p\u003e","title":"爱因斯坦与诺贝尔奖"},{"content":"相信大家都知道下载正版软件的重要性。盗版软件可能被恶意篡改过，而且可能带有病毒，窃取你电脑里的信息。但是我们怎么验证软件是正版还是盗版呢？有人肯定会说：“在官网下载的肯定就是正版啦！”确实，那么我们怎么验证一个网站是真正的“官网”呢？这就要用到一个叫做“证书”的东西。\n证书是由专门的机构（Certificate Authority，简称CA）颁发的，里面包含了网站的域名。我们用浏览器上网时，可以方便地看到每一个网站的证书，例如在谷歌Chrome浏览器访问“www.google.com”时，点击域名左侧的小锁：\n再点击“证书”（Certificate）按钮，就可以看到详细信息：\n可以看到“www.google.com”这个域名是有证书的，所以我可以信任它。如果你们访问的是其他官网，也会有类似的证书。\n但是，我怎么知道这个证书是真的呢？\n这就要引入“公钥”和“私钥”的概念了。其实，我们刚才在浏览器里看到的“证书”并不是明文，而是经过加密后的密文，只不过浏览器帮你自动解密了。CA有一对类似于“钥匙”的密钥，称为“公钥”和“私钥”。这对钥匙非常神奇——用其中一把钥匙加密的数据，必须用另一把钥匙才可以解密。此外，私钥是极度保密的，只有CA自己能用，而公钥是公开在CA的网站上的。CA颁发证书时，先用私钥将其加密，再把它颁发出去，这样就只有它的公钥能解开证书。如果我从CA的网站那里拿到公钥，然后成功解密了该证书，那就说明证书真的是它颁发的了。\n但是，我怎么知道这个CA不是和骗子一伙的呢？要是骗子自己造了一个假的CA，给自己的假官网颁发了假证书，我又找谁说理去？\n答案很简单——让另一个更值得信任的“高级CA”，给这个CA颁发证书。你用CA的公钥验证某网站的证书，那么你也可以用“高级CA”的公钥，验证CA的证书。\n但是，我怎么知道这个“高级CA”不是和骗子一伙的呢？要是骗子自己造了一个假的“高级CA”……\n答案很简单——让另一个更值得信任的“特级CA”，给“高级CA”颁发证书……\n但是……\n答案很简单……\n但是……\n好吧，咱们别套娃了，因为理论上没有什么是不能造假的。但是在现实世界中，我们有一些最高级的CA来颁发所谓的“根证书”，例如微软、苹果、Mozilla用自己本身的存在做担保，给它们信任的下层CA发证书。“根证书”就像树根一样开枝散叶，形成了一棵“信任之树”。如果你信任树根处的微软、苹果或者Mozilla，以及数字证书的认证体系，那么你就可以信任所有下属的CA，以及它们颁发的证书。这是我们在充满欺骗和谎言的世界里所能做到的最好的保障。\n这一期我只稍微提及了非对称加密和CA的树状体系。如果你们喜欢这个主题，下一期我会继续深入证书的工作原理，是怎样的算法保证了它的有效性。\n","permalink":"https://daichao1997.github.io/posts/life/%E6%8E%A8%E9%80%81%E6%95%B0%E5%AD%97%E8%AF%81%E4%B9%A6%E4%BD%93%E7%B3%BB/","summary":"\u003cp\u003e相信大家都知道下载正版软件的重要性。盗版软件可能被恶意篡改过，而且可能带有病毒，窃取你电脑里的信息。但是我们怎么验证软件是正版还是盗版呢？有人肯定会说：“在官网下载的肯定就是正版啦！”确实，那么我们怎么验证一个网站是真正的“官网”呢？这就要用到一个叫做“证书”的东西。\u003c/p\u003e","title":"数字证书体系"},{"content":"听说最近《脱口秀大会》整了个节目效果，可把我逗笑了。\n什么节目效果呢？杨笠刚上场还没开始说话，罗永浩就拍灯了，然后直接起立给她鼓掌。\n罗永浩拍完了灯，大张伟装作自己也要拍的样子，然后李诞开玩笑地劝他说“别拍了，你拍我也得拍了”。\n当时台下有四位嘉宾：罗永浩、大张伟、李诞、杨澜——这也是他们拍灯的顺序。\n之后杨澜点评，罗永浩顺便解释了一下：“我每次看到她的消息或者她本人的时候，我都会为我是个男的感到很过意不去，所以她没讲我就拍灯是带着赎罪的那种……我特别喜欢她的演出。”\n然后杨澜继续说：“我一直把灯留到了最后，因为除了这些态度和价值观的东西，我还是特别希望杨笠展示她的技术。在这个舞台上，她真正是靠自己的能力赢得这个比赛的。”\n以上就是关于杨笠的部分。\n什么？你问我杨笠具体讲了些啥？不好意思，其实我压根没仔细看，而且四位评委也压根没提。大张伟和李诞都没说话，罗永浩只解释了自己为什么要爆灯，而杨澜也只是表达了对杨笠本人的欣赏，以及“无论男女都可以自信”的正能量观点。没有人对这次脱口秀本身的内容作出评价。\n种种迹象都表明，杨笠并不是传统意义上只负责把观众逗笑的脱口秀演员，而是一面旗帜、一个标杆。\n我大半年前第一次听到杨笠的脱口秀时，觉得她的段子并不怎么优秀，有些还是直接的人身攻击，所以对她还有她身边的欢呼声感到反感。但现在我懂了，我根本不应该用普通喜剧演员的标准来评价她。她来《脱口秀大会》的使命并不是讲出优秀的段子，或者奉献精彩的表演，而是替女性观众说出一直憋在心里的话。\n在很多支持杨笠的人看来，杨笠在舞台上讲的不是脱口秀，而是演讲，是示威，是男权社会中处于下位的女性对男性发出的挑战状。\n只要你明白了这一点，就能理解为什么她可以受到那么多女性的支持和爱戴。\n当然，反对的声音也有不少，我在此提炼一下核心矛盾：\n1. 什么？我们生活在一个男权社会吗？\n这是决定大部分女权主义观点正确与否的根本问题，也是当前舆论分歧最大的地方。你可以自行查找资料，不要靠主观判断下结论。\n2. 这里是脱口秀大会，不是演讲大会，为什么要在讲脱口秀的地方靠女权给自己拉票？\n有人认为合理，有人认为不合理。合理的地方在于，如果你承认这是个男权社会，而女性需要发声的地盘，那么脱口秀大会可以成为女性舆论的“保护性栖息地”。不合理的地方在于，脱口秀大会有比赛的性质，利用意识形态搞站队会影响比赛的公平性，而且会影响部分观众的体验。另外，恭喜你喜提成就“感受政治正确”。\n3. “凡是支持杨笠的，我们都支持；凡是反对杨笠的，我们都反对。”这样真的好吗？\n我认为这样不好，但是支持者自然有别的看法：现在杨笠面对的不仅是反对声音，而是人身攻击、举报和造谣，是整个男权社会的恶意反噬。如果任由反对者斗倒她，之后将再也不会出现第二个杨笠，到时候谁又来为女性发声呢？我们不得不承认，现实斗争远比观念斗争更复杂，理性主义不能解决所有问题。\n4. 是杨笠攻击男性在先，我作为男性，抵制她有错吗？\n在一个公平的社会里，这一点也不错。无论是谁攻击了谁，被攻击的一方都应该有反抗的权利，但是“有被杀的觉悟，才有开枪的资格”。无论你如何抵制杨笠，都应该允许女性以同样的方式抵制冒犯了女性的人。如果你抵制杨笠的方式是举报、造谣、阴阳怪气、起侮辱性的绰号，那么当你下次看到女性也这么做时，不要大惊小怪。\n我们再说回罗永浩。老罗提前拍灯的原因，有人理解为强烈支持杨笠，但也有人认为他是在讽刺杨笠引发的站队现象。我们无法得知是哪种情况，但有一点是可以肯定的：杨笠的表演精不精彩并不重要，重要的是她带来的其他东西。这就说回了刚才提到的杨笠的象征意义——老罗在为杨笠代表的立场拍灯。至于他是不是在阴阳怪气，就见仁见智了。杨澜把灯留到最后，显然是为了平衡老罗的“立场论”，不让场面过于倾斜，以至于形成“这不显得您枪法准吗”的讽刺效果。\n总体来说，杨笠这次又成功地引爆了一波话题，赚取了大量流量，但是这些讨论大部分都是站队和掐架，鲜少有对“杨笠现象”进行透彻思考的。希望这篇文章能帮助大家更好地认识“杨笠现象”，也欢迎各位的批评与指正。每个人都可以有自己的立场，希望无论你支持还是反对某一种观点、立场或某一个人，都能充分聆听、友善发言。谢谢各位的阅读。\n","permalink":"https://daichao1997.github.io/posts/life/%E6%8E%A8%E9%80%81%E8%B0%88%E7%BD%97%E6%B0%B8%E6%B5%A9%E7%9E%AC%E9%97%B4%E7%BB%99%E6%9D%A8%E7%AC%A0%E7%88%86%E7%81%AF%E4%B8%80%E4%BA%8B/","summary":"\u003cp\u003e听说最近《脱口秀大会》整了个节目效果，可把我逗笑了。\u003c/p\u003e\n\u003cp\u003e什么节目效果呢？杨笠刚上场还没开始说话，罗永浩就拍灯了，然后直接起立给她鼓掌。\u003c/p\u003e\n\u003cp\u003e罗永浩拍完了灯，大张伟装作自己也要拍的样子，然后李诞开玩笑地劝他说“别拍了，你拍我也得拍了”。\u003c/p\u003e","title":"谈罗永浩瞬间给杨笠爆灯一事"},{"content":"一年一度的总结记事多少有些慢了，很多发生在年初的事情等到年末再回忆会有些困难，所以我决定从今年起，尽量在三个长假（五一、十一、春节）都写一篇记事，恰好将一年划分为冬去春来、炎炎夏日和凛冽寒冬的三个阶段。如果春节还有多的时间，可能还会再推出一篇偏抽象的思考。\n十二月：新冠\n这次就从去年十二月初放开之后讲起吧。之前已经因为“国庆返京”居家隔离了5天，十一月底又因所谓“十混一阳性”居家隔离了5天，结果才放出来一个礼拜，人人都变成阳性了，我又不得不在家吃一个礼拜布洛芬。还好全员变阳的这段时间，盒马和大部分餐饮店还开着，外卖员也在坚持工作，饮食方面有了着落。发高烧时不想吃太多油盐，也咽不下偏硬的食物，白粥反而成了最可口的主食。看超级小桀玩工艺图是我这段时间最大的乐趣，让我暂时忘记了身体上的痛苦。但话说回来，我的症状并不算重，既没有烧特别高，也没有呕吐或者心肌受损，只是退烧后咳了很久，让我不得不感到庆幸。\n一月：武汉\n北京的冬天真的很糟糕，再加上这一系列折腾，我已经迫不及待想换个环境了。元旦放假时，我瞒着爸妈悄悄回到武汉，只联系了我姐，让出差在外的她把房子借我住一段时间。我甚至把刚买不久的重20千克的加湿净化器一起寄了过去（事实证明在武汉根本不需要加湿）。姐姐的房子在二楼，出门很方便，而且远比我的“小开间”宽阔：有单独的卧室，睡觉时可以隔绝外部的嘈杂；客厅里有一个大餐桌，很适合办公；小区对面是一个大型超市，附近还有大量小摊；最大的缺点是没有被地铁覆盖，出行必须先坐公交或的士——这些都为我四月份换房提供了重要参考。\n可以看到，我在姐姐家里住得挺舒服的。年前的工作强度不大，我大可以九点起床，悠哉地踱到小区外面过早，再悠哉地踱回来泡杯咖啡。午饭和晚饭可以在小摊吃，缺什么东西就顺手从超市里带回来，也可以尽情探索附近的外卖。下班后，去探一探光谷附近的游戏厅，回来打游戏刷手机。\n走在小区里，我不止一次思考这里和北京的区别是什么，为什么我在北京没那么快乐，最后得出的结论是：首先是新鲜感，其次才是居住环境，因为生活会不可避免地陷入重复：路边摊再多也总有一天会吃腻，游戏厅再多最后也只会固定去一两家，我为自己设计的新的生活方式总有一天会沦为囚禁自己的牢笼。既然我选择了按部就班的工作，那么这样的结局就是注定的。\n因此，小住半个月之后，我就直接拎着行李去爸妈那边了（他们唯一的疑惑是为什么我带着一套新买的睡衣）。那边又有新的店可以探——武商梦时代！继续高强度出勤ww\n二月、三月：日常\n回到北京第一件事：置办大桌子。体会到大桌子的好处后，我很难想象自己居然坚持用那个不到一米宽的小屁桌上办公了两年之久。\n回到北京第二件事：置办工学椅。这个纯粹是听群友推荐，于是我又掉进了可恶的消费主义陷阱。当我的身体被椅子轻轻托住的时候，我忍不住叹道：哎，资本。\n炉石传说退出大陆后，我的娱乐主要就是maimai，要说还有什么乐趣，那就是出勤之前吃巴奴然后大推分。我对maimai的热情已经过去，再加上工作变忙后下班没功夫跑那么远出勤，于是我又捡起了三国杀。\n四月：搬家\n住在双井已经有33个月，这个大开间看上去越来越狭窄，睡觉时又觉得过于宽阔。落地窗挡不住楼下酒吧和汽车的杂音，笼子也关不住半夜上蹿下跳的两位猫咪。坚持住下去的理由越来越少，搬家的理由越来越多。终于，在四月初我开始物色新的住处。这一次，我直接将地点锁定在大望路地铁站附近，原因有４：1. 去公司要坐14号线；2. 常去的机厅都在1号线；3. 地铁站距离与生活质量成反比。\n大望路是个热闹的商圈，著名的SKP商城就坐落于此，附近的几所高档公寓都给了我一点小小的富人震撼。附近还有一座上世纪八十年代建成的小区，离地铁站不到百米，其中一间自如2居室基本符合我的所有要求，最酷的是它居然在一楼！我衡量了一下自己毕业以来的收入水平，发现自己已经有实力承担更多的房租了，于是很快定了下来。要是在以前，我可能会在搬走的前一晚造访一遍楼下的酒吧和烧烤店，边喝闷酒边回忆往事，但这次我甚至没有时间停下来，匆匆地拍了照片就离开了这所承载了大部分毕业后生活的房子。十年后如果有机会回到那里，不知道又会是怎样的一番风景。\n搬家过后又是一通折腾：要把打包好的一大堆东西重新拿出来放到新的位置；要调整房间布局，拆掉多余的床，把东西挪来挪去；要去宜家买柜子和晾衣架；书桌、垃圾桶、扫帚、拖把、微波炉等各种东西都要重新购置；本来给猫装了一个直通屋顶的爬架，结果质量太差，没半个月板子被踩断了，要拆下来、退货再物色一个新的。生活中的琐事越来越多，工作任务也越来越重，有时候会让我想逃避甚至放弃一切。但我转念一想，如果连小事都处理不好，如果连按部就班的生活都承受不住，怎么敢突然调转船头，航向未知？\n五一假期：上海\n之前计划五一来上海旅游主要是为了凑cp29的热闹，结果由于抢票活动过于拖拉，等确定自己抢不到票之后已经来不及变更计划了。\n来到上海的当晚十二点，我听说有些机厅一直营业到凌晨，于是直奔“街机烈火”。这是一家传说级别的老店，里面摆放的是一排排经典街机，完全为玩家能够玩得尽兴而服务，没有那些无脑吞币的低质游戏。音游机种之全面远胜于一般机厅，而maimai DX居然有7台之多（另有maimai FiNALE三台）。直到我凌晨一点半离开时，maimai区依然有十几个玩家。以游戏质量、机台数量、玩家热情而言，这一家的确是我见过的最接近“街机天堂”的地方。相比之下，北京的街机文化要稀薄得多，机厅以大型连锁店为主，看不到日本经典街机的影子，充斥着各种为吞币而服务的低质量游戏。\n心满意足地回到酒店睡觉，结束第零天。第一天跟着在上海工作的同学去逛了各种杂七杂八的地方：小资风情街、书店、多抓鱼、咖啡厅等等。一路上全是人，那些有名的景点或店铺根本不用指望。到了吃午饭的时候，同学想带我到一家馄饨店排长队，说是“必吃”，但由于实在饿得不行，我们就去了旁边的一家黄鱼面馆，根本没人排队但也很好吃。后来同学坚持去馄饨店排了队并带回来两碗，结果味道也没有多惊艳，所以说队伍长度和美味程度关系不大，出去旅游真没必要把时间花在排队上。还有一件令我印象深刻的事情是某处厕所门口的洗手池用的是戴森牌水龙头，造型很独特，除了中间出水外，两边还各伸出一根管子用来风干，属实让我开了眼界，不得不再次感叹一句：哎，资本。\n第二天去环球港吃巴奴，刚好“世嘉都市乐园”也在这里（实际上就是个机厅），又去看了看，最后在书店停留了很久。整个商场的二次元浓度很高，有很多穿着二次元服装的顾客，一些二次元主题的店，包括墙上贴的大型海报也有各种纸片人。晚上到朗玩转了一圈，主要优点是地盘大，能摆下很多综合娱乐设施（如台球），但机种没什么特别的，所以看了几眼就回去了。在这里还看到了传说中的新一代舞立方，造型酷炫，甚至能给手机充电，这一点属实是完爆maimai了。同一栋楼里还有一家风云再起，里面还有几台maimai。粗略算了一下，上海含有maimai的街机厅密度大概是北京的3.5倍，如果按台数算这个数字还会更高。放到《文明6》的数值体系里，上海的宜居度和文化值估计是北京的好几倍。\n第三天下午居然去了一家furry主题的公馆，并带走一条项链。晚上去烈火消磨时间。\n第四天又来到一家商场吃饭，坐摩天轮并逛来逛去。毫不意外，这家商场也有maimai。\n最后一天早起赶路，回到武汉，写下了这篇流水账。上海的确是消费和娱乐的天堂，有太多地方能让你迷失其中。这里有数不清的购物中心、写字楼、剧院、广场、风情街，能容纳各种各样的小众文化，也没有那么多把人拒之门外的神秘区域。如果说北京的肃穆让人感受到巨大权力结构下个人的渺小，那么上海的繁华则会带你走进一个由资本建立的乐园，只要你愿意，你可以把全部的人生投入到赚钱与消费的循环中并乐此不疲。不管是北京还是上海，总有人喜欢去偏安一隅的小县城走走，可能本质上都是为了逃离一个巨大的存在吧。消费和权力带来的“自由”，和内心挣脱一切外在束缚的自由，对我们这些注定要在metropolis里度过一生的人来说，更渴望的可能还是后者吧。\n心理咨询 bullet point\n本来已经毫无瓜葛了，但是被逐一取消关注的时候还是感觉不好受 脚踏实地 vs 仰望星空，我该怎么平衡 大部分时候对我很顺从，偶尔会突然情绪低落，事后又很自责 渴望和父母拉近距离，建立一种和谐的关系，但始终做不到 ","permalink":"https://daichao1997.github.io/posts/life/2023.05/","summary":"\u003cp\u003e一年一度的总结记事多少有些慢了，很多发生在年初的事情等到年末再回忆会有些困难，所以我决定从今年起，尽量在三个长假（五一、十一、春节）都写一篇记事，恰好将一年划分为冬去春来、炎炎夏日和凛冽寒冬的三个阶段。如果春节还有多的时间，可能还会再推出一篇偏抽象的思考。\u003c/p\u003e","title":"2023.05"},{"content":"Prologue OK兄弟们，全体目光向我看齐嗷，看我看我，我宣布个事儿：\n我是个废物！\nPart A 时至今日，我终于可以毫无负担地说出这句话了。如果说我2022年取得了什么“进步”，这大概就是最大的进步。\n可别小看它，能够坦然承认自己是废物并不是一件容易的事，尤其是在这个从小就把孩子当角斗士培养的社会。\n咱就别说小初高了，对那时候的我们来说，不专心做题是要被批评的，成绩下滑是要深刻反省的，熄灯了玩两盘三国杀是要请家长的。这些还只是学校的小小惩罚，要是碰到暴戾的家长，后果不堪设想。\n我也不例外，上大学前想玩任何东西都只能偷偷玩，一边玩一边提防老师和爸妈，还要一边自责愧对了他们的期望。更别提那个时候我觉得自己是超人，似乎一学就会、一点就通，付出一半努力就能收获双倍果实，不仅要拿第一，还要拿得顺理成章理所当然。在这样的环境下，我怎么会有自己是废物的念头，又怎么敢有这样的念头？\n磕磕绊绊地走完化竞之路，来到群英荟萃的北京大学之后，我很快就见识到了自己和别人的差距，但还是执拗地相信只要努力一点就能追上。不幸的是，我既不能坚持努力，也不知道到底该怎么努力。就拿ICS这门课来说，我在暑假就出于兴趣将教材通读了一遍，lab也能磕磕绊绊地完成，但是听高分大佬讲解他用“magic number”、“deadbeef”、“平衡二叉树”刷到lab最高分时，我只感到茫然。那是一种正在努力钻木取火的原始人看到打火机时的茫然。期末考试之后，茫然变成了绝望。\n但我觉得，这一定是我打了太多游戏导致的，只要把打游戏的时间拿来学习不就好了？或者，这一定是我作息不规律、白天学习效率不高导致的，以后早睡早起不就好了？再或者，是我这学期选的课太多了，没有分配好时间，下次吸取教训不就好了？\n总之，我一定是超人，我怎么会不是超人？超人也会遇到挫折，这很正常，只要我把每件事都做对，最后一定能脱颖而出。\n“天才”、“超人”、“脱颖而出”，这些曾经对我来说真的很重要。成为超人的执念已经紧紧地缠绕在我的内心深处，只有经受现实的一次次锤打才能让它在痛苦中解开。\n直到今日。\n无论是假努力还是真努力，我都试过了，没用。不会就是不会，比不过就是比不过。\n本科刚毕业就年薪百万的少年天才，怎么比？\n不知道哪里学的技巧、哪里来的本金，总之还没毕业就炒股赚了一百万，怎么比？\n就算是参加十佳歌手比赛，都能碰到一堆声乐大佬。欸，有些也是信科的。\n更别说还没毕业家里就给买好了房子的，不知道哪里来的钱没事就出国旅游的，一天可以只睡四个小时的……这难道也要比？\n当然，这些都无所谓。北大嘛，全国的人才都往这里跑，我算老几啊？\n总之最后混到了毕业证进入了社会，但是peer pressure并没怎么减少。参加了工作的同学们互相打听薪资待遇：你赚100万我只赚50万，我怎么这么loser！他只赚30万？没事，30万很多了，钱够用就行！\n我实在是厌了。除了会读书和会赚钱，一个人的优秀还可以体现在很多方面啊。无论是健身还是弹钢琴还是读书，只要日积月累，想必一定能变得优秀起来吧。\n但很可惜，没有。去年总结时我就发出过迷茫：我明明很喜欢钢琴，为什么坚持不下去？练琴的确有些枯燥，但我应该坚持下去啊，要是连这点困难都克服不了……\n我不就是个废物了么？\n这，这可千万、千万不要是真的。我一定不是废物，一定不是。\nPart B 我大概是九年前开始玩音游的。一开始主要玩节奏大师，后来是Deemo，四年前拥有iPad后开始玩Arcaea，不过这些都可以统一称为“移动端音游”。从去年开始，我入坑了第一款街机音游maimai。\n和健身、旅游、读书等“主流爱好”不同，音游是相对难以“拿得出手”的一种爱好。想象一下，你试图和一群新认识的朋友介绍自己的爱好“我喜欢打maimai”，但这只会引起他们的困惑：maimai是什么？你解释“maimai是一种街机音游”，他们更疑惑了：街机？音游？你又不得不继续解释，最后他们只能打个哈哈说“厉害厉害”。\n与外部世界的毫不在意形成强烈对比的是，音游玩家大都非常重视自己的游戏成绩，也会关注和认可其他玩家，尤其是ranker（问鼎游戏排行榜的顶尖高手）们取得的成就。\n所谓“成就”，基本上就是将难度最高的几个谱面打到满分，有时候还会区分“All Perfect”和“理论值”（前者仅意味着零失误，后者则要求达到理论上的极限分数）。\n第一个达成的玩家可以获得“首杀”的殊荣，根据地理范围还可以分成“全球首杀”、“全国首杀”、“大陆首杀”。歌曲的难度越高、“首杀”的空缺时间越长，在玩家之间造成的轰动就越大。这其中的代表性事件，便是日本玩家“kocster059”完成的“FREEDOM D↓VE”（又称“里FD”）世界首杀（参考资料：https://mzh.moegirl.org.cn/FREEDOM_DiVE#Cytus），而没过几天，中国玩家“Skisk刺球”紧随其后又完成了“二杀”。我还记得那一个礼拜，整个音游圈都在纪念里FD的“死亡”，可以看出这件事对他们来说意义非常重大。\n【衔接震撼】\n第一次让我震撼的，是“Maintain7716”为了获得Dynamix一首歌的满分而在9天之内重复2000次的努力（https://www.bilibili.com/video/BV17341127rY）。玩过音游的都知道，这种游戏需要高度集中精神。一天之内玩同一首歌两百多遍，考验的已经不是手速和技巧了，而是纯粹的毅力。更别提在这2000次努力中，有十多次都只有一处失误，那种离成功只有一步之遥的遗憾对心态的打击也是巨大的。\n第二次让我震撼的，是“MooseRiven”为了获得maimai一首歌的All Perfect而重复1192次的努力（https://www.bilibili.com/video/BV1WA411d7PE）。和移动端的Dynamix不同的是，maimai是一款街机音游，它需要玩家调动整个上肢去游玩。我一天打50首歌肩膀就会发酸发痛，但在追求All Perfect的过程中，他平均每天要打这首歌一百多次，而且是最难、强度最大的几首歌之一，同时还要不断检视失误并改进自己的手法。这何止是手速可以概括的？这完全是对心态、体力和意志力的多重考验！\n然而，对于社会上绝大多数人来说，这件事有什么意义？造成了什么影响？\n恐怕不如某明星放了个屁。\n就是为了这样一个不会带来任何收益、造成不了任何影响、没有多少人能理解的虚拟世界的“成就”，顶尖玩家们却投入了常人难以想象的心血。\n也就是在这个时候，我同时意识到了两件事：1. 意义和价值的相对性；2. 以大部分人的努力程度真的轮不着比天分。（还有一件是啥来着？？？）\nPart C 北京 VS 武汉\n【待展开】时间完全属于自己\nMoments 二月：通州北关，东方树叶，星巴克，夕阳余晖，何为良好生活，圣剑2 四月：隔离宾馆，风骚律师，Jade Star \u0026amp; Grin \u0026amp; and I\u0026rsquo;m home 五月：好梦青年旅馆，风云再起江汉路，大汗淋漓，solips \u0026amp; 夜驱 七月：梦与宝藏，空调，日语书，长沙发，大长队，暴雨停电，Flower of Life 八月：骑单车下班回家 十月：江汉路帝盛酒店 十一月：拍立得维修，英雄无敌，西西友谊 十二月：新冠 2023 2022年，我每天凌晨两点左右睡觉，十点左右起床刷牙洗脸，坐到电脑前看工作邮件和消息，没多久就点个外卖，边看视频边吃饭。下午忙的话就专注点，不忙的话就摸鱼玩手机，傍晚再点个外卖吃个饭。一天时间的七成大抵就是这样过去的。被雇佣关系固定了生活方式之后，我很难从一成不变的循环中提炼出什么东西，可能是因为我像许多人一样并不热爱自己的工作。虽然公司将我从L4提拔到了L5，但对我来说，包括升职在内的一切都只是按部就班而已。即便银行卡的余额在增加，这种增加也随着总量的积累而显得无足轻重，无法再给我带来兴奋感。我努力地回忆这一年取得了什么“成就”或者“进步”，然而浮现出来的只有一些零零散散的片段。\n二月的某个周末，我突然发现了一段完美的“一日游”路线：先坐地铁到常营的蔡林记吃热干面，一路上看看书、打打音游，再去通州北关的爱琴海购物中心玩maimai。从通州北关地铁站到商场还要走两公里路，会路过通惠河和西海子公园。那时正值冬春之交，前几次去的时候河边都积着雪，到三月中旬就完全是春意盎然的景象了。走到商场买瓶东方树叶，脱掉衣服就开始玩。底分五千多的时候我经常打圣剑2的红谱，每次都累得喘不上气，但我还是倔强地推分。有一次眼看着就要推到97%，却在最后掉了2%，气得我蹲在地上疯狂地锤打地面。玩累了，去星巴克点一杯冰摇红莓黑加仑，然后拿出iPad看书，透过座位旁的落地窗可以看到夕阳的余晖，还有来来往往的行人。再打一会肚子饿了，去必胜客点一个铁板鳕鱼披萨，吃不完的打包带回家里当夜宵。此时已经是晚上九点，肩膀又酸又痛，是时候坐上返程的地铁了。这个片段的配乐，是我当时最喜欢打的几首歌：「Excalibur Revived resolution」「Saika」「ECHO」。\n四月底，在巴彦淖尔的隔离宾馆过劳动节，14天点了10次德克士，因为实在没什么可吃的。没事的时候就躺在床上晒太阳，看窗外蓝色的天空。这是个与世隔绝的地方，没有高楼大厦挡住视线，也没有车水马龙和匆匆的行人。人们只是生活在这里，生活在当下，任凭外面的世界风起云涌。那一瞬间，我也找到了一丝宁静的感觉。这个片段的配乐，是我当时反复在房间里播放的几首歌：「Jade Star」「Grin」「and I\u0026rsquo;m home」「青空」「You」。\n五月的片段则是回到武汉之后，在江汉路风云再起玩到了「夜に駆ける」和「s∅lips」。为了方便远程工作，我在家附近找到了一所青年旅馆。它的大厅实际上是一家咖啡厅，摆放着许多木制桌子，大部分都有插座，而且网速飞快。不仅如此，大厅一侧还摆了一架电子琴和一把吉他，供有雅兴的住客们弹唱几曲。靠窗的地方是猫笼，里面住着一只毛色纯黑的小猫。一天下午，在咖啡厅写着文档，外面下着小雨，空气潮湿而凉爽。忽而听到电子琴那边有人在断断续续地试着演奏一曲，又有人和着琴声伴唱，恍惚间似乎感觉一切都“对劲”了起来，但想到不久之后又要回到北京，顿时又觉得迷茫了。这个片段在我的回忆里没有配乐。\n北京的防疫政策从那时起持续加码，等我回去之后，所有机厅都关门大吉了，只有一家叫做“梦与宝藏”的店成了北京maimai玩家的救命稻草。这家店业务很杂，约等于小型游戏厅+桌游吧+水吧，还顺带出售JK/DK服装。店里的环境十分安静，最美妙的是有空调和Wi-Fi，还有一大片桌椅供人歇息。三月份的时候我来过几次，那时还鲜少有人光顾，但从机厅全面关停开始，这里每天都有差不多十个人排队打maimai。七月份正值盛夏，周末中午吃完饭坐地铁来这里排队，要一个装了冰块的玻璃杯，把带来的雪碧倒进去，气泡哗啦啦地从冰块冒出来。拿出刚开始学的日语书背五十音图，此时店里的音响正在播放「FLower of Life」——这就是我的年度瞬间。该怎么形容这个瞬间？我只不过是在一个平凡的夏日，为无聊的周末生活找了一点平凡的娱乐方式，但每每回想起来还是有所触动。这个片段的配乐，当然是「FLower of Life」。\n秋冬之际，天气逐渐转冷，严格的防疫和突然的放开又使得民生凋敝，整个城市的气氛越来越压抑，于是我也变成了缩进了壳的蜗牛。也有发生一些事情——实际上每天都在发生大大小小的事情，但我不想写在这里。每年冬天最大的盼头就是捱过去，因为冬天过了就是春天和夏天，到时候温暖的阳光会再次照进每个人的心里。\n2022年发生了很多很多事情，我实在没有时间逐一细写。我甚至没有写自己剪掉了长发。上面这些片段也只是过去365天一丁点小的部分，但每次回忆时，它们都会第一个出现。我说不清为什么，也许人的一生这么长，就是为了追求这样的瞬间吧。\n","permalink":"https://daichao1997.github.io/posts/life/2023/","summary":"\u003ch2 id=\"prologue\"\u003ePrologue\u003c/h2\u003e\n\u003cp\u003eOK兄弟们，全体目光向我看齐嗷，看我看我，我宣布个事儿：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e我是个废物！\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"part-a\"\u003ePart A\u003c/h2\u003e\n\u003cp\u003e时至今日，我终于可以毫无负担地说出这句话了。如果说我2022年取得了什么“进步”，这大概就是最大的进步。\u003c/p\u003e","title":"2023"},{"content":"随便聊聊 昨天我收到了居家隔离的通知，下周二才放出来，现在还剩68小时。虽然被限制人身自由很不爽，但和上次去内蒙隔离相比根本不算什么。正好现在周末了，我又不能出门，不如一边隔离一边给大家讲讲上次隔离的故事吧。\n故事的开始是我要去内蒙古某县城看望前女友。她从去年秋天开始在某中学支教，所以我们一直处于异地的状态。从今年开始，防控政策越来越严，当地的要求逐渐演变为“行程码带星者集中隔离7天”，而北京基本上没有消停过，我碍于隔离的损失太大，就一直没有过去。就这么等到四月下旬，北京市的中高风险区终于全部清零，行程码的星号也消失了。当时我和她还兴冲冲地讨论什么时候过去，要不要等到五一，但没想到第二天北京就又发现了一波大的。\n我早就清楚这破烂事没完没了。为了尽量减少异地对关系的影响，我当晚立刻买了第二天的火车票和机票，做了个核酸，然后去公司拿了开发用的设备，订购了一个小行李箱，顺便在外卖软件上订了她想吃的东西。由于披萨店开门太晚，我没法等披萨送到家再出发，所以找了火车站附近的一家披萨店直接送到那里。到呼和浩特下车后，我得验一下核酸结果才能出站，再坐地铁赶去机场，但没想到这破核酸到第二天下午两点还没出结果。我不断地刷新刷新再刷新，祈祷着千万不要误机，最后终于在快要排到我的时候刷出了结果——也许是因为数据库感受到了我的诚意吧。\n就这样，虽然时间紧迫、准备不足，很多事情都是掐着表卡着点做的，但一切都还算顺利。起飞前，我轻松地坐在飞机座椅上，心情甚好，拿出手机一看：\n北京又有中高风险啦！\n我赶紧打开行程码——回来了，全都回来了，还是那颗熟悉的星星。我急忙问空姐怎么办，会不会还要隔离，她表示这不关航空公司的事。此时有乘客劝我原路返回，还有人劝我回呼市办个新号，第二天再拿新号的行程码去那边。其实办新号的方法是最稳妥的，但我已经无法理性思考，只想着今天无论如何都要见到她，不能让她失望，于是决定赌一把，坚持飞到了某县。\n下了飞机，免不了又是查这个查那个的，我又用一些方法混过了行程码的检查，终于见到了前女友。当时我还想着，虽然这一路很不容易，但好在有惊无险，我们之间的感情一定会越来越牢固。\n4月26日，周二。才过一天，我就接到了当地社区的电话，让我去集中隔离7天。前女友为我万分细心地收拾了行李，往里面放了各种各样的东西。和她依依不舍地告别后，我住进了一间普通得没法形容的旅馆，费用是90元一天，只提供饮用水。工作人员带我来到一间没有窗户的狭小房间，说是带窗户的房间第二天才能空出来，让我先在这暂住一晚。这个房间几乎没有能自由活动的地方，除了厕所就只有两张单人床和一张小桌子。白光灯照在四面浅蓝色的墙上，让人分不清白天和黑夜。\n整理完东西后，我躺在床上开始试着打发时间。这里唯一一家大品牌是德克士，我中午点了他们的虾堡套餐。饭后，我看了两集Better Call Saul，又搞了一会工作，但很快我就没有什么想做的事情了，只能默默刷手机。\n晚上点了个鸭腿炒饭。我已经忘记当时是怎么混到晚上的了，也许只是反反复复地做那几件可供消遣的事情吧。在这样逼仄的环境里，我不得不想办法让自己忘掉被囚禁的事实。\n4月27日，周三。我照常远程工作，打发掉了不少时间。中午点了一碗菠菜粥和炒驴肉，看了一两集剧之后又继续开会和工作。与此同时，我一直在催促工作人员给我换房，结果他说昨晚空出来的房给了另一家人，女的怀孕了很不方便，让我理解一下。我十分生气地质问，但可想而知会得到什么答复：“你来这里隔离不是度假嗷，听从安排就完了！明天再给你换！”\n晚上点的德克士虾堡。\n4月28日，周四。在我不懈的努力催促下，这天晚上我终于住进了有窗户的房间。不知道是不是为了补偿我，新房间甚至比其他的双人房间还要大，差不多有30多平米，还有一面大大的窗户，我非常满意。中午点的德克士虾堡，外加一瓶善存维生素片和西瓜霜喷剂——我好像有点口腔溃疡。晚上点的麻辣大饼。\n4月29日，周五。新房间的通风和阳光都很好。中午，我把窗户和门全都打开，让凉风吹满整个房间。下午三点，我把床单、毛巾、袜子铺在床上，让阳光充分照射。隔离只剩下三天，再坚持一下就过去了！房间环境这么好，心情也不会很糟糕！中午点的粥和炒驴肉，晚上点的鸭腿炒饭。\n4月30日，周六，五一假期，离出狱还剩两天。中午点的砂锅米线，晚上点的德克士鸡排饭。\n5月1日，周日，五一假期，明天就刑满释放了。中午点的德克士鸡腿饭和一些小吃，晚上直接就着中午剩的和前女友给我带的甜点对付了。\n5月2日，周一，五一假期。起床接到电话，说是北京疫情严重（？），防疫政策有变，凡是北京来的都改为隔离14天，原定隔离7天的要再补7天。\n？？？\n你一开始直接告诉我隔离14天我都好受些。好不容易盼了7天，以为自己熬到头了，又把我打回去再加7天，不如把我杀了烧成灰泡进消毒液再冲进下水道进行无害化处理吧？\n我打电话给前女友，告诉她还得再关7天。我们都有点崩溃，但还是彼此安慰。当天下午她给我送了大量补给，甚至取走了我的一些衣服回去洗。\n5月3日，5月4日，五一假期。Better Call Saul的前五季已经看完了，实在没什么事情做了。我开始研究一些工作中遇到的问题，在网上参与各种鸡零狗碎的话题讨论，仔细打磨了篇没什么人看的议论文发到网上。\n5月5日，周四，我继续照常上班。这几天我究竟做了些什么来度过最后的72个小时？我已经完全没有印象了。\n5月8日，周日下午三点，我拖着行李箱重新走到旅馆外面的街道上，好像做了一个漫长的梦。这一周我点了5次德克士，2次煎饼，1次炒饼，1次粥，1次牛肉拉面，1次杨国福麻辣烫，1次麻辣香锅。就这样，为了配合“防疫”，我人生中宝贵的14天悄无声息地溜走了，肠胃里只留下了德克士。\n自由，牢笼，舒适圈 半年后再回首这336个小时，我发现了一件可怕的事实。当我清楚地知道自己被囚禁的时候，我会想各种办法给生活寻找意义。然而当我回到北京、以为重获自由了之后，我却不由自主地开始重复这两年形成的生活习惯，以至于几个月都一成不变。这时我才意识到，我可能只是从一个狭窄而简陋的牢笼回到了另一个宽敞而精致的牢笼。\n但牢笼并不是问题的关键，毕竟地球就是最大的牢笼，而我们人类已经被“囚禁”了上万年。这里不得不提到前些年很火的一个概念，叫“舒适圈”。以前常听人说“要跳出自己的舒适圈”，不过最近已经没什么人提这茬了，可能大家开始觉得有个圈（juan）就足够了吧。\n假想一下，如果我对现在的生活非常满意，而且房价很低、工作随便找、一点压力都没有、只要喜欢的女孩跟她一追求就同意（再加一个走10分钟路就能玩到maimai），这时跑来一个人劝我说“要跳出自己的舒适圈”，我肯定回答：“不会吧？”\n当前我所焦虑的事情包括：书摆在手边不想看，健身环摆在旁边不想玩。我知道这些是有价值的事情，做了能让我学到东西，或者让我的身体更加强壮，可我就是不想做。为什么呢？我的工作压力并不大，但我总是感到疲惫。\n被囚禁的时候，我会舍不得虚度光阴，留给我的选择只有“什么都不做”和“好歹做点什么”。我可以天天吃德克士，洗完头湿着头发睡觉，只因为我心中有个盼头：只要捱过去了，一切都会好起来。\n重获自由之后，我又发现自由其实没什么用。\n","permalink":"https://daichao1997.github.io/posts/life/2022.05%E5%B7%B4%E5%BD%A6%E6%B7%96%E5%B0%94%E9%9A%94%E7%A6%BB%E4%B9%8B%E6%97%85/","summary":"\u003ch1 id=\"随便聊聊\"\u003e随便聊聊\u003c/h1\u003e\n\u003cp\u003e昨天我收到了居家隔离的通知，下周二才放出来，现在还剩68小时。虽然被限制人身自由很不爽，但和上次去内蒙隔离相比根本不算什么。正好现在周末了，我又不能出门，不如一边隔离一边给大家讲讲上次隔离的故事吧。\u003c/p\u003e","title":"巴彦淖尔隔离之旅"},{"content":"2022 从2013年第一篇年记的萌芽开始，到2014年的第二篇，到2015年的第三篇，…，到2022年的第十篇，我已经历许多。现在，结束我最摆烂的一年吧：从一开始的慷慨激昂到现在的无话可说。[1]\n今天是2月7日，已经是回武汉的第九天了，这几天我时时刻刻都在逃避写年记。无论是玩手机、打游戏、看书，还是出去走亲戚，只要一想到年记还没写，我的心里就有个疙瘩。但是一打开电脑，一个字都写不出来。明天我就坐高铁回北京，开始新一年的打工生活了。如果还没法把2021年总结出来，连载了十年的年记系列就要断掉。\n这种状态就跟以前写课程论文一样，不想写又不得不写，于是一天天地拖下去，直到deadline的前一天才开始动笔。不同的是，写课程论文是为了迎合别人，写年记却是为了安慰自己。不写论文的后果是挂科，但不写年记就相当于承认了过去的这一年“挂科”了。我不知道这样的想法对不对，但即使写不出来，写一篇“我为什么写不出来”的检讨也是好的。\n作为对比，我们来看看上一篇年记。就在那时，我还相信自己做的事情一定可以“lead to somewhere”。第一次出国旅行，意味着将来的更多次；第一次养猫，意味着将来更丰富的生活；第一次辞职，意味着我将重启自己的职业生涯；而后报名的健身课与钢琴课，以及一段新的恋情，都预示着一种充实生活的开始。\n然而从3月起，我就再也没有去过琴房；5月起，我就再没有去过健身房。忙吗？一点也不。何止是不忙，简直是大把的时间没地方浪费。我连公司都不用去，每天就呆在自己的小屋子里，按部就班地做着leader给我的任务，剩下的时间用各种可能的方式取悦自己：刷论坛刷到写好几篇议论性别的文章、重新捡起已经通关了两遍的塞尔达、不断刷新音游的最高成绩……\n既然这样，为什么不练琴？为什么不健身？\n其实应该反过来问：为什么要逼迫自己去健身或者练琴？如果只是为了获得快乐，那么搞笑视频和游戏能带给我多得多的快乐，而且触手可及，何必要把自己累得气喘吁吁，或者坐在钢琴前艰苦地摸索呢？\n很显然，除了快乐，这两项活动里还有我想要的其他东西。\n第一，成就感。所谓成就感，就是做到之后让我觉得自己很牛逼、想夸夸自己的感觉。\n第二，社会认可。熟知B站热梗和考取钢琴十级，哪一个更受认可，不需要多说。\n这两样目前都是我急缺的东西。对它们的渴望应该保持在一个正常的范围内，否则生活容易失去重心，但实际上从我进入北大以来，我就很少再有被认可的感觉了。大多数情况下，我都在学着接受自己和别人的差距，但似乎不起作用。躺平躺久了会觉得焦虑，努力的时候又不能持之以恒。久而久之，我越来越依赖于那些能使我暂时忘记内心挣扎的东西：搞笑视频使我乐乐呵呵，游戏帮我遁入虚拟世界，时事新闻占据我的思考线程，网络卫道助我发泄不满。[2]\n唯一的缺点是，这些东西带来的快乐转瞬即逝。在放下平板电脑和手柄的一瞬间，我又不得不面对空荡荡的房间，面对依旧不知去向何方的现实，于是我又忍不住重新拿起，如此循环往复，离群索居的生活方式更是让我无法从中摆脱。现实与愿望之间的鸿沟使我变得焦虑，过分地关注自身，以至于伤害了身边的人。\n我的2021年基本上就是被这样的心态填满的，而且越来越严重。到了今年一月，我已经完全没有心思工作了，一下班就沉迷文明6，生活规律也一塌糊涂，更别说写年记了。我只盼着春节快点到来，盼着回家后的新环境能把我救出来。然而一周的假期很快就要过去，明天我又会回到那间与宇宙隔绝的出租屋了。希望这一年，我能找到逃脱循环的方法。\n你好，2022。\n[1] 此段拙劣地模仿了游戏《文明6》远古时代的开场白：从水下第一个生命的萌芽开始……到石器时代的巨型野兽……再到人类第一次直立行走，你已经历许多。现在，开启你最伟大的探索吧：从早期文明的摇篮到浩瀚星宇。\n[2] 当然，这些事情并非毫无意义，例如时事新闻推动我关注历史和社会，游戏里的很多瞬间也给我留下了难忘的回忆，这些都在朋友圈分享过，所以不再多说。\n","permalink":"https://daichao1997.github.io/posts/life/2022/","summary":"\u003ch1 id=\"2022\"\u003e2022\u003c/h1\u003e\n\u003cp\u003e从2013年第一篇年记的萌芽开始，到2014年的第二篇，到2015年的第三篇，…，到2022年的第十篇，我已经历许多。现在，结束我最摆烂的一年吧：从一开始的慷慨激昂到现在的无话可说。[1]\u003c/p\u003e","title":"2022"},{"content":"昨天跟广宇吃晚饭，深入交流了我现在这个岗位的发展规划。\n我一个很大的缺点是喜欢让人推着走，多一事不如少一事。issue分配给我，我不会想着主动监测进度，也不会顺着issue去学习不懂的东西，而是浅尝辄止，等着vendor把问题解决，只要不找我我就不管。\n另一个缺点是犹疑不决，一直没有确定未来的发展方向。一方面，我很少主动了解行业信息，缺少做出决定的自信；另一方面，我又害怕将来会后悔自己选错了方向。我想做的事情太多，但是沉不下心来去做，而且害怕失败，所以总是沉迷于能立刻让我满足的东西，比如游戏。\n就目前来说，我想做system，但我又会想AI是不是更有前途、做一辈子system会不会太枯燥、我真正的passion到底是什么，于是我踌躇不前，一会想在当前的岗位上好好干，一会又想学MIT6.824，最近还学起了投资。至于健身和钢琴，虽然我投入了两万多元买课，但练到一半还是放弃了。最讽刺的是，我一直将钢琴视作真正的“passion”，但其实已经半年多没有练过了。我想要的东西总是在变，但我似乎一直没有做什么。\n最近看江泽民和乔布斯的传记，可以很明显地感受到自己和他们的差距。首先，他们都是行动派，想好了就会去做，能保持旺盛的精力，并能忍受其中的艰辛。其次，他们都有吸引人的特质，都懂得参与社交，这使他们更容易获得机会。最重要的是，他们并没有从一开始就规划好一生。江泽民一开始是学工程的，后来当厂长、研究所所长、一机部外事局副局长，这才慢慢转入政坛。而乔布斯不喜欢自己的大学专业，从那里退学之后又留下来旁听各种杂七杂八的课程，然后攒钱去印度寻找“精神导师”，把自己搞得人不人鬼不鬼。江泽民会在刚毕业当工程师的时候就想到从政吗？离经叛道的乔布斯会先给自己定一个“成为大公司CEO”的目标再步步为营吗？不可能的，他只是看到机会，然后牢牢抓住了而已。当然，智力、成长环境等因素也很重要，但这些不是我能够改变的。我只能时刻提醒自己，不要再陷入以前的思维陷阱，有什么想法就赶快去做。\n","permalink":"https://daichao1997.github.io/posts/life/2022.01%E8%87%AA%E7%9C%81/","summary":"\u003cp\u003e昨天跟广宇吃晚饭，深入交流了我现在这个岗位的发展规划。\u003c/p\u003e\n\u003cp\u003e我一个很大的缺点是喜欢让人推着走，多一事不如少一事。issue分配给我，我不会想着主动监测进度，也不会顺着issue去学习不懂的东西，而是浅尝辄止，等着vendor把问题解决，只要不找我我就不管。\u003c/p\u003e","title":"自省"},{"content":"完整版链接：https://www.usenix.org/sites/default/files/osdi20-full_proceedings.pdf\nCorrectness 1. Theseus: an Experiment in Operating System Structure and State Management 这篇论文用 Rust 写了一个叫做 Theseus 的操作系统。\nTheseus 希望解决的问题是：1. 提升 OS 的模块化程度；2. 充分利用 Rust 的特性。\n提升 OS 的模块化程度，意味着：1. 避免一个模块的错误波及至其他模块；2. 能让模块迅速地从错误中恢复；3. 可以实时更新一些模块而不影响其他模块的工作。\n模块之间错误传递的现象叫做 state spill。例如，安卓系统的某个系统服务一旦出现故障，整个用户空间的进程都会崩溃，哪怕一些应用并没有使用出现故障的服务。再比如，传统的内存管理模块（MM）需要维护一张表来记录分配出去的虚拟内存区域（VMA），其他进程只能通过 MM 分配给它们的 handler 访问这些区域。因此，一旦 VMA 失效，所有使用 handler 的进程都会出错。\n为了让模块迅速恢复，Theseus 采取了三步走：首先清除报错的 task，如果不行就重启之。如果还不行，就直接重装模块。这种策略之所以能够实现，是因为 Theseus 具有实时更新模块的特性。\n不同于传统操作系统，Theseus 不需要停止所有进程就能完成一些模块的替换，而这要归功于模块之间的弱依赖性。Theseus 把整个系统拆分成很多个相对独立的 cell ，每个 cell 都由一份代码文件实现，编译成单独的 object file，运行时被装载到不同的内存区域，实现了高度隔离。\nTheseus 将 Rust 的特性充分利用在了内存管理和进程管理中。例如，进程申请分配虚拟内存时，Theseus 会将这片内存打包为 object 返回给申请者，而不是像传统 OS 那样返回一个指针。根据 Rust 的 Ownership 机制，申请者就“拥有”了该片内存。当申请者试图访问它时，必须通过对应的接口指定一个类型。Theseus 检查该类型不会越界后，才会返回一个该类型的 slice，这样就把潜在的地址越界扼杀在了编译阶段。当拥有该内存的申请者离开其作用域后，该内存也会随之被释放（Ownership 机制），所有对它的引用都会报错，这就在编译阶段消除了 use-after-free 的可能。总之，Theseus 充分利用了 Rust 编译阶段严格的安全检查，拥有了比 C 语言系统更好的安全性。这种将语言特性整合进系统特性的做法，作者称之为 intralingual approach。\n从第二篇文章起，我主要会关注它解决的问题和提出的新思路，不会再仔细研究具体做法。\n2. RedLeaf: Isolation and Communication in a Safe Operating System 这篇论文用 Rust 写了一个叫做 RedLeaf 的操作系统。\nAbstract 本系统探索OS的语言安全性。比起商业OS，RedLeaf不通过地址空间隔离错误，而是通过类型与内存的安全性。提出了domain的概念，一种基于语言的轻量级“隔离域”，可以隐藏信息、隔离错误。domain可以动态装载、干净地清除。一个domain的错误不会影响其他domain的运行。通过RedLeaf的隔离机制，我们实现了设备驱动的“end-to-end zero-copy, fault isolation, and transparent recovery”。我们还验证了它的性能。\nIntroduction 内核子系统之间的隔离是保证系统可靠性与安全性的重要机制，但是现代OS依然以单内核为主，在隔离方面软硬件层面做得都不够。这主要是因为隔离机制需要牺牲性能。同样的道理，一些安全性较强的语言不适合写底层的OS，因为它们需要GC或者特定的runtime，开销很大（比C语言慢20～50%）。Rust的出现缓解了隔离与性能的矛盾，它通过ownership机制保障了类型与内存安全，不需要GC。\n我们想证明：语言安全的真正好处，在于提供轻量级、细粒度的隔离机制，以及其他数十年都未能落地的重要机制（fault isolation, transparent device driver recovery, safe kernel extensions, fine-grained capability-based access control）。\nRedLeaf不使用硬件提供的隔离机制，只通过Rust的类型与内存安全实现这些机制。然而语言本身提供的机制不足以实现一个足以隔离内核与用户的系统。它还需要fault isolation机制，能在不影响系统正常运行的情况下，处理模块的异常情况。它需要回收该模块的资源、保留分配给其他模块的资源、使调用其接口的程序报错（而不是继续运行或阻塞）。\nRedLeaf domain遵循以下原则：\n堆隔离，domain不使用指向其他domain私有堆的指针，保证了释放私有堆的安全性（另外有共享堆用于domain之间的通信） 可交换类型，仅指定类型的对象可以在domain之间交换 所有权跟踪，跟踪共享堆上所有对象的owner，确保对象传递时原来的domain里没有别的alias（？），这样就能安全地释放故障domain在共享堆上的资源 接口有效性，跨domain的接口只允许传递可交换类型 跨domain调用代理，domain之间的通信要通过一个安全的proxy传递，它会更新对象的owner，处理故障domain 这些原则实现了开销很小的数据隔离和有效的错误隔离：domain故障时由proxy终止线程、回收资源，再被呼叫时由proxy报错而不会panic；由故障domain传递给其他domain的对象不会被回收，因为我们跟踪了对象的ownership。\nRedLeaf Architecture 略，我们先看看其他文章。总之本文的重点是用Rust写一个模块之间充分隔离的、实现了高效fault isolation的操作系统。\n3. Specification and verification in the field: Applying formal methods to BPF just-in-time compilers in the Linux kernel 本文涉及了一个我不了解的领域：Linux内核扩展、Berkeley Packet Filter、Just-in-time compiler。\n扩展内核的常用方式，是将应用层的代码下载到内核。应用向内核提交一个由专用语言写成的程序，然后内核通过解释器执行，或者用JIT编译成machine code执行。BPF就是这样一个语言，它已经被广泛应用在了Linux内核的多个模块，例如网络、安全、tracing以及其他服务。\nBPF JIT compiler非常重要，但又容易出一些小错，所以本文实现了一个叫做Jitterbug的框架，辅助了JIT的实现和验证过程。\n由于我并不了解该领域，而且也不太感兴趣，就先跳过吧。\n4. Cobra: Making Transactional Key-Value Stores Verifiably Serializable 不了解数据库，本文的核心问题是“用户如何验证黑盒数据库满足了serializability”，这与数据库的正确性、稳定性、安全性相关。本文有一些数学模型和算法设计。\n5. Determinizing Crash Behavior with a Verified Snapshot-Consistent Flash Translation Layer 现代计算机在存储数据时，会经过一个存储栈（storage stack），例如应用-文件系统-物理设备。要设计一个存储栈没那么简单，例如为了维持性能、节省等待时间，I/O操作的顺序可能会被打乱；系统断电或崩溃时，存储系统需要正确地恢复数据。本文针对系统断电或崩溃的场景，设计了一个叫做 snapshot-consistent flash trasaction layer (SCFTL) 的硬盘模型，能保证硬盘崩溃时能够恢复到上一次进行 flush 操作前的状态。这个领域我也不懂，就不深究了。\n6. Storage Systems are Distributed Systems (So Verify Them That Way!) 完了，这篇文章我从Introduction开始就一句话都看不懂，似乎是跟software verification有关。我只知道它的核心idea是把分布式系统的一些特点（异步、易出错的环境）类比到了存储系统。\nStorage 7. Fast RDMA-based Ordered Key-Value Store using Remote Learned Cache Network-attached in-memory key-value stores 是许多数据中心应用（数据库、分布式文件系统、Web服务、Serverless computing等）常用的技术。随着网络技术的发展，CPU成了数据传输的瓶颈。于是出现了一种技术叫做RDMA，是用来访问远程机器数据的，相当于联网版的DMA，可以绕过CPU传输数据。不过RDMA的一个缺点是“traversing tree-based index with one-sided RDMA primitives is costly and complex”，于是又出现了“index caching”的办法。本文针对index caching，用机器学习（真是万能）训练了一个learned cache，以及其他相关的解决方案。\n8. CrossFS: A Cross-layered Direct-Access File System 终于有了一个我懂的领域。\n本文提出了一个跨层级的文件系统（FS）。所谓层级，即该FS处于计算机系统的哪个位置，例如用户层、内核层、固件层。最典型的是内核层FS，它能满足最基本的要求，例如 integrity, consistency, durability, and security，但它有三大瓶颈。1. 要做I/O必须经过OS，这会带来1-4us的延迟；2. 常出现不合理的串行操作，并行性不够好，例如即使在访问一个文件的不同部分时也要给inode上锁；3. 没有充分利用存储硬件的能力，例如计算、几千个I/O队列、固件层的调度，最终影响了I/O密集型应用的延迟、吞吐量、并行性。\n用户层FS可以绕过OS读写存储介质，但是用户层往往不可信任，很难有一个满足基本要求的设计。固件层FS能利用好存储层的计算能力，但无法利用 host-level multi-core parallelism，而且都继承了 inode-centric design for request queuing, concurrency control, and scheduling, leading to poor I/O scalability。总之，这些FS都缺乏不同层面间的配合。\n然而，本文提出的跨层级FS可以解决上述一切问题！牛！具体的我就不看了哈。\n9. From WiscKey to Bourbon: A Learned Index for Log-Structured Merge Trees 本文针对LSM树做了一些优化：https://yetanotherdevblog.com/lsm/。\nB树是数据库系统里一种传统的索引结构，它可以用ML优化，称为 learned index。LSM树也有广泛的应用，例如BigTable、LevelDB等。本文将 learned index 应用到了LSM树。这里有一大挑战，就是LSM树适合写操作频繁的场景，但 learned index 更适合读操作，频繁的写操作会使它的模型失去效用。不过本文仔细研究了LSM的特性，找到了一种方法融合两种技术，并得到了一个不错的结果。\n10. LinnOS: Predictability on Unpredictable Flash Storage with a Light Neural Network 又是一篇将ML融合进传统技术的。本文要优化的问题是：闪存设备的内部结构日益复杂，使得I/O操作的延迟难以预测（比如当你向某设备发出I/O请求时，你很难知道它是不是在进行垃圾回收、自我修复等操作），影响了I/O性能。\n目前有三种解决方案：“白盒子”重新设计了闪存设备的内部结构，但问题是供应商不一定愿意这么制造；“灰盒子”同时从设备层与软件层入手，问题是一样的；“黑盒子”将设备视为完全的黑盒，只依靠软件层的解决方案。目前 speculative execution 是比较受欢迎的。本文提出了一种新的“黑盒子”方案，不修改文件系统或者应用，只是用ML学习设备的行为。LinnOS就是这样一种可以学习并推导每次I/O延迟的操作系统。\n这个模型需要在准确率和计算量之间平衡，因为每次I/O都要用它推断延迟。预测精确的延迟太难了，而且90%的情况下延迟都比较稳定，只有10%的情况下的延迟较大，形成一个“长尾”图案。因此，该模型的输出仅有“快”和“慢”，是一个简单的二分类模型，当判断结果为“慢”时就拒绝该请求并通知应用层处理（例如换用其他节点）。模型的输入特征也很简单，主要是当前pending I/O的个数，以及近几次I/O的实际延迟。\n个人觉得这篇文章写得挺好，有时间了可以详细读一读。\n11. A large scale analysis of hundreds of in-memory cache clusters at Twitter 这一篇与服务器缓存（in-memory caching）有关，主要研究如何提升大型网络服务的缓存利用率，并与Twitter进行了合作。他们研究了Twitter提供的大量数据，找到了一些规律，不过大部分内容都在展示数据和图表，没看到哪里跟系统设计有关。跳过。\n12. Generalized Sub-Query Fusion for Eliminating Redundant I/O from Big-Data Queries 好了，这一篇我一个字也没看懂，完全不懂SQL。跳过。\nOS \u0026amp; Networking 13. A Simpler and Faster NIC Driver Model for Network Functions 现在的网络驱动模型过于灵活，其实现的功能超过了一般情况下的需求，使得性能下降。例如，现在的驱动支持乱序处理packet，然而互联网的很多骨干架构都不需要这个功能。所以本文提出了一个新的网卡驱动模型，并为Intel 82599编写了长度仅550行的驱动，使其性能大大提升（吞吐率提高60%）。\n本文用两节的篇幅做了一些科普，对读者十分友好（不过内行人看了可能要说在水长度吧）。第二章介绍了 network function 的概念，我不知道中文名，后面就用 NF 代替吧。第三章介绍了网卡（NIC）的工作步骤，也让我好好补充了一下网络知识。\nNetwork Functions In this section, we introduce network functions: packet-processing appliances performing tasks such as routing, rate limiting, access control or caching.\n硬件NF是实现高负荷网络的传统方式，它的处理逻辑全部写在硬件上，一经部署不可修改，所以灵活性很差。要想改变这部分网络，必须从物理意义上替换掉它们。\n软件NF运行在通用的计算平台（例如x86 \u0026amp; Linux）上，通过网络协议栈（例如驱动、IP、TCP）与网卡通信。显然，软件NF更加灵活，更容易调试、部署和升级。不过，它的形式化验证是一个难题，性能也是个问题。网络性能的下限会影响到商业层面的行为，例如服务级别协议（Service Level Agreements）。一个10Gb/s的网络要保证在67.2ns内处理一个84B的packet，跟读取内存在一个数量级。换句话说，NF甚至没时间从CPU cache之外获取数据，由此可见性能的重要性。\n现代OS的网络协议栈不适合NF的原因有三。第一，传统的协议栈使用的是push模型（硬件通过中断通知软件packet到了），这在低负荷的情况下管用，但是高负荷的NF承受不起大量中断带来的开销，它更适合pull模型（软件主动查询硬件的packet处理情况）。第二，传统的协议栈通过OS与硬件交互以实现隔离，中间隔着的这一层OS带来了额外的开销，然而NF一般是独立运行的，没必要与上层软件隔离。第三，传统的协议栈允许以完全灵活的方式处理packet buffer，然而NF的行为是比较固定的，没必要为它们实现这么灵活的处理方式，这只会给性能带来负担。\nKernel-bypass stacks 允许程序直接与硬件交互，并且着重于pull模型、批量处理packet以及更加固定的buffer管理模式。实际应用中的例子有DPDK。\n驱动很不受开发者们喜欢。首先，开发者只能从制造商的手册里了解硬件的行为，有些手册甚至不是公开的，逆向工程也不甚可行。由于驱动常常由制造商内部维护，所以文档和注释都很缺乏，程序的bug也很多。于是开发者们只好把驱动看作黑盒子。但是驱动对于整个网络栈的性能至关重要。DPDK的驱动代码都超过了1000行，最长的甚至有66000行。\n14. PANIC: A High-Performance Programmable NIC for Multi-tenant Networks 现在网速越来越快，CPU处理数据的速度有点跟不上了，于是出现了一种智能网卡（SmartNIC），除了提供联网功能，还能帮助CPU完成一些处理数据的任务，减轻CPU的负担。本文设计了一种新的SmartNIC，可以适用于很多场景，填补了现在SmartNIC适用范围窄的缺陷。\n15. Semeru: A Memory-Disaggregated Managed Runtime 有关分布式计算的，说服务器之间有一种 resource disaggregation 的搞法，就是让每种资源由专门的服务器管，而不是给每样都给服务器一点。这么搞的好处有 1. 更容易通过调度来高效地利用资源；2. 某个服务器崩了只会影响一种资源（但是这个资源也全没了不是吗）3. 可以给服务器配备专门的硬件，便于替换和升级。\n不过这么搞的问题在于，远程访问内存的延迟比较大。以往的解决办法都是从底层入手，但本文提出了一个新的视角，就是从runtime的角度提升数据的locality，例如减少cache missing，减少远程访问的次数等。具体的我不太懂，就不多说了。\n16. Caladan: Mitigating Interference at Microsecond Timescales Web搜索、社交网络、在线零售都是互动型、数据密集型的Web服务，会把收到的请求分发给上千个服务器。尾延迟（tail latency）很关键，因为它决定了最终的响应时间。不过为了提升数据中心整体的效率，同一台机器往往会同时处理多个任务*（据我理解，这里的意思是，虽然多个任务分享资源会让尾延迟上升，但也避免了其他机器空闲着等待最慢的响应结果）*。于是，任务之间必须抢占资源，导致延迟大大提高，这种现象叫做 interference。\n为了缓解这种干扰，有一种办法是明确地为各个任务划分CPU资源，互相不得抢占。然而在现实中，负载往往是突变或者周期性的，例如在微秒级别的时间内突然爆发出大量请求，或者每隔一段时间被GC占据大量的内存带宽。现在的系统都检测不出这么突然的变化。\n本文提出的 Caladan 就是用来解决这个问题的。它是一个CPU调度器，由一个专门的调度核和一个Linux内核模块 KSCHED 组成。该调度器能迅速给出分配的决策，提高CPU的使用率。\n17. Overload Control for μs-Scale RPCs with Breakwater 现代数据中心的应用由一系列微服务 (microservice) 组成，通过RPC交互。为了满足现代应用对低延迟的要求，微服务有严格的“服务级别目标” (Service-level objective, SLO)，是对客户作出的服务保证的量化指标，有些是微秒级别的。虽然现代OS和网络硬件能在普通负载下满足微秒级SLO，但是系统过载时就很难了。\n服务器过载会导致 receive livelock，服务器忙着处理新收到的packet（高优先级）以至于没空把处理完的请求发出去。这种情况对于微秒级RPC更严重，因为稍微延迟一会就达不到SLO的要求。此外，一些短RPC只需要较少的资源，服务器每秒被允许处理几百万个请求。当大量客户端同时发起请求，服务器会堆积起庞大的队列，造成系统过载，这个叫做 RPC incast 导致。\n控制过载是为了移除超量的负荷，保证服务的高利用率和低延迟。目前的方案大概分两种，一种在系统过载时主动丢包，另一种则限制客户端的请求频率。这些方案都不适合微秒级的RPC。如果丢包，在通信上损失的时间都与处理请求差不多了；如果限制频率，客户端首先需要了解服务器的状态，而这又需要额外的通信时间。\n另一个难题是把过载控制系统扩散到大量的客户端。在这种大规模系统中，客户端很少对某一个特定的服务器有需求，对它发送请求的频率很低，所以它对服务器状态的了解往往是落后的。如果在发送请求之前主动询问，又会带来额外的负担，尤其是对于微秒级的RPC。\n本文为微秒级RPC设计了一个叫做 Breakwater 的过载控制系统，主要思路是根据SLO的完成情况对客户端进行“加分”和“扣分”。由此想法引发的后续问题及其解决方案不再赘述。\n18. AIFM: High-Performance, Application-Integrated Far Memory 内存是现在数据中心最紧缺的资源，但也是最不灵活的。如果某个服务器的内存不足了，必须终止一些进程，导致之前消耗的运行资源都白费了。而且这时候其他服务器上往往还有多余的内存，但就是没法用。甚至本地的内存也不能用，例如服务器有30%的内存都是几分钟都未曾访问过的，说明有些内存是可以暂时回收的。\n说到内存回收，现在OS普遍使用了内存交换技术，但它有几个问题：1. 粒度很低。哪怕只需要很少的数据，也至少得换入一页，即4KB（可是这也可以理解为prefetching啊）；2. 开销很大。为了换入一个页面，必须触发page fault进入内核，然后等待换入完成，这期间的CPU都被浪费了。\n本文提出了 application-integrated far memory (AIFM) ，是一种把本地内存交换到远程服务器上的技术。开发者可以指定数据结构的“可远程性”，表示它是否可以被交换到远端。当AIFM发现内存压力很大时，它会把一些对象交换出去，并把指向它们的指针改为“远程指针”。当应用解引用远程指针时，会有一个 green thread runtime 把对象恢复到本地的内存。由于这个runtime上下文切换的开销很小，它可以利用恢复对象的等待时间，让其他线程做事，从而抵消了远程访问的延迟，保持了较高的吞吐量。\nConsistency 19. Performance-Optimal Read-Only Transactions 本文只研究分布式存储系统里的一次性读事务。读事务是最主要的事务（其他的可能只占千分之几），发送的读事务会分散到各个服务器，返回数据的不同碎片。这种碎片化的访问方式可以提升读事务的性能，但是数据会有连贯性不足的问题。如何在保持一定连贯性的条件下，尽可能优化性能呢？本文对“最优性”作了严格的定义，并提出了“最优性能的读事务”。这篇理论性较强，而我对该领域了解少，不能把握它的idea，故止步于此。\n20. Toward a Generic Fault Tolerance Technique for Partial Network Partitioning 本文解决了一种“奇怪的”（取自原文“peculiar”）网络错误，叫做 partial network partition，即一个集群里面仅有部分节点无法建立连接，但整个连接图是连通的。例如，A、B不能连接，但它们都能与C连接。另外一种将集群分成两半的 complete partition 与此不同，这种错误已经被深入研究过了，遗憾的是它的解决办法并不适用于 partial partition。本文的话题对我来说过于陌生，跳过。\n21. PACEMAKER: Avoiding HeART attacks in storage clusters with disk-adaptive redundancy 分布式存储系统用冗余机制保护磁盘数据，例如100%复制和纠删码（Erasure Code）。不同的冗余级别对应着不同质量的磁盘——磁盘越容易出错，就越需要更多的冗余数据。\n存储集群由多种品牌及型号的磁盘组成，并且会随时间变化。不同种类的磁盘有不同的失效率，但是集群往往“一刀切”，把所有磁盘都按照同一种冗余级别处理，以此保护其中最容易出错的磁盘。这样做的开销是非常大的，因为很多磁盘并不需要这么多冗余数据。\n为了解决这一问题，有个不错的方法是根据失效率（AFR）调整磁盘的冗余级别。这使得磁盘必须经过一个转变级别的过程，然而它需要耗费相当多的I/O资源。当一大批磁盘需要同时转变时，集群的I/O带宽会被立刻占满，造成“转变过载”（transition overload）的问题。本来在转变前，数据就已经处于保护不足的状态（从失效率升高到开始转变之间有一定的延迟），结果还必须等待转变完成才能恢复安全。文中举了一个例子，某集群的数据整整一个月都保护不足，而且带宽全部都在了状态转变上。\n本文提出的 PACEMAKER 是一个更优秀的冗余级别转变算法，解决了上述问题。细节我就不深究了。\n22. Pegasus: Tolerating Skewed Workloads in Distributed Storage with In-Network Coherence Directories 本文解决的问题：分布式存储系统中有一些非常受欢迎的数据，每天可能要被访问几百万次。这样的访问量超出了单个服务器的负荷，所以必须把它们复制很多份存放到其他服务器来分担压力。然而在互联网的大背景下，“热搜”每天都在变化，我们无法预测哪些数据在将来会成为“热搜”。如果把所有数据都复制几份，这样的开销太大了，所以我们需要对数据进行实时跟踪。不过这个问题并不简单，\n本文提出了一个叫做 Pegasus 的分布式存储系统，它受CPU cache coherency protocol的启发，用 in-network coherence directory 跟踪被复制的数据。每次进行写操作时，Pegasus都能重新平衡replica set，同时保持一致性。\n23. FlightTracker: Consistency across Read-Optimized Online Stores at Facebook Facebook专属研究。从Introduction章节来看，整个研究都建立在Facebook的系统上，作出的贡献也只适用于Facebook，除了一些“lesson learned”。本文解决的问题：用户每发送一个请求，我们就要向数据库发起成百上千个请求，然后汇总到一起。我们已经有了足够多的机制来优化这一过程，但是还要为开发者提供一个统一且符合直觉的一致性模型。\n24. KVell+: Snapshot Isolation without Snapshots 快照隔离（Snapshot Isolation，SI）是数据库事务处理中的一个隔离级别，保证事务的读操作能看到一个一致的数据库的版本快照。例如联机分析处理（OLAP）需要扫描数据库的大量内容，此时如果有并发的联机事务处理（OLTP），修改了尚未扫描的数据，那么OLAP应该以旧数据为准。\n传统的SI实现方式是留下数据的每一个版本，等OLAP扫描完数据后，再由GC线程回收旧版本。这显然会带来额外的存储负担，而且OLAP的时间越长，存储负担越大。本文强调了存储空间的重要性，例如Facebook发现“瓶颈在存储空间”、阿里将GC的优先级设为最高来防止存储空间的浪费，由此引出了他们“无需快照的快照隔离”。\n他们发现，大部分OLAP对扫描的顺序没有要求，并且每个数据一般只扫描一次（例如求平均值等）。如果在OLAP的过程中有OLTP修改了还未扫描的数据，OLAP可以立刻跳转到该数据，扫描其旧版本并丢弃，然后回到原来的位置继续扫描。这样做并不会影响结果，但是节省了很多存储空间，因为旧版本的寿命大大减少了。他们把这种改良的OLAP称作OLCP，C代表“commutative”，可交换的。\n不过这里会引申出几个问题：1. 如果同一个数据被更新了多次，如何保证只扫描最老的版本？2. 如何在相对乱序的情况下记住哪些是已经扫描过的数据？这些都在文章里有回答，不多赘述。\nMachine Learning 1 25. Serving DNNs like Clockwork: Performance Predictability from the Bottom Up 模型inference广泛地应用在了Web服务中，Facebook一天要处理200万亿个inference请求。不过inference请求的响应时间会受尾延迟影响，为此传统的解决办法是为inference请求提供充分或过量的计算资源，确保它可以立刻执行而不会pending，不过这会使计算资源的利用率降低。\n当前的系统普遍把组件的延迟量视作不可预测的。尽管inference服务的不稳定性有时候来自外界（例如任务量陡增），但我们可以消除它内部的不稳定性，例如缓存决策、分支预测、与其他进程的并行性等。本文提出并实现了Clockwork，用来提供稳定延迟的模型inference服务。\n26. A Unified Architecture for Accelerating Distributed DNN Training in Heterogeneous GPU/CPU Clusters 本文提出了BytePS，一种分布式训练集群。这个成果在工业界还比较有影响力，可以直接在网上搜到中文介绍，我在这里只总结一些知识点。\n分布式训练主要有两种架构，一种是all-reduce，它只需要GPU。每个GPU都存有相同的模型，然后分别取不同的数据计算梯度，再互相同步计算结果并更新模型，保持彼此状态的一致。它的主要问题是如何让多个GPU之间保持同步。另一种是PS（Parameter Server），其中运行在GPU上的多个worker取数据、计算梯度，并把结果传递给运行在CPU上的PS，由PS计算新的参数。worker开始下一轮训练时，统一从PS更新参数。根据我的理解，这两种方法的区别在于参数同步的“去中心化”与“中心化”方式。\n本文对PS的优化在于PS更新参数的方式。原本是在CPU上进行“梯度优化”、“梯度整合”，但BytePS把优化器放到GPU上运行了。我的理解不一定准确，读者可以自行上网搜索相关资料。\n27. Heterogeneity-Aware Cluster Scheduling Policies for Deep Learning Workloads 现在DNN训练经常要放到集群上进行，而一个集群的计算资源多种多样。例如，一些公有云有NVIDIA GPU和Google TPU可供租借；本文作者的实验室的集群里有NVIDIA Titan V、Titan X、P100 GPU。\n这些资源的性能表现各有差异：ResNet-50在NVIDIA V100上训练比K80快10倍，但是从性价比（per dollar）的角度出发，P100又比V100好。有时候便宜和贵的GPU混用，可以在满足时限的条件下节省最多的资源（也就是钱）。在多任务的情况下，这个问题更加复杂。\n用户的需求也有多种：有时候希望同一批训练尽快结束，有时候需要为临时任务安排适当的资源，有时候同一集群的资源会被划分给不同级别的用户。现在常用的调度策略（fairness、makespan、least attained service）可以对应一些场景，但是并不能做到最优，因为它们都没有考虑计算资源之间的差异，更不能实现多种资源相互配合的“混合训练”。\n本文提出的 Gavel 是一种新的集群调度器，它把目前最常用的一些调度策略综合起来，抽象为一个任务吞吐量的优化问题，例如makespan相当于求最长完成时间的最小值。它会为每个任务寻求 “混合分配”，例如60%的时间独占V100，40%的时间与其他任务共用A100。它也实现了易于使用的API，兼容Tensorflow和PyTorch。\n28. PipeSwitch: Fast Pipelined Context Switching for Deep Learning Applications 现在模型的training和inference往往使用分开的计算资源，导致GPU总体的使用效率不高。本文受CPU上下文切换的启发，使不同的training和inference任务能够以较低的开销在同一个GPU上切换，达到接近100%的使用率。\nGPU任务切换的难点在于开销太大。CPU内存比GPU充足得多，大部分时候都可以把数据留在内存，因此只需要切换一些寄存器和指针。而GPU的内存紧俏，很难把所有任务的模型都容纳进来，所以必须把不使用的模型换出内存才能为另一个任务腾出空间，而这是相当费时的。相比于inference通常所需的几十到几百毫秒，装载模型可能需要数秒才能完成。本文针对这一难题，提出了流水线式的切换方法：利用模型的分层结构，让GPU一边计算，一边装载模型的剩余部分。\n29. HiveD: Sharing a GPU Cluster for Deep Learning with Guarantees 大公司使用集群为自己的各个team提供训练服务，每个team都能分到一些“quota”，即计算资源的配额。然而quota只能保证数量，不能保证其他要求（文中叫做GPU affinity）。例如，有些任务希望使用8个8-GPU node训练，如果此时没有足够多的8-GPU node，就只能等待，或者使用别的组合（例如16个4-GPU node）。无论选择哪种，都会影响训练的效率。\n本文针对该问题提出了一种更加高效的GPU资源划分方案，它和解决内存碎片化、分配连续物理内存的方法非常相像，即内核常用的virtualization、buddy system。刚才提到的问题，本质上也是资源碎片化的问题。具体的我不想看了，毕竟它只是整理了一下碎片化的GPU资源，没什么特别的地方（在我看来）。\n30. AntMan: Dynamic Scaling on GPU Clusters for Deep Learning 这篇idea比较难懂，放着以后再看，网上有不少相关资料。\nConsensus 31. Write Dependency Disentanglement with Horae 这篇idea比较难懂，我就大概讲一下它解决的问题。\n写依赖（Write Dependency）指的是IO请求之间的依赖关系。软件层发起一系列IO请求，这些请求进入 Software Queue，又被分发到设备的Hardware Queue（HWQ），最后由设备完成操作。但SWQ会被I/O scheduler调整顺序，HWQ又会为了性能（device-side scheduling）和简洁（ request retries）而无视队列里的请求顺序。这样当IO请求之间有依赖时，IO stack只好每次只发送一个请求，直到完成后才发送下一个。这种做法不能利用多设备、多队列的并发机制，使得效率大打折扣。例如，当IO之间没有依赖时，设备越多、HWQ越多，IO效率越高；但是有依赖时，IO效率一直很低，无论有多少个设备或HWQ。\n作者的Horae是一个新的IO stack，核心idea好像是把IO请求的数据与metadata分开，使得数据的处理能够并行，而metadata保持顺序。\n32. Blockene: A High-throughput Blockchain Over Mobile Devic 本文提出了一种新型区块链Blockene，计算量小，可以在手机上运行。它的核心idea是两种节点：“公民”和“官员”。公民节点运行在手机上，是区块链的核心成员，它们参与consensus，并且假定其中三分之二是诚实的（在数百万规模的情况下还算合理）；官员节点运行在服务器上，它们不受信任，不参与consensus，并且只要求其中20%是诚实的。尽管存储区块链的任务由官员节点承担，但Blockene可以保证：即使80%的官员节点与三分之一的公民节点都存有恶意，另外三分之二的公民依然可以发现并纠正它们的异常行为。这样一来，官员节点分担了大部分计算和存储任务，公民节点则只负责监督，大大减少了负担。（所以有了政府，社会效率还是高一些哦，不过前提是这个政府能被有效监督……）\n这篇文章的idea相当有趣，但是区块链并不是我的感兴趣的领域，所以不再深究细节，等有需要再看吧。\n33. Tolerating Slowdowns in Replicated State Machines using Copilots 本文和下面三篇文章都是围绕一致性算法展开的（其实上一篇也是），读懂它们需要先了解一下基础知识，就以Raft为例吧：\nhttps://raft.github.io/ https://zhuanlan.zhihu.com/p/32052223 34. Microsecond Consensus for Microsecond Applications 35. Virtual Consensus in Delos 36. Byzantine Ordered Consensus without Byzantine Oligarchy Bugs 37. From Global to Local Quiescence: Wait-Free Code Patching of Multi-Threaded Processes 互联网有很多24/7在线的服务，当它们发现自己的系统存在漏洞后，必须尽快修补以防黑客趁虚而入。然而补丁一般需要服务重启之后才能生效，这会给用户造成不便。*（我不太明白，为什么不能轮流更新？例如100台服务器，一次更新10台、分10次完成，这样的代价很大吗？）*一个主要的例子就是OS更新，重启一次需要花掉几分钟。此外，有些应用的补丁更新也很麻烦，例如Redis这样的in-memory database，它的寿命非常长，有很多运行时产生的volatile state。如果要重启，要么 (1) 事先把它的volatile state转移到nonvolatile memory，重启后再转移回去，要么 (2) 在重启后经历一个warm-up phase。无论哪种方法都会影响性能。\n热补丁（live patching）是解决上述问题的一个方法，它将binary形式的补丁直接插入进程的地址空间，避免了重启。不过这一方法有个要求：给函数f打补丁之前，所有进程/线程的调用栈上都不能有f（该状态称为“quiescent”），否则当它们从f返回时可能会跳转到被补丁覆盖后的地址，引发系统崩溃。以往进入quiescent状态的方法是设置一个全局屏障（global barrier），让所有相关线程运行到某个地方就停下来，然后再打补丁。但这个方法有很多问题：\n如果其他线程都进入了屏障，只有一个线程要计算很久或等待IO操作，那么其他线程也必须等待，等待时间甚至没有上限。虽然可以设置一个timeout，但是万一一直碰不到所有线程都在timeout之内进入屏障的情况呢？ 如果A线程进入了屏障，但B线程block了，需要A线程signal才能进入屏障，那么就会产生死锁。当然，事先设计好屏障的位置可以避免死锁，但这么做很复杂，也很不靠谱。 本文提出了一个 local quiescent 的方法，能够避免 global quiescent 的问题。它的思路很简单，就是让线程们不再需要等待彼此，只要自己进入了屏障就能打补丁。具体的实现方法是复制出另一份地址空间（Address Space，AS），其中所有页面的映射都维持原样，但是把需要打补丁的页面映射到补丁的位置。当一个线程进入屏障时，就把它线程控制块（TCB）里的AS切换成新的，这样就实现了单个线程的热补丁。\n但该方法会造成一个明显的问题：打补丁的过程中，有些线程运行的是新版本，有些线程却还在运行老版本。作者声明说这在大部分情况下都不会造成影响，如果真的需要，则可以在 local 和 global 之间取一个平衡，即 group quiescence。\n此外，本文提出的方法只能修改程序的text和rodata段，无法修改data段（例如一些数据结构、全局变量），虽然绝大部分软件升级都只涉及text段。所以作者建议只把WFPATCH作为短暂的过渡，为正式的维护提供缓冲。\n38. Testing Database Engines via Pivoted Query Synthesis 数据库管理系统（DBMS）已经有了很多寻找 crash bug 的方法，本文提供了一个寻找 logical bug 的方法。所谓 logical bug 不会使系统崩溃，但是会返回错误结果。本文的思路是在数据库里设置一个 pivot row，然后合成很多测试语句，并保证它们的正确结果一定包含 pivot row。如果某次结果不包含，就说明有 logical bug。\n本文还提到了另一个叫做 differential testing 的方法，就是对比同一语句在多个DBMS上的结果。虽然这个方法很有效，但是它要求这些DBMS的底层实现完全一致，然而现实中的DBMS针对SQL有各种各样的扩展，有比较微妙的差别。作者的意思应该是，本文抓到了其他方法抓不到的bug。\n39. Gauntlet: Finding Bugs in Compilers for Programmable Packet Processing 本文针对P4的编译器寻找bug。P4是一个领域特定语言（DSL），用来实现可编程NIC和交换机。本文的核心idea是观察P4和C这种通用语言的区别，然后对症下药，提出了几个适合P4的debug方法，叫做Gauntlet。Gauntlet已经合入了P4的官方集成pipeline。\n40. Aragog: Scalable Runtime Verification of Shardable Networked Systems 这篇文章似乎要用runtime verification的方式捉network function的bug。前面的第13篇论文提到过network function的概念。该方法是相对于static verification来说的，作者认为随着NF越变越复杂，static方法已经不好用了，所以作者提出了一个runtime方法。由于本文长句子很多，比较难读，还涉及很多我不了解的概念，就不仔细读了。\n41. Automated Reasoning and Detection of Specious Configuration in Large Systems with Symbolic Execution 很多软件都支持大量的自定义参数，使用户有更大的定制空间，例如 MySQL 是一个拥有超过300个自定义参数的大型系统。不过有时候错误的配置会带来性能损失，甚至是软件故障。以往有很多捕捉引起软件故障的“无效配置”，但本文着重于寻找那些不足以引起故障、却会严重影响性能的配置，我自己称之为“垃圾配置”。\n当前的捕捉办法主要是运维人员根据经验手动做一些测试，然后根据一些指标（例如吞吐量）判断是否存在垃圾配置，但这种方法并不十分有效，因为有些配置只有在特定的条件下才表现得很差，例如与特定的参数、输入、运行环境相关。例如，文中举了一个MySQL AUTOCOMMIT的例子，当系统负荷不大时，打开AUTOCOMMIT并不会明显地降低性能，但是系统很忙的时候就会很明显。\n作者发现，这些垃圾配置拖慢性能的根本原因，在于它们引导代码走向了更慢的执行路径。因此，本文提出了一套叫做Violet的分析工具，使用符号执行（symbolic execution）对程序进行静态分析，为配置们生成一个“性能影响模型”（performance impact model）。用户使用软件时，Violet会根据模型检查其配置是否合理。如果发现了可疑配置，则会向用户报告潜在的风险。\n42. Testing Configuration Changes in Context to Prevent Production Failures 本文又是一个寻找配置bug的成果，不过和上一篇的程序分析思路不同，它的主要方法是建立更完善的测试。作者观察到，配置bug很少由typo引起（因为一般都有review和validation流程），其根本原因常常是与配置相关的代码写错，或者配置了一个看上去有效却不合理的值（这与上一篇的论点有点像）。如果只看配置更改的diff本身，是定位不到这些bug的。\n本文提出的 ctest 工具不仅测试配置本身，还测试与之相关的代码；它在选取测试参数时，会参考实际应用场景下的值，而不是随机选取；它可以根据配置diff，选择性地测试某次更新的配置。此外，它还能将以往的测试用例转化成自己的用例。\nScheduling 43. Providing SLOs for Resource-Harvesting VMs in Cloud Platforms 云计算服务商为了利用多余的计算资源，常常低价提供低优先级的“spot VM”，等更高优先级的VM需要资源时再把它们回收。这虽然能够提升资源的利用率，但存在一些问题。首先，一个较大的spot VM可能会因为仅仅要腾出一点点资源而被整个回收，这是很不合理的，如果使用多个小VM，又会带来额外的创建和维护VM的开销。其次，spot VM并不能充分利用所有的多余资源，所以利用率依然有提升空间。\n本文观察了Microsoft Azure多余资源的情况，发现它们随时间涨落，有充分的利用空间。于是本文提出了Harvest VM来改进spot VM。Harvest VM一开始拥有“最低需求”的资源，但它会根据系统的多余资源扩张或收缩自己的资源，只有当系统需要用到自己“最低需求”的资源时它才会被释放。为了给用户更清晰的预估，本文还为Harvest VM配备了SLO。用户给定自己要创建多少个Harvest VM以及每个VM的最低资源，由后台预测出它们不同时间段内的存活率并告知用户，例如一小时后会剩下60%、一天后会剩下40%、一周后会剩下20%，等等。这个预测过程是由机器学习实现的（又是一个AI for system）。\nHarvest VM目前只支持CPU资源的动态利用。\n44. The CacheLib Caching Engine: Design and Experiences at Scale 大型Web服务依赖缓存系统来提升性能，例如在Facebook，CDN缓存满足了70%的请求。\nFacebook有很多个独立的缓存系统，例如CDN cache、key-value cache、social-graph cache、media cache。每个cache系统都是针对某种资源专门设计的，因此架构各不相同，需要单独维护。随着缓存系统越来越复杂，维护成本也越来越高，产生了大量冗余代码（简称屎山要塌了？），于是CacheLib应运而生。CacheLib是一个专门为缓存而写的C++ library，它提供了一套公共的缓存功能，例如驱除策略、缓存索引、稳定性优化。有了CacheLib，不同的缓存系统可以共享它们的缓存策略，并且大大简化了代码。\n在开发CacheLib的过程中，作者也总结了一些有价值的发现。\n缓存系统并非越专一化越好，使用通用的CacheLib反而能利用其他缓存系统的优点； 高效缓存需要的工作集比我们想象的大。以往的研究都假设缓存对象的popularity满足alpha=0.9的Zipf分布，于是以为DRAM能在大部分情况下满足需要，而忽略了对flash memory缓存的研究。 还有其他关于实际应用中的cache的论述，参见论文的前三节。虽然看不太懂，但我感觉这是一篇总结了大量现实数据和经验的深度好文。\n45. Twine: A Unified Cluster Management System for Shared Infrastructure 这篇文章讲infra-structure的，太抽象了，我看不懂。。。。。。\n46. FIRM: An Intelligent Fine-Grained Resource Management Framework for SLO-Oriented Microservices 现在很多Web服务都是由多个“微服务”（microservice）组成的，每个微服务运行在容器里，只提供一项特定的功能，然后通过通信机制协同工作。比起以前将多个进程绑定在一个容器里的整体服务架构，微服务更容易更新、缩放，使用更加灵活。例如，整体型服务中的一环负载过重时，必须扩大整个服务的规模；而某个微服务负载过重时，只需要扩大这个微服务的规模。\n微服务有时候无法满足SLO对响应延迟的限制，这一般是因为某一关键服务的负载出现峰值，或者计算资源不足。传统的解决办法有“overprovisioning”、“recurrent provisioning”、“autoscaling”，但是它们有两个缺点。第一，没有充分multiplex每种资源，无法检测出所有资源的短缺；第二，制作策略的过程耗时耗力，而且不能适应系统整体随时间的变化。\n本文提出了FIRM框架，它能检测出是哪个微服务的实例导致了超时，以及为该服务制定降低延迟的策略（例如分配更多资源、增加实例数）。这两个关键功能分别由不同的机器学习模型实现。检测超时的是一个SVM模型，制定策略的是一个增强学习（RL）模型。FIRM通过在系统中人为制造高负载来训练它们。\n47. Building Scalable and Flexible Cluster Managers Using Declarative Programming 现在的数据中心由Kubernetes、DRS、Openstack这样的集群管理器支持，它们需要管理容器的部署。管理器对部署方式有一些规定，例如硬性的“每个容器至少要有多少磁盘空间”、软性的“使容器尽可能分散在不同的机器上”。为了满足这些规定，管理器需要提前计算好的部署方式，而这往往涉及复杂的算法问题。尽管如此，现在的管理器依然使用 custom, system-specific best-effort heuristics，这对于其开发者来说是一场灾难。随着新规定的引入，开发者需要解决非常困难的组合优化问题，导致部署算法的开发成本极高。\n以Kubernetes为例，它将待部署容器排成一个队列，然后逐个使用“greedy, best-effort heuristic”计算部署策略。它首先选出所有符合硬性规定的机器，然后给它们符合软性规定的程度打分，最后为该容器选择得分最高的机器。这种做法有很多问题：\n无法保证选中最佳的机器。在多个机器上部署多个容器，其本质上是一个 NP-hard 的多维背包问题，无法用贪心算法得到最好的结果。而当问题规模较大时，Kubernetes不得不限制备选机器的数量。 不支持全局的容器再分配。例如A、B容器之间有anti-affinity，给某机器分配A容器可能会挤走B容器，此时需要为B容器寻找新的机器，但Kubernetes并不支持这一功能。 代码实现非常复杂：这种调度算法流程很长、跨度很大、条件分支很多，因此开发难度很大，而且写出的代码往往是“铁板一块”动不得，可扩展性很差。 本文提出了 Declarative Cluster Managers (DCM)，它与典型的集群管理器截然不同。它将集群状态存储在关系数据库，然后用户使用SQL指定要采用的部署规定。DCM的编译器将用户的SQL语句合成一个程序（encoder），该程序会从数据库获取集群状态，并计算出满足用户要求的部署策略。encoder将集群状态和部署规定编码为一个优化模型（相当于更加一般的数学问题），然后使用专门的工具“constraint solver”（例如ILP算法）解出部署策略。这样，DCM兼具可规模性、可扩展性，并且能做出高质量的决策。\n可规模性：Can scale to problem sizes in large clusters (e.g., 53% improved p99 placement latency in a 500-node cluster over the heavily optimized Kubernetes scheduler). 高质量决策：constraint solver能保证算出最佳结果 可扩展性：DCM的集群状态、对部署规定的描述、以及算法逻辑高度模块化，使得加入新部署规定、支持新型容器变得更加简单 本人18年在百度实习的时候恰好碰到过类似的问题，同样是把很多实例部署到集群上，同样是背包问题的变种。当时也想过用数学方法求解，不过实力有限无法实施，没想到今天能在OSDI的论文上看到正式的系统性的解法。\n48. Protean: VM Allocation Service at Scale 本文介绍了Microsoft Azure的VM分配器Protean。Azure的服务规模非常大，几百万台物理机遍布世界各地，500多种不同的VM满足各种用户的需求。要在合理的时间内为用户的每个请求做好分配，需要一个健壮的、高扩展性的（便于适应新特性）、灵活的（可运用于各种场景）、高性能的（即使是1%的资源碎片也会造成每年100万美元的浪费）分配器。\n本文不仅提出了Protean，还总结了Azure workload的规律。Protean正是按照这些规律设计的，例如作者发现Azure常常遇到重复的请求，甚至有80%的请求是连续重复的，所以Protean会缓存请求的处理结果，大大提升了性能。\n本文经常强调自己的大规模，我只能说，云服务这块还是大公司拿捏得死死的，其他机构根本没有这么多实操经验。\nMachine Learning 2 49. Ansor: Generating High-Performance Tensor Programs for Deep Learning 本文讲了一个高效寻找“tensor program”的方法。tensor program是一种自动生成的tensor运算符，主要靠搜索不同的优化组合得到。\n妈的，看不懂，而且实在没啥兴趣。深度学习模型是黑盒也就算了，连优化方法都要靠搜？\n50. RAMMER: Enabling Holistic Deep Learning Compiler Optimizations with rTasks 终于看到一篇北大的论文了，泪目。\n现在DNN计算方式一般分为两层，上层“inter-operator parallelism”把模型看作一个由operator组成的图（data flow graph，DFG），每个operator都是DFG的节点，数据的流向是边；下层“intra-operator parallelism”是利用硬件加速的手段使单个operator并行计算。这两层计算的scheduler一般是分开的，例如inter-operator scheduler只关注哪些operator可以并行计算，然后将它们分配到不同的加速设备上，而设备上的intra-operator scheduler只关注如何利用自己的加速机制，使某个operator用最快的速度完成计算。作者指出：\n运行时的动态调度会给计算带来大量额外开销。调度由CPU完成，模型计算由GPU、FPGA等专用设备完成，随着后者的性能不断提升，调度占用的时间比例越来越大，影响了总体性能。例如，inter-operator scheduler会消耗16%-55%的时间在调度上，并且batch size越小，调度占用的时间越长；intra-operator scheduler也会降低GPU的利用率。 分层的scheduler不能获得最佳的并行状态。例如A、B两个operator是并行的，A的可并行性很高，而B的可并行性比较差。其中A先被上层的scheduler分配到了某个GPU上，所以为A分配了全部的并行线路。此时，上层scheduler也将B分配到了同一个GPU，但由于A已经占用了所有线路，所以B只能等待A完成后才能开始计算。由于B的可并行性较低，它并不能利用GPU的全部线路。如果scheduler能提前看到A、B的需求，那么它其实可以为B预留一些线路，这样就能达到更好的并行效果。 为了解决上述问题，本文提出了一个新的深度学习编译器RAMMER。它统一了上下两层的scheduler，利用静态的编译信息为调度节省了很多不必要的重复计算。此外，它抽象了底层硬件，因此可以应用在绝大部分并行计算设备上。\n51. A Tensor Compiler for Unified Machine Learning Prediction Serving “传统ML”是区分于DNN的机器学习方式，它的模型使用scikit-learn、Pandas等框架，这些框架为了适配不同的底层，需要为每种底层编写operator的实现，这导致了大量的重复性工作。例如所有框架一共有 NN 种operator，运行在 MM 种环境里，就需要 M×NM \\times N 种实现。与之相比，DNN的计算都可以抽象为tensor的转换，因此深度学习的框架只需要用tensor operator实现框架层的operator，再由底层硬件实现每种tensor operator即可。假如tensor operator有K种，所有框架总共就只需要 M×K+K×NM \\times K + K \\times N 种实现。\n本文提出了HUMMINGBIRD，它创新性地将传统ML的operator转换成了tensor operator，然后使用并行计算领域已经比较成熟的infra来进行模型的inference。虽然训练传统ML模型依然需要使用传统的框架，但模型一经训练完毕，就可以与训练方式脱钩。实验结果证明，这不仅提升了inference的性能，还加强了传统ML模型inference的跨平台兼容性。\n52. Retiarii: A Deep Learning Exploratory-Training Framework 这篇好像是AutoML领域的啊……实在超出我的知识范围了。\n个人理解，这篇文章提出了一个高效的模型探索方式。它一改之前“大量训练+人工筛选”的方式，采用“基准模型+变化样式”的组合，使模型在训练的过程中半自动化地调整自身。\n53. KungFu: Making Training in Distributed Machine Learning Adaptive ML训练时必须提前设置好一大批参数，例如与模型相关的超参数、与训练系统相关的系统参数，tune起来很麻烦，而且训练期间不能变动。但实际上一些参数需要动态变化才能收到更好的效果，例如学习率、batch size、训练的worker个数。本文的KungFu是一个“distributed ML training library”，可以在训练过程中动态地调节各种参数。本文的三大贡献是：1. Expressing Adaptation Policies; 2. Making training monitoring efficient; 3. Distributed mechanism for adapting parameters。\nHardware 54. FVM: FPGA-assisted Virtual Device Emulation for Fast, Scalable, and Flexible Storage Virtualization 不太懂硬件……本文好像是提供了一种利用了FPGA辅助的虚拟存储机制，相比纯硬件和纯软件的虚拟存储更有优势。\n55. hXDP: Efficient Software Packet Processing on FPGA NICs 又是一个关于FPGA的，先跳过吧，一大堆缩写根本看不懂。\n56. Do OS abstractions make sense on FPGAs? 我也不知道。\n57. Assise: Performance and Availability via Client-local NVM in a Distributed File System 非易失性内存（non-volatile memory，NVM）是指当电流关掉后，所存储的数据不会消失的存储器。目前分布式文件系统的常用范式是由server存储所有文件，client的内存相当于这些文件的缓存。这种设计简化了数据管理，但是cache miss的代价很大，不能利用好NVM的性能。所以本文提出了分布式文件系统Assise，能充分发挥client-local的文件系统。\n58. Persistent State Machines for Recoverable In-memory Storage Systems with NVRam 在数据量越来越大的今天，分布式系统的各个部件性能越来越高，只有in-memory的存储系统能跟得上，但是它最大的缺点是易失性。有些系统引入了persistence机制提高系统的稳定性，但也带来了额外的性能负担。这一问题有望通过非易失性内存（Persistent Memory，PM）解决，但PM系统还需要“crash consistency”，即保证系统崩溃后仍然能维持其性质。这又需要所有操作都具有“failure atomicity”，例如释放内存和更新指针必须同时成功或失败，否则会导致野指针的问题。failure atomicity让PM系统变得非常复杂和精巧，但即便如此，它的性能还是比不上in-memory系统。本文针对以上问题提出了Persimmon，“a PM-based system that converts existing in-memory distributed storage systems into durable, crash-consistent versions with low overhead and minimal code changes”。\n59. AGAMOTTO: How Persistent is your Persistent Memory Application? 本文专门解决PM系统的测试问题，由于我见都没见过PM系统，就不深入了。\nSecurity 60. Orchard: Differentially Private Analytics at Scale 我对用户隐私的话题很感兴趣，差分隐私算法也很有趣，但是一些关键的句子我看不懂。\n本文主要探讨如何高效而安全地使用用户数据，提出了Orchard系统：“We present a system called Orchard that can automatically perform these steps for a large variety of queries. Orchard accepts centralized queries written in an existing query language, transforms them into distributed queries that can be answered at scale, and then executes these queries using a generalization of the CaT mechanism from Honeycrisp.”\n61. Achieving 100Gbps Intrusion Prevention on a Single Server Intrusion Detection and Prevention Systems (IDS/IPS) 面临着一个问题：网络流量太大，连接数太多，现有的硬件和软件不足以支撑。但本文提到的FPGA、SmartNIC我都不懂，不细看了。\n62. DORY: An Encrypted Search System with Distributed Trust 云端存储服务为了安全起见，会把服务器上的用户文件加密，然后把密钥放在用户本地。然而为了用户的方便，服务商还需提供在线搜索的功能，所以“searchable encryption”成为了一个研究课题，如何让用户在服务器上搜索加密过的内容，又不造成安全隐患？攻击者虽然看不到文件的具体内容，但他可以观察其访问模式。举个例子：攻击者给Alice发送一封只含有单词“flu”的邮件，如果服务器上的index 924（我还不太清楚这个index具体指什么）更新了，那么攻击者就能知道924对应着单词“flu”。如果攻击者以此法穷举所有单词，那么Alice之后收到邮件时，攻击者就能根据index的更新读出所有内容。\n当然，有一种叫做Oblivious RAM的技术，可以隐藏RAM的访问模式，然而作者认为其成本过高，不够实用。\n本文提出了DORY (Decentralized Oblivious Retrieval sYstem), an encrypted-search system that splits trust to meet the real-world efficiency and trust requirements summarized above (and detailed in §2). DORY ensures that an attacker who cannot compromise every trust domain does not learn search access patterns.\n63. SafetyPin: Encrypted Backups with Human-Memorable Secrets 前言：安全不止是加密、网络、软件，分布式的“安全系统”也是重要的组成部分。\n云端备份虽有加密，但真正的密钥往往不是直接交给用户保管，而是用易于记忆的PIN来认证用户。为了防止PIN轻易被猜出来，现代的备份系统都使用了 hardware-security modules (HSMs)。用户设备上保存着经HSM加密的密钥（不是PIN），以及PIN的hash值，供HSM识别身份。为了更好地容错，设备上会同时存放由几个不同的HSM加密的密钥，只要通过任何一个HSM的认证都可以。这一系统存在许多问题：攻击者只需要hack掉一个HSM，就能获得许多用户的备份密钥，而且该系统很难检测到攻击痕迹，例如攻击者真的猜出了某用户的PIN，该用户也不会发现。\n本文的SafetyPin仍然是基于PIN的备份系统，但是安全性更强。如果攻击者猜不出PIN，那么他必须能够hack一定数量的HSM，并且这些HSM的具体位置是隐藏的（在系统后台由PIN决定），攻击者无法得知。作者称之为“location-hiding encryption”。其他安全性质暂不详述。\n64. Efficiently Mitigating Transient Execution Attacks using the Unmapped Speculation Contract Transient execution CPU vulnerabilities are vulnerabilities in a computer system in which a speculative execution optimization implemented in a microprocessor is exploited to leak secret data to an unauthorized party. The classic example is Spectre that gave its name to this kind of side-channel attack, but since January 2018 many different vulnerabilities have been identified.\n本文说现在kernel防止transient execution的开销太大了，很影响性能，所以提出了新的解决办法“unmapped speculation contract”，大概的意思是将所有进程的内核空间分开，每个进程都不会映射其他进程的物理内存。本文主要介绍了USC的思路及其实现方法。\nClusters 65. Predictive and Adaptive Failure Mitigation to Avert Production Cloud VM Interruptions Microsoft Azure团队又来结合实际经验投论文了。这次他们研究的问题是如何预测和避免VM的崩溃，而不是等崩溃后再应急处理。Narya is an end-to-end service with predictive and smart failure mitigation fully integrated in the Azure compute platform for its Virtual Machine (VM) host environment. The design goal of Narya is to prevent VM failures ahead of time and enhance the self-managing capability of the Azure compute platform for providing smooth VM experience to customers.\n一说到“预测”，我心里就开始怀疑是不是又要搞AI for system。果不其然，Narya使用了强化学习：To address the limitation of rule-based prediction, Narya employs an additional learning-based predictor, which analyzes more signals and patterns during a larger time window.\n66. Sundial: Fault-tolerant Clock Synchronization for Datacenters Sundial大大缩短了数据中心不同节点之间的时钟同步偏差，提高了事务响应的延迟。Sundial provides ∼100ns time-uncertainty bound (ε) under failures including temperature-related, link, device and domain failures and reports ε to applications – two orders of magnitude better than current designs.\n67. Fault-tolerant and Transactional Stateful Serverless Workflows Serverless Functions，指的是用户可以直接在云计算平台定义一个函数，让平台完成计算资源的调度，无需关心服务器的具体细节。基本上来说，每当函数被调用时，平台都要创建一个临时的VM或容器，装载好运行环境，执行代码，然后将其释放。\n一开始Serverless Functions是没有状态的，毕竟它原本的定位只是提供一个临时的计算，但是后来人们提出了Stateful Serverless Functions (SSFs)，尝试把运行时产生的数据存放到数据库里。然而这样做有个问题：如果某个VM在计算过程中挂了，下一个VM接替时，就可能再次改变状态。\n如何能让SSF具有良好的容错性呢？于是本文提出了Beldi, a library and runtime system for writing and composing fault-tolerant and transactional stateful serverless functions。\n68. Unearthing inter-job dependencies for better cluster scheduling “任务间依赖”对集群调度的影响：现在有些集群是共享数据的（称为Data lakes），因此任务之间有可能形成依赖关系，但是目前的调度系统并不会考虑这些，反而有可能产生“优先级倒挂”，即高优任务依赖低优任务。作者通过分析发现这种现象广泛存在于Microsoft Cosmos data lake，所以提出了Wing。The Wing dependency profiler analyzes job and data provenance logs to find hidden inter-job dependencies, characterizes them, and provides improved guidance to a cluster scheduler.\n69. RackSched: A Microsecond-Scale Scheduler for Rack-Scale Computers Head-of-line blocking (HOL blocking) in networking is a performance issue that occurs when a bunch of packets is blocked by the first packet in line. It can happen specially in input buffered network switches where out-of-order delivery of packets can occur.\n为了满足越来越大的流量、越来越严格的SLO，多核处理已经不够了，需要多个机器同时处理（即机架式电脑）。本文提出的RackSched是第一个机架级、微妙级的调度器，能将整个机架抽象为一个对外的服务。要做到这一点并不容易，因为现代OS的调度几十个核就需要几微秒，但机架有几百、几千个核。如果简单地用单个核进行中心化调度，那么它会成为性能瓶颈。\n70. Thunderbolt: Throughput-Optimized, Quality-of-Service-Aware Power Capping at Scale 数据中心的机房很花钱。2019年，五大公司（亚马逊、微软、谷歌、苹果、脸书）一共在数据中心上花了1200亿美元，这些钱主要都用来建造机房、冷却设备、供电，等等。于是有聪明人想到，虽然我一个机房的供电量有限，但并非所有机器都会同时用电，所以我可以多放点机器（power oversubscription），这样能少修好多机房，节约好多钱。万一真的超过额定功率了，就采取措施把用电量压下去（power capping），例如暂停低优先级任务、给CPU降频等各种操作。这些操作既要把用电量降下来，还要保证服务质量稳定。这对要求高吞吐量和低延迟的集群提出了挑战。\n本文提出了Thunderbolt，a simple, robust, and hardware-agnostic power capping system, Thunderbolt, to address these challenges. It throttles the CPU shares of throughput-oriented workloads to slow them down “just enough” to keep power under specified budget, while leaving latency-sensitive tasks unaffected.\n本文让我明白，除了CPU、内存、网络等计算资源，电力也是集群重要的有限资源，power oversubscription的概念也很有意思。\n结语 70篇论文，最好当科学杂志一样看个乐，涨涨见识。如果强迫自己每一篇都细看，一是没有必要，二是劳心费神。\n","permalink":"https://daichao1997.github.io/posts/tech/2021-11-05-osdi2020%E8%AE%BA%E6%96%87%E6%8F%90%E8%A6%81/","summary":"\u003cp\u003e完整版链接：https://www.usenix.org/sites/default/files/osdi20-full_proceedings.pdf\u003c/p\u003e","title":"OSDI 2020论文摘要"},{"content":"这次我的意见不仅和官媒相左，还跟不少熟悉的同学相左，但我还是想说出来。\n“艺人购买毒品的钱会变成子弹打在缉毒警察的身上”，这句话很感人，但它把“艺人吸毒”和“缉毒警察身亡”的联系暗示得过于紧密。这两者有关系吗？肯定是有的，但是全国有多少吸毒人士，艺人才占百分之几？诚然，无论毒品多少，只要买了就要为此负责，但是这跟复出的联系在哪儿呢？现有的解释并不能说服我。 “吸毒艺人复出会给社会带来负面影响”，我不完全同意。还记得薛之谦、陈赫等明星数不清的丑闻吗？还记得范冰冰漏税8亿吗？有人因为看到他们复出而学会出轨、逼女友打胎或者偷税漏税吗？如果有，为什么他们没有被封杀？如果没有，为什么唯独毒品是个例外？更何况，一些有吸毒史的海外艺人在中国依然有大批粉丝。你听过Eminem的《Not Afraid》吗？这首歌是他戒毒后写的，很多中学生都喜欢听，要不要封杀一下？ “毒品在中国是不能碰的底线”，这句话一点错都没有，但它的含义很模糊，你可以从中理解出各种含义。它是想说“吸了毒该坐牢”、“吸了毒该遭受唾弃”吗？那确实没问题。可不可以理解成“吸了毒该枪毙”、“吸了毒该被驱逐出境”、“吸了毒该被抹去存在的痕迹”？不一定吧，至少还有一些可商榷的空间吧？ 那么这些商榷的空间为什么被挤压干净了呢？为什么我感觉只要一提“也可以复出试试”，就会被十亿人的口水淹没呢？这样不容商榷的舆论环境，是人民自发形成的，还是……？我不知道。 总而言之，我不是很反对吸毒艺人复出，重点在于他们复出的方式和姿态。只要他们能弥补曾经犯下的过错，我觉得后果并不会像很多人说的那样严重。\n","permalink":"https://daichao1997.github.io/posts/life/2021.10%E8%AF%A5%E4%B8%8D%E8%AF%A5%E5%85%81%E8%AE%B8%E6%B6%89%E6%AF%92%E8%89%BA%E4%BA%BA%E5%A4%8D%E5%87%BA/","summary":"\u003cp\u003e这次我的意见不仅和官媒相左，还跟不少熟悉的同学相左，但我还是想说出来。\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e“艺人购买毒品的钱会变成子弹打在缉毒警察的身上”，这句话很感人，但它把“艺人吸毒”和“缉毒警察身亡”的联系暗示得过于紧密。这两者有关系吗？肯定是有的，但是全国有多少吸毒人士，艺人才占百分之几？诚然，无论毒品多少，只要买了就要为此负责，但是这跟复出的联系在哪儿呢？现有的解释并不能说服我。\u003c/li\u003e\n\u003cli\u003e“吸毒艺人复出会给社会带来负面影响”，我不完全同意。还记得薛之谦、陈赫等明星数不清的丑闻吗？还记得范冰冰漏税8亿吗？有人因为看到他们复出而学会出轨、逼女友打胎或者偷税漏税吗？如果有，为什么他们没有被封杀？如果没有，为什么唯独毒品是个例外？更何况，一些有吸毒史的海外艺人在中国依然有大批粉丝。你听过Eminem的《Not Afraid》吗？这首歌是他戒毒后写的，很多中学生都喜欢听，要不要封杀一下？\u003c/li\u003e\n\u003cli\u003e“毒品在中国是不能碰的底线”，这句话一点错都没有，但它的含义很模糊，你可以从中理解出各种含义。它是想说“吸了毒该坐牢”、“吸了毒该遭受唾弃”吗？那确实没问题。可不可以理解成“吸了毒该枪毙”、“吸了毒该被驱逐出境”、“吸了毒该被抹去存在的痕迹”？不一定吧，至少还有一些可商榷的空间吧？\u003c/li\u003e\n\u003cli\u003e那么这些商榷的空间为什么被挤压干净了呢？为什么我感觉只要一提“也可以复出试试”，就会被十亿人的口水淹没呢？这样不容商榷的舆论环境，是人民自发形成的，还是……？我不知道。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e总而言之，我不是很反对吸毒艺人复出，重点在于他们复出的方式和姿态。只要他们能弥补曾经犯下的过错，我觉得后果并不会像很多人说的那样严重。\u003c/p\u003e","title":"该不该允许涉毒艺人复出"},{"content":"成都49中的事情尘埃落定后，我的愤怒也随之消退。这次事件引起这么大的风波是我意料之外的，有很多人（包括我在内）这几天都在持续地质疑官方公布的消息，尤其是通告里“排除刑事案件”、“家属表示无异议”等暧昧不清的用词更让人浮想联翩。从警方公布的结果来看，很多质疑都落了空，例如校方故意销毁监控、尸体直接火化、抢占留学名额等。同时，有人开始劝导其他人要相信通告，相信警方，相信政府。\n我现在正坐在北京市朝阳区的一间小房子里，距离事发现场大概有1840公里。如果没有互联网，我可能根本不知道千里之外有一位中学生自杀。就算媒体报道了这件事，我可能也只是在茶余饭后和身边的朋友聊两句。无论如何，我都不可能看到几十万人聚在一起七嘴八舌地争论、吵着要官方给一个令人信服的说法的情况。\n互联网给了我参与公共讨论的力量，但对于这股力量，我并没有很好地认识。我以为它能驱散隐藏在权力背后的黑暗，但我却忽略了任何由人掌控的力量必然受制于人性之缺陷的道理。\n人性最大的缺陷之一，就是容易受恐惧和愤怒的支配。\n我随手翻了翻最近的朋友圈。你们应该能感受到我看到新闻时愤怒，还有隐藏在愤怒背后的恐惧——害怕滥用权力者将自己踩在脚下的恐惧，害怕自己和其他弱势群体一样被时代的车轮碾过的恐惧：\n《大车司机服毒自尽，为什么“北斗掉线”成了他们的噩梦？》 豆瓣女权组被封杀 《大LOGO等多名博主删除炫富视频并道歉》 《成都49中一学生坠楼身亡》 清华大学强制扣押学生的电动车 这些新闻我没有目睹过任何一件，并且只是从各种消息中拼凑出了事件的样貌，但我根本没有怀疑它们的真实性——为什么？为什么我要呼吁一个我根本不了解的政府，为一个我根本不确定真假的事件负责？我什么时候觉得自己这么牛了，有知晓千里之外的新闻的力量？与其说是我的力量，不如说是互联网的力量吧？万一整个网络世界都是虚构的，那我岂不是在对着空气呼喊正义？\n这就是我可悲的地方。我自以为在理性地思考，但我连思考对象的最基本的真实性都无法保证。我自以为政府有义务解决我所听闻的每一件不公，但我都不知道怎么监督政府。我自以为在呼喊正义，但这个世界混沌到让人看不清正义。我所有的能力，仅限于在网上获取信息，用自己的价值观下判断，然后分享到没几个人看的朋友圈，呼喊着“我要公道！我要说法！”老成世故者看了，多半会摇头叹气，说任何呼喊都是徒劳，与其每天愤怒不安，不如在短暂的一生中苟活着寻求快乐。\n但我无法做一只把头埋进沙子的鸵鸟（对不起鸵鸟，我知道你不是为了逃避危险。还有骆驼，也给你说声对不起，我把你俩搞混了）。我是一棵苇草，但这棵苇草有自主的意识，会思考、会愤怒、会恐惧、会怀疑，所以不能忍受被拥有权力的人轻视和践踏。这是我的幸运，也是我的不幸。\n是的，我不够全知全能。网友可以带偏我，谣言可以蛊惑我，“境外势力”可以煽动我。新闻可以是和谐社会，公众号可以是收钱办事，通告可以是避重就轻。但我一定要在大海浮沉中紧紧握住那一根代表“理性”的木棒，不让自己沉入偏见的深渊。\n","permalink":"https://daichao1997.github.io/posts/life/2021.05%E6%88%90%E9%83%BD49%E4%B8%AD/","summary":"\u003cp\u003e成都49中的事情尘埃落定后，我的愤怒也随之消退。这次事件引起这么大的风波是我意料之外的，有很多人（包括我在内）这几天都在持续地质疑官方公布的消息，尤其是通告里“排除刑事案件”、“家属表示无异议”等暧昧不清的用词更让人浮想联翩。从警方公布的结果来看，很多质疑都落了空，例如校方故意销毁监控、尸体直接火化、抢占留学名额等。同时，有人开始劝导其他人要相信通告，相信警方，相信政府。\u003c/p\u003e","title":"成都49中"},{"content":"一年一度的五一小长假就这样过去了。说是五天的小长假，实际上有两天是周末，还有两天是通过国务院的“调休”政策、把另外两个周日设为工作日换来的。众所周知，周末是用来躺的。既然这五天假有四天都取自周末，那我岂有不躺的道理？\n然而就在这时，我的几个狐朋狗友正在狐朋狗友群里商量着回武汉做一些“狐朋狗友”的事情。狐朋M是我的大学室友，海南人，在杭州做游戏策划；狗友L则是高中兼大学同学，武汉人，在深圳当数学老师；再加上初中兼高中兼大学同学、在武汉搞芯片材料的S，以及在北京当程序员的我。\n这注定是一次历史性的会面。不是因为凑齐了“SML”三个尺码，而是因为我们抵抗了混沌定理，让四颗扩散了的粒子重新发生了聚合——虽然只有短暂的几个小时。\n于是5月4日，正当北大2020届的毕业生在迟来一年的毕业典礼上求婚时，我们几位毕业两三年的校友分别从北京、杭州、深圳赶来武汉，边吃烤牛肉边吹牛皮。不出意外，还没感慨几句“你变瘦了”、“你头发好长”，我们就开始互相起哄“讲故事”。也难怪，“饮食男女，人之大欲存焉”。我们四个虽然都未婚，但没见面的这几年多少有些感情生活。但比起学生时代一有人脱单就起哄要请客的兴奋，现在的我们已经习以为常了。没有人再诉说追不到某个女孩的烦恼（可能都学会放手去找下一个了），没有人再把女朋友拉进狐朋狗友群（可能都想保留点独立的圈子了），也没有人再挤眉弄眼地问“一起睡了吗”（毕竟都二十四五岁了）。我们都不再觉得有对象是什么稀奇的事情了，反而调侃起女朋友给自己带来的烦恼。\n是的，当我们都单身时，如何追到女生是重要的共同话题，所以那时候最喜欢“听故事”；等我们习惯恋爱后，如何谈得开心又成了共同话题。我已经可以看到不远的将来，我们会讨论买房买车、要不要孩子；再过十年，讨论怎么带孩子、两口子的钱给谁保管。等到四十岁再聚会时，如果有人还在问怎么追妹子、怎么培养共同爱好，我们的脑袋上多半会冒出几粒“😅”，不知所措吧。\n由于时间安排问题，我们只吃了顿饭就不得不送M去机场了。在机场逛了一个小时后，我们和M道别；紧接着，L也必须回家了；我和S吃完晚饭短暂地坐了一会之后，也不得不离开了。\n不过在这个信息时代，距离并不是断掉联系的原因，感情不够才是。我们今后会不断地和更多人建立关系，当有限的假期必须按优先级分配给不同的人时，有些朋友注定要相忘于江湖。不过在那之前，我还是期待下一次举杯，一起讲几年、十几年前的陈芝麻烂谷子事。\n第二天，我翻出了小学、初中和高中的同学录。刚才提到的这几位兄弟还算是常联系的，但有些朋友确实是再也没见过面了。小学时，我因为跳级不得不提前分发同学录；初中时，我因为提前被高中录取，又成了唯一在班上发同学录的人；高三下学期，我和班上其他同学一起被保送，但这时候已经不兴写同学录了，所以还是只有我发了。看来我真的很喜欢留下自己过去的痕迹。\n然而这些痕迹似乎只适合留在过去。我每次看同学录的时候都会笑，倒不是因为怀念，而是在笑我们尽管时常念旧、时常说“不忘初心”，每个人却或多或少地与过去告别了。\n这当然不是因为毕业。相反，毕业美好的地方之一，就在于它替你做了离开的决定。临近毕业，原本形同路人的同学也可以显得亲近，原本视而不见的风景也变得怡人，毕竟我们更愿意相信离开的原因是不可抗拒的毕业，而不是自己的喜新厌旧。这样，等毕业以后往回看时，我们就不用因为遗忘而感到愧疚，而只会感到怀念和惋惜。当然，也有早就想走却不得不等到毕业的人，但无论哪种情况似乎都说明，我们的内心深处需要告别，只是有时候我们不愿意承认。\n现在我又离开武汉来到了北京。无论是从小长大的家，还是曾经玩得好的老同学，甚至是曾经的自己，我都不得不承认告别他们的必要性，因为只有告别，我的人生才能有更好的遇见。\n","permalink":"https://daichao1997.github.io/posts/life/2021.05%E5%91%8A%E5%88%AB%E6%98%AF%E4%B8%BA%E4%BA%86%E6%9B%B4%E5%A5%BD%E7%9A%84%E9%81%87%E8%A7%81/","summary":"\u003cp\u003e一年一度的五一小长假就这样过去了。说是五天的小长假，实际上有两天是周末，还有两天是通过国务院的“调休”政策、把另外两个周日设为工作日换来的。众所周知，周末是用来躺的。既然这五天假有四天都取自周末，那我岂有不躺的道理？\u003c/p\u003e","title":"告别是为了更好的遇见"},{"content":"我们的消费原则就是，不需要原则。\n我和女朋友在一起七个月，感情比较稳定，平时经常一起看生活组的家常话题，也被一些奇葩的送礼故事逗笑过。看到经常有豆友纠结对象送的礼物够不够用心，红包够不够大，或者要不要扶贫，我也想分享一下我俩的消费情况，看看礼物和红包是不是真的那么重要。\n我工作了一年，她还没毕业，所以我在经济上比她宽松。一起吃饭时，我会自觉买单，她有时也会主动结账。有次去唐宫吃了三百多块钱，对学生来说已经很贵了，但她依然付了钱。换句话说，我作好了次次都买单的准备，她也尽自己所能地负担消费。我们不必用AA的方式确保公平，也不用设定“你请一顿我请一顿”的机械规定，但我们内心都有一本帐，这本账不计数字，只计人情。我多付100，亏了吗？不亏，因为她少付了100。她少付100，赚了吗？不赚，因为我多付了100。\n说起送礼有点搞笑。\n一、七夕节我送了她一个星巴克的保温杯，她比较喜欢。不过她刚好在圣诞节前几天把它弄丢了，于是我圣诞节又送了一个保温杯，顺便给自己买了个玻璃杯。我俩都很高兴。\n二、我买的那个玻璃杯不到两天就被家里的猫打碎了，昨天她又给我买了一个。我俩都很高兴。\n三、快到情人节的时候我们聊到该怎么过，她囧了一下说还没开始准备礼物，我乐了，因为我也是。我俩都很高兴。\n四、正经礼物送过，但都不是过节的时候。我们刚在一起的时候她送我一瓶香水，因为她想闻。后来我送她一条项链，因为想看她戴。春节她送我一套比较贵的衣服，因为她有爸妈单位发的购物卡。我俩都很高兴。\n再说红包。今年春节，我也给她发了200元的红包，她回了我199。如果只看金额，我只发了她1元红包。但是，我俩都很高兴，因为这不是某种必须完成的“义务”，而是表达新春祝福的方式。\n话说回来，既然是表达祝福，发20元行不行呢？发0.52元行不行？可以算礼轻情意重吗？这个真不行。有钱的话发20000行不行？不一定，收红包的人心理也会有压力。还是那句话，我们心中都有一本不计数字、只计人情的账本，人情多少和红包大小不是绝对的正比例关系，但金额越大，人情肯定越重。按照我和女朋友的经济情况，发200不多也不少，就是个不轻不重的“意思”。小学生给班上的同学发0.52元，也是个不错的“意思”。如果老是纠结对方发了多少，或者对方回不回，就有点本末倒置了。这时候要反思的就不是一次的钱多钱少，而是平时的感情到没到位了。\n","permalink":"https://daichao1997.github.io/posts/life/2021.02%E7%A4%BC%E7%89%A9/","summary":"\u003cp\u003e我们的消费原则就是，不需要原则。\u003c/p\u003e\n\u003cp\u003e我和女朋友在一起七个月，感情比较稳定，平时经常一起看生活组的家常话题，也被一些奇葩的送礼故事逗笑过。看到经常有豆友纠结对象送的礼物够不够用心，红包够不够大，或者要不要扶贫，我也想分享一下我俩的消费情况，看看礼物和红包是不是真的那么重要。\u003c/p\u003e","title":"礼物"},{"content":"其实不只是这一本教材，国内对英文计算机文献的翻译质量普遍不高，以至于我直接读英文原著都要比读这种狗屁不通的汉语句子流畅许多。下面这个例子是第14章开头的第一段话，我来试试把原版翻译改良一下，你们品评一下有没有好读许多。\n英文原文：\nSome engineering situations require no more than a “textbook” data structure—such as a doubly linked list, a hash table, or a binary search tree—but many others require a dash of creativity. Only in rare situations will you need to create an entirely new type of data structure, though. More often, it will suffice to augment a textbook data structure by storing additional information in it. You can then program new operations for the data structure to support the desired application. Augmenting a data structure is not always straightforward, however, since the added information must be updated and maintained by the ordinary operations on the data structure.\n机械工业出版社的翻译：\n一些工程应用需要的只是一些“教科书”中的标准数据结构，比如双链表、散列表或二叉搜索树等，然而也有许多其他的应用需要对现有数据结构进行少许地创新和改造，但是只在很少情况下需要创造出一类全新类型的数据结构。更经常的是，通过存储额外信息的方法来扩张一种标准的数据结构，编写新的操作来支持所需要的应用。然而对数据结构的扩张并不总是简单直接的，因为添加的信息必须要能被该数据结构上的常规操作更新和维护。\n这个翻译的槽点实在太多了！\n首先第一句“……然而……但是……”双重转折绕得人云里雾里，其实完全可以按照原文用两句话说完。\n“对现有数据结构进行少许地创新和改造”，“的地得”都弄错了，而且原文就一句\u0026quot;a dash of creativity\u0026quot;，怎么被扩写得这么长？\n“一类全新类型的数据结构”，不觉得累赘吗？\n“更经常的是，通过XXX来YYY，YYY”，为什么不把“来”用逗号分出来，而要把“YYY”分成两句？还有，这句话的主语呢？\n我自己的翻译：\n某些工程应用只需要用“教科书式”的标准数据结构就够了，例如双链表、散列表、二叉搜索树等，然而有时候也需要我们发挥一点创造力。当然，只有在极少数情况下我们才需要从零开始创造一种全新的数据结构。一般来讲，我们只需要在标准数据结构中添加额外的信息就够了，然后你就可以为它编写新的操作来支持你的需求。然而扩张一个数据结构并不总是简单直接的，因为你还需要保证你额外添加的信息能够通过该数据结构原有的操作来更新和维护。\n我个人觉得，除了少数需要严格描述的地方（如定义、证明），其他只需要表意的内容要做到简洁易懂，要让读者最快地理解你的核心观点。这一点英文版做到了，但中文翻译却把它变得又冗长又累赘又不通顺。本来中文就缺少断句用的空格，你再把一句话弄得长长的，真是在阻碍广大计算机爱好者学习新知识啊！\n","permalink":"https://daichao1997.github.io/posts/tech/2020-09-01-itoa-ch14-translation/","summary":"\u003cp\u003e其实不只是这一本教材，国内对英文计算机文献的翻译质量普遍不高，以至于我直接读英文原著都要比读这种狗屁不通的汉语句子流畅许多。下面这个例子是第14章开头的第一段话，我来试试把原版翻译改良一下，你们品评一下有没有好读许多。\u003c/p\u003e","title":"《算法导论》的翻译真叫一个垃圾！"},{"content":"本章提及了数据结构的“扩张”概念。当我们需要支持一种特殊操作的时候，应该优先考虑去扩展现有的数据结构，而不是自创一种。扩展的方法就是“加信息”，然后用原有的基本操作去维护它。作为例子，本章讲了“顺序统计树”和“区间树”，并给出了扩展数据结构的一般方法。\n顺序统计树 需求 在 O(lg⁡n)O(\\lg{n}) 时间内算出任一元素的次序；在 O(lg⁡n)O(\\lg{n}) 时间内找出任一次序的元素。\n扩展 在红黑树的结点中加入额外信息size，表示以自己为根的子树含有多少内部结点。显然我们有\nx.size=x.left.size+x.right.size+1x.size=x.left.size+x.right.size+1\n新操作 要选出第 ii 小的元素，我们从根结点开始，查看左子树的大小。若大于 i−1i-1，则返回左子树的第 ii 小元素；若等于 i−1i-1，则自己就是要找的元素；若大于 i−1i-1，则在右子树寻找第 i−1−x.left.sizei-1-x.left.size 小的元素。\n要算出任一元素 pp 的次序，则可以往上一直追溯到根，然后每路过一个作为右子的结点，把它兄弟子树的 sizesize 再加1的值累加起来，就能得到自己的次序。伪代码如下：\nOS-RANK(T,x) r = x.left.size + 1 y = x while y != T.root if y == y.p.right r = r + y.p.left + 1 y = y.p return r 要证明该算法的正确性，可以用归纳法证明，每次进入while循环时，rr 都是 xx 在根为 yy 的子树中的次序。\n维护 红黑树经过插入和删除之后，内部结构会发生变化，这时需要更新结点的 sizesize。\n插入时（修复前），只有一个叶子变成了内部结点，因此把根到新结点路径上的 sizesize 全部加 1 即可。做插入后的修复时，只有旋转操作会影响树的结构（可以去复习一下上一章的内容），其他都只是对颜色的改变。更重要的是，一次旋转只会影响两个结点的 sizesize，这简直太棒了！按照下图的示意，我们只需要在旋转操作后加上两行代码即可（以左旋为例）：\ny.size = x.size x.size = x.left.size + x.right.size + 1 删除时（修复前），若 zz 是计生结点，则应更新z的父亲到根路径上的 sizesize；否则它会有它的后继 yy 来顶替，此时应更新 yy 原本的父亲到根路径上的 sizesize。无论如何，删除时的缺位结点都是用 yy 表示的（见上一章的代码），这样实现起来比较方便。做删除后的修复时，同样只有旋转操作改变了树的结构，因此处理方法与插入后的修复相同。\n如何扩展数据结构 书中介绍了几个要点，它们之间不是顺序关系，而是平行关系：\n选择一个基本数据结构 决定要加入什么额外信息 确保该信息能在操作数据结构的过程中保持正确（例如插入、删除） 实现新的操作 至于红黑树，书中提出了一个关于要点2的纲领，以保证信息维护的效率：\n如果在每个结点上都维护一个新的属性 ff，且对于任一结点 xx，x.fx.f 都只由 xx 及其左右子决定，那么就可以保证每次插入或删除后都能在 O(lg⁡n)O(\\lg{n}) 的时间内修复 ff 的正确性。 区间树 需求 每个元素代表一个区间，用 [low,high][low,high] 表示。现在给出一个区间 ii，需要在 O(lg⁡n)O(\\lg{n}) 的时间内查询是否有能与之重合的元素 i′i\u0026#x27;。区间 ii 与 i′i\u0026#x27; 重合的定义是：i.low\u0026lt;i′.highi.low \u0026lt; i\u0026#x27;.high 且 i′.low\u0026lt;i.highi\u0026#x27;.low \u0026lt; i.high。\n扩展 用红黑树的形式组织每个区间的 lowlow，然后每个结点记录一个属性 maxmax，表示以它为根的子树中所有结点的 highhigh 的最大值。maxmax 的定义符合上一节所说的“纲领”，因为 x.max=max⁡(x.high, x.left.max, x.right.max)x.max=\\max(x.high,\\ x.left.max,\\ x.right.max)。\n新操作 INTERVAL-SEARCH(T,i) x = T.root while x != T.nil and i does not overlap x if x.left != T.nil and x.left.max \u0026gt;= i.low x = x.left else x = x.right return x 这份算法很巧妙，但不是那么直观，因为 maxmax 的含义不太清晰，所以也不好理解向左/向右找的判断条件，但我们可以抓住一个不变量，然后证明它的正确性：若树 TT 中存在与 ii 重合的元素，那么根为 xx 的子树里一定也有。\n循环开始时，xx 为根，所以命题成立。\n每次循环的过程中，\n若 xx 没有左子或 x.left.max\u0026lt;i.lowx.left.max \u0026lt; i.low，应该往右走。若 xx 没有左子，显然只有往右走才可能找到；若 x.left.max\u0026lt;i.lowx.left.max \u0026lt; i.low，说明 xx 左子树中所有结点的 highhigh 都小于 i.lowi.low，从而不可能与 ii 重合，同样必须往右走。 若 xx 有左子且 x.left.max\u0026gt;=i.lowx.left.max \u0026gt;= i.low，应该往左走。若左子树不含与 ii 重合的结点，那么取其中 highhigh 最大的结点 i′i\u0026#x27;，有 i.low\u0026lt;=x.left.max=i′.highi.low\u0026lt;=x.left.max=i\u0026#x27;.high 且 i′.low\u0026gt;i.highi\u0026#x27;.low\u0026gt;i.high（否则就符合重合条件），又因为 xx 右子树中所有结点的 lowlow 都不小于 i′.lowi\u0026#x27;.low（红黑树性质），所以此时右子树也不可能有与 ii 重合的元素。也就是“若左边有，右边可能没有；若左边没有，根为x的整棵子树都没有”，所以必须往左走。 总地来说，每次循环前都保证了“若 TT 有，则 xx 树有”，且每次循环后能保证 xx 的某一子树也有，否则会推出“xx 树没有”的矛盾，从而维持了命题的正确性。建议多绕几遍加深理解。\n循环结束后，根据跳出循环的条件有两个可能：若 xx 与 ii 重合，则已找到；若 xx 是 T.nilT.nil，则根据命题的逆否形式，可以推出树 TT 不存在与 ii 重合的元素。\n维护 需要维护的只有 maxmax，只要像前面顺序统计树那样维护 sizesize 一样就好了。\n","permalink":"https://daichao1997.github.io/posts/tech/2020-09-01-itoa-ch14-augment/","summary":"\u003cp\u003e本章提及了数据结构的“扩张”概念。当我们需要支持一种特殊操作的时候，应该优先考虑去扩展现有的数据结构，而不是自创一种。扩展的方法就是“加信息”，然后用原有的基本操作去维护它。作为例子，本章讲了“顺序统计树”和“区间树”，并给出了扩展数据结构的一般方法。\u003c/p\u003e","title":"《算法导论》第14章：数据结构的扩张 (Augmenting Data Structures)"},{"content":"并查集的基本操作 并查集是一种由不相交集合组成的数据结构，它可以用 S={S1,S2,...,Sk}S=\\{S_1,S_2,...,S_k\\} 表示，其中 SiS_i 是由若干元素组成的集合，任意两个集合之间的交集都为 ∅\\empty，每个集合都有一个元素作为它的代表。\n并查集需要支持以下操作：\nMAKE-SET(x)创建一个仅含 x 的新集合（x 不得同时属于其他集合） UNION(x,y)将集合 x、y 合并到一起，形成新的集合 FIND-SET(x)返回集合 x 的代表 一般来说，每个元素都会先通过 MAKE-SET 形成只包含自己的集合，然后通过 UNION 相互兼并。MAKE-SET 和 UNION的运行次数是有限的，不会超过所有元素的总数 nn。与此同时，外部会通过FIND-SET查询某个元素当前属于哪个集合，该操作的次数没有上限。所以，一个并查集的运行效率可以通过两个变量衡量——MAKE-SET 的运行次数 nn，以及上述所有三种操作的总次数 mm。这里假设了每个元素都会先形成自己的集合。\n**并查集的应用举例：**有若干个结点，一开始是互相分开的（相当于各自 MAKE-SET），现在要在它们之间加入无向边（相当于 UNION），并且随时查询当前某两个结点之间是否存在一条路径（相当于判断 FIND-SET 的结果是否相同）。\n并查集的链表实现 用一个 dummy 作为链表头以及集合代表，串起集合的所有元素，让它们都有一个指针指向 dummy（便于 FIND-SET），同时 dummy 还记录着链表尾部的位置（便于插入元素）。\nMAKE-SET 和 FIND-SET 的实现自不必说，都只需要常数时间。UNION 需要遍历一个集合的所有元素，依次插入另一个集合的尾部，时间正比于前者的元素个数。最坏情况下，用 n−1n-1 次 UNION 合并 nn 个独立元素需要 O(n2)O(n^2) 时间。\n为了优化时间，我们可以在 dummy 记录每个集合的元素个数，然后每次 UNION 时都让更小的集合并入更大的集合。\n定理：用上述方法实现的并查集，操作时间为 O(m+nlg⁡n)O(m+n\\lg{n})\n证明：\nUNION 重复地将一个集合的元素移动至另一个集合链表的末尾，这样的移动次数决定了它的时间复杂度。对于任意一个元素，当它被移动到另一个集合中去时，它都属于更小的那个集合。也就是说，它所属集合的大小会在移动后至少翻倍。由于集合大小不会超过 nn，所以它最多被移动 ⌈lg⁡n⌉\\lceil{\\lg{n}}\\rceil 次，所以UNION的时间复杂度是 O(nlg⁡n)O(n\\lg{n})。\nMAKE-SET 和 FIND-SET 只需常数时间，所以三种操作总的时间复杂度是 O(m+nlg⁡n)O(m+n\\lg{n})。\n并查集的森林实现 把每个集合的元素组织成一棵有根树，根作为集合的代表，每个结点只保存父亲的位置（根的父亲指向自己）。那么 MAKE-SET 相当于创造一棵单结点树，FIND-SET 相当于从结点向上追溯到根，UNION 相当于把一棵树作为另一棵树的子树。\n如果不加限制地用上面的方法实现并查集，其效率不比链表实现好多少。书中介绍了两个提升效率的办法：\n1. Union by rank，为每个结点记录一个属性值 rank，表示它的“高度上限”。如果集合只包含自己，那么rank就是0；两个集合合并时，选择 rank 较低的树并入 rank 较高的树；若两者rank相等，则接受另一棵树的根结点 rank 加1（因为它现在有一个高度最多为 rank 的儿子，所以自己的高度最多为 rank + 1）。这和之前链表实现的思路类似，可以控制树的高度。 2. Path compression，每次 FIND-SET 的时候，让路过的结点都指向最终返回的根结点，从而削减树的高度。\n伪代码如下：\nMAKE-SET(x) x.p = x x.rank = 0 UNION(x,y) LINK(FIND-SET(x), FIND-SET(y)) LINK(x,y) if x.rank \u0026gt; y.rank y.p = x else x.p = y if x.rank == y.rank y.rank += 1 FIND-SET(x) if x != x.p x.p = FIND-SET(x.p) return x.p 值得品味的是FIND-SET的递归结构。从上到下递归时，每一层的参数x都是上一层的父亲，直到最后一层返回root。从下到上返回时，每一层的 parent 指针都会被设为下一层的返回值，再返回给上一层。所以FIND-SET最终不仅会返回 root，并且从 x 到 root 的所有结点都会直接指向树根，从而实现了 path compression。\n森林实现的时间复杂度是 O(m α(n))O(m\\ \\alpha(n))，其中 α(n)\\alpha(n) 是一个增长极慢的函数，当 n\u0026lt;16512n\u0026lt;16^{512} 时其值不超过4，所以可以近似地看作常数。具体证明略（参见21-4小节）。\nPS：单独使用 union by rank 的效果是 O(mlg⁡n)O(m\\lg{n})，单独使用 path compression 的效果是 Θ(n+f⋅(1+log⁡2+f/nn))\\Theta(n+f\\cdot(1+\\log_{2+f/n}{n}))，ff 是FIND-SET的次数。（参见21-3小节）\n","permalink":"https://daichao1997.github.io/posts/tech/2020-08-31-itoa-ch21-disjoint_set/","summary":"\u003ch3 id=\"并查集的基本操作\"\u003e并查集的基本操作\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e并查集\u003c/strong\u003e是一种由不相交集合组成的数据结构，它可以用 \u003cspan class=\"katex\"\u003e\u003cmath xmlns=\"http://www.w3.org/1998/Math/MathML\"\u003e\u003csemantics\u003e\u003cmrow\u003e\u003cmi\u003eS\u003c/mi\u003e\u003cmo\u003e=\u003c/mo\u003e\u003cmo stretchy=\"false\"\u003e{\u003c/mo\u003e\u003cmsub\u003e\u003cmi\u003eS\u003c/mi\u003e\u003cmn\u003e1\u003c/mn\u003e\u003c/msub\u003e\u003cmo separator=\"true\"\u003e,\u003c/mo\u003e\u003cmsub\u003e\u003cmi\u003eS\u003c/mi\u003e\u003cmn\u003e2\u003c/mn\u003e\u003c/msub\u003e\u003cmo separator=\"true\"\u003e,\u003c/mo\u003e\u003cmi mathvariant=\"normal\"\u003e.\u003c/mi\u003e\u003cmi mathvariant=\"normal\"\u003e.\u003c/mi\u003e\u003cmi mathvariant=\"normal\"\u003e.\u003c/mi\u003e\u003cmo separator=\"true\"\u003e,\u003c/mo\u003e\u003cmsub\u003e\u003cmi\u003eS\u003c/mi\u003e\u003cmi\u003ek\u003c/mi\u003e\u003c/msub\u003e\u003cmo stretchy=\"false\"\u003e}\u003c/mo\u003e\u003c/mrow\u003e\u003cannotation encoding=\"application/x-tex\"\u003eS=\\{S_1,S_2,...,S_k\\}\u003c/annotation\u003e\u003c/semantics\u003e\u003c/math\u003e\u003c/span\u003e 表示，其中 \u003cspan class=\"katex\"\u003e\u003cmath xmlns=\"http://www.w3.org/1998/Math/MathML\"\u003e\u003csemantics\u003e\u003cmrow\u003e\u003cmsub\u003e\u003cmi\u003eS\u003c/mi\u003e\u003cmi\u003ei\u003c/mi\u003e\u003c/msub\u003e\u003c/mrow\u003e\u003cannotation encoding=\"application/x-tex\"\u003eS_i\u003c/annotation\u003e\u003c/semantics\u003e\u003c/math\u003e\u003c/span\u003e 是由若干元素组成的集合，任意两个集合之间的交集都为 \u003cspan class=\"katex\"\u003e\u003cmath xmlns=\"http://www.w3.org/1998/Math/MathML\"\u003e\u003csemantics\u003e\u003cmrow\u003e\u003cmi mathvariant=\"normal\"\u003e∅\u003c/mi\u003e\u003c/mrow\u003e\u003cannotation encoding=\"application/x-tex\"\u003e\\empty\u003c/annotation\u003e\u003c/semantics\u003e\u003c/math\u003e\u003c/span\u003e，每个集合都有一个元素作为它的\u003cstrong\u003e代表\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e并查集需要支持以下操作：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eMAKE-SET(x)\u003c/code\u003e创建一个仅含 x 的新集合（x 不得同时属于其他集合）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eUNION(x,y)\u003c/code\u003e将集合 x、y 合并到一起，形成新的集合\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eFIND-SET(x)\u003c/code\u003e返回集合 x 的代表\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e一般来说，每个元素都会先通过 MAKE-SET 形成只包含自己的集合，然后通过 UNION 相互兼并。MAKE-SET 和 UNION的运行次数是有限的，不会超过所有元素的总数 \u003cspan class=\"katex\"\u003e\u003cmath xmlns=\"http://www.w3.org/1998/Math/MathML\"\u003e\u003csemantics\u003e\u003cmrow\u003e\u003cmi\u003en\u003c/mi\u003e\u003c/mrow\u003e\u003cannotation encoding=\"application/x-tex\"\u003en\u003c/annotation\u003e\u003c/semantics\u003e\u003c/math\u003e\u003c/span\u003e。与此同时，外部会通过FIND-SET查询某个元素当前属于哪个集合，该操作的次数没有上限。所以，一个并查集的运行效率可以通过两个变量衡量——MAKE-SET 的运行次数 \u003cspan class=\"katex\"\u003e\u003cmath xmlns=\"http://www.w3.org/1998/Math/MathML\"\u003e\u003csemantics\u003e\u003cmrow\u003e\u003cmi\u003en\u003c/mi\u003e\u003c/mrow\u003e\u003cannotation encoding=\"application/x-tex\"\u003en\u003c/annotation\u003e\u003c/semantics\u003e\u003c/math\u003e\u003c/span\u003e，以及上述所有三种操作的总次数 \u003cspan class=\"katex\"\u003e\u003cmath xmlns=\"http://www.w3.org/1998/Math/MathML\"\u003e\u003csemantics\u003e\u003cmrow\u003e\u003cmi\u003em\u003c/mi\u003e\u003c/mrow\u003e\u003cannotation encoding=\"application/x-tex\"\u003em\u003c/annotation\u003e\u003c/semantics\u003e\u003c/math\u003e\u003c/span\u003e。这里假设了每个元素都会先形成自己的集合。\u003c/p\u003e","title":"《算法导论》第21章：并查集"},{"content":"这一章主要研究这几个问题：\n怎么求最大/最小 怎么同时求最大\u0026amp;最小 怎么求第二大/小（习题9.1-1） 怎么求第k小 这些问题的算法并不难，本质上我们是想探讨它们所需要的最小比较次数，所以希望大家把重点放在理解证明过程上，学习证明思路，理解排序的本质。下文中，数据规模都用 nn 表示。\n求最大/最小 至少需要比较 n−1n-1 次才能保证求出。\n证明：最开始一共有 nn 个潜在的最大/最小候选，每次比较都能且只能淘汰1个候选。\n同时求最大\u0026amp;最小 至少需要比较 ⌊3n/2⌋−2\\lfloor{3n/2}\\rfloor-2 次才能保证求出。\n证明：\n最开始潜在的最大、最小候选各有 nn 个，所以一共需要排除 2n−22n-2 个候选。\n每次进行比较时，如果双方同时是最大或最小的候选，就能保证排除1个对应的候选。例如，若A、B都是最大候选，则比较后必然有一个不再是最大候选；但如果B不是最大候选，那么在A大于B的情况下无法排除任何候选。这样的策略一定可以实施，因为在选出最大/最小值前，对应的候选者必然多于1个。\n我们尽量让未比较过比较的数碰在一起，这样可以一次排除2个候选。若 nn 为奇数，那么最后会剩下一个未参与过比较的数，那么让它再和任意一个数比较一次就行了。这一步骤需要 ⌈n/2⌉\\lceil{n/2}\\rceil 次比较，排除了 nn 个候选。\n剩下 n−2n-2 个候选继续用之前的策略进行排除，每次比较排除一个，共 n−2 n-2 次比较。\n所以最少需要比较⌈n/2⌉+n−2=⌊3n/2⌋−2\\lceil{n/2}\\rceil+n-2=\\lfloor{3n/2}\\rfloor-2 次。\n求第二大/小 可以保证通过 n+⌈lg⁡n⌉−2n+\\lceil{\\lg{n}}\\rceil-2 次比较求出。\n方法：两两淘汰至冠军，然后从冠军击败的候选中选出最大/小值，即为亚军的值。\n正确性：亚军不可能被冠军以外的其他候选击败。\n比较次数：决出冠军需要 n−1n-1 次比较，期间冠军经历了 ⌈lg⁡n⌉\\lceil{\\lg{n}}\\rceil 轮比赛，击败了同等数量的对手。它们之间决出冠军又需要 ⌈lg⁡n⌉−1\\lceil{\\lg{n}}\\rceil-1 次比较，所以总共是 n+⌈lg⁡n⌉−2n+\\lceil{\\lg{n}}\\rceil-2 次比较。\n求第k小 期望是线性时间的算法 RANDOMIZED-SELECT(A,l,r,i)从数组A[l..r]中选出第i小的数。\nRANDOMIZED-SELECT(A,l,r,i) if l == r return A[l] q = RANDOMIZED-PARTITION(A,l,r) k = q-l+1 if i == k return A[q] elseif i \u0026lt; k return RANDOMIZED-SELECT(A,l,q-1,i) else return RANDOMIZED-SELECT(A,q+1,r,i-k) RANDOMIZED-PARTITION(A,l,r)随机从数组选择一个数，把小于它的移到它左边，大于它的移到它右边。如果左半边的元素个数大于等于i，那么第i小的数一定在左半边，否则在右半边。每深一层递归，搜索范围就小一点，所以最后一定可以找到。\n最坏情况下，搜索范围每次只缩小1，因此成了 Θ(n2)\\Theta(n^2) 时间的算法，但是书上证明了期望时间是 O(n)O(n) 。\n最坏情况下是线性时间的算法 本算法叫做SELECT，输入为长为 nn 的数组 AA 和正整数 ii，输出 AA 中第 ii 小的元素。大家欣赏一下就好。\n将 nn 个元素分为 ⌊n/5⌋\\lfloor{n/5}\\rfloor 组，每组 5 个，剩下的不超过五个的元素再成一组 用常数时间求出每组的中位数，共 ⌈n/5⌉\\lceil{n/5}\\rceil 个 递归调用 SELECT 算法，求出上述中位数的中位数 xx 用 PARTITION 算法将其他元素排列在 xx 的左侧和右侧，设左侧有 kk 个元素 若 i=ki=k，则返回 xx；若 i\u0026lt;ki\u0026lt;k，则在 xx 左侧调用 SELECT 寻找第 ii 小的元素；若 i\u0026gt;ki\u0026gt;k，则在 xx 右侧调用 SELECT 寻找第 i−ki-k 小的元素 这个算法为什么是线性时间的呢？看书本的9-3节吧。大概意思是说，每次递归执行第3步时，数据规模都是原来的20%，而每次递归执行第5步时数据规模都是原来的70%，相当于把上一节的 RANDOMIZED-PARTITION 变得更加稳定了，每次都能按一定比例排除候选。\n好，牛！但我还是会选择 RANDOMIZED-PARTITION，正如我选择随机版 quicksort 一样。\n","permalink":"https://daichao1997.github.io/posts/tech/2020-08-31-itoa-ch09-order_statistics/","summary":"\u003cp\u003e这一章主要研究这几个问题：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e怎么求最大/最小\u003c/li\u003e\n\u003cli\u003e怎么同时求最大\u0026amp;最小\u003c/li\u003e\n\u003cli\u003e怎么求第二大/小（习题9.1-1）\u003c/li\u003e\n\u003cli\u003e怎么求第k小\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这些问题的算法并不难，本质上我们是想探讨它们所需要的最小比较次数，所以希望大家把重点放在理解证明过程上，学习证明思路，理解排序的本质。下文中，数据规模都用 \u003cspan class=\"katex\"\u003e\u003cmath xmlns=\"http://www.w3.org/1998/Math/MathML\"\u003e\u003csemantics\u003e\u003cmrow\u003e\u003cmi\u003en\u003c/mi\u003e\u003c/mrow\u003e\u003cannotation encoding=\"application/x-tex\"\u003en\u003c/annotation\u003e\u003c/semantics\u003e\u003c/math\u003e\u003c/span\u003e 表示。\u003c/p\u003e","title":"《算法导论》第9章：中位数与顺序统计量 (Medians and Order Statistics)"},{"content":"今天来讲个有挑战性的内容——红黑树。这是我从本科开始一直没敢去碰的东西，主要是嫌麻烦，实际编程中又不怎么用，但最近就想看着玩。本文只能让你了解红黑树大概的样子，真要理解并形成深刻印象，还得去看《算法导论》的第12、13章。第12章（平衡二叉树）的笔记在上一篇文章里。\n红黑树的性质 红黑树是BST的变种，它的结点额外保留了一个颜色(color)属性，要么是红(RED)要么是黑(BLACK)。普通的BST可能会因为不恰当的插入顺序而变得极度“不平衡”，例如当我们从小到大插入时，每个结点都只有右子，那查询的时候跟链表就没有本质区别了。但是我们可以证明，一颗红黑树所有叶子结点的最大深度不会超过最小深度的两倍，因此是比较“平衡”的，可以保证单次操作时间是O(lgn)的。\n一棵红黑树必须同时符合以下五条性质：\n结点的颜色不是RED就是BLACK root是BLACK 叶子结点都是BLACK RED结点的儿子必须是BLACK 对于任一结点，它到它任一叶子结点的简单路径上，都包含相同的BLACK结点数 由于性质3，我们可以把树T的所有叶子用同一个“守护结点”T.nil表示，以及root的父亲，因为它不包含有效的key值，也没有左右子，它的父亲是谁也并不重要（后面你就能理解这个简化操作了）。\n红黑树的平衡性证明 下面的定理说明了红黑树的平衡性——当内部结点数量一定时，红黑树的高度可以控制在O(lgn)的范围内。\n定理：具有n个内部结点（即非叶子结点）的红黑树，高度最多为2lg(n+1)\n证明：\n定义一个红黑树的结点x的black-height（官方中译为黑高）bh(x)为它到叶子的简单路径上BLACK结点的个数（不含它自身）。\n我们先证明根为x的子树至少具有2^bh(x)-1个内部结点：若x高度为0，结论显然成立（x是叶子）；若x高度大于0，那么两个儿子的“黑高”至少是bh(x)-1（性质5），因此以x儿子为根的两棵子树都有至少2^(bh(x)-1)个内部结点（归纳法），因此根为x的子树至少具有2^(bh(x)-1) + 2^(bh(x)-1) + 1 = 2^bh(x)-1个内部结点。\n设红黑树的高度为h，那么根据性质4，从根到叶子的简单路径上，黑结点至少要占一半，因此根结点的“黑高”至少是h/2，因此其内部结点总数n \u0026gt;= 2^(h/2)-1，所以h \u0026lt;= 2lg(n+1)。\n平衡二叉树的旋转操作 “旋转”操作可以调整两棵子树的相对高度，并且维持BST的性质。设X的右子为Y，父亲为P，Y的左子为Z，那么“左旋”一个结点分为以下几步。书中代码的空间效率更高，但我觉得我的更方便记忆：认父亲ZXYP，收儿子PYXZ（倒过来）。\nZ拜X为父 X拜Y为父 Y拜P为父（若X为根，则Y成为新根） P认Y为子 Y认X为左子 X认Z为右子 LEFT-ROTATE(T,x) y = x.right z = y.left p = x.p if z != T.nil z.p = x x.p = y y.p = p if p == T.nil T.root = y elseif x == p.left p.left = y else p.right = y y.left = x x.right = z 左旋X的逆操作就是右旋Y，此时Y的左子为X，父亲为P，X的右子为Z，认父亲ZYXP，收儿子PXYZ。据此我们可以如法炮制出右旋的代码：\nRIGHT-ROTATE(T,y) x = y.left z = x.right p = y.p if z != T.nil z.p = y y.p = x x.p = p if p == T.nil T.root = x elseif y == p.left p.left = x else p.right = x x.right = y y.left = z 注意，X、Y一定是内部结点，否则无法进行旋转。\n红黑树的插入 下面是红黑树最精髓、也是最难搞的部分——插入和删除。红黑树的插入过程与BST类似，但新插入的结点会被染成RED，左右子会配上T.nil，且最后需要修复自己对红黑树性质造成的破坏。这个修复函数是这样的：\nRB-INSERT-FIXUP(T,z) while z.p.color == RED if z.p == z.p.p.left y = z.p.p.right if y.color == RED \u0026lt;case 1\u0026gt; else if z == z.p.right \u0026lt;case 2\u0026gt; \u0026lt;case 3\u0026gt; else ... // symmetric T.root.color = BLACK 示意图是这样的：\n三个case是这样划分的：\nz的叔叔y是红色（可能进入下一次while循环） y是黑色，且z是右子（转化为case 3） y是黑色，且z是左子（一定会跳出while循环，因为z.p被染成了黑色） 我说不出这套算法的设计思路，但我可以证明它的正确性。\n证明：调用RB-INSERT-FIXUP(T,z)后，T是一颗红黑树\n我们逐个验证红黑树的每一条性质。\n1. 所有结点不是黑色就是红色\n这是显然的。\n2. 根结点是黑色\n调用fixup前，root的颜色只有一种被改变的情况，那就是新插入的结点z成为root并被染为红色。这种情况下RB-INSERT-FIXUP只会将其染成黑色，形成一棵仅有一个内部结点的红黑树。其他情况下，root也是黑色，因为在插入z之前T就是一棵红黑树，插入过程中也没有改变原有结点的颜色。\n调用fixup时，在while循环内部，若执行了case 2/3，则一定会跳出while循环；若执行了case 1，那么可能被染成红色的只有z.p.p，若此结点不为root，则root保持为黑色；若此结点恰好为root，则下一次判断while条件时，由于其父亲为黑色的T.nil，故将跳出while循环。\n跳出while循环后，root将被染成黑色，然后函数返回。\n综上，调用RB-INSERT-FIXUP函数后，root一定是黑色，并且每一次进入while循环时，root也一定是黑色。\n因此，调用RB-INSERT-FIXUP后，T符合性质4。同时可以进一步推出，z.p一定不是根（因为进入循环时它是红色），因此z的叔叔y是存在的，上述三个case都是合法的。\n3. 叶子是黑色\n进入while循环时，唯一可能被染成红色的是z.p.p。如果它是T.nil，那么z.p就是root，其颜色一定为黑（上面已经证明过了），这与进入while循环的条件“z.p为红色”相矛盾。所以，可能被染红的只有内部结点，叶子会一直保持黑色。\n因此，调用RB-INSERT-FIXUP后，T符合性质3。\n4. 红色结点的儿子一定是黑色结点\n进入case 1有两种可能：要么是从函数外部进入，要么是从上一个case 1进入。如果是前者，那么“红红”冲突最多只有一处；如果是后者，那么上一个case一定已经解决了一个“红红”冲突。因此，case 1的“红红”冲突最多只有一处，就是z与z.p。该冲突被解决后，可能会引入另一个“红红”冲突，然后进入下一次while循环。\nwhile循环一定会终止，因为case 1会让z的深度会减2，而case 2/3会消灭冲突并跳出循环。即使z.p来到了最顶层的root或T.nil，由于其颜色必然为黑色（前面已经证明），因此冲突必然会被消灭，并结束循环。\n因此，调用RB-INSERT-FIXUP后，T符合性质4。\n5. 对于任一结点，它到每个叶子结点的简单路径都包含相同数目的黑色结点\n进入函数前，新插入的z不会影响性质5，因为它是红色的。\n在函数内部，由图示可以得知，while循环的每个case也不会影响性质5。注意进入循环时，z.p.p一定是黑色。\n因此，调用RB-INSERT-FIXUP后，T符合性质5。\n自制记忆窍门 叔叔红，换颜色，我再往上爬两格 叔叔黑，先掰直(?)，爷爸换色再转爷 红黑树的删除 红黑树的删除也和BST类似。\nRB-TRANSPLANT和BST的TRANSPLANT几乎一致，但最后一行不会判断新子树v是否为T.nil，因为T.nil是一个合法结点。实际上，我们之后甚至会用到T.nil.p的值。\nRB-TRANSPLANT(T,u,v) if u.p == T.nil T.root = v elseif u == u.p.left u.p.left = v else u.p.right = v v.p = u.p RB-DELETE里除了被删除的结点z，还出现了x和y。我们可以这样理解：若z是计生结点（仅有0或1个儿子），则y就是z；否则，y是z的后继，y会继承z原来的位置和颜色。无论哪种情况，y都不会在它原来的地方，其颜色信息也会丢失。若y原本是黑色，红黑树的性质会被破坏，需要用RB-DELETE-FIXUP恢复红黑树的性质。\n（接下来基本就是翻译课本了）\n如果y原本是红色呢？首先，每个结点的“黑高”不受影响；其次，y是红色代表y不可能是根，所以根的颜色不受影响；最后，y是红色代表x一定是黑色，因此x取代y后不会出现“红红”冲突，而y由于继承了z的颜色，也不会在新的位置产生冲突。\nRB-DELETE(T,z) y = z y-original-color = y.color if z.left = T.nil x = z.right RB-TRANSPLANT(T,z,x) elseif z.right = T.nil x = z.left RB-TRANSPLANT(T,z,x) else y = TREE-MINIMUM(z.right) y-original-color = y.color x = y.right if y.p = z x.p = y else RB-TRANSPLANT(T,z,x) y.right = z.right y.right.p = y RB-TRANSPLANT(T,z,y) y.left = z.left y.left.p = y y.color = z.color if y-original-color == BLACK RB-DELETE-FIXUP(T,x) y原本是黑色的情况下，哪些性质会被破坏呢？首先，如果y是根且接替y的x是红色，那么性质2会被破坏；其次，如果y原本的父亲是红色，x也是红色，那么x接替y后，性质4会被破坏；最后，假如某结点原本到叶子的路径上含有y，而另一条不含y，那么性质5也会被破坏。\n为了将性质5的影响最小化，我们采用一个特殊的办法：想象y在离开时把自己的黑色“传递”给了x。也就是说，x除了自身的颜色，还额外带着y遗留下来的黑色。这样虽然违背了性质1，但起码救回了性质5。性质1的破坏只集中在x身上，但性质5的破坏是影响全局结点的，所以这个方法实际上简化了问题。\n在下面的代码中，我们关注每次进入while循环时的规律：\nx始终指向一个“双黑”结点，即自身为黑色、且携带了一个额外黑色的结点。第一次进入循环时x为黑，且y的黑色给了x，此后的操作也表明x一直是“双黑”结点 x的兄弟w存在且不是T.nil。因为x不是根，所以w存在。因为x提供了2点“黑高”，所以根为w的子树中至少有两个黑色结点，所以w不是T.nil case 1会转变为case 2 case 2将多余的黑色转移给x.p，若后者为红色，那么把它染成黑色就能解决冲突并退出循环；若后者依然是黑色，那么将产生另一个“双黑”结点，重新进入while循环 case 3会转变为case 4 case 4解决冲突并退出循环 多的我不知道该怎么讲了，这玩意简直像魔方公式一样，你唯一能做的就是记住几个case及其对应的“魔法”操作。你也可以像上一节那样证明函数返回后五条性质都能得到恢复，且时间复杂度为O(lgn)。请慢慢欣赏。\nRB-DELETE-FIXUP(T,x) while x != T.root and x.color == BLACK if x == x.p.left w = x.p.right if w.color == RED \u0026lt;case 1\u0026gt; if w.left.color == BLACK and w.right.color == BLACK \u0026lt;case 2\u0026gt; else if w.right.color == BLACK \u0026lt;case 3\u0026gt; \u0026lt;case 4\u0026gt; else ... // symmetric x.color = BLACK 下图灰色圈表示任意颜色，外面套的大圈表示y遗留下来的额外黑色。\n","permalink":"https://daichao1997.github.io/posts/tech/2020-08-30-itoa-ch13-rbtree/","summary":"\u003cp\u003e今天来讲个有挑战性的内容——红黑树。这是我从本科开始一直没敢去碰的东西，主要是嫌麻烦，实际编程中又不怎么用，但最近就想看着玩。本文只能让你了解红黑树大概的样子，真要理解并形成深刻印象，还得去看《算法导论》的第12、13章。第12章（平衡二叉树）的笔记在上一篇文章里。\u003c/p\u003e","title":"《算法导论》第13章：红黑树"},{"content":"本文简单介绍二叉搜索树，为下一篇文章讲红黑树作铺垫。\n二叉搜索树 二叉搜索树(Binary Search Tree, BST)是一棵二叉树，每个结点都记录着左子left、右子right、父结点p、关键字key。BST里的关键字总是满足二叉搜索树性质：对于每个结点，其关键字都大于或等于它的任意左子，小于或等于它的任意右子。简单来说，就是“左小右大”。下面比较结点大小时，指的就是比较结点关键字的大小。\n搜索 BST很便于搜索。例如要搜索k，那么从根结点开始，如果k比当前结点大，就往右下走；如果更小，就往左下走；如果相等，就搜索成功；如果搜到叶子结点都没找到，就搜索失败。\nTREE-SEARCH(x,k) if x == NIL or k == x.key return x if k \u0026lt; x.key return TREE-SEARCH(x.left, k) else return TREE-SEARCH(x.right, k) 最大值与最小值 很显然，一直往左下走就是最小值，一直往右下走就是最大值。\n前驱与后继 某结点的前驱(predecessor)是小于它却最接近它的结点，后继(successor)是大于它却最接近它的结点。\n后继在哪里找呢？在一棵二叉树中，对于任一结点M，另一个结点与它的关系可能是：\n是M的祖先 是M的后代 与M有另一个最低的共同祖先P 所以，M的后继N一定在这三类位置当中。\n如果N是M的祖先，那么M必然是N的左子树的最大值，即M = min(T, N.left)。这样的N最多只存在一个，可记为N1。\n如果N是M的后代，那么N必然是M的右子树的最小值，即N = max(T, M.right)。这样的N最多只存在一个，可记为N2。\n如果M与N有另一个最低的共同祖先P，那么P的值一定在M与N之间，N不可能是M的后继。\n当N1和N2都存在的情况下，因为M在N1的左子树上，又是N2的祖先，故N2 \u0026lt; N1。\n综上，M的后继是它右子树的最小值（若存在），否则是使M成为它左子树上最大值的那个结点（若存在），否则没有后继（是整棵树的最大值）。\n前驱的情况与后继对应。\nTREE-SUCCESSOR(x) if x.right != NIL return TREE-MINIMUM(x.right) y = x.p while y != NIL and x == y.right x = y y = y.p return y 插入和删除 插入实际上类似于搜索，只不过搜索的是一个不存在的key值，然后当我们到达一个空结点时，把它变成具有新key值的结点就行了。\nTREE-INSERT(T,z) y = NIL x = T.root while x != NIL y = x if(z.key \u0026lt; x.key) x = x.left else x = x.right z.p = x if y == NIL // tree T was empty T.root = z else if z.key \u0026lt; y.key y.left = z else y.right = z 删除稍微要麻烦一些。\n首先我们要认识到，删除一个仅有0或1个儿子的结点（下称“计生”结点）很容易，只要直接删除，或者把它的儿子“接上来”就可以了。下面是用子树v替换子树u的代码，它只做了简单的拆装，维持u以外的原树的结构：\nTRANSPLANT(T,u,v) if u.p == NIL T.root = v elseif u == u.p.left u.p.left = v else u.p.right = v if v != NIL v.p = u.p 当我们要删除有2个儿子的结点Z时，可以找另一个结点代替它。代替者必须满足：1. 大于左子树的最大值（即Z的前驱）；2. 小于右子树的最小值（即Z的后继）。这样看来，似乎我们只能用Z的前驱或后继代替Z了，而恰好它们俩都是“计生”结点（前驱无右子，后继无左子）。下面说用Z的后继Y代替Z的方法。\n若Y恰好是Z的右子，则可以直接用Y代替Z的位置，Y的左子取代Z的左子，右子保持不变。 若Y不是Z的右子，则可先用Y代替Z的位置，Y的左右子取代Z的左右子；与此同时，Y原来的右子需挂靠在Y的父结点上，相当于将“删除有2个儿子的Z”转化为“删除仅有0或1个儿子的Y”。 TREE-DELETE(T,z) if z.left == NIL TRANSPLANT(T,z,z.right) elseif z.right == NIL TRANSPLANT(T,z,z.left) else y = TREE-MINIMUM(z.right) if y.p != z TRANSPLANT(T,y,y.right) y.right = z.right y.right.p = y TRANSPLANT(T,z,y) y.left = z.left y.left.p = y 模板 可以用C++写个简单的模板。\nstruct BSTNode { int key; BSTNode* left; BSTNode* right; BSTNode* p; }; struct BST { BSTNode *root; BSTNode* query(int val); BSTNode* mininum(BSTNode* node); BSTNode* maximum(BSTNode* node); BSTNode* insert(int val); // return the inserted node BSTNode* del(BSTNode *node); // return NULL if deletion fails, or return a pointer pointing to the original place in the tree where node was deleted } ","permalink":"https://daichao1997.github.io/posts/tech/2020-08-28-itoa-ch12-bst/","summary":"\u003cp\u003e本文简单介绍二叉搜索树，为下一篇文章讲红黑树作铺垫。\u003c/p\u003e\n\u003ch3 id=\"二叉搜索树\"\u003e二叉搜索树\u003c/h3\u003e\n\u003cp\u003e二叉搜索树(Binary Search Tree, BST)是一棵二叉树，每个结点都记录着左子left、右子right、父结点p、关键字key。BST里的关键字总是满足\u003cstrong\u003e二叉搜索树性质\u003c/strong\u003e：对于每个结点，其关键字都大于或等于它的任意左子，小于或等于它的任意右子。简单来说，就是“左小右大”。下面比较结点大小时，指的就是比较结点关键字的大小。\u003c/p\u003e","title":"《算法导论》第12章：二叉搜索树"},{"content":"一道并不怎么难的题，提交了N次才成功，究其原因是边缘情况特别多，所以一定得好好复盘，练好我一直写不清楚的二分法——我经常绕不清楚奇偶两种情况，也常常照顾不好数组越界。\n问题描述 给两个长为m、n的升序数组nums1、nums2，求这些数的中位数。两个数组不全为空。\n思路 何为中位数？答：以此数为界，能刚好将一群数分为大小两堆，其元素个数差不大于1，且大堆最小值 \u0026gt;= 小堆最大值 记小堆的元素个数为mid = (m+n)/2（整数除法都向下取整，下同），则大堆元素个数为m+n-mid 现已经有两堆排好序的数，我们只需分别取出nums1的前k个数、nums2的前mid-k个数，就能求出中位数。这个k要满足一些条件：\nEq1: k \u0026gt;= 0 Eq2: k \u0026lt;= m，因为nums1只有m个元素 Eq3: k \u0026lt;= mid，因为小堆只需要mid个元素 Eq4: n+k \u0026gt;= mid，因为小堆必须要有mid个元素 Eq5: nums1[k-1] \u0026lt;= nums2[mid-k]，因为nums1的小堆最大值 \u0026lt;= nums2的大堆最小值 Eq6: nums2[mid-k-1] \u0026lt;= nums1[k]，因为nums2的小堆最大值 \u0026lt;= nums1的大堆最小值 如果以上条件都满足，那么这个k就能把两个数组分成刚才说的大小堆。\n可以简化一下：假设m \u0026lt;= n，那么k \u0026lt;= m \u0026lt;= mid \u0026lt;= n，因此可以消掉Eq3和Eq4，只考虑0 \u0026lt;= k \u0026lt;= m\n寻找k的办法就采用经典的二分法。首先明确k的上下界，再不断取中点、验证、更新上下界。\nk的上下界分别是0和m，接下来应该取中点并验证：若Eq5不满足，则降低k；若Eq6不满足，则升高k；若都满足，则寻找成功。有可能都不满足吗？不可能，否则nums2[mid-k] \u0026lt; nums1[k-1] \u0026lt;= nums1[k] \u0026lt; nums2[mid-k-1]，与nums2的单调递增性矛盾。\n找到k之后，即可分情况求出中位数。若总数为偶数，则取小堆最大值v1与大堆最小值v2，取平均值即可；若总数为奇数，则v2为中位数。\n若nums1或nums2为空，则取另一数组的中位数即可。\n总之，如果用这个思路，那么难点在于统一奇偶两种情况，把大小堆划分清楚，并且要考虑某一数组全部被划分到大堆/小堆的边缘情况，复杂度为O(logm)\nclass Solution { public: double findMedianSortedArrays(vector\u0026lt;int\u0026gt;\u0026amp; nums1, vector\u0026lt;int\u0026gt;\u0026amp; nums2) { // 让nums1不长于nums2，简化情况 if(nums1.size() \u0026gt; nums2.size()) swap(nums1, nums2); int m = nums1.size(), n = nums2.size(); // 特殊情况：空数组 if(m == 0) {return ((double)nums2[n/2] + (double)nums2[(n-1)/2]) / 2.0;} // 初始化 int mid = (m+n)/2; int mmin = 0, mmax = m, k = (mmin+mmax)/2; while(mmin \u0026lt; mmax) { // Eq1不成立，降低k的上界 // k若等于0，说明nums1已经全部被划到大堆 // 这时不存在“nums1的小堆最大值”，可按负无穷处理，Eq1成立 if(k != 0 \u0026amp;\u0026amp; nums1[k-1] \u0026gt; nums2[mid-k]) { mmax = k-1; k = (mmin+mmax)/2; // Eq2不成立，提高k的下界 // k若等于m，说明nums1已经全部被划到小堆 // 这时不存在“nums1的大堆最小值”，可按负无穷处理，Eq2成立 } else if(k != m \u0026amp;\u0026amp; nums2[mid-k-1] \u0026gt; nums1[k]) { mmin = k+1; k = (mmin+mmax)/2; // Eq1与Eq2同时成立，或上下界相遇，则找到k } else { break; } } double v1, v2; // nums1[k-1], nums[mid]: 处于nums1大小堆分界线的两个元素（不一定同时存在） // nums2[mid-k-1], nums2[mid-k]: 处于nums2大小堆分界线的两个元素（不一定同时存在） // case 1: nums1全部分到大堆 if(k == 0) { // v1是nums1与nums2的小堆部分的最大值，但此时nums1没有小堆 v1 = nums2[mid-1]; // 考虑此时nums2全部在小堆的情况，充要条件为n=mid，此时nums2[mid-k]不存在 v2 = (n == mid) ? nums1[0] : min(nums1[0], nums2[mid]); } // case 2: nums1全部分到小堆 else if(k == m) { // 考虑此时nums2全部在大堆的情况，充要条件为m=mid，此时nums2[mid-k-1]不存在 v1 = (m == mid) ? nums1[m-1] : max(nums1[m-1], nums2[mid-m-1]); // v2是nums1与nums2的大堆部分的最小值，但此时nums1没有大堆 v2 = nums2[mid-m]; } // case 3: nums1、nums2都存在大堆和小堆部分 else { v1 = max(nums1[k-1], nums2[mid-k-1]); v2 = min(nums1[k], nums2[mid-k]); } if((m+n)%2 == 0) { return (v1+v2)/2; } else { return v2; } } }; ","permalink":"https://daichao1997.github.io/posts/tech/2020-07-13-median-of-two-sorted-arrays/","summary":"\u003cp\u003e一道并不怎么难的题，提交了N次才成功，究其原因是边缘情况特别多，所以一定得好好复盘，练好我一直写不清楚的二分法——我经常绕不清楚奇偶两种情况，也常常照顾不好数组越界。\u003c/p\u003e","title":"记一次愚蠢的Leetcode刷题"},{"content":"上次我们编译并成功运行了myOS，这次我们来仔细看看这个简单的操作系统有哪些构成要素，要以怎样的方法去安装它。\n编译环境的建立 源代码的编译是靠shell脚本而不是Makefile总控的，不过这并不影响我们学习。大致来讲：\nconfig.sh定义一大堆环境变量\nheaders.sh在每个$PROJECT下执行$MAKE install-headers\nbuild.sh在每个$PROJECT下执行$MAKE install\n所谓$PROJECT就是libc和kernel这两个文件夹，而install-headers和install做的事情需要到Makefile里具体看一看。\n首先看libc下面的Makefile，我们挑一部分讲。\nDEFAULT_HOST!=../default-host.sh HOST?=DEFAULT_HOST HOSTARCH!=../target-triplet-to-arch.sh $(HOST) default-host.sh会打印i686-elf\ntarget-triplet-to-arch.sh会判断输入的参数是否包含i[[:digit]]86-，若满足则打印i386，否则打印参数的开头部分，直到遇见字母、数字、下划线以外的字符（例如x86_64)\n所以默认情况下，$DEFAULT_HOST与$HOST的值为i686-elf，而$HOSTARCH等于i386。绕了一大圈就设置几个字符串，何必呢？\n然后是这些编译用的flag，别的都好说，但重要的是这个-ffreestanding选项：\nCFLAGS:=$(CFLAGS) -ffreestanding -Wall -Wextra CPPFLAGS:=$(CPPFLAGS) -D__is_libc -Iinclude LIBK_CFLAGS:=$(CFLAGS) LIBK_CPPFLAGS:=$(CPPFLAGS) -D__is_libk -ffreestanding选项的含义 这个选项我们在Bare Bone里就见过了，但当时说得比较模糊，现在我们对着GCC的文档来看：该选项将编译目标指定为“独立环境”。什么是独立环境呢？\n参考：\nAssert that compilation targets a freestanding environment. This implies -fno-builtin. A freestanding environment is one in which the standard library may not exist, and program startup may not necessarily be at main. The most obvious example is an OS kernel. This is equivalent to -fno-hosted.\nFreestanding Environment 参考：\nThe ISO C standard defines (in clause 4) two classes of conforming implementation. A conforming hosted implementation supports the whole standard including all the library facilities; a conforming freestanding implementation is only required to provide certain library facilities: those in \u0026lt;float.h\u0026gt;, \u0026lt;limits.h\u0026gt;, \u0026lt;stdarg.h\u0026gt;, and \u0026lt;stddef.h\u0026gt;; since AMD1, also those in \u0026lt;iso646.h\u0026gt;; since C99, also those in \u0026lt;stdbool.h\u0026gt; and \u0026lt;stdint.h\u0026gt;; and since C11, also those in \u0026lt;stdalign.h\u0026gt; and \u0026lt;stdnoreturn.h\u0026gt;. In addition, complex types, added in C99, are not required for freestanding implementations.\n上文说，C标准规定了C编译器可以有两种实现方式，（个人译作）宿主实现和独立实现。前者要能够提供完整的C标准库，后者则只用提供特定的头文件，且不同的C版本有不同的要求。\nThe standard also defines two environments for programs, a freestanding environment, required of all implementations and which may not have library facilities beyond those required of freestanding implementations, where the handling of program startup and termination are implementation-defined; and a hosted environment, which is not required, in which all the library facilities are provided and startup is through a function int main (void) or int main (int, char *[]). An OS kernel is an example of a program running in a freestanding environment; a program using the facilities of an operating system is an example of a program running in a hosted environment.\n上文说，C标准也定义了两种程序运行环境：独立环境和宿主环境。独立环境是任何C编译器必须支持的，而且程序的启动与终止也可以随实现方式不同而不同。宿主环境不是C编译器所必需的，它要求提供完整的C标准库，且程序必须通过main函数启动。\nGCC aims towards being usable as a conforming freestanding implementation, or as the compiler for a conforming hosted implementation. By default, it acts as the compiler for a hosted implementation, defining __STDC_HOSTED__ as 1 and presuming that when the names of ISO C functions are used, they have the semantics defined in the standard. To make it act as a conforming freestanding implementation for a freestanding environment, use the option -ffreestanding; it then defines __STDC_HOSTED__ to 0 and does not make assumptions about the meanings of function names from the standard library, with exceptions noted below. To build an OS kernel, you may well still need to make your own arrangements for linking and startup. See Options Controlling C Dialect.\n上文说，GCC希望能同时提供上述两种实现。默认情况下它使用“宿主实现”，但如果提供了-ffreestanding选项，它将不会从C标准库里寻找函数名（除了下面的特例）。\nGCC does not provide the library facilities required only of hosted implementations, nor yet all the facilities required by C99 of freestanding implementations on all platforms. To use the facilities of a hosted environment, you need to find them elsewhere (for example, in the GNU C library). See Standard Libraries.\nMost of the compiler support routines used by GCC are present in libgcc, but there are a few exceptions. GCC requires the freestanding environment provide memcpy, memmove, memset and memcmp. Finally, if __builtin_trap is used, and the target does not implement the trap pattern, then GCC emits a call to abort.\n上文说，GCC并不在所有平台上都自带“宿主实现”专用的库，或者100%完整地自带“独立实现”要用的库。如果你要用“宿主实现”的库，就得看操作系统有没有自带，或者去GNU C library里面找。libgcc提供了编译器需要的大部分东西，但也有例外，例如GCC需要独立环境提供memcpy、memmove、memset和memcmp的实现。\n关于C标准库的一点补充 参考：\nGCC by itself attempts to be a conforming freestanding implementation. See Language Standards Supported by GCC, for details of what this means. Beyond the library facilities required of such an implementation, the rest of the C library is supplied by the vendor of the operating system. If that C library doesn’t conform to the C standards, then your programs might get warnings (especially when using -Wall) that you don’t expect.\nFor example, the sprintf function on SunOS 4.1.3 returns char * while the C standard says that sprintf returns an int. The fixincludes program could make the prototype for this function match the Standard, but that would be wrong, since the function will still return char *.\nIf you need a Standard compliant library, then you need to find one, as GCC does not provide one. The GNU C library (called glibc) provides ISO C, POSIX, BSD, SystemV and X/Open compatibility for GNU/Linux and HURD-based GNU systems; no recent version of it supports other systems, though some very old versions did. Version 2.2 of the GNU C library includes nearly complete C99 support. You could also ask your operating system vendor if newer libraries are available.\nGCC会尽量去满足独立实现，但C函数库的剩余部分就需要操作系统的供应商去提供。如果你的库不符合C标准，那么编译程序时可能会产生大量warning，例如Sun OS 4.1.3的sprintf函数会返回char*，但C标准要求返回int。\n如果你需要符合标准的库，你得自己去找，因为GCC并不提供。GNU C library（又称“glibc”）为一些系统提供了一些标准的支持，你也可以去问问供应商是否有更新它们的库。\n","permalink":"https://daichao1997.github.io/posts/tech/2020-05-13-meaty-skeleton-2/","summary":"\u003cp\u003e上次我们编译并成功运行了myOS，这次我们来仔细看看这个简单的操作系统有哪些构成要素，要以怎样的方法去安装它。\u003c/p\u003e\n\u003ch3 id=\"编译环境的建立\"\u003e编译环境的建立\u003c/h3\u003e\n\u003cp\u003e源代码的编译是靠shell脚本而不是Makefile总控的，不过这并不影响我们学习。大致来讲：\u003c/p\u003e","title":"跟着OSDev学习搭建操作系统（四）"},{"content":"前几天跑通了一个空空如也的内核（教程里叫做Bare Bone），只能说是搭建好了环境，接下来要搭建的myOS会稍微正式一点。如同教程标题“Meaty Skeleton”所说的那样，myOS虽然有一点“meat”，但依然只是一具“skeleton”，没有实际功能。我们既可以从中学到一个科学的OS源代码的结构，也可以直接以myOS为基础，向里面添加新的功能。\n这一篇教程的信息量要远多于上一篇，其难点不在于看懂每个函数（函数都很简单），而在于把握全局，从整体出发去体会细节。这么说可能有点抽象，但我个人感觉这是搞OS最需要培养的一种感觉。所以，本文会非常详细地记录自己遇到的每一个疑惑，并尝试去解答它们。虽然我现在离成为真正的OS开发者还有很大差距，但我相信差距一定可以一步步拉近。那么来拉取代码吧：\ngit clone https://gitlab.com/sortie/meaty-skeleton.git 在macOS上编译的注意事项 拿到代码后，第一步当然是自己编译试试，于是本macOS用户遇到了这个error：\nMakefile:23: arch//make.config: No such file or directory 经过一番调查，发现macOS自带的make，也就是通过Xcode Command Line Tools安装的make，与GNU make并非同一版本，这会导致!=赋值符号不受支持：\n\u0026gt; make --version # 自带make GNU Make 3.81 \u0026gt; brew install make # GNU make GNU \u0026#34;make\u0026#34; has been installed as \u0026#34;gmake\u0026#34;. \u0026gt; gmake \u0026gt; gmake --version GNU Make 4.3 解决办法很简单，要么改用gmake\t，要么改一下PATH：\nPATH=\u0026#34;/usr/local/opt/make/libexec/gnubin:$PATH\u0026#34; 后面可能还会报一个关于cp的错，原因同上，我们需要安装一下GNU coreutils，感兴趣的可以参考这个帖子多装一些别的：\nbrew install coreutils 不过这样安装之后GNU cp会以gcp的名字存在，所以可以再改一下PATH：\nPATH=\u0026#34;/usr/local/opt/coreutils/libexec/gnubin:$PATH\u0026#34; 其他GNU命令都可以用类似的办法取代macOS版本的命令。\n编译结果的初步观察 运行./build.sh，可以看到生成了两个文件夹isodir与sysroot，前者与我们在Bare Bone里看到的一样，是给GRUB用来启动的，后者则有下属文件夹boot与usr。boot里装的myos.kernel我们也见过，它就是编译与链接后形成的内核本身，但多出来的usr文件夹是什么呢？\n我们知道，“操作系统”与“内核”是前者包含后者的关系。操作系统除了内核，还应该提供给用户一些交互工具，如命令行、编译器、文本编辑器、软件包管理工具等。如果你用过GNU/Linux系统，你一定见过根目录下诸如/usr/bin之类的文件夹，里面装着各种二进制文件，这些就是留给用户使用的。myOS也是一样，虽然现在我们什么都没有，但起码可以给用户留一个文件夹，里面放一些最简单的头文件等内容。类似地，你也可以在GNU/Linux系统里找到/boot文件夹，里面的东西也是与内核本身相关的东西，不过比myOS丰富得多。\n总而言之，sysroot文件夹是myOS的安装目的地，同时也是myOS用户的“根目录“。OS编译完成的同时，这个文件夹也成为了一个合格的启动目录。如果我们新开一个磁盘分区，把它复制过去，再从该分区启动，就会发生这些事情：bootloader会将内核读进内存，内核里的硬盘驱动与文件系统驱动又会把剩下的文件读进根目录（实际过程可以非常复杂，参考GNU/Linux系统的initrd），最后形成用户看到的完整的根目录。\n","permalink":"https://daichao1997.github.io/posts/tech/2020-05-09-meaty-skeleton-1/","summary":"\u003cp\u003e前几天跑通了一个空空如也的内核（教程里叫做Bare Bone），只能说是搭建好了环境，接下来要搭建的myOS会稍微正式一点。如同\u003ca href=\"https://wiki.osdev.org/Meaty_Skeleton\"\u003e教程\u003c/a\u003e标题“Meaty Skeleton”所说的那样，myOS虽然有一点“meat”，但依然只是一具“skeleton”，没有实际功能。我们既可以从中学到一个科学的OS源代码的结构，也可以直接以myOS为基础，向里面添加新的功能。\u003c/p\u003e","title":"跟着OSDev学习搭建操作系统（三）"},{"content":"上回我们讲（翻译）了一些预备知识，并准备好了交叉编译器和其他工具链，现在开始动手写代码吧！我们的“空内核”需要三个源文件：\nboot.S，作为内核的入口，用于初始化运行环境 kernel.c，作为内核的主函数 linker.ld，作为链接上面两个文件的脚本 Booting the Operating System 当你编译好一个内核后，它是存储于磁盘上的，然而一个没有内核的机器该如何把磁盘上的内核读进内存，从而去运行里面的指令呢？这就要用到一个内核之外的东西，叫做bootloader。原文提到了GNU有一个叫做GRUB的现成工具可以直接用。\n那么为什么推荐用GRUB，而不是自己写一个bootloader呢？说好的“自己动手”呢？这就要提到GNU的操作系统多重引导规范（Multiboot Specification）。此规范针对的是这样一个问题——操作系统有茫茫多，平台架构也有不少，而bootloader是同时取决于这两者的，因为它既要初始化平台环境，又要装载内核。假如大家随意发挥，那么当我在一台PC上运行其他内核时就有可能出现冲突，因为其他内核的bootloader不一定支持该PC。因此，Multiboot Specification同时对bootloader和内核作了约束，保证符合规范的bootloader能装载任何符合规范的内核。GRUB就是这样的bootloader，而且它还具备其他特性，例如可配置等。\n可能有人会问：bootloader又是怎么被读进内存的呢？实际上bootloader并不需要其他程序去读取，因为它存在于一个特殊的固件里，叫做BIOS (Basic Input/Output System)。电脑一通上电，最先做的事情之一就是去执行BIOS里的指令，这是出厂时就预设好的。当然，现在BIOS快被淘汰了，取而代之的是UEFI。\nInstalling GRUB 2 on OS X 首先你需要一个交叉编译器及其目标平台的工具链（这个在前篇就完成了），然后安装objconv。如果后面遇到了关于“aclocal”的错，你需要安装automake。\n下载grub的源码，编译并安装。注意我的下载地址，参考这里。\ngit clone git@github.com:ar-OS/grub.git --depth=1 mkdir build-grub cd build-grub ../grub/configure --disable-werror TARGET_CC=i686-elf-gcc TARGET_OBJCOPY=i686-elf-objcopy TARGET_STRIP=i686-elf-strip TARGET_NM=i686-elf-nm TARGET_RANLIB=i686-elf-ranlib --target=i686-elf make make install Bootstrap Assembly 首先创建一份boot.S代码，下面我把原文里的注释稍微概括一下。\n/* 多重引导规范所要求的一些常量 */ .set ALIGN, 1\u0026lt;\u0026lt;0 /* align loaded modules on page boundaries */ .set MEMINFO, 1\u0026lt;\u0026lt;1 /* provide memory map */ .set FLAGS, ALIGN | MEMINFO /* this is the Multiboot \u0026#39;flag\u0026#39; field */ .set MAGIC, 0x1BADB002 /* \u0026#39;magic number\u0026#39; lets bootloader find the header */ .set CHECKSUM, -(MAGIC + FLAGS) /* checksum of above, to prove we are multiboot */ /* 声明一个符合“多重引导规范”的头部，让bootloader能找到并确认这里是内核的开头 bootloader会在内核的前8KiB里去寻找这些内容 */ .section .multiboot /* 单独起一个程序段，确保它能在内核的最开头出现 */ .align 4 /* 要求32-bit对齐 */ .long MAGIC .long FLAGS .long CHECKSUM /* 内核自己起一个内核栈，并用符号标明栈顶和栈底的位置 x86的栈是从栈顶向栈底生长的 */ .section .bss /* BSS段不占据程序空间，而是在装载时由装载程序分配空间 */ .align 16 /* 一定要对齐，否则会产生未定义行为 */ stack_bottom: .skip 16384 # 16 KiB stack_top: .section .text .global _start /* 链接脚本会将这里指定为程序入口，bootloader装载完内核后会跳转到这里，并且再也不返回 */ .type _start, @function _start: /* bootloader会开启x86的32-bit保护模式，同时关闭中断和分页， 将CPU设置为Multiboot规范所要求的状态。此时没有printf函数， 没有安全级别限制，也没有debug机制，只有内核自己。 此时内核拥有对机器的完全控制权。 */ /* 设置esp寄存器。只能用汇编语言完成， 因为C语言程序的运行必须依赖栈，而此时还没有栈 */ mov $stack_top, %esp /* 在进入内核的更上层之前，我们最好先初始化一些关键的CPU状态。 浮点运算指令、扩展指令集都还没有启用。 应该在这里装载GDT、开启分页。 It\u0026#39;s best to minimize the early environment where crucial features are offline. C++ features such as global constructors and exceptions will require runtime support to work as well. */ /* 进入内核的更上一层。根据System V ABI的要求，执行call指令时， 栈必须是16-byte对齐的，现在我们满足这个要求 */ call kernel_main /* 如果系统已经没事做了，就进入无限循环： 1. 用cli指令禁止中断，我们在bootloader已经做过了。不过 你可能还会在kernel_main里打开中断并返回这里，所以还 是要cli一次。当然，从kernel_main返回这件事本身就挺扯的。 2. 用hlt指令锁住电脑，直到下一次中断来临。 3. 由于我们已经关掉了中断，所以只有不可屏蔽中断和x86的 “系统管理模式”会唤醒电脑。万一这种情况发生了，我们再跳回hlt指令。 */ cli 1:\thlt jmp 1b /* Set the size of the _start symbol to the current location \u0026#39;.\u0026#39; minus its start. This is useful when debugging or when you implement call tracing. */ .size _start, . - _start 附：系统管理模式 - 维基百科\nImplementing the Kernel 独立环境和宿主环境 我们平时在用户空间写C/C++程序时，都处于“宿主环境”下，其重要特征之一就是C标准库的可用性。然而现在我们处于一个“独立环境”，只有一个交叉编译器，因此我们只能在程序里include非常非常基础的头文件，例如编译器自带的\u0026lt;stdbool.h\u0026gt;（定义了布尔类型）、 \u0026lt;stddef.h\u0026gt;（定义了size_t和NULL）、 \u0026lt;stdint.h\u0026gt;（定义了定长的数据类型intx_t和uintx_t ，这对操作系统编程非常重要——万一哪天short的长度就变了呢？）。此外还有 \u0026lt;float.h\u0026gt;、\u0026lt;iso646.h\u0026gt;、\u0026lt;limits.h\u0026gt;、\u0026lt;stdarg.h\u0026gt;等头文件。\n下面的kernel.c非常self-explanatory，我就不多解释了，大意就是。注意缺少C标准库是一件多么麻烦的事情，我们得自己实现strlen函数。还有，很多新机器已经不支持VGA文字模式（以及BIOS）了，而是转向了UEFI，而在后者的情况下你甚至需要自己设计每个字符的像素buffer。\n用C语言写一个内核 #include \u0026lt;stdbool.h\u0026gt; #include \u0026lt;stddef.h\u0026gt; #include \u0026lt;stdint.h\u0026gt; /* Check if the compiler thinks you are targeting the wrong operating system. */ #if defined(__linux__) #error \u0026#34;You are not using a cross-compiler, you will most certainly run into trouble\u0026#34; #endif /* This tutorial will only work for the 32-bit ix86 targets. */ #if !defined(__i386__) #error \u0026#34;This tutorial needs to be compiled with a ix86-elf compiler\u0026#34; #endif /* Hardware text mode color constants. */ enum vga_color { VGA_COLOR_BLACK = 0, VGA_COLOR_BLUE = 1, VGA_COLOR_GREEN = 2, VGA_COLOR_CYAN = 3, VGA_COLOR_RED = 4, VGA_COLOR_MAGENTA = 5, VGA_COLOR_BROWN = 6, VGA_COLOR_LIGHT_GREY = 7, VGA_COLOR_DARK_GREY = 8, VGA_COLOR_LIGHT_BLUE = 9, VGA_COLOR_LIGHT_GREEN = 10, VGA_COLOR_LIGHT_CYAN = 11, VGA_COLOR_LIGHT_RED = 12, VGA_COLOR_LIGHT_MAGENTA = 13, VGA_COLOR_LIGHT_BROWN = 14, VGA_COLOR_WHITE = 15, }; static inline uint8_t vga_entry_color(enum vga_color fg, enum vga_color bg) { return fg | bg \u0026lt;\u0026lt; 4; } static inline uint16_t vga_entry(unsigned char uc, uint8_t color) { return (uint16_t) uc | (uint16_t) color \u0026lt;\u0026lt; 8; } size_t strlen(const char* str) { size_t len = 0; while (str[len]) len++; return len; } static const size_t VGA_WIDTH = 80; static const size_t VGA_HEIGHT = 25; size_t terminal_row; size_t terminal_column; uint8_t terminal_color; uint16_t* terminal_buffer; void terminal_initialize(void) { terminal_row = 0; terminal_column = 0; terminal_color = vga_entry_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK); terminal_buffer = (uint16_t*) 0xB8000; // VGA默认的彩色显示器缓存地址，可以去查查这个数 for (size_t y = 0; y \u0026lt; VGA_HEIGHT; y++) { for (size_t x = 0; x \u0026lt; VGA_WIDTH; x++) { const size_t index = y * VGA_WIDTH + x; terminal_buffer[index] = vga_entry(\u0026#39; \u0026#39;, terminal_color); } } } void terminal_setcolor(uint8_t color) { terminal_color = color; } void terminal_putentryat(char c, uint8_t color, size_t x, size_t y) { const size_t index = y * VGA_WIDTH + x; terminal_buffer[index] = vga_entry(c, color); } void terminal_putchar(char c) { terminal_putentryat(c, terminal_color, terminal_column, terminal_row); if (++terminal_column == VGA_WIDTH) { terminal_column = 0; if (++terminal_row == VGA_HEIGHT) terminal_row = 0; } } void terminal_write(const char* data, size_t size) { for (size_t i = 0; i \u0026lt; size; i++) terminal_putchar(data[i]); } void terminal_writestring(const char* data) { terminal_write(data, strlen(data)); } void kernel_main(void) { /* Initialize terminal interface */ terminal_initialize(); /* Newline support is left as an exercise. */ terminal_writestring(\u0026#34;Hello, kernel World!\\n\u0026#34;); } Linking_the_Kernel 在用户空间下编程时，GCC可以用自带的脚本自动链接多个object文件，但是系统编程时不要这么做。我们要写一个自己的链接脚本linker.ld。你可能需要先了解一下程序链接的知识，例如程序段、链接地址、加载地址的概念，还可以参考ld的文档来学习编写链接脚本：\n/* bootloader会从_start处进入内核 */ ENTRY(_start) /* 指定内核每个程序段的链接地址、加载地址等 */ SECTIONS { /* 从1MiB这个地址开始，因为bootloader一般都是把内核加载到这里的 */ . = 1M; /* 把multiboot段放在最前面 */ .text BLOCK(4K) : ALIGN(4K) { *(.multiboot) *(.text) } /* 只读数据 */ .rodata BLOCK(4K) : ALIGN(4K) { *(.rodata) } /* 已初始化的可读写数据 */ .data BLOCK(4K) : ALIGN(4K) { *(.data) } /* 未初始化的可读写数据（包括栈） */ .bss BLOCK(4K) : ALIGN(4K) { *(COMMON) *(.bss) } /* The compiler may produce other sections, by default it will put them in a segment with the same name. Simply add stuff here as needed. */ } 编译、链接成内核镜像 i686-elf-as boot.s -o boot.o i686-elf-gcc -c kernel.c -o kernel.o -std=gnu99 -ffreestanding -O2 -Wall -Wextra i686-elf-gcc -T linker.ld -o myos.bin -ffreestanding -O2 -nostdlib boot.o kernel.o -lgcc 你应该可以看到上面的程序合成了一个内核镜像myos.bin。\n运行你的内核！ 安装xorriso，安装QEMU\n把下面的代码保存为grub.cfg文件。\nmenuentry \u0026#34;myos\u0026#34; { multiboot /boot/myos.bin } 通过grub-mkrescue把myos.bin打包成GRUB能直接用的CD-ROM镜像文件myos.iso mkdir -p isodir/boot/grub cp myos.bin isodir/boot/myos.bin cp grub.cfg isodir/boot/grub/grub.cfg grub-mkrescue -o myos.iso isodir 虚拟环境启动：运行qemu-system-i386 -cdrom myos.iso或者qemu-system-i386 -kernel myos.bin，都能启动内核，但可以看出前者调出了GRUB的图形界面，而后者没有。 真实环境启动：把myos.iso烧录至你的U盘、光盘或磁盘，然后插在电脑上，选择用外部存储介质启动！（我没敢试，哈哈） （全文完）\n","permalink":"https://daichao1997.github.io/posts/tech/2020-05-05-osdev-barebone-2/","summary":"\u003cp\u003e上回我们讲（翻译）了一些预备知识，并准备好了交叉编译器和其他工具链，现在开始动手写代码吧！我们的“空内核”需要三个源文件：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eboot.S，作为内核的入口，用于初始化运行环境\u003c/li\u003e\n\u003cli\u003ekernel.c，作为内核的主函数\u003c/li\u003e\n\u003cli\u003elinker.ld，作为链接上面两个文件的脚本\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"booting-the-operating-system\"\u003e\u003ca href=\"https://wiki.osdev.org/Bare_Bones#Booting_the_Operating_System\"\u003eBooting the Operating System\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e当你编译好一个内核后，它是存储于磁盘上的，然而一个没有内核的机器该如何把磁盘上的内核读进内存，从而去运行里面的指令呢？这就要用到一个内核之外的东西，叫做\u003cstrong\u003ebootloader\u003c/strong\u003e。原文提到了GNU有一个叫做\u003ca href=\"https://wiki.osdev.org/GRUB\"\u003eGRUB\u003c/a\u003e的现成工具可以直接用。\u003c/p\u003e","title":"跟着OSDev网站搭建个人操作系统（二）"},{"content":"发现一个宝藏网站：osdev.org（我没火星吧），这是我见过的内容最丰富最全面的操作系统开发者社区，里面的wiki不仅教你从零开始搭建内核与操作系统，还传授了一些人生经验，给刚准备起航的newbie指路（或是劝退），可以说是OS新手开发者的福地了。我今天也没干什么，就按照它给的教程一步步走，在QEMU上跑通了一个空空如也的内核，然后准备把大致的步骤记录在这里。当然，我现在是个纯newbie，最多只能做个概括+翻译而已。如果你有兴趣，可以直接去读原文，我会把每篇文章的链接附上。\nIntroduction 欢迎加入操作系统的开发中来。这里是编程之巅，但你并非孤身一人。我们建立OSDev网站的目的不仅是为了磨练编程技术，更是为了创建社区和结交朋友。\n（介绍了操作系统、内核、shell、GUI的概念，略）\n为什么要开发一个操作系统？主要有四个原因：\n从裸机上建立自己的操作系统，能让你拥有对机器的100%的控制权，这是一件很有成就感的事情 科学研究 取代现有的操作系统 好玩。底层编程很困难，但也十分有趣，因为它能让你对程序执行的每一个细节都了如指掌 Required Knowledge 这里列举了开发OS所需的各种技能，并强调说千万别以为自己随随便便就能完成这项任务。（当然，我自己就不具备里面的很多技能）\nBeginner Mistakes 这篇文章大部分内容都在劝退，估计是作者见得多了，觉得与其给新手们毫无保留的鼓励，不如先泼盆冷水。作者有几段话很有意思，我给大家翻译翻译：\nA Hard Truth No one who isn\u0026rsquo;t already a seasoned developer with years of experience in several languages and environments should even be considering OS Dev yet. A decade of programming, including a few years of low-level coding in assembly language and/or a systems language such as C, is pretty much the minimum necessary to even understand the topic well enough to work in it.\n一个不太好接受的事实 如果你不是掌握多种语言、熟悉多种环境和拥有多年开发经验的老鸟，那你根本不用想着去开发一个操作系统。光是要理解这个话题并上手，你就得先修炼十年，而且还得是底层汇编与系统语言（例如C语言）兼修。\nOh, and for the record, Linus Torvalds wasn\u0026rsquo;t quite one of them\u0026ndash;he was a graduate student when he wrote the Linux kernel and had been coding in C for years. While he was well short of that ten year mark, as a grad student who had turned his hobby into his master\u0026rsquo;s thesis, he had more time on his hands to work on the project than most people would. In any case, the \u0026lsquo;Linux 0.0.1\u0026rsquo; release he famously posted to USENET in 1991 was little more than a round-robin scheduler, nowhere close to a full system. Getting to that point took him a year. Get the picture?\n哦还有，据记载，Linus Torvalds（Linux之父）并不是其中之一（指前文凭一己之力开发了整个操作系统的人物）。他写Linux内核的时候已经是研究生了，写了好几年的C语言。虽然不到十年，但是他把这个业余爱好写成了毕业论文，因此他有比绝大部分人都充足的时间花在这上面。无论如何，他1991年发布在USENET上的著名的Linux 0.0.1不过是在round-robin调度器的基础上稍微加了点东西而已，根本不能算一个完整的系统，而即使是这样也花了他整整一年的时间。你懂我意思吧？\n当然，来都来了，我不可能因为这个就被劝退，所以继续吧。\nGetting Started 这里给了一些比较general的建议，包括制定开发计划、准备开发环境、用GitHub托管代码等等，但对我来说没什么用，毕竟我只是图一乐，只想学点东西而已。\nGCC Cross-Compiler 本文教你安装GCC交叉编译器。所谓交叉编译器，就是让你在A平台(host)上编译，B平台(target)上运行。交叉编译器是OS开发必不可少的工具，毕竟你写的OS必须脱离host平台运行，而直接用host平台的原生编译器会让你的程序里掺杂host平台的库和头文件，以及其他host平台专用的东西。所以，我们可以选用i686-elf-gcc，它编译出的i686-elf程序是符合System V ABI标准的，它可以让你更方便地上手，因为该标准下还有很多配套的工具链可以使用（例如Binutils、GCC）。\n接下来我们会用到很多工具，其中一些需要我们自己从源码编译和安装。下载源码时，建议先从清华大学开源软件镜像站的GNU FTP镜像开始找，然后是GitHub。GNU FTP真的不行，我下载时只有5KB/s的速度。下面是编译GCC所需的环境或依赖包：\n类Unix环境 内存和磁盘空间（看情况，256 MB也许不太够） GCC \u0026amp; G++，或其他系统自带的编译器 Make, Bison, Flex, GMP, MPFR, MPC, Texinfo ISL (optional), CLooG (optional) 我下载了gcc-9.3.1 \u0026amp; binutils-2.34 \u0026amp; libiconv-1.16的源码，其他则通过Homebrew安装。顺便一提，我使用的是MacBook Pro 2017，操作系统是macOS Mojave 10.14.6，x86_64平台。之所以要下载libiconv，是因为原文提到macOS系统自带的libiconv严重过时，需要自行更新，更新方法是将其源码拷贝至GCC的根目录下，命名为libiconv，之后编译GCC时会自动找到并使用它。\n现在设置一些环境变量：\nexport PREFIX=\u0026#34;$HOME/opt/cross\u0026#34; export TARGET=i686-elf export PATH=\u0026#34;$PREFIX/bin:$PATH\u0026#34; # 可以写入~/.bashrc以便之后使用 然后新建一个文件夹，存放以上这些代码：\nmkdir ~/src mv ~/Downloads/gcc-9 ~/src mv ~/Downloads/binutils-2.34 ~/src 编译并安装Binutils（一些选项的含义见原文，下同）：\nmkdir build-binutils cd build-binutils ../binutils-2.34/configure --target=$TARGET --prefix=\u0026#34;$PREFIX\u0026#34; --with-sysroot --disable-nls --disable-werror make make install 编译并安装GCC：\n# The $PREFIX/bin dir must be in the PATH. We did that above. which -- $TARGET-as || echo $TARGET-as is not in the PATH cd ~/src mkdir build-gcc cd build-gcc ../gcc-9/configure --target=$TARGET --prefix=\u0026#34;$PREFIX\u0026#34; --disable-nls --enable-languages=c,c++ --without-headers make all-gcc make all-target-libgcc make install-gcc make install-target-libgcc 很简单对吧？如果没问题，你现在已经可以执行i686-elf-gcc --version并得到正确输出了。下一篇我们将介绍如何通过复制粘贴编写一个可在虚拟环境下运行的内核。\n","permalink":"https://daichao1997.github.io/posts/tech/2020-05-05-osdev-barebone/","summary":"\u003cp\u003e发现一个宝藏网站：osdev.org（我没火星吧），这是我见过的内容最丰富最全面的操作系统开发者社区，里面的wiki不仅教你从零开始搭建内核与操作系统，还传授了一些人生经验，给刚准备起航的newbie指路（或是劝退），可以说是OS新手开发者的福地了。我今天也没干什么，就按照它给的教程一步步走，在QEMU上跑通了一个空空如也的内核，然后准备把大致的步骤记录在这里。当然，我现在是个纯newbie，最多只能做个概括+翻译而已。如果你有兴趣，可以直接去读原文，我会把每篇文章的链接附上。\u003c/p\u003e","title":"跟着OSDev网站搭建个人操作系统（一）"},{"content":"The End Ashim Global Premier VIP Fan Club II 这里是阿西木全球粉丝后援团高端vip二群\n最开始，我们的称号是磁福の圆桌骑士\n但后来我跟你们征战四方\n跟你们吃过西门烤翅、平娃三宝、北华涮肉、老丁西门、重庆火锅，还有神秘消失的川菜馆\n跟你们去过唱吧麦颂、东方斯卡拉\n跟你们玩过星杯，三国杀，狼人杀，剧本杀\n听你们讲过“故事”，也跟着七手八脚地出谋划策\n其实我跟你们一起混的时间并不多，基本上都是你们有活动，我凑个热闹就来了。之前你们去云南，我缺钱没去；后来你们去青海，我又突然有事没去，想想也是遗憾。你们的日常应该是联机打游戏，可惜我没这个习惯，只是有一两次跟你们联机打帝国时代，那是真的欢乐。还有一次跟你们吃完夜宵后，去网吧通宵打WOW3欢乐自定义地图，那是真的欢乐。我觉得我以后再也不会有类似的欢乐了，这四年/七年有你们，我感到无比幸运。\n三年前我本可以搬到31楼，和信科同行住在一起。现在看来……搬过去简直要亏爆。\nMWD的歌单 MWD的歌单是我四年死宅生活的重要组成部分。大一主要是五月天和信，后来加入了林俊杰、Fall Out Boy等欧美摇滚，还有日本热血动漫的配乐，再后来是中国有嘻哈的一些好曲子、华晨宇等等。在MWD的影响下，我也开始听其中一些，并且喜欢上了很多佳作。现在听到火烧的寂寞和海口妹，还会立刻想起大一上的生活，甚至能闻到学五CBD飘来的香味。\n但是最好玩的还是MWD不拘一格的听歌范围——这也许是我俩最大的共同点——包括但不限于摩的大飚客、海口妹、考个锤子试、闹啥子嘛闹、丫蛋、小沈阳。这些搞笑的歌在寝室里放，换别人可能觉得你很土，或者很吵，但我们就觉得很有趣，每次听到都要模仿几句，笑得不能自已。\n我和MWD其实是截然不同的人。如果我俩在路上一起走，我会不知道该说些什么。尽管如此，我和MWD还是一起报名过两次唱歌比赛，一次唱的是自由飞翔，另一次是闹啥子嘛闹。从此我再也没见过愿意在正式比赛上唱凤凰传奇的人了。\n今年七月份的某天下午，MWD突然开始单曲循环California Dreaming。我不知道为什么，也许是他要学着唱，但那种年代感一下子让我怅然若失。歌声在狭小的空间里不断回响，夏日阳光被遮挡在窗帘之外，房间里一片幽暗的黄色。这本来是我最熟悉的日常，但它马上就不是了。\nXFB的故事 【隐藏内容需喝酒后显示】\nFun Facts 我从来没有和室友一起去食堂吃过饭，或者一起上过课。也许有过，但应该不超过三次。简要分析了一下原因：第一，我们点外卖；第二，我们翘课；第三，要是真的出去吃，也是拉帮结伙的一群人。 316没有吵过架。也许有过一两次矛盾，但转眼就忘了，所以我现在也记不起来。 316是文明卫生寝室。 合照（少了几个人） ","permalink":"https://daichao1997.github.io/posts/life/2019.07/","summary":"\u003ch1 id=\"the-end\"\u003eThe End\u003c/h1\u003e\n\u003ch3 id=\"ashim-global-premier-vip-fan-club-ii\"\u003eAshim Global Premier VIP Fan Club II\u003c/h3\u003e\n\u003cp\u003e这里是阿西木全球粉丝后援团高端vip二群\u003c/p\u003e\n\u003cp\u003e最开始，我们的称号是\u003cem\u003e磁福の圆桌骑士\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e但后来我跟你们征战四方\u003c/p\u003e\n\u003cp\u003e跟你们吃过西门烤翅、平娃三宝、北华涮肉、老丁西门、重庆火锅，还有神秘消失的川菜馆\u003c/p\u003e","title":"2019.07"},{"content":"人类——食物采集者 现代人对历史的了解比古代人还多。\n从类人猿到人类 从最宏观的视角来看，地球发展进程有两大转折点。第一，生命的诞生，生物改变基因以适应环境。第二，人类的诞生，人类改变环境以适应基因。（作者的畅想）第三，基因技术，人类同时能够改变基因和环境。\n社会的进步往往跟不上科技的进步，因为前者要求人类进行自我评估和调整，执行难度大；而后者能显著提高生产率和生活水平，很受欢迎。\n从这里开始，我们讨论的人类在基因上就已经和现代人相差无几了。\n食物采集者的生活 食物采集者聚集成20-50人的自治团体。\n社会组织：没有权力中心，只有自然产生的首领。最会打猎的，领导其他人打猎；最懂宗教仪式的，被推举为司仪。男女地位平等，男人狩猎、保卫部落，女人采集、生育后代。所有人共同为生存而进行艰苦斗争。\n饮食结构：食物种类繁多、供应充足。亢人能用近500种不同类型的动植物作食物、药品、化妆品。其中单是昆虫，就有甲虫幼虫、毛虫、蜜蜂蛹、白蚁、蚂蚁和蝉。今天的我们会认为其中大部分都不能吃，但实际上它们都富含营养。相比于只种植几种作物的农民，食物采集者更能应对自然灾害带来的饥荒。这些健康的纯天然食品，搭配上充满运动的生活，使得亢人很少患上高血压、肥胖症等工业社会常见病。但由于缺医少药，很多亢人都死于意外伤害。\n信仰：原始人相信世界永远一成不变。原始人对自然界有大量的第一手知识，但不知其所以然，于是在遇到生存危机时，只能祈求超自然的存在物，例如各种图腾、符号、神。部落中产生了巫医和巫师这样的角色，但他们并没有脱离日常生产生活，也没有形成统治集团，而只是部落较为特殊的成员罢了。\n成就：构成了人类遗产中的一个重要的、决定性的成分。刀具、斧头、锤子等基本工具就是在这个时代诞生的，此外还有很多你以为原始人不懂的东西——南美的印第安人懂得从树薯粉中分离出氢氰酸用作毒药，你敢信？史前时期的波利尼西亚人能在夏威夷和塔西提之间作2350英里的定期航行，你敢信？\n负面评价：个人完全俯首听命于部落，不遵守部落传统者会被杀死，食物短缺和长期迁徙时，婴儿和弱者也会被杀死。生产率低，无法积累生产资料，发展停滞。\n种族的出现 食物采集者的人口缓慢增长，渐渐扩散到全球各地，适应不同的环境，从而形成种族，但他们的基因和现代人几乎没有差别。\n人类——食物生产者 农业的起源 ","permalink":"https://daichao1997.github.io/posts/life/%E5%85%A8%E7%90%83%E9%80%9A%E5%8F%B2%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/","summary":"\u003ch2 id=\"人类食物采集者\"\u003e人类——食物采集者\u003c/h2\u003e\n\u003cp\u003e现代人对历史的了解比古代人还多。\u003c/p\u003e\n\u003ch3 id=\"从类人猿到人类\"\u003e从类人猿到人类\u003c/h3\u003e\n\u003cp\u003e从最宏观的视角来看，地球发展进程有两大转折点。第一，生命的诞生，生物改变基因以适应环境。第二，人类的诞生，人类改变环境以适应基因。（作者的畅想）第三，基因技术，人类同时能够改变基因和环境。\u003c/p\u003e","title":"《全球通史》读书笔记"},{"content":"外来务工人员的最后一天寒假 1 我叫代超，今年21岁，来自武汉，是一名外来务工人员。\n我在中关村的一家小公司上班，住集体宿舍，准备攒够了钱去租房。由于宿舍离公司近，我每天九点才起床。草草洗漱后，坐三站公交就能来到上班的地方。我的同事都是和我一样的技术人员，工作时只关心项目，因此不需要额外疏通关系。在那里埋头工作八九个小时之后，我便下班回家。\n下班后的时间并不足以用来肆意挥霍。我是个贪睡又贪玩的人，回来后先睡一会儿，再打打游戏，刷刷手机，一天就走到了尽头。这样的生活节奏，让我不需要花费时间去思考人生，只需要把发条上好，跟着节奏摆动就可以了。\n我每个月会跟父母通一两次电话，问候一下他们。\n我叫菜月昴，今年19岁，来自日本，是一名外来务工人员。\n我在露格尼卡的一名大臣的宅邸里做仆人，住在宅子里，暂时没有离开的打算。我每天八点起床，擦擦地板、剪剪盆栽、缝缝衣服。我的同事是一对可爱的双胞胎姐妹，要做饭的时候我就去给她们打打下手。虽然身份卑微，但我一直在推动历史的进程，有时候甚至要以死亡为代价（也就是没有代价）。\n下班后，我喜欢多管闲事，例如拯救热心村民、插手国家大选、讨伐魔族巨兽、清理邪教组织，等等。宅邸里还住着一位美丽善良的总统候选人，当初就是她介绍我来这里的。我跟她很熟，经常找她聊天。\n我不需要担心父母，因为作者没有告诉我他们是谁。\n我叫菜月昴，我因为勇斗恶犬而受了重伤，现在刚从床上醒来。\n我竭力睁开眼睛，看到了蕾姆焦急的眼神。她正坐在床边握着我的手，看着我昏迷不醒的样子，内心满是对愧疚和自责。我坐起身来，用作者赐予的撩妹光环进行了一番开导，洗涤了蕾姆的心灵。看着蕾姆纯净无瑕的笑靥，我感到自己的佣人生活充满了意义。\n我叫代超，我因为睡得太多，现在刚从床上醒来。\n我竭力睁开眼睛，看到了孙🐶刚发的微博，然后瑟瑟发抖地穿上衣服，刷牙洗脸，去蔡林记吃热干面。回到家中，我紧闭房门，打开Arcaea随便打了几首歌——天呐，是“Pure Memory”！我立即截图分享到社交网站，然后焦急地等待着回复。看着网友陆续发来的“tql”“bsr”“awsl”，我感到自己的寒假生活充满了意义。\n我叫菜月昴，我因为死得太多，准备找个地方了此余生。因为喜欢蕾姆，所以想到了和她一起私奔。\n蕾姆畅想未来的可爱模样深深触动了我，我的心灵被救赎了——从此以后，我下定决心，要甩掉过去的自己，那个傲慢、冲动、自负、弱小的自己。\n我叫代超，我因为无聊，准备找一部番剧看看。因为喜欢蕾姆，所以刷起了《Re: 从零开始的异世界生活》。\n蕾姆畅想未来的可爱模样深深触动了我，我的心灵被救赎了——从此以后，我下定决心，要成为蕾姆党。\n2 其实，我也不知道生活为什么会突然变成这个样子。\n其实，我也不知道生活为什么会突然变成这个样子。\n去年，我明明还是一个普普通通的小镇青年，现在却来到了异世界，进行着梦幻般的冒险。\n去年，我明明还是一个时刻准备着冒险的北大学生，现在却来到了中关村，混进了熙熙攘攘的人群。\n不，我不觉得这是我的问题。我明明在努力。我一直在努力。从我识字的那一天开始，我就开始在努力了。\nRick and Morty最有名的一集（S03E07）讲述了一个充满Rick和Morty的世界，名叫Citadel。按理来说，世界上只要有一个Rick就会被搅得鸡犬不宁，什么蟑螂怪啊粘液怪啊超级病毒啊满天乱飞。但Citadel\n","permalink":"https://daichao1997.github.io/posts/life/%E6%9C%80%E5%90%8E%E7%9A%84%E5%AF%92%E5%81%87/","summary":"\u003ch1 id=\"外来务工人员的最后一天寒假\"\u003e外来务工人员的最后一天寒假\u003c/h1\u003e\n\u003ch2 id=\"1\"\u003e1\u003c/h2\u003e\n\u003cp\u003e我叫代超，今年21岁，来自武汉，是一名外来务工人员。\u003c/p\u003e\n\u003cp\u003e我在中关村的一家小公司上班，住集体宿舍，准备攒够了钱去租房。由于宿舍离公司近，我每天九点才起床。草草洗漱后，坐三站公交就能来到上班的地方。我的同事都是和我一样的技术人员，工作时只关心项目，因此不需要额外疏通关系。在那里埋头工作八九个小时之后，我便下班回家。\u003c/p\u003e","title":"最后的寒假"},{"content":"2019 生活篇 一月：啥时候放假啊 1月2日上午10点，二教窗外敲响了集图考试结束的铃声。我心不在焉地拎起书包，回到了45甲316，一言不发地躺在了床上。\n我没有像其他同学那样收拾行李准备去火车站。我连火车票都没有买好。实际上，我并不知道自己什么时候可以回家。越过荆棘密布的期末季，迎接我的不是无忧无虑的假期，而是**“后期末时代的三座大山”**——操统大作业、操统实习大作业、网络大作业。（福利：当代话剧研究之操作系统，别忘了给操操投币🧐）\n但是这些作业……我一个字都不想写！\n我要睡很久很久的觉！我要打很多很多的游戏！我要浪费自己的人生！\n我就这样躺着，想着，慢慢进入了幻想乡。忽然，我的耳边传来一句低吟：“只要胆子大，天天都放假。”\n听罢此言，我两眼一睁，两腿一蹬，两手一撑，大喊一声：“作业太坑！不干了，哼！”\n于是，两位16级学弟帮我完成了操统大作业，GitHub帮我完成了操统实习大作业。至于剩下的网络大作业，我要么在实验室扮演无法胜任工作的痴呆儿童，要么在寝室做一只把头埋进被子的鸵鸟，幻想着奇迹能在DDL前一刻出现。\n终于，1月27日上午10点，我坐上G487，结束了这个从八月份就开始了的漫长学期。\n二月：啥时候开学啊 每次放假都要给自己找件事情做，例如看一本书。这次，我打算看那本大一就买了但从没看过的《西方哲学简史》。\n其实看什么书并不重要，重要的是不让自己空虚。\n我又想了想，还有什么一直想学却没有学的技能，于是我给自己报了一个声乐培训班。\n其实报什么班也不重要，重要的是不让自己后悔。\n然后是吃年夜饭、给奶奶上香。一来二去，寒假就这么结束了。西方哲学忘得差不多了，唱歌也没有变好听。\n三月 生日 你一定知道3月3日是我的生日（因为你已经读到这句话了）。去年满20岁，我点了淘汰郎小火锅（广告费五毛），跟BG\u0026amp;MWD在寝室吃火锅嚯啤酒。今年心情尤其好，就去了西门烤翅，边嚯啤酒边讲故事。\n电子游戏通论 3月11日，《中华人民共和国宪法修正案》通过。在这庄严的时代背景下，我在31楼的自习室里想着《电子游戏通论》的课程论文该怎么写。这门网红课不仅让我上了新闻（的一张配图的一个像素），还让我受到了记者采访（然后没有然后）。最重要的是，我作为一个自幼饱受电子游戏“毒害”的无业青年，第一次看到一大群“幼稚”的游戏迷聚在一起正儿八经地讨论各种与游戏有关的社会议题，这一幕使我倍感欣慰。\n数论小测 3月22日，我经历了第一场数论小测，但可能是最后一次燃烧自己的心算能力了。这场考试除了计算密度高，别的都很简单。虽然我忘带计算器，但还是提前交卷并拿到了94分。于是我大概明白为什么自己的数学水平到高中和大学就越来越不行了——对于复杂理论和抽象逻辑，我还是不那么敏锐，至少不是超级敏锐。无论如何，心算也是我为数不多的特长了，拿出来吹一吹，不怕被笑话！\n新垣结衣 月末突然想看《逃避虽可耻但有用》，但我看得太快，三天就刷完了，反而没仔细体会。不过我作为（准）程序员，要代入内向务实+纯真善良+清心寡欲+按部就班的津崎，实在太容易了。对比高中时沉迷的爱情公寓，这部剧洗去了虚幻的童话气息，又保留了爱情里最纯粹的美好。在偶像文化泛滥的今天，我强烈推荐情窦初开的少男少女们看看这部剧，既不会击碎对美好爱情的憧憬，又能勇敢面对幻想和现实的差距。\n什么？我已经不是“情窦初开的少男少女”了？我已经是大叔了？\n……\n切腹自尽.jpg\n四月 信科十佳 4月1日，又报名了信科十佳，又有动力练歌了。那半个月里，五四地下的停车场不再是死一般的寂静，而是充满着一位热血青年对世界的呐喊和诉说（以鬼哭狼嚎的形式）。路过车辆的喇叭声，是对我不懈精神的鼓励；路人看猴子的目光，是对我惊人才华的肯定。无论如何，我还是和其他10人一起混进了信科十佳的复赛。\n看，大佬！ 4月19日，ACM-ICPC世界总决赛在邱德拔举行。这是我第一次亲临世界级赛事的现场，但其实除了上下浮动的分数板有点刺激以外，比赛全程毫无观赏性。你最多能发现大佬写代码也是和你一样坐在椅子上，用手指击打键盘，并没有长出四只手或者两颗脑袋什么的。\n数论小测II 4月26日，下午四点，第二场数论小测，我依然提前半小时交卷……但实际上我并没有写完…………不然我赶不上去上海的火车了！\n五月： 40+小时不睡觉 5月12日，SBN、LYQ、JXT三位大佬带我参加Hackathon PKU。比赛于当晚21:00开始，来自中国各大高校的40支队伍展开了激烈角逐——还不到一个小时，咖啡厅里的零食便被一扫而空。偌大的双创中心传来阵阵键盘声，还有我怎么也连不上Wi-Fi的抱怨声。具体比赛过程请看这里。\n数论小测III 是的，每月连载，提醒着自己数学水平如何每况愈下。\n六月 保研夏令营 本来想好好说这段故事的，但越回忆越觉得自己好蠢。总而言之，就是我不打算留学，但是感觉直接工作不太好（也不知道为什么），就想找个“好地方”保研。问题是哪里才是“好地方”？我根本不知道。我没有主动调研过，也不喜欢找老师套瓷，说一些“我对您的研究很感兴趣”的屁话。最后我颜面尽失地丢掉了所有的保研机会。PS：GPA = 3.47。PPS：现在我供职于Face++。\n数论小测IV完结篇之数论期末考试 是的，还是数论。\n七月 The End 刚目送最后一位好友收拾行李离开，我在百度的实习便开始了。此时的45甲如同《百年孤独》结尾描写的残垣断壁，充满了末日的气息。第二天，我便把床铺搬运到45甲的对面，准备在45乙和17级学弟们度过最后一年。仅用了48个小时，平稳前进了四年的生活便戛然而止，变成了完全不同的样子。\n去上海 小插曲\n世界杯 我不怎么和家人说话，也没什么可说的，但是像这样一声不吭坐在客厅看世界杯，还是有一种在学校体会不到的安宁的感觉。这一届我最喜欢的两个球队是俄罗斯和克罗地亚，前者硬扛掉了西班牙，后者更是硬扛硬扛再硬扛杀入决赛。我对这种弱队逆袭情有独钟，上届哥斯达黎加的表现我也很喜欢。本来我是荷兰的伪球迷，但今年……唉。\n八月 上班下班 上班下班，上班下班，上班下班。这可能是我头一次过这样高度重复的生活。即使是高中冲刺化学竞赛的时候，也会有不一样的卷子、不一样的知识点，努努力总能看到分数上的进步。更重要的是，身边的同学关系都很亲密，每天都能过得其乐融融，心里也一直有个盼头。但像现在这样，每天对着同一个不甚了解的project写啊写、调啊调，每天在固定的时间和几个关系生疏的同事吃饭，没有共同话题可聊，根本不了解团队的核心工作，扮演可有可无的边缘角色，看不到成果，看不到进步，工资也低得离谱。我知道自己现在还能从中学到东西，但下个月呢？下下个月呢？恐怕只会沦为我最厌恶的重复劳动吧。\n九月 上班下班 上班下班，上班下班，上班下班。这可能是我第二次过这样高度重复的生活。即使是高中冲刺化学竞赛的时候，也会有不一样的卷子、不一样的知识点，努努力总能看到分数上的进步。更重要的是，身边的同学关系都很亲密，每天都能过得其乐融融，心里也一直有个盼头。但像现在这样，每天对着同一个不甚了解的project写啊写、调啊调，每天在固定的时间和几个关系生疏的同事吃饭，没有共同话题可聊，根本不了解团队的核心工作，扮演可有可无的边缘角色，看不到成果，看不到进步，工资也低得离谱。我知道自己现在还能从中学到东西，但下个月呢？下下个月呢？恐怕只会沦为我最厌恶的重复劳动吧。（是的，这段是我复制粘贴的。）\n十月 国庆节回家 经历了两个月的社畜生活后，我越发珍惜起我们家小区的养老氛围（寒假回家的时候只觉得脏乱差）。用AirPlay把寒假没看完的《地球脉动2》投到电视上，往沙发上一躺，时间仿佛静止了一样……\n离职百度 溜了溜了。我也不想说太多不好的话，毕竟这里是我职业生涯第一站路。最后归还办公用品的时候，突然找不到转接头了，直接赔了340，刚好是两天的工资（前几天发现就在书包里）。这段实习的主要收获，除了工程能力，就是明白了Macbook有多好用，而渣渣Windows有多难受，从此转为Windows黑。\n游手好闲 我是一个惰性非常大的人，没有什么自驱力。大学四年里，我几乎没有额外用过功（除了赶DDL的那几天），而在百度的三个月正好锻炼了我的“耐操”能力。离职后的我一下子有了很多可以自由支配的时间，反而觉得空虚了起来。我开始仿照上班时的作息，把上班改成去理教自习，突然觉得自己四年来要是能像这样努力，是不是就……算了吧，我高中和初中也是这么想的。\n十一月 继续实习 原本打算来年春招再找工作的我，突然接到同学的邀请，内推我去Face++实习。虽然我对算法一窍不通，但还是厚着脸皮投了（毕竟生活费告急）。然后我就来了。\n上班下班 上班下班，上班下班，上班下班。这可能是我第三次过这样高度重复的生活。\n十二月 上班下班 上班下班，上班下班，上班下班。这可能是我第四次过这样高度重复的生活。\n思考篇（🐦） 我大概已经知道接下来的路该怎么走了。我不再那么期待生命中的变数，而只是希望自己能默默无闻地活着，然后就这样走下去。现在我和同学聚完了餐，也已经和我挂念的人通了电话。我们就这样，各自过自己的人生吧！祝各位新年快乐。\n你好，2019。\n","permalink":"https://daichao1997.github.io/posts/life/2019/","summary":"\u003ch1 id=\"2019\"\u003e2019\u003c/h1\u003e\n\u003ch2 id=\"生活篇\"\u003e生活篇\u003c/h2\u003e\n\u003ch3 id=\"一月啥时候放假啊\"\u003e一月：啥时候放假啊\u003c/h3\u003e\n\u003cp\u003e1月2日上午10点，二教窗外敲响了集图考试结束的铃声。我心不在焉地拎起书包，回到了45甲316，一言不发地躺在了床上。\u003c/p\u003e\n\u003cp\u003e我没有像其他同学那样收拾行李准备去火车站。我连火车票都没有买好。实际上，我并不知道自己什么时候可以回家。越过荆棘密布的期末季，迎接我的不是无忧无虑的假期，而是**“后期末时代的三座大山”**——操统大作业、操统实习大作业、网络大作业。（福利：\u003ca href=\"http://www.bilibili.com/video/av17794295\"\u003e当代话剧研究之操作系统\u003c/a\u003e，别忘了给操操投币🧐）\u003c/p\u003e","title":"2019"},{"content":"小作文 为期半年的排练已经告一段落了，在这半年里，有欢笑，有泪水，更有无数珍贵的回忆。请以\u0026quot;我与汉密尔顿\u0026quot;为题，写一篇不少于800字的文章，文体不限（诗歌、戏剧除外）。\n我与汉密尔顿 从大一到大四，我一直没有想过加入什么社团。我就喜欢在寝室宅着，和熟悉的化院老铁打各种各样的游戏，从狼人到三国杀到明星大侦探，半夜出去吃夜宵侃大山喝啤酒讲故事。我没有什么成就，但也没什么烦恼。既然每天宅在寝室就能收获一堆乐趣，又何必去想那该死的未来呢？\n只是一转眼，你们就已经毕业九个月了。\n九个月……九个月！\n你知道这九个月我怎么过的吗？\n我每天都躲在B209排汉密尔顿，你知道有多好玩吗？\n嘿，真难相信，我居然能和\u0026quot;音乐剧\u0026quot;这三个字扯上关系。我一度认为化院老铁们走后，洒脱欢快的日子就过去了，接下来的生活会被无趣的事物填满。但与此同时，我的内心却时刻有一种强烈的感觉，提醒自己还有很多有趣的事情没做。这不，眼看本科就要毕业，就要拿到死宅学位证书的时候，我居然获得了人生中的第一次演出经历，认识了几十个热爱音乐剧的同学（要知道我读了四年大学也没认识多少人，更别提女生了）。\n这确实是一次神奇的经历，就好比某天钻进家里的衣柜，结果不小心去纳尼亚拯救了世界。我还记得，去年钻进B209面试的那天，我厚着脸皮说自己没看过Hamilton，却想扮演Lafayette的场景。我还记得，剧组第一次集会，我面对几十个喜爱音乐剧的陌生人时，一句话都不敢说的场景。后来，有人开始用Jefferson称呼我，有人表示很期待我和Ham的对手戏，有人被我笨拙的表演逗笑，有人耐心地教我走出骚气的步伐，我才意识到自己其实是大家的朋友，而不是公司里只谈项目和技术的同事。我已经太久没有接触过性格鲜明的人，于是我开始发现每个人身上的魅力，和他们交谈，哪怕不知道名字，甚至从没有见过他。这大概是因为我产生了对这个集体的信任感吧，只要属于这个集体，就会被自动打上\u0026quot;友好\u0026quot;标记。\n临近演出的高强度排练令人心力交瘁，却让我见识到了更多幕后的东西。这是一个用爱发电的剧组，导演爱，社长爱，主创爱，群演、主演、幕后都爱，只不过每个人爱的程度不同，爱的内容也不同。有人为爱憔悴，有人为爱流泪，有人为爱奋笔疾书，有人为爱争得脸红脖子粗。内部矛盾也好，外部压力也罢，一个集体最伟大的力量和最真实的缺陷，都在这几天展露无遗。\n演出后的聚餐也是令人难忘：你们精神是真的好啊，刚折腾一整天，又是排练又是演出又是收拾东西，从早上十点忙到晚上十一点，你们居然还能兴冲冲地等半小时的士去吃海底捞？一边吃还能一边唱得这么嗨？最后还能去KTV蹦到天亮？我还一直自诩修仙老手，真是惭愧……\n我与汉密尔顿的故事大概就只有这么多了。演出结束之后，我们就没有什么共同的奋斗目标了，但留存下来的革命友谊还会继续发展下去。我们坐在石舫上唱起了歌，在湖心亭打起了狼人杀。至于我自己，也获得了打开其他新世界大门的勇气，不会再像以前那样因为惧怕而一直停留在自己的舒适区了（但是真的很舒适）。\n你看这人生它又长又宽，\n就像这舞台它又大又圆。\n你们，\n来这里，\n排Ham，\n觉得，\nHam很，\n好，\n听，\n我看行。\n你们，\n来这里，\n排Ham，\n就像……\n","permalink":"https://daichao1997.github.io/posts/life/2019%E5%8F%82%E5%8A%A0hamilton%E6%8E%92%E7%BB%83%E5%8F%8A%E6%BC%94%E5%87%BA/","summary":"\u003ch1 id=\"小作文\"\u003e小作文\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"https://daichao1997.github.io/pic/night.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e为期半年的排练已经告一段落了，在这半年里，有欢笑，有泪水，更有无数珍贵的回忆。请以\u0026quot;我与汉密尔顿\u0026quot;为题，写一篇不少于800字的文章，文体不限（诗歌、戏剧除外）。\u003c/strong\u003e\u003c/p\u003e","title":"参加Hamilton排练及演出"},{"content":"近来发生的一些政治敏感事件，激起了全校范围内的讨论。我从小都是那种两耳不闻窗外事的书呆子，辩不明大是大非，因此不想针对事件本身作出评价。我想说的，是我作为一个旁观者，在这十几天里得到的启示。\n邓布利多曾说：“Always use the proper name for things. Fear of a name increases fear the thing itself”。作为一个相信魔法世界存在的书呆子，我决定践行这句话。\n一\n还记得七八月份，朋友圈传闻岳昕失踪，我先是十分惊讶，后来转为对世道不公的愤怒。我愤愤不平地转发了那则传闻，配上一番激昂文字。几分钟后，有人点赞，有人一同声援，安抚着我那颗害怕被孤立的心。但当我渐渐放下心来的时候，那条转发的链接也渐渐打不开了。后来，这则传闻渐渐被吴亦凡与虎扑的“战争”所取代，“岳昕”也渐渐成了敏感词汇。再后来，我也渐渐在颠簸的通勤路上失忆了。\n二\n十一月初，我去学五食堂吃饭，意外地看到一群人聚集在一起围观着什么。接了一份递来的传单后，我才找回了不久前的记忆。原来，这群马克思主义协会的同学还在通过聚众演讲的方式，试图鼓动大家关注岳昕和顾佳悦（另一名据称失踪的同学）的下落。\n打开未名热点，果然看到了同学们的议论。有人说，他们是借寻人来煽动学生闹事；有人说，他们扰乱了食堂的公共秩序；有人说，他们寻人的方式不妥。当然，这其中也不乏支持者。最后的结果，当然是双方各执一词，你一楼我一楼地盖了起来。\n这时，原本力挺岳昕的我却开始慌了。真相到底是什么？为什么有这么多人和我的想法不一样？为什么有人这么不理解马会的同学？难道我真的很幼稚、很容易被煽动？\n想着想着，我往嘴里扒了几口鸡腿饭，本应香甜的味道竟变得沉重起来。说实在的，我根本不想面对这些事情。我不想面对岳昕失踪的事实，也不想面对岳昕所面对的，更不想面对身边每一位拿着手机偷偷发表意见的人。我就是不想面对这个我永远都不知道自己是少数还是多数，是正确还是错误的世界。\n三\n紧接着，11月9日晚十一点，我打开未名热点，发现有帖子称燕南附近有一群人发生了肢体冲突，还有个人被强行带上了一辆黑色SUV。\n四\n再后来，未名BBS就只剩下对马会的无尽声讨了。\n","permalink":"https://daichao1997.github.io/posts/life/%E6%94%BF%E8%A7%81%E4%B8%8D%E5%90%8C/","summary":"\u003cp\u003e近来发生的一些政治敏感事件，激起了全校范围内的讨论。我从小都是那种两耳不闻窗外事的书呆子，辩不明大是大非，因此不想针对事件本身作出评价。我想说的，是我作为一个旁观者，在这十几天里得到的启示。\u003c/p\u003e","title":"政见不同"},{"content":"Color Hue: pure color, presented by a circle Saturation: purity of color, how far it is from pure white, which is every color combined Brightness: subjective perception, how far it is from grey Luminance: objective, cd/m2 Contrast: Sharpness: Noise Image noise is random variation of brightness or color information in images, and is usually an aspect of electronic noise.\nGaussian noise Salt-and-pepper noise Shot noise Quantization noise Film grain Anisotropic noise Periodic noise Photography Backlight The process of illuminating the subject from the back. The lighting instrument and the viewer face each other, with the subject in between, creating a glowing effect on the edges of subject, which helps to seperate the subject and the background. In photography, a back light (often the sun) that is about sixteen times more intense than the key light produces a silhouette.\nDynamic range Limits of luminance range that a given digital camera can capture, aka reflectance range.\nHigh-dynamic-range imaging A HDR technique to reproduce greater dynamic range of luminosity than is possible with standard digital imaging or photographic techniques. The human eye adjusts constantly to adapt to a broad range of luminance, but most cameras do not. Two primary types of HDR images: computer renderings and images resulting from merging multiple LDR photographs.\nTone mapping A technique to map one set of colors to another to approximate the appearance of HDR images in a medium that has a more limited dynamic range. Global or local operators have been developed.\nIn some cases, tone mapped images are produced from a single exposure which is then manipulated with conventional processing tools to produce the inputs to the HDR image generation process. This avoids the artifacts that can appear when different exposures are combined, due to moving objects in the scene or camera shake. However, when tone mapping is applied to a single exposure in this way, the intermediate image has only normal dynamic range, and the amount of shadow or highlight detail that can be rendered is only that which was captured in the original exposure.\nEdge-preserving smoothing filter An image processing technique that smooths away noise or textures while retaining sharp edges. Examples are the median, bilateral, guided, and anisotropic diffusion filters.\nExposure fusion A technique for blending multiple exposures of the same scene into a single image. As in high dynamic range imaging (HDRI or just HDR), the goal is to capture a scene with a higher dynamic range than the camera is capable of capturing with a single exposure.\nHowever, because no HDR image is ever created during exposure fusion, it cannot be considered an HDR technique\nImage histogram An image histogram is a type of histogram that acts as a graphical representation of the tonal distribution in a digital image.[1] It plots the number of pixels for each tonal value. By looking at the histogram for a specific image a viewer will be able to judge the entire tonal distribution at a glance.\nHistogram equalization This method usually increases the global contrast of many images, especially when the usable data of the image is represented by close contrast values. Through this adjustment, the intensities can be better distributed on the histogram. This allows for areas of lower local contrast to gain a higher contrast. Histogram equalization accomplishes this by effectively spreading out the most frequent intensity values.\nImage enhancement Process the image (e.g. contrast improvement, image sharpening\u0026hellip;) so that it is better suited for further processing or analysis.\nImage enhancement belongs to image preprocessing methods, and are based on subjective image quality criteria. No objective mathematical criteria are used for optimizing processing results. Performed by reversing the process that blurred the image.\nImage restoration The operation of taking a corrupt/noisy image and estimating the clean, original image. The objective of image restoration techniques is to reduce noise and recover resolution loss.\nImage restoration is different from image enhancement in that the latter is designed to emphasize features of the image that make the image more pleasing to the observer, but not necessarily to produce realistic data from a scientific point of view. With image enhancement noise can effectively be removed by sacrificing some resolution, but this is not acceptable in many applications.\nmulti-exposure The superimposition of two or more exposures to create a single image.\nExposure is the amount of light per unit area (the image plane illuminance times the exposure time) reaching a photographic film or electronic image sensor, as determined by shutter speed, lens aperture and scene luminance.\nFlash/non-flash image pairs illumination component\nimage component\nimage degradation\nreflectance component\nRetinex filtering\nsingle image HDR\ntime-division/space-division acquisition\nvirtual multi-exposure illumination\n","permalink":"https://daichao1997.github.io/posts/tech/image_basics/","summary":"\u003ch2 id=\"color\"\u003eColor\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eHue: pure color, presented by a circle\u003c/li\u003e\n\u003cli\u003eSaturation: purity of color, how far it is from pure white, which is every color combined\u003c/li\u003e\n\u003cli\u003eBrightness: subjective perception, how far it is from grey\u003c/li\u003e\n\u003cli\u003eLuminance: objective, cd/m2\u003c/li\u003e\n\u003cli\u003eContrast:\u003c/li\u003e\n\u003cli\u003eSharpness:\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"noise\"\u003eNoise\u003c/h2\u003e\n\u003cp\u003eImage noise is random variation of brightness or color information in images, and is usually an aspect of electronic noise.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eGaussian noise\u003c/li\u003e\n\u003cli\u003eSalt-and-pepper noise\u003c/li\u003e\n\u003cli\u003eShot noise\u003c/li\u003e\n\u003cli\u003eQuantization noise\u003c/li\u003e\n\u003cli\u003eFilm grain\u003c/li\u003e\n\u003cli\u003eAnisotropic noise\u003c/li\u003e\n\u003cli\u003ePeriodic noise\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"photography\"\u003ePhotography\u003c/h2\u003e\n\u003ch3 id=\"backlight\"\u003eBacklight\u003c/h3\u003e\n\u003cp\u003eThe process of illuminating the subject from the back. The lighting instrument and the viewer face each other, with the subject in between, creating a \u003cstrong\u003eglowing\u003c/strong\u003e effect on the edges of subject, which helps to seperate the subject and the background. In photography, a back light (often the sun) that is about sixteen times more intense than the \u003cstrong\u003ekey light\u003c/strong\u003e produces a \u003cstrong\u003esilhouette\u003c/strong\u003e.\u003c/p\u003e","title":"Image Basics"},{"content":"初次尝试刷题，当然要刷简单的啦。\nRoot :: Competitive Programming 3: The New Lower Bound of Programming Contests (Steven \u0026amp; Felix Halim) :: String Processing :: String Matching :: Standard\n455 - Periodic Strings **问题描述：**求一个字符串的最小周期，例如\u0026quot;123123123\u0026quot;的最小周期为3。从数据规模来看，穷举法完全可行，但还是要学习一下这位老哥对KMP的巧妙运用：\n设字符串A长度为s，求出整个字符串的最大border长度t。如果s-t | s，那么最小周期为s-t；否则，最小周期为s。\n证明：如果s-t | s，那么A可以分为若干个长度为s-t的部分，叫做A1, A2, \u0026hellip;, An。 最长border作为前缀时为A[1\u0026hellip;n-1]，作为后缀时为A[2\u0026hellip;n]。 因此，A1 = A2 = \u0026hellip; = An，最小周期长度为s-t。\n886 - Named Extension Dialing **问题描述：**给一堆英文名字，都由两个单词组成，每个名字都有唯一的4位编号，例如Tirion Fordring 0303。现在有一个用户拿手机拨号，按照九键输入的方式拨出了若干个号码，问对于每个号码，有哪些名字能与之匹配，输出其编号。匹配的规则是，如果号码与某个编号一致，则成为唯一匹配并忽略其他匹配，否则寻找满足\u0026quot;the first letter of the first name followed by the first letters of the last name\u0026quot;的名字，例如8367对应\u0026quot;T For\u0026quot;，输出0303。\n这本来是一道比较简单的题目。\n初始化8个Trie树，分别对应九键键盘上的2-9，再初始化一个大小为10000的布尔数组number。 每读入一个名字，就把它翻译成2-9的数字 翻译后，若first name的首位为i，就在Trie[i]中插入last name，并在每个结点存储其编号，再令number[编号]=true。 对于每个号码，先查看number[号码]，若为true则直接输出该号码，匹配结束 否则，根据号码的第一位找到Trie树，然后用剩下的位走这个Trie树 如果能走到一个结点，说明可以匹配，该结点存储的所有编号就是匹配结果 构造Trie树的复杂度为last name的长度和，查找匹配的复杂度为号码的长度和。\n然而我TM看错题了！！！\n我把匹配条件看成了\u0026quot;the first letters of the first name followed by the first letters of the last name\u0026quot;。\n于是在我眼里，这道题问的是，已知字符串A,B,P，请问P能否由A和B的各一个前缀组成。\n我甚至把它做了出来，简要描述一下方法：求A与P的最长公共前缀长度lcp，再求B+P的前缀函数pf，从而求出B+P的最长、又不长于P的border（设长度为bd）。若len(P)-bd小于lcp，说明P长为bd的后缀是B的前缀，而剩下的前缀长度小于lcp，因此是A的前缀。\n10298 - Power Strings **问题描述：**和455完全一样，除了输入输出的要求。\n由于UVa不给用户看自己交的代码，因此这道题之前的代码全都丢了。后面的题目解答我会保存下来，留作纪念。\n11362 - Phone List **问题描述：**给一堆字符串，问是否存在某个字符串是另一个的前缀。\n用Trie树依次插入。如果路过叶子结点，或插入最后一个字符时没有新建结点，则答案为是，此后的所有输入可以不做处理。\n11475 - Extend to Palindrome **问题描述：**给一个字符串，往后面加最少的字符，使其成为回文串。\n首先学习一下Manacher算法。该算法输入一个字符串s，输出两个数组d1和d2，d1[i]代表以s[i]为中心的奇数回文子串个数（包含s[i]自己），d2[i]代表以s[i]为中心的偶数回文子串个数。\n算法记录了一个“边界”[l,r]，表示目前找到的最右边的回文子串，也就是让r最大 边界的初始值为[0,-1] 从左到右遍历s。由于只有遍历到边界中心才能算出边界，因此现在的位置i必定在边界中心的右侧 如果i已经超出边界，就采取简单算法：向两边依次扩张、比较，得到该位置的d1/d2值 如果i在边界内，就以边界中心为轴，找到该位置的对称点i2（边界中心的左侧） 由于边界本身是回文串，因此以i2为中心的、不超出边界的最长回文子串，也一定是以i为中心的、不超出边界的最长回文子串（无法保证边界外的对称性） 超出边界的部分，用简单算法求出 现在你可以发现为什么要记录，并且只需要记录最右的回文子串——因为能跳过最多的比较 int d1[n],d2[n]; // 奇数长度，中心唯一 for(int i=0,l=0,r=-1; i\u0026lt;n; i++) { // k表示现在已知有几个以i为中心的回文子串 // 若i\u0026gt;r，不能利用边界内部的信息，因此k=1（中心本身） // 否则，取 对称位置的d1值 与 中心到边界右侧的距离 的较小值 int k=(i\u0026gt;r)?1:min(d1[l+r-i],r-i); // r-i+1? // 简单算法，将k增加为正确的d1值 // 每次都试图找到第k+1个回文子串，因此比较s[i-k]与s[i+k] while(0\u0026lt;=i-k \u0026amp;\u0026amp; i+k\u0026lt;n \u0026amp;\u0026amp; s[i-k]==s[i+k]) k++; // 赋值后，k需要减1，迎合下一步更新边界的计算 d1[i]=k--; // 现在，以i为中心的最长子串为[i-k,j+k] // 判断一下是不是需要更新一下边界 if(i+k\u0026gt;r) {l=i-k;r=i+k;} } // 偶数长度，规定中心处于偏右的位置 for(int i=0,l=0,r=-1; i\u0026lt;n; i++) { // 中心本身还需要和自己左邻居比较，因此k最小为0（一个都不能跳过） // 与对称回文子串的中心，应该是i的对称点的右邻居，因此是d2[l+r-i+1] int k=(i\u0026gt;r)?0:min(d2[l+r-i+1],r-i+1); while(0\u0026lt;=i-k-1 \u0026amp;\u0026amp; i+k\u0026lt;n \u0026amp;\u0026amp; s[i-k-1]==s[i+k]) k++; d2[i]=k--; if(i+k\u0026gt;r) {l=i-k-1;r=i+k;} } 现在来看这道题就非常简单了：在Manacher算法的过程中，每次更新边界时都检查r==s.length()-1。如果是，则可以终止循环，将此数组用到的l记录为x1和x2（有两个数组需要计算，初始值应该都是s.length()-1，代表最多从末尾添加起）。取x1和x2的较小值记为x，然后依次向字符串添加s[x-1]到s[0]的字符。\n11576 - Scrolling Sign **问题描述：**按顺序输入N个长度为len的小字符串，求最短的字符串S，使得这些小字符串都是S的子串，且第一次出现的位置不随着输入顺序递减。输出这个字符串的长度。（例如，ABCD/DEFG/DEFG/GGGG可以合并为ABCDEFGGGG）\n我的方法：开足够大的字符数组，反向输入小字符串，两两之间插入\u0026rsquo;$\u0026rsquo;。从输入第二个字符串起，求当前位置开始、长度为2*len+1的子串，也就是str[i+1]+'$'+str[i]的border。这样就求出了前一个小字符串的后缀最多有多长，可以作为后一个小字符串的前缀。把这些求出的长度从N*len中减去，就得出了结果。\n12467 - Secret Word **问题描述：**给一个字符串S，要求找出最长的子串，使其倒置为S的某个前缀。\n方法：构造字符串S+'$'+S2，其中S2为S的倒置，计算其前缀函数。在计算过程中，从S2起，记录出现的最大函数值maxbd及当前位置i。求出i的对称点mark = len-i-1，那么从该点开始、长度为maxbd的子串，就是要输出的结果。\n11888 - Abnormal 89\u0026rsquo;s **问题描述：**给一个字符串，判断它能不能由两个长度至少为2的回文串组成。如果不能，判断它是不是回文串。\n。。。。。。\n明天就开始上班了，有空补上。\n总而言之，迈出了摆脱算法废柴的第一步，还是挺开心的，希望以后能继续进步。\n","permalink":"https://daichao1997.github.io/posts/tech/2018-11-13-%E7%AE%80%E5%8D%95%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%BB%83%E4%B9%A01/","summary":"\u003cp\u003e初次尝试刷题，当然要刷简单的啦。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://uva.onlinejudge.org/index.php?option=com_onlinejudge\u0026amp;Itemid=8\u0026amp;category=748\"\u003eRoot :: Competitive Programming 3: The New Lower Bound of Programming Contests (Steven \u0026amp; Felix Halim) :: String Processing :: String Matching :: Standard\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"455---periodic-strings\"\u003e455 - Periodic Strings\u003c/h3\u003e\n\u003cp\u003e**问题描述：**求一个字符串的最小周期，例如\u0026quot;123123123\u0026quot;的最小周期为3。从数据规模来看，穷举法完全可行，但还是要学习一下\u003ca href=\"https://github.com/morris821028/UVa/blob/master/volume004/455%20-%20Periodic%20Strings.cpp\"\u003e这位老哥\u003c/a\u003e对KMP的巧妙运用：\u003c/p\u003e","title":"简单的字符串练习1"},{"content":"LCP数组 定义 有了后缀数组，也就有了每个后缀的大小顺序。\n但现在我们有另一个奇怪的需求：\n对于任意两个后缀，求它们的最长公共前缀(Longest Common Prefix, LCP)。\n啊？为什么有这个需求？我也不知道，但大佬说很有用，那就求吧。\n设i \u0026lt; j，记LCP(i,j)为suffix(sa[i])与suffix(sa[j])的LCP长度。\n换句话说，就是把后缀从小到大写出来，第i个和第j个的LCP长度就是LCP(i,j)。\n现在假设这个LCP(i,j)的值为8，内容是\u0026quot;fordring\u0026quot;（不失一般性）。\n也就是说，第i大和第j大的后缀，都以\u0026quot;fordring\u0026quot;开头。\n那岂不是说，夹在它俩之间的后缀，都以\u0026quot;fordring\u0026quot;开头？（毕竟它们是排列好的）\n于是我们得到了一条重要的结论（虽然没有严格证明）：\nLCP(i,j) = min(LCP(k,k+1)), k = i, i+1, ..., j-1\n所谓LCP数组（常记为height），其元素height[k]的值就是LCP(k,k+1)。\n有了LCP数组后，就可以结合上面的结论，用RMQ（留坑）迅速查询到任意两个后缀的LCP长度了。\n思路 先把后缀按照位置顺序写出来，比如assassin, ssassin, sassin, assin, ssin, sin, in, n。\n然后，求一个后缀数组sa的反查数组rank。如果说sa[i]是第i小后缀的位置，那么rank[i]就是位置i后缀的名次。\n也就是说，sa[rank[i]]就是i，rank[0]就是\u0026quot;0\u0026quot;在sa中的下标。\n然后，取assassin，再取刚好比它大的后缀assin，直接比较得到ass，height值为3。\n观察两者下家，可以肯定ssassin一定比ssin小。\n于是，刚好比ssassin大的那个后缀，一定在(ssasin, ssin)的区间内。\n因此，assassin的下家ssassin，它对应的height值至少是3-1=2，那我直接从第3个字符开始比较就稳了！\n换句话说，按位置顺序求height值，每次递减不超过1！\n复杂度 将字符串比作一列高为N的、从下往上搭建的积木，将N个后缀比作N列积木，于是形成了一个N*N格子的房间。\n直接比较字符，意味着顺着积木向上走一步；跳到下一字符串，意味着向右下角走一步（height值递减不超过1）。\n想像一个N*N个格子的房间，你从左下角出发，要到达最右一排。不能穿墙，每次只能往上或者右下走一步。请问到达最右一排时，你要走多少步？\n首先，走到最右时，必定走了N-1个右下步。\n其次，为了不穿底，至少要走N-1个上步；为了不穿顶，至多能走2N-2个上步。\n因此，复杂度为O(N)。\n代码 void height(int *r, int *sa, int n) { int i, j, k; for (i = 1; i \u0026lt;= n; i++) rank[sa[i]] = i; for (i = k = 0; i \u0026lt; n; height[rank[i++]] = k) for (k ? k-- : 0, j = sa[rank[i] - 1]; r[i+k]==r[j+k]; k++); return; } 懒得讲了。\n后缀自动机 Suffix Automaton, SAM.\n这个东西我昨天看了一晚上，确实很强大，但也很抽象。鉴于自己没有理解透彻，就只粘贴一下这篇比较完美的教程（需要知道自动机是什么）。下面说一下文章梗概。\n（试着讲讲）关键概念 最关键、最抽象的概念：equivalence class（等价类）, suffix link（后缀链）。\n据我理解，把字符串str的所有子串的集合称为S，对每个子串定义一个endpos集合，包含了该子串所有出现的结束位置（一个子串可能出现多次）。在S上定义等价关系，两子串等价当且仅当它们的endpos集合相等。于是，S被划分成了若干等价类，每个等价类都对应SAM里的一个状态，同样也对应一个endpos集合。\n例如对于abcbcbc，\u0026ldquo;bc\u0026quot;和\u0026quot;c\u0026quot;的endpos集合都是{2,4,6}，因此属于同一状态，但\u0026quot;bcbc\u0026quot;则是{4,6}，诸如此类。\n接下来，文章论证了每个状态所对应的子串集合，都是形如[i,j],[i+1,j],...,[i+k,j]这样的，例如{\u0026ldquo;abcc\u0026rdquo;,\u0026ldquo;bcc\u0026rdquo;,\u0026ldquo;cc\u0026rdquo;}。\n接下来，文章论证了SAM的状态可以通过单个字符来转换，其意义是：假如状态A通过字符c转移到状态B，那么A的最长子串加上c，恰好就是B的最短子串。\n后缀链，很抽象却很神奇。文章论证了每个状态的endpos集合，要么交集为空，要么呈真包含关系，而这恰好满足树的性质。顺着这种包含关系，就能形成一棵以起始状态为根的树，且SAM里的每个状态都包含于其中。后缀链就是这棵树的有向边。\n构造算法的思路 接下来是粘贴过来的构造算法，但我理解不深，无法解释整个算法为什么正确，只好做一些脚注便于大家理解。\nInitially the automaton consists of a single state t0 with len = 0, link = −1 len就是每个状态的子串集合中，最长子串的长度；link是该状态的后缀链所指向的状态（编号）。起始状态啥也没有，所以这样赋值，而-1代表树根。\nNow the whole task boils down to implementing the process of adding one character c to the end of the current string. Let us describe this process:\nLet last be the state corresponding to the entire string before adding c. (Initially we set last = 0, and we will change last in the last step of the algorithm accordingly.)\nCreate a new state cur, and assign it with len(cur) = len(last) + 1. The value link(cur) is unknown at this time.\n旧状态机包含了所有旧字符串的子串，所以新状态cur就要（试图）包含多出来的子串，也就是所有以c结尾的子串，其len值理所当然等于新字符串的长度。\nStart from state last, and follow the suffix link. While there are no transitions through c, add one to cur. If we reach the fictitious state −1, assign link(cur) = 0 and leave. If at some point there is a transition through c, stop and denote this state with p. 顺着后缀链，如果没有状态接收c，就说明旧串不含c**（需证明），所以所有新子串的endpos都只有一个元素——新串末尾的位置。这种情况下，所有路过的状态都应该接受c到达cur（需证明）**，新子串都属于cur，且cur应该指向树根，插入结束。\nSuppose now that we have found a state p, from which there exists a transition through c. Denote the state to which the transition leads with q.\nIf len(p) + 1 = len(q), assign link(cur) = q and leave.\n如果旧串含c，那就麻烦了，因为新子串可能与旧子串重复，无法立即推断endpos了。\n我不懂该怎么解释这种情况下算法的行为，但我猜关键在于后缀链的某些特性。\nOtherwise we clone q: create a new state clone, copy all the data from q (suffix link and transition) except len. Assign len(clone) = len(p) + 1. Direct suffix links from cur and q to clone. Walk through suffix links from p. While there is a transition through c to q, redirect it to clone.\nUpdate last with cur.\nTo figure out terminal states, start from last and follow suffix links. All visited states are terminal.\n构造算法的实现 struct state { int len, link; map\u0026lt;char, int\u0026gt; next; } st[MAXLEN * 2]; const int MAXLEN = 100000; int sz, last; void sa_init() { st[0].len = 0; st[0].link = -1; sz = 1; last = 0; } void sa_extend(char c) { int cur = sz++; st[cur].len = st[last].len + 1; int p = last; while (p != -1 \u0026amp;\u0026amp; !st[p].next.count(c)) { st[p].next[c] = cur; p = st[p].link; } if (p == -1) { st[cur].link = 0; } else { int q = st[p].next[c]; if (st[p].len + 1 == st[q].len) { st[cur].link = q; } else { int clone = sz++; st[clone].len = st[p].len + 1; st[clone].next = st[q].next; st[clone].link = st[q].link; while (p != -1 \u0026amp;\u0026amp; st[p].next[c] == q) { st[p].next[c] = clone; p = st[p].link; } st[q].link = st[cur].link = clone; } } last = cur; } 用途 实在太广了。如你所见，SAM以O(N)的方式涵盖了所有子串的信息，因此可以回答几乎所有关于子串的问题（用解决图论问题的方式）。\n嗯……我只是知道可以回答，不知道怎么回答，还需要刷点题才行。听说后缀数组在实战中更好用，某些极端情况下才需要祭出SAM（我肯定是碰不到了）。\n后缀树 就是把后缀做成Trie树。\n可以直接从字符串构造，Ukkonen的O(n)神仙算法我懒得看了\n可以先求后缀数组/后缀自动机，然后间接构造\n具体用途还不清楚，以后有经验了再更新吧。\n后记 字符串问题真尼玛好玩！\n","permalink":"https://daichao1997.github.io/posts/tech/2018-11-02-lcp%E6%95%B0%E7%BB%84%E5%90%8E%E7%BC%80%E8%87%AA%E5%8A%A8%E6%9C%BA/","summary":"\u003ch2 id=\"lcp数组\"\u003eLCP数组\u003c/h2\u003e\n\u003ch3 id=\"定义\"\u003e定义\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://daichao1997.github.io/%E5%90%8E%E7%BC%80%E6%95%B0%E7%BB%84.html\"\u003e有了后缀数组\u003c/a\u003e，也就有了每个后缀的大小顺序。\u003c/p\u003e\n\u003cp\u003e但现在我们有另一个奇怪的需求：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e对于任意两个后缀，求它们的最长公共前缀(Longest Common Prefix, LCP)。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e啊？为什么有这个需求？我也不知道，但大佬说很有用，那就求吧。\u003c/p\u003e","title":"LCP数组、后缀自动机、后缀树（？）"},{"content":"Kickstart Round F 2018第一名Benq的参赛代码，光开头几行就把我吓得魂飞魄散。\n#pragma GCC optimize (\u0026#34;O3\u0026#34;) #pragma GCC target (\u0026#34;sse4\u0026#34;) #include \u0026lt;bits/stdc++.h\u0026gt; #include \u0026lt;ext/pb_ds/tree_policy.hpp\u0026gt; #include \u0026lt;ext/pb_ds/assoc_container.hpp\u0026gt; #include \u0026lt;ext/rope\u0026gt; using namespace std; using namespace __gnu_pbds; using namespace __gnu_cxx; #define sz(x) (int)(x).size() template \u0026lt;class T\u0026gt; using Tree = tree\u0026lt;T, null_type, less\u0026lt;T\u0026gt;, rb_tree_tag,tree_order_statistics_node_update\u0026gt;; void setIn(string s) { freopen(s.c_str(),\u0026#34;r\u0026#34;,stdin); } void setOut(string s) { freopen(s.c_str(),\u0026#34;w\u0026#34;,stdout); } void io(string s = \u0026#34;\u0026#34;) { ios_base::sync_with_stdio(0); cin.tie(0); if (sz(s)) { setIn(s+\u0026#34;.in\u0026#34;); setOut(s+\u0026#34;.out\u0026#34;); } } int main() { io(\u0026#34;A\u0026#34;); // ... } GCC Function Attributes #pragma GCC optimize (\u0026#34;O3\u0026#34;) #pragma GCC target (\u0026#34;sse4\u0026#34;) Common Function Attributes x86 Function Attributes\n指定GCC做O3优化，支持SSE4指令。\nC++中#pragma的用法\nbits/stdc++.h 这是一个“万能”头文件，涵盖了所有STL头文件，刷题够用了。\nPolicy Based Data Structure 属于STL扩展。\n#include \u0026lt;ext/pb_ds/tree_policy.hpp\u0026gt; #include \u0026lt;ext/pb_ds/assoc_container.hpp\u0026gt; using namespace __gnu_pbds; template \u0026lt;class T\u0026gt; using Tree = tree\u0026lt;T, null_type, less\u0026lt;T\u0026gt;, rb_tree_tag, tree_order_statistics_node_update\u0026gt;; 这段代码搞了一个set的扩展，让它支持find_by_order和order_of_key。你也可以修改一下第二个参数\u0026quot;null_type\u0026quot;，让这个模板成为map的扩展，例如\ntemplate \u0026lt;class T1, T2\u0026gt; using Tree = tree\u0026lt;T1, T2, less\u0026lt;T1\u0026gt;, rb_tree_tag, tree_order_statistics_node_update\u0026gt;; 注意，它们不支持多重值。\n博客1 博客2 博客3 GNU官方文档 Rope #include \u0026lt;ext/rope\u0026gt; using namespace __gnu_cxx; 一个STL扩展，支持区间的插入、删除。\n博客1 博客2 博客3 博客4 （过期的）官方文档 I/O {ios_base::sync_with_stdio(0); cin.tie(0);} 取消C++与C的I/O缓冲的同步；取消cin与cout的“绑定”。\nstackoverflow cppreference ","permalink":"https://daichao1997.github.io/posts/tech/2018-11-02-%E5%AD%A6%E4%B9%A0%E4%B8%80%E4%B8%8B%E5%A4%A7%E4%BD%AC%E7%9A%84%E9%AB%98%E7%BA%A7%E4%BC%98%E5%8C%96%E6%8A%80%E5%B7%A7/","summary":"\u003cp\u003eKickstart Round F 2018第一名\u003ca href=\"https://code.google.com/codejam/contest/3314486/scoreboard#vf=1\"\u003eBenq\u003c/a\u003e的参赛代码，光开头几行就把我吓得魂飞魄散。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#pragma GCC optimize (\u0026#34;O3\u0026#34;)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#pragma GCC target (\u0026#34;sse4\u0026#34;)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#include\u003c/span\u003e \u003cspan class=\"cpf\"\u003e\u0026lt;bits/stdc++.h\u0026gt;\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#include\u003c/span\u003e \u003cspan class=\"cpf\"\u003e\u0026lt;ext/pb_ds/tree_policy.hpp\u0026gt;\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#include\u003c/span\u003e \u003cspan class=\"cpf\"\u003e\u0026lt;ext/pb_ds/assoc_container.hpp\u0026gt;\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#include\u003c/span\u003e \u003cspan class=\"cpf\"\u003e\u0026lt;ext/rope\u0026gt;\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"k\"\u003enamespace\u003c/span\u003e \u003cspan class=\"n\"\u003estd\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"k\"\u003enamespace\u003c/span\u003e \u003cspan class=\"n\"\u003e__gnu_pbds\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"k\"\u003enamespace\u003c/span\u003e \u003cspan class=\"n\"\u003e__gnu_cxx\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#define sz(x) (int)(x).size()\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003etemplate\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eT\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"n\"\u003eTree\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etree\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003enull_type\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eless\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erb_tree_tag\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"n\"\u003etree_order_statistics_node_update\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e \u003cspan class=\"nf\"\u003esetIn\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003efreopen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ec_str\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;r\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"n\"\u003estdin\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e \u003cspan class=\"nf\"\u003esetOut\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e \u003cspan class=\"n\"\u003efreopen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ec_str\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;w\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"n\"\u003estdout\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e \u003cspan class=\"nf\"\u003eio\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eios_base\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003esync_with_stdio\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"n\"\u003ecin\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etie\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003esz\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esetIn\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;.in\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003esetOut\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;.out\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"nf\"\u003emain\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003eio\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;A\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"c1\"\u003e// ...\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"gcc-function-attributes\"\u003eGCC Function Attributes\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#pragma GCC optimize (\u0026#34;O3\u0026#34;)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#pragma GCC target (\u0026#34;sse4\u0026#34;)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ca href=\"https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html\"\u003eCommon Function Attributes\u003c/a\u003e\n\u003ca href=\"https://gcc.gnu.org/onlinedocs/gcc/x86-Function-Attributes.html\"\u003ex86 Function Attributes\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e指定GCC做O3优化，支持\u003ca href=\"https://en.wikipedia.org/wiki/SSE4\"\u003eSSE4\u003c/a\u003e指令。\u003c/p\u003e","title":"神奇的优化方式"},{"content":"Not difficult to understand. I learned it from Coursera.\nPrefix Function A \u0026ldquo;Border\u0026rdquo; of a string, is a substring that both acts as a prefix and a suffix of that string The \u0026ldquo;Prefix Function\u0026rdquo; of a string records the length of the longest border of every prefix of that string Symbol s is a string s[:i] is a prefix of s made up by s[0], s[1], ..., s[i-1] L[i] is the length of the longest border of s[:i] In other words, L is the \u0026ldquo;Prefix Function\u0026rdquo; of s Conclusion L[i+1] \u0026lt;= L[i]+1, and the equality holds if and only if s[L] = s[i] The longest border of the longest border of a string, is the second longest border of that string. if some border of s[:i] has a length of len and s[len] = s[i], then there must be a border in s[:i+1] with a length of len+1 Method L[0] = 0 L[i+1] = len + 1, where len is the length of the longest border of s[:i] that satisfies s[len] = s[i] To find len, try every border of s[:i] from the longest to the shortest To find the next longest border, see Conclusion 2 If no border is found, simply compare s[0] with s[i] If s[0] = s[i], then L[i+1] = 1 Else, L[i+1] = 0 Implementation int *pf(string s) { int len=s.size(),b=0; int *p=new int[len]; p[0]=0; for(int i=1;i\u0026lt;len;i++) { while(b\u0026gt;0 \u0026amp;\u0026amp; s[i]!=s[b]) b=p[b-1]; if(s[i]==s[b]) b++; p[i] = b; } return p; } KMP Symbol p is the pattern string, t is the text string L is the \u0026ldquo;Prefix Function\u0026rdquo; of p Conclusion If p matches t partially with a longest common prefix p[:i], then no matches will be found before p is shifted len(p) - L[i] positions rightwards. There are two \u0026ldquo;submatches\u0026rdquo; in this partial match, both of which are the longest border of p[:i] The left one acts as a prefix, while the right one acts as a postfix p is shifted rightwards, so does p[:i] No matches are possible before the left submatch arrives at the previous position of the right submatch The length of this \u0026ldquo;vacuum area\u0026rdquo; is len(p) - L[i] Method Make s = p + '$' + t, where \u0026lsquo;$\u0026rsquo; is absent from both p and t Calculate the \u0026ldquo;Prefix Function\u0026rdquo; of s, marked as L A match is found at position i - 2 * len(p) when L[i] = len(p) Under such circumstances, the longest border of s[:i] happens to be p itself, as p is a prefix of s This border is also a substring of s Thanks to the \u0026lsquo;$\u0026rsquo; sign, we can guarantee that this border never crosses the real \u0026ldquo;border\u0026rdquo; between p and t, so it is also a substring of t Thus the starting position of the match is i - (len(p) - 1) in s, and i - (len(p) - 1) - (len(p) + 1) in t Implementation int main() { string pattern, text, s; int *pf; while(cin \u0026gt;\u0026gt; pattern \u0026gt;\u0026gt; text \u0026amp;\u0026amp; pattern != \u0026#34;\u0026#34;) { if(pattern.size() \u0026gt; text.size()) { cout \u0026lt;\u0026lt; \u0026#34;-1\u0026#34; \u0026lt;\u0026lt; endl; continue; } s = pattern + \u0026#34;$\u0026#34; + text; pf = prefix_func(s); for(int i = pattern.size()+1; i \u0026lt; s.size(); i++) { if(pf[i] == pattern.size()) { cout \u0026lt;\u0026lt; i - 2 * pattern.size() \u0026lt;\u0026lt; \u0026#34; \u0026#34;; } } cout \u0026lt;\u0026lt; endl; delete pf; } return 0; } ","permalink":"https://daichao1997.github.io/posts/tech/2018-10-29-kmp/","summary":"\u003cp\u003eNot difficult to understand. I learned it from \u003ca href=\"https://www.coursera.org/learn/algorithms-on-strings/home/week/3\"\u003eCoursera\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"prefix-function\"\u003ePrefix Function\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eA \u0026ldquo;Border\u0026rdquo; of a string, is a substring that both acts as a \u003cstrong\u003eprefix\u003c/strong\u003e and a \u003cstrong\u003esuffix\u003c/strong\u003e of that string\u003c/li\u003e\n\u003cli\u003eThe \u0026ldquo;Prefix Function\u0026rdquo; of a string records the length of the longest border of every prefix of that string\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"symbol\"\u003eSymbol\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003es\u003c/code\u003e is a string\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003es[:i]\u003c/code\u003e is a prefix of \u003ccode\u003es\u003c/code\u003e made up by \u003ccode\u003es[0], s[1], ..., s[i-1]\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eL[i]\u003c/code\u003e is the length of the longest border of \u003ccode\u003es[:i]\u003c/code\u003e\n\u003cul\u003e\n\u003cli\u003eIn other words, \u003ccode\u003eL\u003c/code\u003e is the \u0026ldquo;Prefix Function\u0026rdquo; of \u003ccode\u003es\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"conclusion\"\u003eConclusion\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003eL[i+1] \u0026lt;= L[i]+1\u003c/code\u003e, and the equality holds if and only if \u003ccode\u003es[L] = s[i]\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eThe longest border of the longest border of a string, is the second longest border of that string.\u003c/li\u003e\n\u003cli\u003eif some border of \u003ccode\u003es[:i]\u003c/code\u003e has a length of \u003ccode\u003elen\u003c/code\u003e and \u003ccode\u003es[len] = s[i]\u003c/code\u003e, then there must be a border in \u003ccode\u003es[:i+1]\u003c/code\u003e with a length of \u003ccode\u003elen+1\u003c/code\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"method\"\u003eMethod\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003eL[0] = 0\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eL[i+1] = len + 1\u003c/code\u003e, where \u003ccode\u003elen\u003c/code\u003e is the length of the longest border of \u003ccode\u003es[:i]\u003c/code\u003e that satisfies \u003ccode\u003es[len] = s[i]\u003c/code\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul\u003e\n\u003cli\u003eTo find \u003ccode\u003elen\u003c/code\u003e, try every border of \u003ccode\u003es[:i]\u003c/code\u003e from the longest to the shortest\u003c/li\u003e\n\u003cli\u003eTo find the next longest border, see \u003cstrong\u003eConclusion 2\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eIf no border is found, simply compare \u003ccode\u003es[0]\u003c/code\u003e with \u003ccode\u003es[i]\u003c/code\u003e\n\u003cul\u003e\n\u003cli\u003eIf \u003ccode\u003es[0] = s[i]\u003c/code\u003e, then \u003ccode\u003eL[i+1] = 1\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eElse, \u003ccode\u003eL[i+1] = 0\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"implementation\"\u003eImplementation\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"nf\"\u003epf\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003elen\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003ewhile\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"o\"\u003e!=\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"o\"\u003e==\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e])\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"kmp\"\u003eKMP\u003c/h2\u003e\n\u003ch3 id=\"symbol-1\"\u003eSymbol\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003ep\u003c/code\u003e is the pattern string, \u003ccode\u003et\u003c/code\u003e is the text string\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eL\u003c/code\u003e is the \u0026ldquo;Prefix Function\u0026rdquo; of \u003ccode\u003ep\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"conclusion-1\"\u003eConclusion\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eIf \u003ccode\u003ep\u003c/code\u003e matches \u003ccode\u003et\u003c/code\u003e partially with a longest common prefix \u003ccode\u003ep[:i]\u003c/code\u003e, then no matches will be found before \u003ccode\u003ep\u003c/code\u003e is shifted \u003ccode\u003elen(p) - L[i]\u003c/code\u003e positions rightwards.\n\u003cul\u003e\n\u003cli\u003eThere are two \u0026ldquo;submatches\u0026rdquo; in this partial match, both of which are the longest border of \u003ccode\u003ep[:i]\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eThe left one acts as a prefix, while the right one acts as a postfix\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ep\u003c/code\u003e is shifted rightwards, so does \u003ccode\u003ep[:i]\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eNo matches are possible before the left submatch arrives at the previous position of the right submatch\u003c/li\u003e\n\u003cli\u003eThe length of this \u0026ldquo;vacuum area\u0026rdquo; is \u003ccode\u003elen(p) - L[i]\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"method-1\"\u003eMethod\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003eMake \u003ccode\u003es = p + '$' + t\u003c/code\u003e, where \u0026lsquo;$\u0026rsquo; is absent from both \u003ccode\u003ep\u003c/code\u003e and \u003ccode\u003et\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eCalculate the \u0026ldquo;Prefix Function\u0026rdquo; of \u003ccode\u003es\u003c/code\u003e, marked as \u003ccode\u003eL\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eA match is found at position \u003ccode\u003ei - 2 * len(p)\u003c/code\u003e when \u003ccode\u003eL[i] = len(p)\u003c/code\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cul\u003e\n\u003cli\u003eUnder such circumstances, the longest border of \u003ccode\u003es[:i]\u003c/code\u003e happens to be \u003ccode\u003ep\u003c/code\u003e itself, as \u003ccode\u003ep\u003c/code\u003e is a prefix of \u003ccode\u003es\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eThis border is also a substring of \u003ccode\u003es\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eThanks to the \u0026lsquo;$\u0026rsquo; sign, we can guarantee that this border never crosses the real \u0026ldquo;border\u0026rdquo; between \u003ccode\u003ep\u003c/code\u003e and \u003ccode\u003et\u003c/code\u003e, so it is also a substring of \u003ccode\u003et\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eThus the starting position of the match is \u003ccode\u003ei - (len(p) - 1)\u003c/code\u003e in \u003ccode\u003es\u003c/code\u003e, and \u003ccode\u003ei - (len(p) - 1) - (len(p) + 1)\u003c/code\u003e in \u003ccode\u003et\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"implementation-1\"\u003eImplementation\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"nf\"\u003emain\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003epattern\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003etext\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003epf\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ewhile\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ecin\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003epattern\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003etext\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003epattern\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epattern\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003etext\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"n\"\u003ecout\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;-1\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003eendl\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"k\"\u003econtinue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epattern\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;$\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003etext\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"n\"\u003epf\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eprefix_func\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003epattern\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003es\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epf\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003epattern\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\t\u003cspan class=\"n\"\u003ecout\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003epattern\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esize\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34; \u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"n\"\u003ecout\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003eendl\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003edelete\u003c/span\u003e \u003cspan class=\"n\"\u003epf\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"The KMP Algorithm"},{"content":"朕听说，后缀数组只需寥寥二十行代码即可求出？\n是，陛下。\n快，快呈上来！\n是，陛下。\nint wa[MAXN], wb[MAXN], wv[MAXN], ws[MAXN]; inline bool cmp(int *r, int a, int b, int len) { return r[a]==r[b] \u0026amp;\u0026amp; r[a+len]==r[b+len]; } void SA(char *r, int *sa, int n, int m) { int i, j, p, *x = wa, *y = wb, *t;\tfor (i = 0; i \u0026lt; m; i++) ws[i] = 0; for (i = 0; i \u0026lt; n; i++) ws[x[i] = r[i]]++; for (i = 1; i \u0026lt; m; i++) ws[i] += ws[i - 1]; for (i = n - 1; i \u0026gt;= 0; i--) sa[--ws[x[i]]] = i; for (j = p = 1; p \u0026lt; n; j \u0026lt;\u0026lt;= 1, m = p) { for (p = 0, i = n - j; i \u0026lt; n; i++) y[p++] = i; for (i = 0; i \u0026lt; n; i++) if (sa[i] \u0026gt;= j) y[p++] = sa[i] - j; for (i = 0; i \u0026lt; m; i++) ws[i] = 0; for (i = 0; i \u0026lt; n; i++) ws[wv[i] = x[y[i]]]++; for (i = 1; i \u0026lt; m; i++) ws[i] += ws[i - 1]; for (i = n - 1; i \u0026gt;= 0; i--) sa[--ws[wv[i]]] = y[i]; for (t=x,x=y,y=t, x[sa[0]]=0, p=i=1; i\u0026lt;n; i++) x[sa[i]] = cmp(y,sa[i-1],sa[i],j) ? p-1 : p++; } } 陛下……陛下！快请御医！\n前言 鉴于我也是第一次接触后缀数组，理解程度还很低，再加上类似的博客早已烂大街，因此不求读者能看懂或学到什么东西了。你还可以向我留言提出指导意见，我会非常欢迎。\n什么是后缀数组 今天，我们来谈谈这份“后缀数组”的模板。\n这份模板，是用来求“后缀数组”的。\n后缀很简单，数组也很简单，但是“后缀数组”就需要解释一番了。\n一个长为N的字符串，有N个后缀，每个后缀都从不同的位置开始。\n我们用suffix(i)表示从第i个字符开始的后缀（从0计）。例如，\u0026ldquo;abcdefg\u0026quot;的suffix(2)就是\u0026quot;cdefg\u0026rdquo;。\n也就是说，只要确定了字符串，我们就可以用[0, N-1]的数字表示任意一个后缀。\n然后，这些后缀作为字符串，是可以按照字典序来排列的。\n把suffix(0)...suffix(N-1)从小到大排列后，会被打乱为suffix(sa[0])...suffix(sa[i])。\n这里的sa是0到N-1的一个排列，同时也可以视为数组。\n它就是后缀数组(suffix array)，sa[i]就是第i小的后缀在原字符串的位置（从0计），而suffix(sa[i])就是第i小的后缀。\n那么，我们为什么要求这个数组呢？\n不知道，听大佬们说很有用，所以先求出来再说吧。\n暴力求法 假如逐一比较两个后缀的字符，就需要比较O(N)次，才能知道两个后缀的相对大小。（因为它们的长度都在1到N之间）\n假如用插入排序，上面的比较就需要进行O(N^2)次。\n乘起来就是O(N^3)，不能忍。\n文明求法 目前已经有了好几种O(N)的算法，确实厉害，但我懒得看。\n倍增法 前面那段外星代码，使用的就是倍增法。\n我先定义一个概念，叫做k-后缀。它也是字符串的后缀，但它长度至多为k——把长于k的部分截断即可。\n比如，\u0026ldquo;abcdefg\u0026quot;的4-后缀分别是\u0026quot;abcd\u0026rdquo;, \u0026ldquo;bcde\u0026rdquo;, \u0026ldquo;cdef\u0026rdquo;, \u0026ldquo;defg\u0026rdquo;, \u0026ldquo;efg\u0026rdquo;, \u0026ldquo;fg\u0026quot;和\u0026quot;g\u0026rdquo;。\n为了方便表示，我就借用之前的符号，让suffix(i,k)表示从第i个字符开始的k-后缀吧。\n相应地，sa(k)就叫做k-后缀数组，suffix(sa(k)[i],k)就是第i小的k-后缀。\n至于倍增法，就是先求sa(1)，再求sa(2)、sa(4)，直到k-后缀成为完整的后缀，sa也就出来了。\n求sa(1) sa(1)，1-后缀数组，sa(1)[i]就是第i小的1-后缀的位置。\n第i小的1-后缀……那不就是第i小的字符么？听上去很简单的样子。\n也就是说，我只要知道每个字符是第几小就好了。下面的代码完成了这件事情。\nfor (i = 0; i \u0026lt; m; i++) ws[i] = 0; for (i = 0; i \u0026lt; n; i++) ws[x[i] = r[i]]++; for (i = 1; i \u0026lt; m; i++) ws[i] += ws[i - 1]; for (i = n - 1; i \u0026gt;= 0; i--) sa[--ws[x[i]]] = i; 变量介绍：\ni：打酱油的递增变量 m：字符串最多有m种字符 n：字符串长度 r：字符串 sa：1-后缀数组 ws：统计每种字符出现了几次 x：此处等同于r for (i = 0; i \u0026lt; m; i++) ws[i] = 0; 清零ws数组。\nfor (i = 0; i \u0026lt; n; i++) ws[x[i] = r[i]]++; 执行x[i] = r[i]，把r的值逐渐复制到x 上述表达式的值就是r[i]，即位置i的字符的值 ws[x[i] = r[i]]++：值为r[i]的字符又多了一个 之后，ws就存储了r（或者x）中每种字符的个数。\nfor (i = 1; i \u0026lt; m; i++) ws[i] += ws[i - 1]; 这句完成后，新的ws[i]成为了原来的ws[0]+ws[1]+...+ws[i]，意为x中有多少字符的值不大于i，而不再是等于i。\nfor (i = n - 1; i \u0026gt;= 0; i--) sa[--ws[x[i]]] = i; // 倒序遍历 结合1-后缀数组的定义，这句话的意思是，位置i的字符，是第--ws[x[i]]小的。\nx[i]是位置i的字符，ws[x[i]]就是不大于该字符的字符个数 所以(ws[k-1], ws[k]]是值为k的字符的排名区间 如果有并列，则希望位置小的字符更小（这样就实现了稳定排序），因此进行倒序遍历，让大的位置先坐进sa，这样它在sa的位置也更大 既然坐进了sa数组，就该从ws[x[i]]中去掉自己，这样下一个值同为x[i]的并列者就会坐到自己的左边（小1的位置） 排名从0开始，如果不大于自己的有k个，那自己就应该是第k-1名，所以是--X而不是X-- 短小精悍👍\n从sa(k)到sa(2k) 知道了sa(k)，怎么求sa(2k)呢？\nsa(k)告诉了我们k-后缀的排列顺序，而2k-后缀相当于两个相邻的k-后缀：suffix(i,2k) = suffix(i,k) + suffix(i+k,k)。\n我们把2k-后缀suffix(i,2k)的前半部分称作第一关键字，后半部分称作第二关键字。\n因此，对于两个2k后缀，只需先比较第一关键字，再比较第二关键字。\n这样的比较并不难，难的是我们需要对第一关键字做基数排序，这样才能保证每次倍增都是O(N)的复杂度。\n我们知道，刚才对单个字符做基数排序是非常简单的。基数排序需要一个数组，记录每种待比较元素的个数。待比较元素有多少种，这个数组的尺寸就要有多大。由于字符的大小可以直接用值区分，因此可以直接把字符值作为数组下标。但是多字符组成的k-后缀，是不能直接作为数组下标的。\n虽然STL map支持该功能，但其插入复杂度为O(logN)，插入N次则为O(NlogN)……你懂我1⃣️4⃣️8⃣️❓\n实际上，k-后缀只有n个，因此最多只有n种。有没有一种办法，能够把每个k-后缀用[0,n)的自然数标记呢？\n答案是肯定的，这个“标记数组”就是x。\n求sa(1)时，x就是字符串本身，x[i]标记的就是r[i]的值。但之后，x[i]将成为小于n的自然数。suffix(i,k)就是第x[i]小的k-后缀。\n有了x数组，我们对k-后缀进行基数排序，就非常轻松了。\n了解了x数组的意义后，我们就正式进入这层可怕的循环吧。\n构造y数组 变量介绍：\nj：相当于我说的\u0026quot;k\u0026quot; p：用于顺序填充y sa：已有的k-后缀数组 y：按第二关键字排序的2k-后缀的位置（相当于sa(2k)的半成品吧） for (p = 0, i = n - j; i \u0026lt; n; i++) y[p++] = i; 把[n-j, n)依次填入y数组。这些位置的2k-后缀是没有第二关键字的，因此直接排在前面，并且位置从小到大（确保稳定性）。\nfor (i = 0; i \u0026lt; n; i++) if (sa[i] \u0026gt;= j) y[p++] = sa[i] - j; 这句话想把剩下的2k-后缀，按照第二关键字的大小顺序，依次填入y数组。\n遍历sa让我们从小到大地访问了k-后缀的位置，即sa[i]。\n把这些k-后缀当作第二关键字，将其位置减去k，就是对应2k-后缀的位置，即sa[i] - j。\n按此顺序填入2k-后缀的位置，就能将2k-后缀按第二关键字排序。\n升级sa数组 变量介绍：\nm：不同k-后缀的个数 sa：即将升级为2k-后缀数组的k-后缀数组 ws：计数用 wv：wv[i]相当于x[y[i]] x：k-后缀的相对名次 y：按第二关键字排序的2k-后缀的位置（相当于sa(2k)的半成品吧） for (i = 0; i \u0026lt; m; i++) ws[i] = 0; for (i = 0; i \u0026lt; n; i++) ws[wv[i] = x[y[i]]]++; for (i = 1; i \u0026lt; m; i++) ws[i] += ws[i - 1]; for (i = n - 1; i \u0026gt;= 0; i--) sa[--ws[wv[i]]] = y[i]; 这段代码其实与前面求sa(1)是同一个样子，但是第二句话和之前不太一样：遍历名次数组x的顺序不是从0到n-1，而是从y[0]到y[n-1]。但由于y数组本身就是0到n-1的另一个排列，因此ws的计算结果依然正确。同时，又令wv[i]成为x[y[i]]，便于后面使用。这一句话相当于做了两句话的事情，非常巧妙。\n第四句话也不太一样：把sa[--ws[x[i]]] = i;换成了sa[--ws[x[y[i]]]] = y[i];。\n这一换，是基数排序的精髓。\n如果不换，那么得到的还是稳定排序后的sa(k)，如同前面计算sa(1)那样。\n但换了之后，k-后缀（2k-后缀的第一关键字）依然是递增的。这是因为，y[i]处的k-后缀越大，wv[i]就越大。由于ws数组递增，ws[wv[i]]也越大，因此该k-后缀的位置在sa的更后面。\n当y[i]处的k-后缀相同时，又会如何呢？\n可以肯定，它们对应的wv[i]值也相同。\n由于ws[wv[i]]会随赋值而递减，因此在这些相同的k-后缀中，先赋值者在sa的下标更大。\n由于i递减，因此第二关键字更大的2k-后缀，是先赋值者。\n结合上面两句话，对于第一关键字相同的2k-后缀，其第二关键字在sa也是递增的。\n于是，sa数组就成为了真正的2k-后缀数组。\n服了👍\n更新x数组 for (t=x,x=y,y=t, x[sa[0]]=0, p=i=1; i\u0026lt;n; i++) x[sa[i]] = cmp(y,sa[i-1],sa[i],j) ? p-1 : p++; 交换x和y。x即将成为2k-后缀的名次，而y则记录着k-后缀的名次。 x[sa[0]]=0意味着最小的2k-后缀的名次为0，是最小的名次。 从1遍历到n-1，计算cmp(y,sa[i-1],sa[i],j)，也就是比较第i小的2k-后缀，和它前面那个第i-1小的2k-后缀。比较的结果有两种：大于或等于。有了y数组，我们不用逐字符比较。先比较第一关键字，再比较第二关键字即可。 若两者相等，则cmp返回true，把现有名次p-1赋给x[sa[i]]；否则返回false，意味着需要一个更大的名次，于是把p赋给x[sa[i]]，再自增。 循环的终止 一次循环结束后，sa数组从sa(k)升级为sa(2k)，x数组也随之更新。\n此外，代表k值的变量j也要加倍，代表不同2k-后缀个数的m，则成为上一轮通过循环递增算出来的p。\n但是循环的终止条件不是k达到n，而是p达到n！\n当p达到n时，说明已经有了n个不同的k-后缀，并且都在sa里排好了序。即使k再增大，它们的顺序也会保持不变。因此p达到n时，循环即可终止。\n服了👍\n后记 之前写代码，命名长得一比，换行频繁，美其名曰“整齐易读”，视压缩代码为毒瘤。\n今日见了传说中的后缀数组，才知代码的压缩也是需要水平的。这么多细致的计算，全部约束于几行紧密而规整的for循环中。\n下篇应该是LCP数组和后缀树的构造。\n","permalink":"https://daichao1997.github.io/posts/tech/2018-10-29-%E5%90%8E%E7%BC%80%E6%95%B0%E7%BB%84/","summary":"\u003cp\u003e朕听说，后缀数组只需寥寥二十行代码即可求出？\u003c/p\u003e\n\u003cp\u003e是，陛下。\u003c/p\u003e\n\u003cp\u003e快，快呈上来！\u003c/p\u003e\n\u003cp\u003e是，陛下。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ewa\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eMAXN\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e \u003cspan class=\"n\"\u003ewb\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eMAXN\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e \u003cspan class=\"n\"\u003ewv\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eMAXN\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e \u003cspan class=\"n\"\u003ews\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eMAXN\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003einline\u003c/span\u003e \u003cspan class=\"kt\"\u003ebool\u003c/span\u003e \u003cspan class=\"nf\"\u003ecmp\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003er\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003er\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"o\"\u003e==\u003c/span\u003e\u003cspan class=\"n\"\u003er\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003er\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"n\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\u003cspan class=\"o\"\u003e==\u003c/span\u003e\u003cspan class=\"n\"\u003er\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"n\"\u003elen\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e \u003cspan class=\"nf\"\u003eSA\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003er\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003esa\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003en\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ewa\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003ey\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ewb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003et\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\t\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003ews\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003en\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003ews\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003er\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]]\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003ews\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e+=\u003c/span\u003e \u003cspan class=\"n\"\u003ews\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003en\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e--\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003esa\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"o\"\u003e--\u003c/span\u003e\u003cspan class=\"n\"\u003ews\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]]]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ej\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003en\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u0026lt;=\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003em\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003en\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003en\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003ey\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003en\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003esa\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003ey\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esa\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003ews\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003en\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003ews\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ewv\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ey\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]]]\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003ews\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e+=\u003c/span\u003e \u003cspan class=\"n\"\u003ews\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003en\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e--\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"n\"\u003esa\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"o\"\u003e--\u003c/span\u003e\u003cspan class=\"n\"\u003ews\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ewv\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]]]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ey\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003et\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003ey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"n\"\u003ey\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003et\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003esa\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e]]\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003en\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\t\u003cspan class=\"n\"\u003ex\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003esa\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]]\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecmp\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"n\"\u003esa\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e\u003cspan class=\"n\"\u003esa\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e],\u003c/span\u003e\u003cspan class=\"n\"\u003ej\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e?\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e \u003cspan class=\"o\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003ep\u003c/span\u003e\u003cspan class=\"o\"\u003e++\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e陛下……陛下！快请御医！\u003c/p\u003e\n\u003ch2 id=\"前言\"\u003e前言\u003c/h2\u003e\n\u003cp\u003e鉴于我也是第一次接触后缀数组，理解程度还很低，再加上类似的博客早已烂大街，因此不求读者能看懂或学到什么东西了。你还可以向我\u003ca href=\"mailto:fordring866@gmail.com\"\u003e留言\u003c/a\u003e提出指导意见，我会非常欢迎。\u003c/p\u003e","title":"破解外星代码：详解后缀数组模板"},{"content":"我已经不怎么记得“程序设计实习”的内容了。\nC与C++的（细微）区别 C++支持，但C不支持 面向对象 重载、模板、继承、虚函数、友元函数 语言层面上的异常处理 引用 输入输出流 new/delete/explicit/class等关键词 结构体中定义成员函数 结构体中定义静态变量 直接在结构体中初始化成员变量 C支持，但C++不支持 调用尚未声明的函数 将const的地址赋给non-const指针 将void*值直接赋给其他指针，malloc()返回的就是void* 不初始化const char *c = 333 其他 C认为func()=func(\u0026hellip;)，即func可以接受任意个数的参数。C++认为func()=func(void)，即func不能接受参数。因此在C中使用func(void)会显得你更专业、更严谨 C认为字面字符常量是整数，C++则认为是字符。sizeof('a')，前者得到4，后者得到1 C的结构体类型必须带上struct前缀。struct T; T a;是不合法的C代码，但C++认为没问题 sizeof(1==1)的C值为4，C++值为1，因为C++支持bool类型 空结构体的sizeof值在C是0，在C++是1 引用和指针的区别 总地来说，引用不如指针灵活，但更安全。引用值必须\n声明时初始化 不能为NULL 不能改绑 这些限制避免了野指针的存在，但也导致开发者无法使用引用来编写链表等数据结构。Java的引用没有上面这些限制，因此Java可以直接抛弃指针的说法。\n\u0026ldquo;delete this\u0026quot;会发生什么？ 首先，只有new出来的对象才能被delete，否则会产生undefined behavior；其次，这句话只能在非静态成员函数中使用，否则会产生编译错误；最后，正常地delete this之后，函数内将无法获取自己的成员变量。\n虚函数 考虑一个简单的场景，基类指针b指向了派生类对象d，然后调用成员函数：\nclass base { public: virtual void print() { cout\u0026lt;\u0026lt; \u0026#34;print base class\u0026#34; \u0026lt;\u0026lt;endl; } }; class derived : public base { public: void print() { cout\u0026lt;\u0026lt; \u0026#34;print derived class\u0026#34; \u0026lt;\u0026lt;endl; } }; int main(void) { //现学现卖 derived d; base *b = \u0026amp;d; b-\u0026gt;print(); } 规则 必须定义在public部分 PS: 假设基类B的某虚函数v为public，但派生类D用来覆盖v的函数为private。由于访问权限是在编译时决定的，因此，我可以在外部函数用类型为B*、指向D对象的指针访问v，但若将该指针的类型改为D*，则不能访问v。\n这\u0026hellip;\nclass B {public: virtual v{};}; class D : B {private: v{};}; int main { D d; B *bptr; D *dptr; bptr = \u0026amp;d; dptr = \u0026amp;d; bptr-\u0026gt;v(); // OK, it\u0026#39;s public dptr-\u0026gt;v(); // No, it\u0026#39;s private // WTF??? } 不能是static函数 不能是友元函数 基类和派生类中的原型相同（这样才能覆盖） 工作原理 若一个类含有虚函数，那么每当它的一个对象被创建时，该类都会新增一个独特的VPTR成员变量，指向该类的VTABLE。VTABLE是每个类都有的静态成员，是一个函数指针的数组，分别指向该类的所有虚函数。\n当指向派生类对象D的基类指针b调用成员函数f时：\n若基类的f不是虚函数，则编译时就决定，要调用的是基类的f 若基类的f是虚函数，则运行时通过 b 所指向的 D 所指向的 VPTR 所指向的 VTABLE，找到要调用的函数（派生类的f），从而实现运行时的多态。 虚析构函数 若基类指针b指向派生类对象d，且基类没有虚析构函数，则delete b会产生未定义行为。加上虚析构函数后，结果才正常——先调用派生类的析构函数，依次向上。\n纯虚函数 虚函数后面加上=0就是纯虚函数，例如virtual int f() = 0;。只要一个类有了纯虚函数作为成员，它就成了抽象类。\n抽象类没有对象，但可以有指针，也可以有构造函数 抽象类的派生类还是抽象类，除非它实现了所有纯虚函数 overload/override/hiding overload: 相同的函数名，不同的原型，称为重载（不是过载，炉宗注意了） override: 相同的函数名，相同的原型，用于覆盖父类虚函数 hiding: 在内层作用域中定义和外层同名的变量，外层的变量就被隐藏了 inline inline只是请求编译器将函数代码直接插入调用处，从而省下函数调用的开销（此处略）。如果该函数具备这些特点，编译器可能不会进行真正的inlining：\n包含循环体 包含静态变量 是递归的 返回值不是void，却可能不执行return语句 包含switch/goto inline同时也具有如下缺点：\n占用更多寄存器 编译而成的可执行文件比较大 对于一些嵌入式系统，空间更重要 可能造成颠簸（thrashing，如果你还记得是什么的话） 降低指令缓存的命中率（还记得指令缓存是什么吗） 假如A模块调用了B模块的inline函数，那么只要该函数改动了，A模块也必须和B模块一起重新编译 定义在结构体内部的函数自带inline属性（这一点我也不太能理解，请参考这篇文章）\nIt is also possible to define the inline function inside the class. In fact, all the functions defined inside the class are implicitly inline. Thus, all the restrictions of inline functions are also applied here. If you need to explicitly declare inline function in the class then just declare the function inside the class and define it outside the class using inline keyword.\n友元函数/友元类 友元关系不能被继承 别记反了：如果想让A成为B的朋友，那么应该在B里写friend A，然后A就可以访问B的private和protected成员 运算符重载 不能被重载的运算符：. :: ?: sizeof 重载过的\u0026amp;\u0026amp; ||不再具有短路性质 流IO\u0026gt;\u0026gt; \u0026lt;\u0026lt;的重载： ostream\u0026amp; operator\u0026lt;\u0026lt;(ostream\u0026amp; os, const T\u0026amp; obj) { os \u0026lt;\u0026lt; obj.a \u0026lt;\u0026lt; obj.b \u0026lt;\u0026lt; endl; return os } istream\u0026amp; operator\u0026gt;\u0026gt;(istream\u0026amp; is, T\u0026amp; obj) { is \u0026gt;\u0026gt; obj.a \u0026gt;\u0026gt; obj.b; return is; } 如果用户自定义的类重载了()运算符，它就会成为一个函数对象(function object/functor)，此处略 自增自减运算符++ --的重载，注意参数和返回类型的区别 struct X { int a; // ++X X\u0026amp; operator++() { a++; return *this; } // X++ X operator++(int) { X tmp(*this); operator++(); // 真正的递增 return tmp; // 返回的是旧值 } }; 重载赋值运算符=可以实现deep copy，当类成员有指针的时候比较有用。Copy Constructor也是一样（见下） 构造函数 默认构造函数 (Default Constructor) 没有参数（有默认值的参数也算参数）的构造函数，就是默认构造函数。如果用户不定义它，编译器会自动生成一个。\n拷贝构造函数 (Copy Constructor) 调用时机：对象作为函数参数、函数返回值、被赋的值时 设成private会禁止拷贝该类对象 参数必须是引用，否则会陷入“拷贝之前先拷贝”的循环 参数应该是const，原因 调用顺序 先基类，后派生类 先成员，后整体 多重继承：按基类列表的顺序 设基类有构造函数B(\u0026hellip;)，派生类有构造函数D(\u0026hellip;)。如果不像下面这样指定B，那么B不会被调用 D(...) : B(...) { \u0026lt;code\u0026gt; } 静态成员 static的成员函数没有this指针 static的成员函数不能是virtual的 凡事都要问个为什么。大部分答案的逻辑是这样的：\n使用虚函数，是为了根据指针所指对象在运行时的具体类型，调用不同的成员函数 静态成员函数不属于具体的对象，而是属于整个类 所以，静态虚函数\u0026quot;make no sense\u0026rdquo; 我认为这个答案才叫\u0026quot;make no sense\u0026quot;。既然只用判断类型，而static的特点就是只和类型相关，为什么不让我在运行时调用静态虚函数呢？\n其他答案也没有说明静态虚函数与C++目前的某些机制相矛盾，于是只能认为，“没这个必要”。\nstatic的成员函数不能是const和/或volatile的 虚继承 定义不难，理解为什么需要虚继承也不难，但还有一个问题——虚继承一定是最好的吗？\n答案太绕了，脑壳疼，略。\nconst成员函数 不允许修改调用对象的成员 const对象不能调用non-const成员函数，其他三种情况都可以（按对象和成员函数是否为const来划分），毕竟non-const成员函数可能会修改const对象，与其修改时报错，不如编译时报错 const成员函数不能是static的，因为static的成员函数不具有this指针，而const成员函数是根据this指针来保护调用者不被写的 ","permalink":"https://daichao1997.github.io/posts/tech/2018-10-26-c++%E6%9F%A5%E6%BC%8F%E8%A1%A5%E7%BC%BA/","summary":"\u003cp\u003e我已经不怎么记得“程序设计实习”的内容了。\u003c/p\u003e\n\u003ch2 id=\"c与c的细微区别\"\u003eC与C++的（细微）区别\u003c/h2\u003e\n\u003ch3 id=\"c支持但c不支持\"\u003eC++支持，但C不支持\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e面向对象\u003c/li\u003e\n\u003cli\u003e重载、模板、继承、虚函数、友元函数\u003c/li\u003e\n\u003cli\u003e语言层面上的异常处理\u003c/li\u003e\n\u003cli\u003e引用\u003c/li\u003e\n\u003cli\u003e输入输出流\u003c/li\u003e\n\u003cli\u003enew/delete/explicit/class等关键词\u003c/li\u003e\n\u003cli\u003e结构体中定义成员函数\u003c/li\u003e\n\u003cli\u003e结构体中定义静态变量\u003c/li\u003e\n\u003cli\u003e直接在结构体中初始化成员变量\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"c支持但c不支持-1\"\u003eC支持，但C++不支持\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e调用尚未声明的函数\u003c/li\u003e\n\u003cli\u003e将const的地址赋给non-const指针\u003c/li\u003e\n\u003cli\u003e将void*值直接赋给其他指针，malloc()返回的就是void*\u003c/li\u003e\n\u003cli\u003e不初始化const\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003echar *c = 333\u003c/code\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"其他\"\u003e其他\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003eC认为func()=func(\u0026hellip;)，即func可以接受任意个数的参数。C++认为func()=func(void)，即func不能接受参数。因此在C中使用func(void)会显得你更专业、更严谨\u003c/li\u003e\n\u003cli\u003eC认为字面字符常量是整数，C++则认为是字符。\u003ccode\u003esizeof('a')\u003c/code\u003e，前者得到4，后者得到1\u003c/li\u003e\n\u003cli\u003eC的结构体类型必须带上struct前缀。\u003ccode\u003estruct T; T a;\u003c/code\u003e是不合法的C代码，但C++认为没问题\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003esizeof(1==1)\u003c/code\u003e的C值为4，C++值为1，因为C++支持bool类型\u003c/li\u003e\n\u003cli\u003e空结构体的sizeof值在C是0，在C++是1\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"引用和指针的区别\"\u003e引用和指针的区别\u003c/h2\u003e\n\u003cp\u003e总地来说，引用不如指针灵活，但更安全。引用值必须\u003c/p\u003e","title":"C++查漏补缺"},{"content":"Introduction 上回书说到，Crowbar的词法分析器将具体的源码文件解析为一串抽象的符号，并伴随着一系列附属动作。接下来轮到Crowbar的语法分析器登场了——它是如何解读这串符号的？解读的同时又做了些什么事情呢？我们一起来看看。\n本篇有非常多杂七杂八的代码，看的时候一定要把握住最大的共同点——语法和数据结构的高度一致。很多代码都是相似的，把握住设计思想才是最关键的。\nCrowbar-Yacc 本层介绍两个最重要的语法符号——语句（expression）和表达式（statement）。\nCrowbar-Yacc-statement Crowbar的程序由一系列语句组成，有的是包含关键字的特殊语句，有的则是普通的表达式。\nstatement /* 语句 */ : expression SEMICOLON /* 表达式 + 分号 */ { $$ = crb_create_expression_statement($1); } | global_statement | if_statement | while_statement | for_statement | return_statement | break_statement | continue_statement ; 这里规约时调用了某函数crb_create_expression_statement，看名字就知道是在生成一个语句。不难想到，Crowbar也用专门的结构体保存语句，也由类型、行号、内容这几个部分组成。随着语句类型的不同，联合体u也有不同的解读方式，但都是为了保留必要的信息。例如，联合体中没有出现break和continue，因为它们并不携带其他信息（除非以后允许它们携带标签）。\nstruct Statement_tag { StatementType type; int line_number; union { Expression *expression_s; GlobalStatement global_s; IfStatement if_s; WhileStatement while_s; ForStatement for_s; ReturnStatement return_s; } u; }; 于是，crb_create_expression_statement就创建了一个Statement结构体，填充了类型、行号和表达式的内容。\nStatement *crb_create_expression_statement(Expression *expression) { Statement *st = alloc_statement(EXPRESSION_STATEMENT); st-\u0026gt;u.expression_s = expression; return st; } static Statement *alloc_statement(StatementType type) { Statement *st = crb_malloc(sizeof(Statement)); st-\u0026gt;type = type; st-\u0026gt;line_number = crb_get_current_interpreter()-\u0026gt;current_line_number; return st; } 至于其他特殊语句，我们后面说。\nCrowbar-Yacc-expression 所谓表达式，就是用运算符关联常量或标识符而得到的组合。为了保证运算符的优先级，我们常会设计一个分层的语法结构，让最简单、最原始的表达式，按照语法上的优先次序，分步组合成复杂表达式。下面是一个示意结构，具体的后面说。\nprimary_expression -\u0026gt; postfix_expression -\u0026gt; unary_expression -\u0026gt; multiplicative_expression -\u0026gt; additive_expression -\u0026gt; relational_expression -\u0026gt; equality_expression -\u0026gt; logical_and_expression -\u0026gt; logical_or_expression -\u0026gt; expression 表达式在互相归约时，常常调用crb_create_XXX_expression以生成Expression对象。上回也有提到Expression对象，但我们没有深究其结构。现在看来，和Statement也是差不多，存着类型、行号和内容。\nstruct Expression_tag { ExpressionType type; int line_number; union { CRB_Boolean boolean_value; int int_value; double double_value; char *string_value; char *identifier; AssignExpression assign_expression; BinaryExpression binary_expression; Expression *minus_expression; FunctionCallExpression function_call_expression; MethodCallExpression method_call_expression; ExpressionList *array_literal; IndexExpression index_expression; IncrementOrDecrement inc_dec; } u; }; Crowbar-Yacc-About_Statement 本层介绍其他语句相关的符号，它们大多是由语句和表达式引出的。介绍有详有略，请大家摸清规律、搞懂套路。\nglobal_statement identifier_list if_statement elsif_list elsif block statement_list XX_statement expression_opt Crowbar-Yacc-global_statement 本语句用于引用全局变量，例如global v1, v2, v3;。遇到这种语句时，v1, v2, v3先归约为identifier_list，然后整个语句才归约为global_statement。\nglobal_statement : GLOBAL_T identifier_list SEMICOLON { $$ = crb_create_global_statement($2); } ; 下面这个函数与crb_create_expression_statement类似。还记得Statement结构体里的GlobalStatement global_s吗？它就是用来存储global语句的信息的。\nStatement *crb_create_global_statement(IdentifierList *identifier_list) { Statement *st = alloc_statement(GLOBAL_STATEMENT); st-\u0026gt;u.global_s.identifier_list = identifier_list; return st; } typedef struct { IdentifierList *identifier_list; } GlobalStatement; Crowbar-Yacc-global_statement-identifier_list global_statement引出了identifier_list。用下面两个函数创建链表或增长链表IdentifierList，一堆标识符名字组成的链表。\nidentifier_list : IDENTIFIER { $$ = crb_create_global_identifier($1); } | identifier_list COMMA IDENTIFIER { $$ = crb_chain_identifier($1, $3); } ; IdentifierList *crb_chain_identifier(IdentifierList *list, char *identifier) { IdentifierList *pos; for (pos = list; pos-\u0026gt;next; pos = pos-\u0026gt;next); pos-\u0026gt;next = crb_create_global_identifier(identifier); return list; } IdentifierList *crb_create_global_identifier(char *identifier) { IdentifierList *i_list = crb_malloc(sizeof(IdentifierList)); i_list-\u0026gt;name = identifier; i_list-\u0026gt;next = NULL; return i_list; } typedef struct IdentifierList_tag { char *name; struct IdentifierList_tag *next; } IdentifierList; Crowbar-Yacc-if_statement 本语句大家都很熟悉，四种情况分别对应elsif/else的有无，都调用crb_create_if_statement。\n// 语法规则 if_statement : IF LP expression RP block { $$ = crb_create_if_statement($3, $5, NULL, NULL); } | IF LP expression RP block ELSE block { $$ = crb_create_if_statement($3, $5, NULL, $7); } | IF LP expression RP block elsif_list { $$ = crb_create_if_statement($3, $5, $6, NULL); } | IF LP expression RP block elsif_list ELSE block { $$ = crb_create_if_statement($3, $5, $6, $8); } ; // 统一动作 Statement *crb_create_if_statement(Expression *condition, Block *then_block, Elsif *elsif_list, Block *else_block) { Statement *st = alloc_statement(IF_STATEMENT); st-\u0026gt;u.if_s.condition = condition; st-\u0026gt;u.if_s.then_block = then_block; st-\u0026gt;u.if_s.elsif_list = elsif_list; st-\u0026gt;u.if_s.else_block = else_block; return st; } // 和语法一致的数据结构 typedef struct { Expression *condition; Block *then_block; Elsif *elsif_list; Block *else_block; } IfStatement; Crowbar-Yacc-if_statement-elsif_list elsif_list就是一堆elsif，是if_statement的一部分。组织链表的方式与前面标识符类似。\nelsif_list : elsif | elsif_list elsif { $$ = crb_chain_elsif_list($1, $2); } ; Elsif *crb_chain_elsif_list(Elsif *list, Elsif *add) { Elsif *pos; for (pos = list; pos-\u0026gt;next; pos = pos-\u0026gt;next); pos-\u0026gt;next = add; return list; } Crowbar-Yacc-if_statement-elsif_list-elsif elsif跟if相似。\nelsif : ELSIF LP expression RP block { $$ = crb_create_elsif($3, $5); } ; Elsif *crb_create_elsif(Expression *expr, Block *block) { Elsif *ei = crb_malloc(sizeof(Elsif)); ei-\u0026gt;condition = expr; ei-\u0026gt;block = block; ei-\u0026gt;next = NULL; return ei; } typedef struct Elsif_tag { Expression *condition; Block *block; struct Elsif_tag *next; } Elsif; Crowbar-Yacc-if_statement-block 所谓block就是用{}包含的代码块，里面包含的是一堆语句，于是用crb_create_block生成一个Block结构体，向其中填入块中的语句链表。\nblock : LC statement_list RC { $$ = crb_create_block($2); } | LC RC { $$ = crb_create_block(NULL); } ; Block *crb_create_block(StatementList *statement_list) { Block *block = crb_malloc(sizeof(Block)); block-\u0026gt;statement_list = statement_list; return block; } typedef struct { StatementList *statement_list; } Block; Crowbar-Yacc-if_statement-block-statement_list 而statement_list就是一堆语句，用链表组织，由crb_create_statement_list生成。\nstatement_list : statement { $$ = crb_create_statement_list($1); } | statement_list statement { $$ = crb_chain_statement_list($1, $2); } ; StatementList *crb_chain_statement_list(StatementList *list, Statement *statement) { StatementList *pos; if (list == NULL) { return crb_create_statement_list(statement); } for (pos = list; pos-\u0026gt;next; pos = pos-\u0026gt;next); // 到链表尾部 pos-\u0026gt;next = crb_create_statement_list(statement); // 插入新元素 return list; } StatementList *crb_create_statement_list(Statement *statement) { StatementList *sl = crb_malloc(sizeof(StatementList)); sl-\u0026gt;statement = statement; sl-\u0026gt;next = NULL; return sl; } typedef struct StatementList_tag { Statement *statement; struct StatementList_tag *next; } StatementList; Crowbar-Yacc-XX_statement 现在不用我说，大家也能猜到下面这些语句做了什么，不一个个贴了。\nwhile_statement : WHILE LP expression RP block { $$ = crb_create_while_statement($3, $5); } ; for_statement : FOR LP expression_opt SEMICOLON expression_opt SEMICOLON expression_opt RP block { $$ = crb_create_for_statement($3, $5, $7, $9); } ; return_statement : RETURN_T expression_opt SEMICOLON { $$ = crb_create_return_statement($2); } ; break_statement : BREAK SEMICOLON { $$ = crb_create_break_statement(); } ; continue_statement : CONTINUE SEMICOLON { $$ = crb_create_continue_statement(); } ; Crowbar-Yacc-XX_statement-expression_opt 可有可无的表达式。\nexpression_opt : /* empty */ { $$ = NULL; } | expression ; Crowbar-Yacc-About_Expression 本层按结合的优先级，介绍其他表达式相关的符号。介绍有详有略，请大家摸清规律、搞懂套路。\nCrowbar-Yacc-primary_expression 最底层的表达式，primary_expression，我理解为“原始表达式”。各种类型的字面常量、标识符都能归约到此。此外，函数调用、用括号包住的表达式，也属于单元表达式。它们的运算优先级是最高的，或者说根本不涉及运算。\nprimary_expression : IDENTIFIER LP argument_list RP /* foo(arg1, arg2) */ { $$ = crb_create_function_call_expression($1, $3); } | IDENTIFIER LP RP /* foo() */ { $$ = crb_create_function_call_expression($1, NULL); } | LP expression RP /* (foo+1) */ { $$ = $2; } /* 标识符和字面常量 */ | IDENTIFIER { $$ = crb_create_identifier_expression($1); } | INT_LITERAL | DOUBLE_LITERAL | STRING_LITERAL | TRUE_T { $$ = crb_create_boolean_expression(CRB_TRUE); } | FALSE_T { $$ = crb_create_boolean_expression(CRB_FALSE); } | NULL_T { $$ = crb_create_null_expression(); } | array_literal ; Crowbar-Yacc-postfix_expression 下一级是postfix_expression，我理解为“试着做后缀运算的表达式”。说“试着做”是因为该表达式可能不含后缀运算，但如果有，则一定比其他运算更优先。\npostfix_expression : primary_expression /* 没有后缀运算，直接规约 */ | postfix_expression LB expression RB /* foo[1] */ { $$ = crb_create_index_expression($1, $3); } | postfix_expression DOT IDENTIFIER LP argument_list RP /* foo.method(arg) */ { $$ = crb_create_method_call_expression($1, $3, $5); } | postfix_expression DOT IDENTIFIER LP RP /* foo.method() */ { $$ = crb_create_method_call_expression($1, $3, NULL); } | postfix_expression INCREMENT /* foo++ */ { $$ = crb_create_incdec_expression($1, INCREMENT_EXPRESSION); } | postfix_expression DECREMENT /* foo-- */ { $$ = crb_create_incdec_expression($1, DECREMENT_EXPRESSION); } ; Crowbar-Yacc-XX_expression 剩下的这一大堆语法从上往下看，就能发现不过是分别尝试了：\n单目运算（取负） 乘法、除法、取模 加法、减法 大于、大于等于、小于、小于等于 等于、不等于 逻辑与 逻辑或 于是它们的优先级也从高到低。\nunary_expression : postfix_expression | SUB unary_expression { $$ = crb_create_minus_expression($2); } ; multiplicative_expression : unary_expression | multiplicative_expression MUL unary_expression { $$ = crb_create_binary_expression(MUL_EXPRESSION, $1, $3); } | multiplicative_expression DIV unary_expression { $$ = crb_create_binary_expression(DIV_EXPRESSION, $1, $3); } | multiplicative_expression MOD unary_expression { $$ = crb_create_binary_expression(MOD_EXPRESSION, $1, $3); } ; additive_expression : multiplicative_expression | additive_expression ADD multiplicative_expression { $$ = crb_create_binary_expression(ADD_EXPRESSION, $1, $3); } | additive_expression SUB multiplicative_expression { $$ = crb_create_binary_expression(SUB_EXPRESSION, $1, $3); } ; relational_expression : additive_expression | relational_expression GT additive_expression { $$ = crb_create_binary_expression(GT_EXPRESSION, $1, $3); } | relational_expression GE additive_expression { $$ = crb_create_binary_expression(GE_EXPRESSION, $1, $3); } | relational_expression LT additive_expression { $$ = crb_create_binary_expression(LT_EXPRESSION, $1, $3); } | relational_expression LE additive_expression { $$ = crb_create_binary_expression(LE_EXPRESSION, $1, $3); } ; equality_expression : relational_expression | equality_expression EQ relational_expression { $$ = crb_create_binary_expression(EQ_EXPRESSION, $1, $3); } | equality_expression NE relational_expression { $$ = crb_create_binary_expression(NE_EXPRESSION, $1, $3); } ; logical_and_expression : equality_expression | logical_and_expression LOGICAL_AND equality_expression { $$ = crb_create_binary_expression(LOGICAL_AND_EXPRESSION, $1, $3); } ; logical_or_expression : logical_and_expression | logical_or_expression LOGICAL_OR logical_and_expression { $$ = crb_create_binary_expression(LOGICAL_OR_EXPRESSION, $1, $3); } ; 等到其他运算符结合完了，再来赋值。\nexpression : logical_or_expression | postfix_expression ASSIGN expression { $$ = crb_create_assign_expression($1, $3); } ; Crowbar-Yacc-About_Expression-动作 本层主要介绍上面的表达式归约时的具体动作。\nCrowbar-Yacc-About_Expression-函数调用 包含函数名，参数列表。这也体现在了数据结构里。\nExpression *crb_create_function_call_expression(char *func_name, ArgumentList *argument) { Expression *exp = crb_alloc_expression(FUNCTION_CALL_EXPRESSION); exp-\u0026gt;u.function_call_expression.identifier = func_name; exp-\u0026gt;u.function_call_expression.argument = argument; return exp; } typedef struct { char *identifier; ArgumentList *argument; } FunctionCallExpression; Crowbar-Yacc-About_Expression-函数调用-参数列表 参数列表就是由逗号分隔的表达式，依旧用链表组织，管理方式同前。\nargument_list : expression { $$ = crb_create_argument_list($1); } | argument_list COMMA expression { $$ = crb_chain_argument_list($1, $3); } ; ArgumentList *crb_create_argument_list(Expression *expression) { ArgumentList *al = crb_malloc(sizeof(ArgumentList)); al-\u0026gt;expression = expression; al-\u0026gt;next = NULL; return al; } ArgumentList *crb_chain_argument_list(ArgumentList *list, Expression *expr) { ArgumentList *pos; for (pos = list; pos-\u0026gt;next; pos = pos-\u0026gt;next); pos-\u0026gt;next = crb_create_argument_list(expr); return list; } typedef struct ArgumentList_tag { Expression *expression; struct ArgumentList_tag *next; } ArgumentList; Crowbar-Yacc-About_Expression-标识符 标识符只是字符串。\nExpression *crb_create_identifier_expression(char *identifier) { Expression *exp = crb_alloc_expression(IDENTIFIER_EXPRESSION); exp-\u0026gt;u.identifier = identifier; return exp; } Crowbar-Yacc-About_Expression-布尔变量 布尔变量是自己定义的，虽然直接用bool也没问题，但此方法可以让你设计更多种类的变量。\nExpression *crb_create_boolean_expression(CRB_Boolean value) { Expression *exp = crb_alloc_expression(BOOLEAN_EXPRESSION); exp-\u0026gt;u.boolean_value = value; return exp; } typedef enum { CRB_FALSE = 0, CRB_TRUE = 1 } CRB_Boolean; Crowbar-Yacc-About_Expression-数组常量 用花括号括起来的表达式列表。\narray_literal : LC expression_list RC { $$ = crb_create_array_expression($2); } | LC expression_list COMMA RC { $$ = crb_create_array_expression($2); } ; Expression *crb_create_array_expression(ExpressionList *list) { Expression *exp = crb_alloc_expression(ARRAY_EXPRESSION); exp-\u0026gt;u.array_literal = list; return exp; } typedef struct ExpressionList_tag { Expression *expression; struct ExpressionList_tag *next; } ExpressionList; Crowbar-Yacc-About_Expression-数组常量-表达式列表 表达式列表还是由逗号分隔的表达式，依旧用链表组织，管理方式同前。等等，这跟参数列表有啥区别？貌似唯一的区别是，表达式列表可以为空，而参数列表不行，真奇怪。\nexpression_list : /* empty */ { $$ = NULL; } | expression { $$ = crb_create_expression_list($1); } | expression_list COMMA expression { $$ = crb_chain_expression_list($1, $3); } ; ExpressionList *crb_create_expression_list(Expression *expression) { ExpressionList *el = crb_malloc(sizeof(ExpressionList)); el-\u0026gt;expression = expression; el-\u0026gt;next = NULL; return el; } ExpressionList *crb_chain_expression_list(ExpressionList *list, Expression *expr) { ExpressionList *pos; for (pos = list; pos-\u0026gt;next; pos = pos-\u0026gt;next); pos-\u0026gt;next = crb_create_expression_list(expr); return list; } Crowbar-Yacc-About_Expression-取数组元素 取数组元素，一要数组，二要下标。\nExpression *crb_create_index_expression(Expression *array, Expression *index) { Expression *exp = crb_alloc_expression(INDEX_EXPRESSION); exp-\u0026gt;u.index_expression.array = array; exp-\u0026gt;u.index_expression.index = index; return exp; } typedef struct { Expression *array; Expression *index; } IndexExpression; Crowbar-Yacc-About_Expression-调用方法 调用方法，一要对象，二要方法名，三要参数列表，例如foo.method(arg1, arg2)。\nExpression *crb_create_method_call_expression(Expression *expression, char *method_name, ArgumentList *argument) { Expression *exp = crb_alloc_expression(METHOD_CALL_EXPRESSION); exp-\u0026gt;u.method_call_expression.expression = expression; exp-\u0026gt;u.method_call_expression.identifier = method_name; exp-\u0026gt;u.method_call_expression.argument = argument; return exp; } typedef struct { Expression *expression; char *identifier; ArgumentList *argument; } MethodCallExpression; Crowbar-Yacc-About_Expression-自增自减 Expression *crb_create_incdec_expression(Expression *operand, ExpressionType inc_or_dec) { Expression *exp = crb_alloc_expression(inc_or_dec); exp-\u0026gt;u.inc_dec.operand = operand; return exp; } typedef struct { Expression *operand; } IncrementOrDecrement; Crowbar-Yacc-About_Expression-取负 负号如果用在字面常量上，得到的也是字面常量。为了减少语义分析的负担，这里先判断表达式的类型。如果是整数或浮点数，就求出其负值，再把该负值作为新的字面常量返回，而不是返回类型为MINUS_EXPRESSION的表达式。\ncrb_eval_minus_expression和convert_value_to_expression在语义分析中才会用到，因此也留到后面说。\nExpression *crb_create_minus_expression(Expression *operand) { if (operand-\u0026gt;type == INT_EXPRESSION || operand-\u0026gt;type == DOUBLE_EXPRESSION) { CRB_Value v = crb_eval_minus_expression(crb_get_current_interpreter(), NULL, operand); /* Notice! Overwriting operand expression. */ *operand = convert_value_to_expression(\u0026amp;v); return operand; } else { Expression *exp = crb_alloc_expression(MINUS_EXPRESSION); exp-\u0026gt;u.minus_expression = operand; return exp; } } Crowbar-Yacc-About_Expression-二元运算 二元运算包括加法、减法、乘法、除法、取模、大于、大于等于、小于、小于等于、等于、不等于、逻辑与、逻辑或。但无论是哪种运算，都执行着相同的操作。\n这里也做了优化：如果运算双方都是字面常量，那么返回的也是字面常量，而不是BinaryExpression。\nExpression *crb_create_binary_expression(ExpressionType operator, Expression *left, Expression *right) { if ((left-\u0026gt;type == INT_EXPRESSION || left-\u0026gt;type == DOUBLE_EXPRESSION) \u0026amp;\u0026amp; (right-\u0026gt;type == INT_EXPRESSION || right-\u0026gt;type == DOUBLE_EXPRESSION)) { CRB_Value v = crb_eval_binary_expression(crb_get_current_interpreter(), NULL, operator, left, right); /* Overwriting left hand expression. */ *left = convert_value_to_expression(\u0026amp;v); return left; } else { Expression *exp = crb_alloc_expression(operator); exp-\u0026gt;u.binary_expression.left = left; exp-\u0026gt;u.binary_expression.right = right; return exp; } } // 很明显，二元运算只需要左右操作数，上层的具体类型则由符号决定 typedef struct { Expression *left; Expression *right; } BinaryExpression; Crowbar-Yacc-About_Expression-赋值 Expression *crb_create_assign_expression(Expression *left, Expression *operand) { Expression *exp = crb_alloc_expression(ASSIGN_EXPRESSION); exp-\u0026gt;u.assign_expression.left = left; exp-\u0026gt;u.assign_expression.operand = operand; return exp; } Expression *crb_alloc_expression(ExpressionType type) { Expression *exp = crb_malloc(sizeof(Expression)); exp-\u0026gt;type = type; exp-\u0026gt;line_number = crb_get_current_interpreter()-\u0026gt;current_line_number; return exp; } typedef struct { Expression *left; Expression *operand; } AssignExpression; 总结 词法和语法的部分应该是事无巨细地讲完了。看这么多杂七杂八的代码，一定要把握住最大的共同点——语法和数据结构的高度一致。语法是人定的，掌握了设计思想才能自由发挥，创造自己的语法。此外，一门合格的编程语言所需的基本要素，也能通过本篇文章看个大概——数值计算、函数调用、数组、语句块……\n下次更新可能要很久之后了，应该会开始讲语义分析。\n","permalink":"https://daichao1997.github.io/posts/tech/2018-10-08-crowbar%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90%E8%AF%AD%E6%B3%95/","summary":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://daichao1997.github.io/Crowbar%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90%EF%BC%9A%E8%AF%8D%E6%B3%95.html\"\u003e上回书说到\u003c/a\u003e，Crowbar的词法分析器将具体的源码文件解析为一串抽象的\u003cstrong\u003e符号\u003c/strong\u003e，并伴随着一系列附属动作。接下来轮到Crowbar的语法分析器登场了——它是如何解读这串符号的？解读的同时又做了些什么事情呢？我们一起来看看。\u003c/p\u003e\n\u003cp\u003e本篇有非常多杂七杂八的代码，看的时候一定要把握住最大的共同点——\u003cstrong\u003e语法\u003c/strong\u003e和\u003cstrong\u003e数据结构\u003c/strong\u003e的高度一致。很多代码都是相似的，把握住\u003cstrong\u003e设计思想\u003c/strong\u003e才是最关键的。\u003c/p\u003e","title":"Crowbar源码剖析：语法"},{"content":"Introduction 上回书说到，Crowbar用了Lex/Yacc这对经典工具来生成词法/语法分析器。它们具体的编写规则我不想细说，请参考这篇文章，但大致来讲，两者都是“一边匹配一边触发动作”。\n本篇文章会集中讨论匹配触发了哪些动作，而不讲如何匹配、为什么匹配。\nCrowbar-Lex 这部分讲述Lex如何对五花八门的字符作出不同反应，用\u0026lt;X\u0026gt;exp{action}表示“状态X下，与exp匹配时，执行action”。为了给Yacc传递符号的解析结果，双方约定了一种“通信方式”：\n符号的字面内容自动进入了char *yytext 符号的值需要手动存入yylval（在Yacc里定义） 用return返回符号的种类（在Yacc里定义） 本来不想说Lex/Yacc的，但这种影响代码理解的规则，还是讲一下比较好。\nCrowbar-Lex-保留字与运算符 匹配到保留字/运算符后，直接返回对应的token。\n\u0026lt;INITIAL\u0026gt;\u0026#34;function\u0026#34; return FUNCTION; \u0026lt;INITIAL\u0026gt;\u0026#34;;\u0026#34; return SEMICOLON; \u0026lt;INITIAL\u0026gt;\u0026#34;++\u0026#34; return INCREMENT; // and more Crowbar-Lex-标识符 匹配到标识符后，调用crb_create_identifier，把标识符装进一个新字符串。真简单！\n\u0026lt;INITIAL\u0026gt;[A-Za-z_][A-Za-z_0-9]* { yylval.identifier = crb_create_identifier(yytext); return IDENTIFIER; } char *crb_create_identifier(char *str) { char *new_str; new_str = crb_malloc(strlen(str) + 1); strcpy(new_str, str); return new_str; } Crowbar-Lex-整数、浮点数 整数和浮点数都属于最简单的表达式，但任何表达式都要用专门的Expression结构体来存储，这样才能保证语法上的统一。因此我们调用crb_alloc_expression生成一个新Expression对象，然后把匹配到的字面值（yytext）以正确的形式（int/double）记入其中。\n// 整数 \u0026lt;INITIAL\u0026gt;([1-9][0-9]*)|\u0026#34;0\u0026#34; { Expression *expression = crb_alloc_expression(INT_EXPRESSION); sscanf(yytext, \u0026#34;%d\u0026#34;, \u0026amp;expression-\u0026gt;u.int_value); yylval.expression = expression; return INT_LITERAL; } // 浮点数 \u0026lt;INITIAL\u0026gt;[0-9]+\\.[0-9]+ { Expression *expression = crb_alloc_expression(DOUBLE_EXPRESSION); sscanf(yytext, \u0026#34;%lf\u0026#34;, \u0026amp;expression-\u0026gt;u.double_value); yylval.expression = expression; return DOUBLE_LITERAL; } crb_alloc_expression只是分配空间，完成简单的初始化。\nExpression *crb_alloc_expression(ExpressionType type) { Expression *exp; exp = crb_malloc(sizeof(Expression)); exp-\u0026gt;type = type; exp-\u0026gt;line_number = crb_get_current_interpreter()-\u0026gt;current_line_number; return exp; } Crowbar-Lex-字符串 识别到单个双引号后，先用crb_open_string_literal清空临时存放字符串的st_string_literal_buffer，然后从INITIAL状态进入STRING_LITERAL_STATE状态，标志着字符串识别的开始。\n#define SSLB st_string_literal_buffer //实在太长了，看不下去\n#define \\\u0026quot;\u0026quot; \\\u0026quot; //Markdown不支持Lex语法，于是我想以C来显示。但如果下面只写一个双引号，后面的内容全都会被视为字符串，从而变成丑陋的红色，所以这里把双引号变成两个。\n\u0026lt;INITIAL\u0026gt;\\\u0026#34;\u0026#34; { crb_open_string_literal(); BEGIN STRING_LITERAL_STATE; } void crb_open_string_literal(void) { SSLB_size = 0; } 在字符串识别模式下，任何双引号之外的字符都会被简单地加入SSLB。如果是换行符，还会增加一下行数。\ncrb_add_string_literal专门用来将字符加入SSLB，它除了往buffer里塞一个字符，还管理着buffer的大小，毕竟字符串（理论上）是不限长度的。由此可见，如果语言的设计者懒得管理内存，那么用户可以有很多种方法把系统整崩溃。\n\u0026lt;STRING_LITERAL_STATE\u0026gt;\\n { crb_add_string_literal(\u0026#39;\\n\u0026#39;); increment_line_number(); } \u0026lt;STRING_LITERAL_STATE\u0026gt;\\\\\\\u0026#34;\u0026#34; crb_add_string_literal(\u0026#39;\u0026#34;\u0026#39;); \u0026lt;STRING_LITERAL_STATE\u0026gt;\\\\n crb_add_string_literal(\u0026#39;\\n\u0026#39;); \u0026lt;STRING_LITERAL_STATE\u0026gt;\\\\t crb_add_string_literal(\u0026#39;\\t\u0026#39;); \u0026lt;STRING_LITERAL_STATE\u0026gt;\\\\\\\\ crb_add_string_literal(\u0026#39;\\\\\u0026#39;); \u0026lt;STRING_LITERAL_STATE\u0026gt;. crb_add_string_literal(yytext[0]); void crb_add_string_literal(int letter) { // 只有buffer满载了，才给它多分配点空间 if (SSLB_size == SSLB_alloc_size) { SSLB_alloc_size += STRING_ALLOC_SIZE; SSLB = MEM_realloc(SSLB, SSLB_alloc_size); } SSLB[SSLB_size] = letter; SSLB_size++; } 另一个双引号标志着字符串的结束，按表达式处理即可，如同整数、浮点数那样。crb_close_string_literal只是把buffer的内容复制到新字符串里罢了。\n\u0026lt;STRING_LITERAL_STATE\u0026gt;\\\u0026#34;\u0026#34; { Expression *expression = crb_alloc_expression(STRING_EXPRESSION); expression-\u0026gt;u.string_value = crb_close_string_literal(); yylval.expression = expression; BEGIN INITIAL; return STRING_LITERAL; } char *crb_close_string_literal(void) { char *new_str; new_str = crb_malloc(st_string_literal_buffer_size + 1); memcpy(new_str, st_string_literal_buffer, st_string_literal_buffer_size); new_str[st_string_literal_buffer_size] = \u0026#39;\\0\u0026#39;; return new_str; } Crowbar-Lex-其他字符 INITIAL状态下遇到#会进入COMMENT状态，再遇到换行符才回到INITIAL，其他字符都无动作 换行符：行数加一 空格与Tab：无动作 其他：报错 总结 Lex将文本提取为一串抽象的符号。符号的种类供Yacc进行语法分析，蕴含的值则以正确的形式保留，而具体的字面内容已经不再重要。\n其实Yacc的内容我已经写了一半，就在本文下方的注释里，但你们就是看不到！哈哈哈气不气！（mdzz）\n注释掉是因为我明天赶火车，今天发不出来完整版。坐高铁的时候应该能完成Yacc剩下的部分，并争取明天发出来，给国庆节一个（相对）完整的交代——原本还奢望看完整本书呢。🚩这是我的flag，不🐦，真的。\n说到离家回校，又有一堆感慨，但鉴于这篇是“技术”文章，我还是忍住吧。\nGTMD实习！\n","permalink":"https://daichao1997.github.io/posts/tech/2018-10-07-crowbar%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90%E8%AF%8D%E6%B3%95/","summary":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://daichao1997.github.io/Crowbar%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90%EF%BC%9A%E6%80%BB%E4%BD%93%E6%A1%86%E6%9E%B6.html\"\u003e上回书说到\u003c/a\u003e，Crowbar用了Lex/Yacc这对经典工具来生成词法/语法分析器。它们具体的编写规则我不想细说，请参考\u003ca href=\"https://segmentfault.com/a/1190000000396608\"\u003e这篇文章\u003c/a\u003e，但大致来讲，两者都是“一边匹配一边触发动作”。\u003c/p\u003e\n\u003cp\u003e本篇文章会集中讨论\u003cstrong\u003e匹配\u003c/strong\u003e触发了\u003cstrong\u003e哪些动作\u003c/strong\u003e，而不讲如何匹配、为什么匹配。\u003c/p\u003e\n\u003ch2 id=\"crowbar-lex\"\u003eCrowbar-Lex\u003c/h2\u003e\n\u003cp\u003e这部分讲述Lex如何对五花八门的字符作出不同反应，用\u003ccode\u003e\u0026lt;X\u0026gt;exp{action}\u003c/code\u003e表示“状态X下，与exp匹配时，执行action”。为了给Yacc传递符号的解析结果，双方约定了一种“通信方式”：\u003c/p\u003e","title":"Crowbar源码剖析：词法"},{"content":"Introduction Crowbar是《自制编程语言》一书中作者自己构思的无类型语言。本书一边向读者讲述编程语言的基本要素，一边讲解具体的实现方法，可作为编译技术的入门材料。此外，书中还有一门叫做Diksam的静态类型语言，等我学完了再来写。不🐦，真的。\nCrowbar 不管什么语言，本质上就是一个文本解析程序，只不过在解析的同时配上了动作的执行，此所谓词法+语法+语义。如果没有特别要求，制作一套自己的编程语言其实并非难事——要分析词法，我们有Lex；要分析语法，我们有Yacc。唯一需要独立编写的就是语义，而这也是最关键的地方。\n既然是C程序，那就从main函数看起吧。读取代码文件，然后初始化一个“解释器”，用它编译、执行代码，最后回收掉解释器。真简单！\nint main(int argc, char **argv) { CRB_Interpreter *interpreter; FILE *fp; if (argc != 2) { fprintf(stderr, \u0026#34;usage:%s filename\u0026#34;, argv[0]); exit(1); } fp = fopen(argv[1], \u0026#34;r\u0026#34;); if (fp == NULL) { fprintf(stderr, \u0026#34;%s not found.\\n\u0026#34;, argv[1]); exit(1); } interpreter = CRB_create_interpreter(); CRB_compile(interpreter, fp); CRB_interpret(interpreter); CRB_dispose_interpreter(interpreter); MEM_dump_blocks(stdout); return 0; } Crowbar-main 简介 本层让大家看看main函数的每一步大概做了些什么事情。\nCRB_create_interpreter：创建解释器 CRB_compile：用Lex和Yacc生成的分析器编译源码，得到很多“东西”，填充到解释器中 CRB_interpret：执行解释器里的“东西” CRB_dispose_interpreter：清除内存等收尾工作 MEM_dump_blocks：debug用 Crowbar-main-CRB_create_interpreter 所谓解释器，其实就是一个数据结构，存储着程序的解析结果，并管理着自己的存储空间。它形成了一个封闭的运行环境。\nstruct CRB_Interpreter_tag { MEM_Storage interpreter_storage; MEM_Storage execute_storage; Variable *variable; FunctionDefinition *function_list; StatementList *statement_list; int current_line_number; Stack stack; Heap heap; CRB_LocalEnvironment *top_environment; }; interpreter_storage / execute_storage是用来管理内存的（后面说）\nvariable / function_list / statement_list都是链表，存储解析代码得来的“东西”（后面说）\ncurrent_line_number记录当前解析到了源代码的哪一行，方便在报错时给出具体位置\nstack / heap是运行时分配的内存，可以给数组用（后面说）\n还没读懂top_environment\nCRB_Interpreter *CRB_create_interpreter(void) { MEM_Storage storage; CRB_Interpreter *interpreter; // 等同于CRB_Interpreter_tag* // 1 storage = MEM_open_storage(0); interpreter = MEM_storage_malloc(storage, sizeof(struct CRB_Interpreter_tag)); // 2 interpreter-\u0026gt;interpreter_storage = storage; // 注意 interpreter-\u0026gt;execute_storage = NULL; interpreter-\u0026gt;variable = NULL; interpreter-\u0026gt;function_list = NULL; interpreter-\u0026gt;statement_list = NULL; interpreter-\u0026gt;current_line_number = 1; interpreter-\u0026gt;stack.stack_alloc_size = 0; interpreter-\u0026gt;stack.stack_pointer = 0; interpreter-\u0026gt;stack.stack = MEM_malloc(sizeof(CRB_Value) * STACK_ALLOC_SIZE); // 注意 interpreter-\u0026gt;heap.current_heap_size = 0; interpreter-\u0026gt;heap.current_threshold = HEAP_THRESHOLD_SIZE; // 注意 interpreter-\u0026gt;heap.header = NULL; interpreter-\u0026gt;top_environment = NULL; // 3 crb_set_current_interpreter(interpreter); // 4 add_native_functions(interpreter); return interpreter; } 本函数的执行过程如下：\n开一片存储空间，为解释器留出位置 初始化各种成员变量，大部分都是平凡的初始值 设置另一个包的static变量，没什么特殊的 “注册”原生函数（后面说） Crowbar-main-CRB_compile 初步的数据结构有了，开始解析代码：\nvoid CRB_compile(CRB_Interpreter *interpreter, FILE *fp) { extern int yyparse(void); extern FILE *yyin; crb_set_current_interpreter(interpreter); yyin = fp; if (yyparse()) { fprintf(stderr, \u0026#34;Error ! Error ! Error !\\n\u0026#34;); exit(1); } crb_reset_string_literal_buffer(); } yyparse()是整个函数的主干，由Lex和Yacc共同生成。简单来说，这个函数逐个字符读取源码，和Lex文件里的词法规则进行匹配。每匹配一次就触发一些“动作”，并可能返回一种“token”，此所谓词法分析。返回的token经过累积后（移进），又会满足Yacc文件里的语法规则（归约），于是又会据此做一些“动作”，此所谓语法分析。整个文件读完（术语叫做“one pass”，一趟），代码就解析得差不多了，解释器里的数据结构也会被填满，留待执行。\nLex和Yacc文件的具体内容，留到后面说。\nCrowbar-main-CRB_interpret 现在就要真正地执行代码了。\nvoid CRB_interpret(CRB_Interpreter *interpreter) { interpreter-\u0026gt;execute_storage = MEM_open_storage(0); crb_add_std_fp(interpreter); crb_execute_statement_list(interpreter, NULL, interpreter-\u0026gt;statement_list); crb_garbage_collect(interpreter); } 第一行，开辟一个“存储器”，后面说。\n第二行，暂时说不清楚\n第三行，执行解释器里的语句链表（由上一步编译生成）。该函数只是遍历链表，对每条语句调用execute_statement，而后者又根据语句的具体类型，再调用不同的处理函数execute_X_statement，其中X可以是expression, global, if, while, for, return, break, continue等种类名称。Layer-2将介绍具体的处理过程。\n第四行，垃圾回收，后面说。\nCrowbar-main-CRB_dispose_interpreter 垃圾回收，内存释放等等，在进一步了解Crowbar的内存管理机制之前不好说细。其实我也没有完全搞懂，且写且珍惜。\nCrowbar-main-MEM_dump_blocks debug用，打印各个内存块的状态，省略了。\n后续 刚才我对Crowbar的代码树进行了BFS，介绍了最简要的框架。接下来我将适当采取DFS，分几篇文章把每个模块的实现讲透彻，也让自己铭记于心。\n国庆即将结束，进度依然捉急。工作日到来后，学习时间将大幅缩水，但这系列我绝对不🐦。真的。\nGTMD实习！\n","permalink":"https://daichao1997.github.io/posts/tech/2018-10-06-crowbar%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90%E6%80%BB%E4%BD%93%E6%A1%86%E6%9E%B6/","summary":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eCrowbar是《自制编程语言》一书中作者自己构思的无类型语言。本书一边向读者讲述编程语言的基本要素，一边讲解具体的实现方法，可作为编译技术的入门材料。此外，书中还有一门叫做Diksam的静态类型语言，等我学完了再来写。不🐦，真的。\u003c/p\u003e","title":"Crowbar源码剖析：总体框架"},{"content":"这两周的工作主要是给自己之前写的代码填坑，也就是写单元测试。\n我做单元测试的方法 裸测很难搞 单元测试（unit testing）是指对软件中的最小可测试单元进行检查和验证，而我要测试的内容是一些用Go语言写的接口，大概长这个样子：\nfunc (g *Foo) ObjPQ(obj *Obj, filterPolicy []string, scorePolicy map[string]int) PQ { filteredContainerList, _, _ := g.Filter(obj, filterPolicy) scoredContainerList, _ := g.Score(filteredContainerList, obj, scorePolicy) containerPQ := NewPriorityQueue() for _, container := range scoredContainerList { newPQ.Insert(container.Name, container.Score) } return containerPQ } 有编程基础的同学很容易理解这个函数的用意。大致的意思是说，我要为一些Obj对象找到最适合它的容器，于是先过滤掉不适合Obj的容器，再对剩下的容器打分，然后按照分数放到优先队列里，方便后面取用。\n按照我以前的思路，测试这个函数当然是先编一些数据，代入验算再说。\nimport \u0026#34;testing\u0026#34; func TestObjPQ(t *testing) { \u0026lt;做测试\u0026gt; } 然而我很快就意识到，我的函数背后是庞大的数据库和更低层的接口。无论是“过滤”还是“打分”，都需要运行在这一堆数据上面。如果只编造参数，我无从手动预测正确结果；如果连背后的数据也模拟一遍，那成本又太大了，因为我很难估计下层接口、下下层接口又要用到什么数据。\n正当我一筹莫展之际，经验丰富的导师给我介绍了\u0026quot;monkey patching\u0026quot;的概念，并塞给了我一个链接。\nPatching大法好 按照我目前的理解，monkey patching就是重新定义一个函数，从而让它返回任何你想要的值。我现在不是预测不了过滤和打分的结果吗？patch一下就可以为所欲为了！如代码所示，我先用monkey.PatchInstanceMethod把一个方法屏蔽为自定义的函数，事后再用monkey.UnpatchAll()解除屏蔽。这样一来，我既免去了编数据的负担，又能专注于函数本身而不用担心底层接口是否可靠。一举两得，岂不美哉？\nimport \u0026#34;reflect\u0026#34; import \u0026#34;testing\u0026#34; import \u0026#34;github.com/bouk/monkey\u0026#34; import \u0026#34;testing\u0026#34; func TestObjPQ(t *testing) { monkey.PatchInstanceMethod(reflect.TypeOf(g), \u0026#34;Filter\u0026#34;, func(*Obj,[]string) []Container { return 任何我想要的过滤结果 }) monkey.PatchInstanceMethod(reflect.TypeOf(g), \u0026#34;Score\u0026#34;, func([]Container, *Obj, []string) []ScoredContainer { return 任何我想要的打分结果 }) \u0026lt;做测试\u0026gt; monkey.UnpatchAll() } 用GoConvey包装测试语法 不多说了，直接看例子。这是矮穷矬的测试代码：\nfunc TestFunc(t *testing.T) { type args struct { target reflect.Type methodName string } tests := []struct { name string args args want int wantErr bool }{ {\u0026#34;Good case\u0026#34;, data[0], 0, false}, {\u0026#34;Bad case\u0026#34;, data[1], -1, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, gotErr := Func(tt.args.target, tt.args.methodName) if (gotErr != nil) != tt.wantErr { t.Errorf(\u0026#34;Func() error = %v, wantErr %v\u0026#34;, gotErr, tt.wantErr) return } if got != tt.want { t.Errorf(\u0026#34;Func() = %v, want %v\u0026#34;, got, tt.want) } }) } } 这是高富帅的测试代码：\nfunc TestFunc(t *testing.T) { Convey(\u0026#34;Good case\u0026#34;, t, func() { res, err := Func(data[0]) So(err, ShouldBeNil) So(res, ShouldEqual, 0) }) Convey(\u0026#34;Bad case\u0026#34;, t, func() { _, err := Func(data[1]) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, \u0026#34;timeout\u0026#34;) }) } 有人问：“高富帅先生，请问您保养头发的秘诀是什么？为什么21岁了还是如此乌黑浓密呢？”\n高富帅轻轻一笑，从容答道：“因为我用goconvey。”\n震惊！800⭐️的GitHub项目居然过不了作者自己写的测试！ 写测试从不怕测出问题，因为写测试就是为了找出问题。\n写测试最怕的是测试本身出了问题。\n最最怕的是测试引用的第三方库出了问题。\n最最最怕的是，打开那个库然后运行作者自己写的单元测试，居然过不了。\n似有似无的bug 前面提到了用PatchInstanceMethod屏蔽函数的函数，但它对应的解蔽函数UnpatchInstanceMethod莫名其妙地失效了。经过一番调查，发现程序记录了一个类型为map[reflect.Value]patch的映射表。patch的时候，程序调用reflect.MethodByName，通过名字找到函数的值（类型为reflect.Value），作为key存进表中。unpatch的时候，又以同样的方式试图找到key，这时却失败了。\n两次返回的值为什么会不一样？我用fmt.Printf(\u0026quot;%v\\n\u0026quot;, value)把它们打印了出来：\nmonkey_test.go: patch: 0x12c1670 monkey_test.go: unpatch: 0x12c1670 看上去简直毫无问题，但是reflect.Value是一个复合的结构体，这里打印的应该是它各个属性的值，怎么会得到一个地址呢？这其中必有蹊跷！\nreflect.Value里都有些啥 // Value is the reflection interface to a Go value. // // To compare two Values, compare the results of the Interface method. // Using == on two Values does not compare the underlying values // they represent. type Value struct { // typ holds the type of the value represented by a Value. typ *rtype // Pointer-valued data or, if flagIndir is set, pointer to data. ptr unsafe.Pointer flag } 打开go/src/reflect/value.go，我看到了Value结构体的内部实现。原来，Go语言用Value存储一个“值”的信息。“值”不只是数据，还包括类型和其他元信息，因此要用结构体表示。而ptr只是一个指针，它最终指向的才是真正的“underlying value”。也就是说，MethodByName之所以会返回不同的Value，很可能是因为这里的ptr。而刚才打印结果之所以相同，又可能是因为Printf函数对打印Value有特殊的处理方式——也许只打印了它的地址罢了。\n经过实验证实，每一次MethodByName返回的ptr确实不同，但它们指向的值却相同。\n为什么？\n不说过程了，直接上调用关系（有所改编）：\n// Method结构包含了属性Func，也就是我们要找的Value reflect/type.go: 870: func (t *rtype) MethodByName(name string) (m Method, ok bool) { return t.Method(eidx), true } reflect/type.go: 836: func (t *rtype) Method(i int) (m Method) { methods := t.exportedMethods() p := methods[i] m.Type = mt tfn := t.textOff(p.tfn) // 临时变量tfn。这里只用知道它是临时变量就好了，展开讲textOff太麻烦，我自己都没看懂。 fn := unsafe.Pointer(\u0026amp;tfn) // fn取的是tfn的地址！ m.Func = Value{mt.(*rtype), fn, fl} // 最终返回的Value将fn作为ptr属性的值 m.Index = i return m } 可以看到最终返回的Value.ptr竟来自一个不知从哪跑来的野孩子！怪不得每次都返回不一样的值！关于这一点文档却没有作出说明……\n……好吧，文档也没想让作者用这种骚操作强行拿到私有变量，大家扯平了🐒\n// 模仿reflect.Value构造盗版结构体 type value struct { _ uintptr ptr unsafe.Pointer } // 强行把reflect.Value的指针转为盗版指针，然后取其ptr func getPtr(v reflect.Value) unsafe.Pointer { return (*value)(unsafe.Pointer(\u0026amp;v)).ptr } 实际上这个库的作者还是很强大的，17岁代表荷兰参加IOI，写这个库玩底层hacking的时候才19岁……想想自己，21岁了还在写最基础的单元测试……感兴趣的朋友可以去GitHub看看，整个库只有五六百行，但实现得很巧妙。出现bug也只是因为随着Go语言不断进化，之前的底层hacking也需要更新了，仅此而已……\n然后我就蹭到了一个800🌟repo的contribution，耶！\n坑爹的fmt.Printf(\u0026quot;%v\u0026quot;) 最后，关于fmt.Printf怎么处理%v标记，为什么Value只打印一个地址，来看看官方文档怎么说：\nThe default format for %v is:\nbool: %t\nint, int8 etc.: %d\n\u0026hellip;（讲别的基本类型怎么打印）\n\u0026hellip;（讲结构体怎么打印，我看到这里就没看了）\n\u0026hellip;（讲如何保留几位小数）\n\u0026hellip;（讲打印字符串的特殊情况）\n\u0026hellip;（讲怎么打印复数）\n\u0026hellip;（讲附加的打印标志）\n\u0026hellip;（接着讲附加的打印标志）\n\u0026hellip;（正常人根本不会看到这里）\n\u0026hellip;\n\u0026hellip;（SURPRISE MOTHERF**KER）\nExcept when printed using the verbs %T and %p, special formatting considerations apply for operands that implement certain interfaces. In order of application:\n1. If the operand is a reflect.Value, the operand is replaced by the concrete value that it holds, and printing continues with the next rule.\n🙂🙂🙂\n缺点 做单元测试的时候，patching是非常必要的工具。如果你想测试main函数却坚决不用patching，那你就是测试界的鬼才。然而，patching虽能简化底层接口，但它们往往会以“微妙”的方式影响程序整体，而且让你很难察觉。回头看看最开始的例子，我为了方便直接把过滤和打分的算法屏蔽了，但如果它们本来要更新一些全局状态呢？如果我后面刚好又要用到这些状态呢？那我是不是还得钻到里头去看个究竟，才能确保不出问题？所以说，接口之间并不会完全泾渭分明，总会相互干涉一些。单元测试的思想是“头痛医头、脚痛医脚”，这显然是有局限性的。\n单元测试是用来debug的，可要是单元测试本身有bug那就很难受了。一旦出了问题，不仅要检查被测试的函数，还要检查测试代码，岂不是很尴尬？（小声BB：其实是因为我自己太菜了，写啥都出bug）\n编数据真的好烦！一不小心编错了更烦！一个结构体50多个属性！一个个地编！编几十个！属性之间有关联还不能乱编！改一个属性得把它关联的全都改了！自己写生成算法又太麻烦了（写错了怎么办？你来帮我debug这个用来为debug函数生成debug数据的函数吗？）\n","permalink":"https://daichao1997.github.io/posts/tech/2018-09-14-%E5%AD%A6%E4%B9%A0%E7%BC%96%E5%86%99%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/","summary":"\u003cp\u003e这两周的工作主要是给自己之前写的代码填坑，也就是写单元测试。\u003c/p\u003e\n\u003ch2 id=\"我做单元测试的方法\"\u003e我做单元测试的方法\u003c/h2\u003e\n\u003ch3 id=\"裸测很难搞\"\u003e裸测很难搞\u003c/h3\u003e\n\u003cp\u003e单元测试（unit testing）是指对软件中的最小可测试单元进行检查和验证，而我要测试的内容是一些用Go语言写的接口，大概长这个样子：\u003c/p\u003e","title":"学习编写单元测试"},{"content":"Hackathon PKU 2018 5月12日，SBN、LYQ、JXT三位大佬带我参加Hackathon PKU。比赛于当晚21:00开始，来自中国各大高校的40支队伍展开了激烈角逐——还不到一个小时，咖啡厅里的零食便被一扫而空。偌大的双创中心传来阵阵键盘声，还有我怎么也连不上Wi-Fi的抱怨声。\n次日凌晨6点，我迎着清晨的第一缕阳光走回了寝室，又熟练地在12点醒了过来。然后……然后我就不记得了。debug本来就很痛苦，忍着困意debug就更痛苦了，更别说Wi-Fi经常在测试的时候断开。\n次次日下午1点，正式比赛终于结束，但各路评委还要一个个过来检查，给各个队伍的作品打分。\n哦？那打完分是不是就可以回去了？呵呵，还早着呢！打分环节的前10名可以光荣地进入下一环节，向全体同学和评委做专门的展示——对了，请自备PPT——然后再参加颁奖仪式，合照留念。\n听到这个消息，我真的佛了。早就听说别的组做的都是高大上的区块链、无人驾驶、机器人什么的，而我们组只做了一个简单的练耳软件，毫无亮点可言，而且最后一刻还在debug。既然第二轮注定与我无缘，那我坐在这里干啥？睡也不能睡，走也不能走，就留在这里等一个失败的消息？\n我耷拉着眼皮，看着专家们分为几路在队伍之间转啊转、停在某支队伍面前问啊问、有模有样地在小本本上写啊写，突然灵光一闪，决定搞点事情。我打起精神，整了整衣襟，拿起主办方发的记事本，来到了一支队伍面前，礼貌而稳重地问道：“同学你好，可以向我介绍一下你们的项目吗？”一个小伙子立刻说了句“您好”，并且热情地向我展示了他们的成果——基于区块链的新闻发布系统。\n我的天！区块链！新闻！系系系系系系统！我们的儿童玩具怎么可能与之匹敌！\n虽然内心受到了暴击，但我还是微笑地问道：“你们的项目代码量如何？用什么语言写的？”\n“大概三千行，用Go语言写的。”\nWhaaaaaat？三千行？Go、go、go、goooooo语言？我他喵为什么要来参加这个比赛？\n我笔挺地站着，让笔尖在记事本上游走了几下，微笑着说：“很不错！谢谢你们的介绍！”\n“谢谢老师！”\n嗯，真是个礼貌的小伙子。\n下午5点，第一轮的打分终于结束了，工作人员通知我们刚好排在第11名。正当我准备收拾东西走人时，工作人员又拦住我们，说之前计分有误，我们其实是第10名，让我们准备PPT。\n搞笑的是，虽然我做的是Echo Speaker上的应用，但我从头到尾没摸过音箱，展示时也来不及熟悉操作了。于是我上台先对大家说“我们做了一个智能音箱的技能，但是我们没有智能音箱”，然后拿出了电脑，用网页演示。\n反正最后莫名拿到了第六名，四人各赚二百五，据说是因为“完成度很高，接近实用”。原来朴实也有朴实的好处。\n一路靠肌肉记忆走回寝室，从19:00睡到12:00。醒来之后，发现那支被我“打分”的队伍加了我的微信说：“我当时真的以为你是评委。”\n对了，他们刚好是那场比赛的第五名，是来自重庆大学的四位大一才俊（现在大二了）。由衷佩服👍\n","permalink":"https://daichao1997.github.io/posts/life/2018%E5%8F%82%E5%8A%A0pku-hackathon/","summary":"\u003ch1 id=\"hackathon-pku-2018\"\u003eHackathon PKU 2018\u003c/h1\u003e\n\u003cp\u003e5月12日，SBN、LYQ、JXT三位大佬带我参加Hackathon PKU。比赛于当晚21:00开始，来自中国各大高校的40支队伍展开了激烈角逐——还不到一个小时，咖啡厅里的零食便被一扫而空。偌大的双创中心传来阵阵键盘声，还有我怎么也连不上Wi-Fi的抱怨声。\u003c/p\u003e","title":"参加PKU Hackathon"},{"content":"30元一晚上的火车站旅馆 七月初去青海的途中，我突然决定凌晨两点在西安下车，然后转车去上海。\n下车后第一件事情，自然是找一个过夜的地方，但你一定料到了－－火车站周围的旅馆早已住满，只剩下几百元一晚上的星级酒店。\n人生中第一次，我将目光投向了那群举着“住宿”牌子到处招揽顾客的大妈。\n果然，一位热心的大妈立马迎了过来，但还是重复着那句“小伙子要不要住宿”。我极为高冷地问了问价格－－只要30元一晚上。这其中的思想斗争我就不说了。总而言之，大妈又把我带到了一个大叔跟前，让大叔带我去旅馆，而大叔要我坐上他的摩托。我非常警惕地拒绝了，并执意要步行去。一路上，我和大叔保持着10米以上的距离。大叔问我是不是学生，我很自然地否认了，并且努力装出一副不好惹的大人模样。\n大概走了五分钟，大叔把我带到了一栋老旧的居民楼跟前，顶上挂着一盏闪烁着“福源招待所”的灯牌。\n我走进去，没想到前台居然找我要了身份证。\n……什么？居然还要走正规程序？\n付好钱，大叔又带我上楼去找房间。这栋居民楼看似平平无奇，其实内部结构错综复杂，甚至还有用钢筋搭成的“跨楼通道”。大叔边绕边问了一句：\n“要不要叫个姑娘？”\n……\n果断拒绝后，我本已警惕的神经更加紧绷，不知道他会搞出什么骗钱或者讹诈的手段。\n屋子的全部，就是一盏老当益壮的吊灯，一张散发着淡淡清臭的木床，一台令人怀旧的坏电视，一座落满灰尘的电风扇，一扇几乎锁不上的木门，和一扇我不敢打开的玻璃窗户。\n“从三点待到七点，待到日出就走。”我躺在床上半闭着眼，告诉自己不要睡着，又拿出手机把自己的地理位置和照片发给了同学，以防极端情况发生。 在这漫长的四个小时里，门外偶尔还会传来那个大叔的脚步声和咳嗽声。我想起了小时候看的古装剧里坏蛋往屋子里吹迷药的情节，于是把行李箱怼在门口，还仔细检查了窗户有没有漏风。时值七月，空气逐渐闷热，但还好有勤劳的电风扇给我降温。这是我头一次面对真实的恐惧。\n……\n最终，这家旅馆只是充当了一处劣质的歇脚点，没有做什么害人的勾当，但我不知道自己为什么一开始就要冒这个险，为什么“明知山有虎，偏向虎山行”。也许我一开始决定在西安下车的时候，内心深处就已经决定要来一场华丽的冒险？也许突然造访这样一座陌生的城市，同时给了我冒险家的错觉和勇气？也许危险让我感觉到了生命的意义？我不知道，我只知道悲剧的是，我没有抢到几小时后去往上海的高铁票。\n我从来没有这样为太阳的升起而欣喜过。打起精神混进早起的人群，我头一次感受到嘈杂的美好。我走进一家永和豆浆－－多么美好的市场经济产物啊！全国统一的店面、菜单和支付方式，权威认证的食品质量！无论你是谁，无论你来自何方，看到这块招牌，你就可以放心地走进来，在这里扫除饥饿，开始新一天的生活！\n接下来呢？接下来应该去哪儿？难道要回学校，然后告诉自己白跑了一趟？有没有哪个地方能像永和豆浆一样，永远欢迎我的到来？\n“喂，妈，我今天可以回来吗？”\n","permalink":"https://daichao1997.github.io/posts/life/2018%E5%9C%A8%E8%A5%BF%E5%AE%89%E8%BF%87%E5%A4%9C/","summary":"\u003ch1 id=\"30元一晚上的火车站旅馆\"\u003e30元一晚上的火车站旅馆\u003c/h1\u003e\n\u003cp\u003e七月初去青海的途中，我突然决定凌晨两点在西安下车，然后转车去上海。\u003c/p\u003e\n\u003cp\u003e下车后第一件事情，自然是找一个过夜的地方，但你一定料到了－－火车站周围的旅馆早已住满，只剩下几百元一晚上的星级酒店。\u003c/p\u003e","title":"在西安过夜"},{"content":"你好，我是 Dai Chao。\n这个站点用来记录一些技术笔记（操作系统、算法、源码剖析等）和生活随笔。\nPut your faith in the Light!\nGitHub：@daichao1997 Email：fordring866@gmail.com ","permalink":"https://daichao1997.github.io/about/","summary":"关于我","title":"关于"}]