读《控糖革命》

你是否经常在午饭后感到困倦、脑子转不动?是否明明吃了很多甜食,却依然觉得“细胞在挨饿”? 我就有这样的困扰。而且我爸爸,奶奶都有糖尿病、高血压,加上我有高尿酸,所以我一直有在关注血糖相关的知识。 最近读完了一本深度改变我饮食观的书——《控糖革命》。作者杰西·安佐佩斯(Jessie Inchauspé)通过科学的角度揭示了一个核心真相:比起计算卡路里,控制“血糖峰值”才是维持健康、保持身材和延缓衰老的关键。 以下是我整理的本书精华,带你重新认识身体里的“糖”。 一、 溯源:植物是如何“造糖”的? 在进入控糖技巧前,我们先看大自然的魔法。植物通过光合作用产生葡萄糖,并根据需要将其转化为三种形态: 淀粉:葡萄糖的储存形态。 纤维:虽然人类无法消化,但它是肠道的守护者,能极大缓冲糖分的吸收。 果糖:比葡萄糖甜2.3倍,是植物吸引动物吃下果实,从而散播种子的诱饵。 正是这些形态的不同,决定了食物进入人体后不同的“命运”。 二、 血糖峰值:身体隐形的“杀手” 人体摄入糖分后,血糖会升高再降下,形成一个“波峰”。这个峰值越高,对身体的伤害就越大。 当血糖剧烈波动时,身体会陷入以下困境: 氧化应激:产生大量自由基,攻击细胞,诱发心脏病、二型糖尿病及认知下降。 糖化反应:糖分与蛋白质结合产生AGEs(糖化终产物),这是皮肤松弛、长皱纹、暗沉发黄的元凶。果糖的糖化速度是葡萄糖的 10 倍。 线粒体“罢工”:细胞忙于处理过载的葡萄糖,无法有效转化能量,导致你出现“晕碳”和疲劳感。 三、脂肪的秘密:为什么果糖更容易胖? 人体处理葡萄糖的过程如下: 肝脏转化:葡萄糖在经过肝脏时会转化为糖原,肝脏以此形态储存一部分葡萄糖 肌肉储存:我们的肌肉也可以储存糖原形态的葡萄糖 转化为脂肪:如果在肝脏和肌肉储存完糖原后,体内还有更多的葡萄糖,就需要把它转化成脂肪,储存在肝脏或肌肉中 但果糖更加霸道:它无法转化为糖原储存,唯一的去处就是直接转化成脂肪。这就是为什么甜食(含果糖)比单纯的面食(只含葡萄糖)更容易让人发胖的原因。 此外,高频率的血糖峰值会导致胰岛素抵抗。只有在胰岛素水平较低时,身体才能有效燃烧脂肪。 四、 9个实操技巧,平滑你的血糖曲线 控制血糖不代表要戒绝一切,而是要讲究“策略”,书中介绍了许多控糖技巧,我整理如下: 调整饮食顺序(核心技巧):按照 纤维(蔬菜)→ 蛋白质/脂肪 → 淀粉/糖的顺序进食。纤维像在小肠铺了一层滤网,能有效减缓糖分的吸收。 餐前先吃点蔬菜:作为开胃菜,提前建立纤维屏障。 停止死磕卡路里:100 卡路里的果糖和 100 卡路里的蛋白质对身体的代谢影响完全不同。 打造“控糖早餐”:早餐要有蛋白质和纤维,拒绝高碳水和果汁(打碎的水果失去了纤维阻挡)。 警惕代糖:阿斯巴甜、麦芽糖醇等会误导胰岛素分泌;如果非要用代糖,建议选择赤藓糖醇、罗汉果甜苷或甜叶菊。 餐后吃甜点,而非单独吃:有正餐垫底,糖分吸收会更慢。 餐前喝点醋:醋酸能暂时抑制淀粉酶活性,减缓转化速度。推荐用油醋汁代替酸奶酱。 餐后动一动:哪怕只是散步,也能帮助肌肉消耗掉多余的葡萄糖。 给甜食找个“伴”:吃甜食时,搭配点坚果(蛋白质)或蔬菜(纤维),能平滑血糖曲线。 五、结语 《控糖革命》带给我们的最大启发是:健康的身体,不在于极端的节食,而在于对代谢规律的尊重。 当你学会通过调整进食顺序、利用纤维和醋等简单工具来抚平血糖波动,你会发现:精力变好了,皮肤亮了,甚至连身材也自然而然地轻盈了。 从下一餐开始,先吃那盘蔬菜吧!

2026/2/10
articleCard.readMore

理财学习笔记(2):不懂不投

这是本系列的第 2 篇,主题是:不懂不投。 我们刚开始投资理财的时候,通常会寻求以下这些方法来找到投资标的。 常见的错误办法 1、问朋友。我们通常会问那些看起来投资理财收益比较高的朋友,问他们应该买什么股票。 对于朋友推荐的股票,我们通常会“无脑”买入。但如果有一天,股票突然大幅回撤,我们通常就会陷入恐慌。我们会怀疑:这个朋友到底靠不靠谱?他之前赚钱是靠运气,还是因为现在判断出了问题?接着,我们就会陷入各种猜忌、焦虑和紧张中,最后甚至睡不着觉。如果股票持续下跌,我们甚至可能割肉离场。所以说,跟着朋友买其实并不那么靠谱。 2、看走势。我们可能会去看某些股票或基金的历史走势。看到它在过去三年或五年涨得很好,我们就买入。这也是理财 App 或者某些理财经理推荐的首选理由:它过去 X 年涨幅 XX,排名 XX。 但这很容易陷入“价值陷阱”,比如: 周期性误判:有些股票仅仅是在某个周期内表现优秀。比如房地产在过去十年涨得很好,但这并非因为单体公司有多好,而是因为当时整个大环境让所有房企都很赚钱。如果你仅仅因为过去业绩好而买入,一旦遭遇经济下滑或泡沫破裂,就会面临巨大的损失。 均值回归陷阱:很多股票或基金某年表现出色,仅仅是因为那一年的风格与它匹配。所有行业都有“大小年”之分,未来遇到“小年”时,表现自然就会变差。我把这叫做“均值回归”。 这就好比考试:你的平均水平可能是第三名。发挥好的时候能考第一名,发挥不好则可能掉到第五名,但你始终是在第三名上下徘徊。 很多基金经理或股票的表现也是在自身价值上下震荡。如果你在高点买入,在回撤时就会损失惨重,甚至被深套。 3、跟风。跟风是 A 股散户的常见操作,某个时间什么热,就跟风买什么,涨了就快速卖掉,主打一个击鼓传花,赌谁是最后接盘的大傻子。 这种情况下,我们假设你的胜率是 50%。每次获胜挣 20%,每次赌失败亏 20%。如果你进行了 10 次这样的操作,那你整体的收益期望就是 (1.2^5)*(0.8^5)=0.82,所以你折腾了半天,最后 1 块钱的本金变成了 0.82 元。 当然,如果有人认为自己跟风总是赢,这也是有可能的,但是因为自己不敢长期持有,只要涨一点点就卖,其实每次挣的是一点点收益。但是如果偶尔遇到亏损的时候,自己舍不得卖掉,就会一次亏很多。做这种短线操作的人,需要极强的止损纪律,大部分人也是很难做到的。 不懂不投 所以回到股票投资,我觉得投资理财一定要自己懂才行。如果你完全不懂或一知半解,这些都会成为你的陷阱。因为: 心理层面:不懂的人往往“拿不住”。当股票大幅下跌时,无论是否割肉,你都会极度焦虑、睡不好觉,担心本金损失。 投资层面:如果你懂,面对下跌说不定还能逆势加仓;即便不加仓,至少能睡个好觉。 此外,世界上还有很多投资陷阱。有些人甚至专门为“制造陷阱”而生,比如搞资金盘、割韭菜或传销。这些行为有些是非法的,有些则游走在法律边缘。如果大家没有能力分辨这些陷阱,很容易就在投资理财中遭遇严重的亏损。 小结 小结一下,常见的错误投资: 问朋友。其实本质上信的是朋友的业绩,朋友如果业绩下滑,就会怀疑。 看走势。其实本质上是用过去业绩替代未来判断,不靠谱。 跟风。纯投机,50% 胜率下期望是负的。 心理层面,只有懂了,才可能拿得住,睡得着觉。 另外,真正懂也可以避免很多骗局。 以上。

2026/1/29
articleCard.readMore

理财学习笔记(1):每个人必须自己懂理财

序言 我打算系统性整理一下这几年投学习投资理财的心得。因为一方面通过总结,可以让自己进一步加深对投资的理解。另一方面我也想分享给同样想学习理财的读者们。 我的女儿虽然还在读小学,但我也给她报了一个针对小学生的财商课。她对理财非常有兴趣,我也想通过这一系列的文章,给她分享她爸爸的理财成长经历。 这是本系列的第 1 篇,主题是每个人必须自己懂理财。 我身边的案例 我是 80 年代出生的,不得不说,我所处的是那个年代是缺乏理财和财商教育的。因此,我发现我身边的人大多不具备优秀的理财能力。 下面我举几个身边朋友的真实例子。 朋友 A: 他都把挣到的钱存银行定期或者余额宝。但是在现在这个年代,收益率是非常低的,只有一点几。但是他非常胆小,怕买其他的产品会导致亏损,所以说不敢碰。 朋友 B: 朋友 B 买了很多基金。但是他胆子很小,每个只买 1000 - 5000 块钱。然后账户里面有着几十只基金。既看不过来,也不知道应该如何操作。 唯一好的一面是:不管任何行业有行情,他都有一只基金命中。这让他的错失恐惧症(FOMO)小了很多。 朋友 C: 我这个朋友之前在快手上班,在 P2P 盛行的年代,把自己的所有积蓄都投在 P2P 上,最后爆雷,损失惨重。 朋友 D: 这个朋友通过另外一个朋友了解到有一个股票正在做庄阶段,未来会大涨,于是就听信买入,最后损失了 90%。 朋友 E: 朋友 E 的大学同学有一个在香港卖保险,于是听朋友的推荐在香港买了很多保险。但是过了 5 年,他发现收益率和最初承诺的相差非常大。这个时候看合同才发现,合同上写的收益测算并不保证。但是现在赎回的话,只能拿到非常少的本金,所以他只能继续硬着头皮每年交钱。 只有理解才能有效持有 听完上面几个朋友的故事,你身边有类似的朋友吗? 我跟一些朋友交流,我问他们,你们为什么不自己先学习投资理财的知识,之后再去做相关的操作呢?他们很多回答说,这个事情太专业了,专业的事情交给专业的人做就可以了。 当我反问他们:假如你买了一个专业人士管理的基金,那你对他的信仰来自于哪呢?你其实对他每个月发的报告并没有完全的判断能力,你只能选择相信他。 大多数时候,你其实相信的是他过去的业绩。如果它连续三年、连续五年一直都盈利,或者有超额收益,你就会持续持有它,甚至买入更多。 如果它连续几年亏损或者某一年大额亏损,你就会质疑它,甚至赎回它。 你的信心其实就是来源于过去的业绩表现。那这和散户的追涨杀跌有什么本质区别呢? 在你持仓持续下跌的那些时间,你能睡好觉吗?如果你不能理解它,那显然不能。 所以我说,每个人必须懂投资理财。 只有你深刻理解了你买入的是什么,才能在它下跌的时候有信心继续持有它,甚至抄底,才能睡得着觉。 小结 每个人都必须懂理财。因为银行的定期存款利率太低,而其他理财产品都需要深刻理解,才可能做到长期持有。 另外,社会上充斥着像 P2P 一类的产品,以及宣传这类产品的巧舌如簧的销售。他们不断地诱惑着我们,如果我们没有辨识能力,也可能将自己辛苦一辈子挣到的钱损失掉。 以上。

2026/1/24
articleCard.readMore

CSPJ 教学思考:背包问题

引言 背包问题是动态规划中的经典问题,也是 GESP 六级必考的知识点。其原理虽然需要花一些时间,但大多数孩子都能掌握,但是到了具体的题目时,因为背包问题变化较多,就不那么容易写出代码来。 本文将试图把背包问题的各种考法都列举出来,帮助大家巩固练习。 背包问题 背包问题之所以叫这个名字,是因为其背景故事是:往一个容量有限的背包里面,放入一些物品。每个物品有不同的体积大小,所以会占用相应的背包的容量。物品不能被分割,所以要么整个放入背包中,要么不放入。我们需要找出放入背包的价值最大的方案。 举一个简单的例子,背包容量是 10L: 物品 1:体积 7 L,价值 8 物品 2:体积 5 L,价值 5 物品 3:体积 4 L,价值 4 虽然物品 1 的价值最大,价值/体积(即单位体积的价值)也最大,但是因为放入物品 1 之后,剩余的空间 3L 无法再放入别的物品而浪费掉了。就不如不放物品 1,而放入物品 2 和物品 3 带来的总价值大。 由此我们也能看出,背包问题不能用简单的贪心来解决,而需要用动态规划。 解题思路 背包问题的转移方程可以被优化为一维,但为了方便理解,我们先看没有优化的版本。我们定义: 每个元素的体积为 a[i],价值为 v[i]。 dp[i][j] 表示用前 i 个物品,放入容量为 j 的背包时,所能达到的最大价值 那对于第 i 个物品,如果我们已经知道了前面的结果,那么我们有两种选择: 不放入 第 i 个物品,这样 dp[i][j] = dp[i-1][j] 放入 第 i 个物品,这样 dp[i][j] = dp[i-1][j-a[i]] + v[i] 而以上就是状态转移方程,我们在上面两种情况下取最优的情况:dp[i][j] = max(dp[i-1][j], dp[i-1][j-a[i]] + v[i]) 。 另外我们需要考虑一下初始化的情况,即 dp[0][1~n] 应该怎么赋值。因为前 0 个物品什么都没选,那么价值肯定都是 0,所以让它们都等于 0 即可。 将以上逻辑写成代码如下: 1 2 3 4 5 6 7 memset(dp, 0, sizeof dp); for (int i = 1; i <= 3; ++i) for (int j = 1; j <= 10; ++j) { dp[i][j] = dp[i-1][j]; if (j-a[i]>=0) dp[i][j] = max(dp[i][j], dp[i-1][j-a[i]] + v[i]); } 在这段代码中,为了保证 j-a[i] 的值为正,加了一个 if 来检查,保证没有下标越界的代码。如果下标越界,有可能会读取到随机值,也可能读取到非法地址,造成运行异常(Runtime Error)。 我们再用刚刚的例子来做一下表格演示:背包容量是 10L。 物品 1:体积 7 L,价值 8 物品 2:体积 5 L,价值 5 物品 3:体积 4 L,价值 4 经过转移方程的计算,最终,我们可以填出下面这个二维表格,表格中的每一项都计算出来了用前 i 个物品,体积为 j 时的最优化方案。这也是符合动态规划的最优子结构的特征。 01 背包 所谓的 01 背包,就是指物品的数量只有 1 个,只有选与不选两种方案。刚刚的例子就是一个 01 背包的例子。 我们发现 dp[i][j] 只与两个值相关 dp[i-1][j] 和 dp[i-1][j-a[i]],这样的二维数组利用的效率很低。所以,我们就想到,能不能把第 i 维省略掉,这样可以节省存储空间(但没有节省运算时间)。 压缩后的代码如下: 1 2 3 4 5 memset(dp, 0, sizeof dp); for (int i = 1; i <= 3; ++i) for (int j = 10; j >= a[i]; --j) { dp[j] = max(dp[j], dp[j-a[i]] + v[i]); } 我们注意到,j 的循环方式从正序变成了逆序。之所以要这么操作,读者可以用表格的方式,把正着循环的结果填一下就能明白。 如果 j 不是倒着循环,在一轮 j 的循环过程中,dp[j] 的值会在修改后,再一次被访问到,这样就会使得一个物品实际上已经计算了放入的价值,又被重复计算第二次。 完全背包 一个物品被多次重复放入和重复计算价值,其实是我们在完全背包问题中需要的效果。所以,刚刚的代码,如果我们把 j 正序循环,就是完全背包的代码,如下所示: 1 2 3 4 5 memset(dp, 0, sizeof dp); for (int i = 1; i <= 3; ++i) for (int j = a[i]; j <= 10; ++j) { dp[j] = max(dp[j], dp[j-a[i]] + v[i]); } 但是为了方便理解,我们还是把完全背包的非压维代码也一并看一下: 1 2 3 4 5 6 7 8 9 memset(dp, 0, sizeof dp); for (int i = 1; i <= 3; ++i) for (int j = 1; j <= 10; ++j) { dp[i][j] = dp[i-1][j]; if (j-a[i]>=0) { dp[i][j] = max(dp[i][j], dp[i-1][j-a[i]] + v[i]); dp[i][j] = max(dp[i][j], dp[i][j-a[i]] + v[i]); } } 因为 dp[i][j-a[i]] >= dp[i-1][j-a[i]],所以以上代码可以省略成: 1 2 3 4 5 6 7 8 memset(dp, 0, sizeof dp); for (int i = 1; i <= 3; ++i) for (int j = 1; j <= 10; ++j) { dp[i][j] = dp[i-1][j]; if (j-a[i]>=0) { dp[i][j] = max(dp[i][j], dp[i][j-a[i]] + v[i]); } } 我们可以记住这个写法,因为后面有一些题因为各种情况可能无法压维,就会需要这种写法。 我们还是用刚刚的例子来填写二维表格,背包容量是 10L。物品数量改为无限。 物品 1:体积 7 L,价值 8 物品 2:体积 5 L,价值 5 物品 3:体积 4 L,价值 4 以下是填写出来的值: 题目变为完全背包后,可以看到最后答案变了,最优方案变成了放入两个物品 2,得到最大价值 10。 学习完以上内容后,可以让学生练习以下两道题: 题目名说明 P1048 采药01 背包问题。NOIP2005 普及组第三题 P1616 疯狂的采药完全背包问题 以下是更多的背包基础练习题: 题目名说明 P2871 Charm Bracelet S01 背包, USACO 07 DEC P1802 5 倍经验日01 背包 P1060 开心的金明01 背包,NOIP 2006 普及组第二题 P1049 装箱问题01 背包,NOIP2001 普及组 P2639 Bessie’s Weight Problem G01 背包变型,容量与价值相同 P13015 学习小组完全背包,GESP 202506 六级 P10721 计算得分背包问题变种,GESP 202406 六级 P1926 小书童——刷题大军01 背包,需拆成两个子问题 多重背包 多重背包描述了这样一种场景,一个物品将同时受两个限制条件的制约,例如:一个背包,即有体积限制,又有重量限制,让你往里放物品,求最大化物品价值的放法。 P1794 装备运输 就是多重背包的一道典型例题,在题目中,每件武器有体积和重量两个限制条件。 对于多重背包,我们同样用前 i 个物品来划分阶段: dp[i][j] 表示 i 体积 j 重量下的最大火力。 转移方程:dp[i][j] = max(dp[i][j], dp[i-v[k]][j-g[k]] + t[k]); 同理,如果物品的数量是无限的,则正着 for,如果物品的数量是有限的,则倒着 for。 P1794 装备运输 的参考代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <bits/stdc++.h> using namespace std; int V, G, N, dp[510][510], v[510], g[510], t[510]; int main() { cin >> V >> G >> N; for (int i = 1; i <= N; ++i) cin >> t[i] >> v[i] >> g[i]; for (int k = 1; k <= N; ++k) for (int i = V; i>= v[k]; i--) for (int j = G; j >= g[k]; j--) dp[i][j] = max(dp[i][j], dp[i-v[k]][j-g[k]] + t[k]); cout << dp[V][G]; return 0; } 如果把 01 背包和完全背包想像成填一个一维的表格,那么多重背包就在填一个二维的表格。我们需要保证表格的填写过程符合动态规划的阶段性,表格总是从一个方向往另一个方向填,填过的数字不会再次被修改(在没压维的情况下),这样才能保证状态无后效性。 动态规划题目能够划分出清晰的阶段,后一个阶段只依赖于前面的阶段,问题就解决了一大部分。 可供练习的题目如下: 题目名说明 P1794 装备运输多重背包 P1910 L 国的战斗之间谍多重背包 P1855 榨取kkksc03多重背包 P2663 越越的组队非多重背包的 DP 背包变型一:物品的相互依赖 P1064 金明的预算方案 描述了一种背包问题的变型:在此题中,物品不是简单的 1 个或多个,而是分为主件或附件,每个主件可以有 0 个、1 个或 2 个附件。 应该如何表示这种复杂的物品关系呢?其实,我们可以把物品的每种组合都枚举出来,因为附件数量最多为 2 个,所以情况就可以枚举出以下情况: 不选主件(当然也就没有附件) 选主件,不选附件 选主件+附件 1 选主件+附件 2 选主件+附件 1+附件 2 于是,我们就可以在处理主件的时候,把以上几种情况都比较一下,选最优的方案。 参考代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <bits/stdc++.h> using namespace std; struct Node { int m; int w; int t; }; int n, m; vector<Node> va; vector<vector<Node> > vb; int dp[40000]; void updateDP(int i, int m, int w) { if (i-m >= 0) { dp[i] = max(dp[i], dp[i-m] + w); } } int main() { scanf("%d%d", &n, &m); va.resize(m); vb.resize(m); for (int i = 0; i < m; ++i) { Node node; scanf("%d%d%d", &node.m, &node.w, &node.t); node.w = node.w*node.m; va[i] = node; if (node.t != 0) { vb[node.t - 1].push_back(node); } } memset(dp, 0, sizeof(dp)); for (int i = 0; i < m; ++i) { // 只处理主件,附件与主体一并处理 if (va[i].t == 0) { for (int j = n; j > 0; j--) { // 选主件,不选附件 updateDP(j, va[i].m,va[i].w); // 选主件+附件 1 if (vb[i].size() > 0) { int money = va[i].m + vb[i][0].m; int weight = va[i].w + vb[i][0].w; updateDP(j, money, weight); } // 选主件+附件 2 if (vb[i].size() == 2) { int money = va[i].m + vb[i][1].m; int weight = va[i].w + vb[i][1].w; updateDP(j , money, weight); } // 选主件+附件 1+附件 2 if (vb[i].size() == 2) { int money = va[i].m + vb[i][0].m + vb[i][1].m; int weight = va[i].w + vb[i][0].w + vb[i][1].w; updateDP(j, money, weight); } } } } cout << dp[n] << endl; return 0; } 背包变型二:求最小值 有些时候,我们不是求背包能够装的物品的最大价值,而是求最小价值。例如 B3873 小杨买饮料 这题,此题我们可以把饮料的容量当作背包的容量,把饮料的价格当作价值,但是此题相对于标准的背包问题有两个变化: 1、题目希望求最小的费用,相当于背包所装的物品价值需要最低。 2、题目给定的背包容量不固定,而是“不低于 L”。 针对以上的变化,我们的状态定义虽然不变,用 dp[i][j] 表示前 i 种饮料在 j 容量下的最小价值,但是状态转移变成了: dp[i][j] = min(dp[i-1][j-l[i]] + c[i], dp[i-1][j]) 在这种情况下,初始的第 0 种饮料什么都喝的值为 0,即:dp[0][0] = 0。 但是其它的值就不能设置成 0 了,如果设置成 0,那么任何情况下 dp[i][j]就已经是最小的值了,就不能被更新了。我们需要把 dp[i][j]默认的值设置成“无穷大”,这样才可能更新出有意义的值。 在设置无穷大这件事情上,有一个使用 memset 的技巧,即:memset(dp, 0x7f, sizeof dp);,此技巧将每个字节都填充成了二进制的 01111111(即 0x7f),因为最高为是符号位,所以保留成 0。这种 memset 技巧虽然初始化的值比 INT_MAX 略小一点,但是写起来更快,另外在进行加法运算的时候,也不用担心结果溢出成负数。 以上方案解决了变化一。我们再来看变化二。 变化二使得答案不一定在 dp[i][L],因为答案不一定是刚好 L 升,所以要取 L ~ L+max(l[i]) 这一段范围。这样就解决了变化二。 最后我们用滚动数组压维,然后因为是 01 背包(每个饮料只能选一次),我们压维之后需要倒着 for 循环背包大小。 以下是参考代码,代码中用 STL 的 min_element 来求最小值,读者也可以参考这种写法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 /** * 01 背包问题的变化 * * 假设第 i 种饮料的费用是 c[i], 容量是 l[i] * dp[i][j] 表示用前 i 种饮料,凑成 j 升的最小费用。 * * 则,转移方程为: * - dp[i][j] = min( dp[i-1][j-l[i]] + c[i] , dp[i-1][j] ) * * 因为 i 只与 i-1 相关,所以这一层可以压缩。转移方式优化为: * - dp[j] = min(dp[j- l[i]] + c[i], dp[j]) * * 初使化: * - dp[0] = 0; * - dp[1-L] = memset(0x7f) * * 其它: * - 倒着 dp,因为每种饮料只能用一次 * - 最大值检查了一下,不会超 int,就不用 long long 了 * - 因为答案不一定是刚好 L 升,所以要取 L ~ L+max(l[i]) 这一段范围 * - 因为是取最小值,所以初使化设置成 0x7f7f7f7f(接近 21 亿,但是又没到 INT_MAX), * 这样运算不会超 int,又可以是较大值 * * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; int dp[1010000], c[550], l[550], N, L, maxL; int main() { ios::sync_with_stdio(0); cin >> N >> L; for (int i = 0; i < N; ++i) { cin >> c[i] >> l[i]; maxL = max(maxL, l[i]); } maxL += L; memset(dp, 0x7f, sizeof dp); dp[0] = 0; for (int i = 0; i < N; ++i) { for (int j = maxL; j - l[i] >= 0; --j) { dp[j] = min(dp[j], dp[j - l[i]] + c[i]); } } // 因为答案不一定是刚好 L 升,所以要取 L ~ L+max(l[i]) 这一段范围 int ans = *min_element(dp+L, dp+maxL+1); if (ans == 0x7f7f7f7f) cout << "no solution" << endl; else cout << ans << endl; return 0; } 以上代码虽然解决了问题,但是还有一点不完美,就是 dp 数组实在太大了。有没有可能 dp 数组更小呢?我们可以想到,因为每种饮料的价格都是正数,所以,如果有一个答案是超过 2*L 升的情况,同时它的价格极低,这种情况下,我们的答案就是只喝这一种饮料。不会出现超过 2*L 升,我们还叠加喝了两种饮料的情况。 我们可以反证:假如有一个答案是喝两种饮料,总容量超过 2*L 升,那么必定有一个饮料的容量是大于等于 L 升的。那么,我们只喝那个大于等于 L 升的饮料,肯定总价格更低。 所以,我们的优化方案就是:我们只需要把 dp 数组的大小开到 2*L 即 4000 即可(题目规定 L 最大为 2000)。在此优化方案下,我们再特判一下每个大于 L 升的饮料,看是不是更便宜。 以下是参考代码,时间和空间复杂度都更优: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <bits/stdc++.h> using namespace std; int dp[4100], c[550], l[550], N, L; int main() { ios::sync_with_stdio(0); cin >> N >> L; for (int i = 0; i < N; ++i) { cin >> c[i] >> l[i]; } memset(dp, 0x7f, sizeof dp); dp[0] = 0; for (int i = 0; i < N; ++i) { for (int j = 4000; j - l[i] >= 0; --j) { dp[j] = min(dp[j], dp[j - l[i]] + c[i]); } } int ans = *min_element(dp+L, dp+4000); // 如果单个饮料就可以超 L,则判断一下 for (int i = 0; i < N; ++i) if (l[i] >= L) ans = min(ans, c[i]); if (ans == 0x7f7f7f7f) cout << "no solution" << endl; else cout << ans << endl; return 0; } 小结:对于求最小值的背包问题,除了 dp[0][0] = 0 外,我们需要把别的初始值设置为 0x7f,以保证递推求 min 的过程中,每个 dp 数组值可以得到更新。 相关的练习题目还有: P2918 Buying Hay S P1679 神奇的四次方数 背包变型三:求平均值 有一类题,虽然看着不像是背包问题,但是最后可以抽象成背包问题。而且,他们背包大小都是 sum/2。 P2392 考前临时抱佛脚 就是一道典型的例题。 在此题中,每一科的复习都可以看成两个并行的任务,而任务最短的时间就是让一个任务的时间尽可能接近 sum/2。这样,我们就可以把 sum/2 当成背包的容量,把每道题的价值和体积看成相等即可。 因为在本题中,sum 最大值为 20*60 = 1200,所以背包大小最大是 600 即可。 参考代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 /** 此题可以用动态规划,也可以用搜索,因为每科只有最多 20 个题目,所以搜索空间最大是 2^20 等于约 100 万。 用动态规划解题时,此题可以把每次复习看作一次 01 背包的选择。每道题的价值和成本相同。背包的目标是尽可能接近 sum/2,因为sum 最大值为 `20*60 = 1200`,所以背包大小最大是 600。 */ #include <bits/stdc++.h> using namespace std; int s[4], v[25], ans, dp[610]; int dpAns(int n) { int cnt = 0; for (int i = 0; i < n; ++i) { cnt += v[i]; } int m = cnt / 2; memset(dp, 0, sizeof(dp)); for (int i = 0; i < n; ++i) { for (int j = m; j>=v[i]; --j) { dp[j] = max(dp[j], dp[j-v[i]] + v[i]); } } int ret = max(dp[m], cnt - dp[m]); return ret; } int main() { scanf("%d%d%d%d", s, s+1, s+2, s+3); for (int i = 0; i < 4; ++i) { memset(v, 0, sizeof(v)); for (int j = 0; j < s[i]; ++j) { scanf("%d", v+j); } ans += dpAns(s[i]); } printf("%d\n", ans); return 0; } 相关的练习: 题目名说明 P12207 划分01 背包的变型,蓝桥杯 2023 国 背包变型四:计数 有一类背包问题,不是问你最大的价值,而是问你相关的计数。 例如:P1832 A+B Problem 就是其中的典型例题。 要解此题,我们可以先把质数算出来保存下来,接下来,我们需要用背包的思路,对表格进行计数: dp[i][j] 表示用前 i 个质数组成 j 一共的可能数 转移方程:dp[i][j] = dp[i-1][j] + dp[i-1][j-a[i]] + dp[i-1][j-a[i]*2]... 参考代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 /** * 解法: * - 无穷背包 * - 先把质数算出来,保存在数组里面 * * dp[i][j] 表示用前 i 个质数组成 j 一共的可能数 * dp[i][j] = dp[i-1][j] + dp[i-1][j-a[i]] + dp[i-1][j-a[i]*2]... * * 初始化:dp[0][0] = 1 * * 注意:答案需要用 long long 保存。 * * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; #define MAXN 1010 int a[200]; long long dp[200][MAXN]; bool isPrime(int a) { for (int i = 2; i*i<=a; ++i) if (a%i == 0) return false; return true; } int main() { int n; cin >> n; for (int i = 2; i <= n; ++i) if (isPrime(i)) a[++a[0]] = i; dp[0][0] = 1; for (int i = 1; i <= a[0]; ++i) // 第 i 个质数 a[i] for (int j = 0; j<= n; ++j) { // 组成 j 这个值 for (int p = j; p >= 0; p -= a[i]) { // 完全背包,试着放 0-n 个 a[i] dp[i][j] += dp[i-1][p]; } } cout << dp[a[0]][n] << endl; return 0; } 背包变型五:负数体积 有一些题目,元素的体积会是负数。 P13018 调味平衡 就是一道典型的例题。它也是 GESP 202506 七级题目。 第一种解法(空间占用过大) 用上面提到的计数类的方法。 dp[i][j][k] 表示前 i 种食材,达到酸度 j,甜度 k 是否可能。 dp[i][j][k] = dp[i-1][j-a[i]][k-b[i]] 是否可能。 把 i 这一层简化dp[j][k] = dp[j - a[i]][k - b[i]] 初始化:dp[0][0] = 1 但是以上的方法时间和空间消耗(500000x500000)太大。 正确的解法 考虑到可以把一种食材的酸度和甜度求差,得出酸和甜的差值。如果两种食材的差值加起来为零,则刚好酸度=甜度。 这样就可以把 dp 简化。 dp[j]表示前 i 种食材的酸甜度差值 j 是否存在,如果存在,其值为酸甜度的和。 dp[j] = dp[j - dif[i]] + a[i] + b[i] 相当于背包元素的体积变成了差值,价值变成了 a[i] + b[i]。 因为 dif[i] 有正有负,所以为了保证值不会覆盖,我又恢复成二维的 dp: dp[i][j] 表示前 i 种食物,凑成 j 的酸甜度差的最大和。 因为 j 可能为负值,所以我们把平衡点设置成 50000(可以想像成刚开始差值就是 50000,求最后差值不变) 这样 j 中间最多从 50000 减成 0(因为每个食材差值最大为 500,最多有 100 个食材),所以不会变成负数。 参考代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /* * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; #define MAXN int(100*500+10) int dp[110][MAXN*2], n, a, b, c, d; int main() { ios::sync_with_stdio(0); cin >> n; memset(dp, 0x8f, sizeof dp); dp[0][50000] = 0; for (int i = 1; i <= n; ++i) { cin >> a >> b; c = a-b; d = a+b; for (int j = 0; j <= 50000*2; j++) if (j-c >= 0) dp[i][j] = max(dp[i-1][j], dp[i-1][j-c] + d); } cout << dp[n][50000] << endl; return 0; } 相关练习题目 除了以上的变化,更多变化的练习: 题目名说明 P1510 精卫填海01 背包,但是输出要求有变化 P2430 严酷的训练01 背包,题目较长,需要仔细读题 P11377 武器购买01 背包的变型,GESP202412 七级

2026/1/11
articleCard.readMore

2025 年个人总结

工作 2025 年是艰难的一年,上半年玩教具业务同比下滑,下半年尝试了一些营销推广,业务量有所稳定,但是赢亏承压。最后一年下来,虽然没亏钱,但是也没挣到钱。好在团队在持续成长,每个岗位的人都比去年有了长足的进步和成长。 2025 年也立项了好几个新项目,这些项目会陆续在 2026 年上线。前面的播种会积累到 2026 年的收获,所以未来怎么样,还是挺值得期待的。 读书 25 年一共读了 8 本书,以下是读书笔记: 个人投资的最佳实践 - 读《不落俗套的成功》 读《真需求》 投机与趋势投资 - 读《股票大作手操盘术》 但斌投资札记-读《时间的玫瑰》 理解大语言模型 - 读《图解 DeepSeek 技术》 理解 MCP -读《这就是 MCP》 和媒体共赢 - 读《广告的没落,公关的崛起》 读《疯狂的尿酸》 其中 《不落俗套的成功》 和 《广告的没落,公关的崛起》 是我今年最喜欢的两本书,一本书指导我开始更多关注配置,另一本书指导我如何做品牌。 25 年还开始订阅了纸质版本的《三联生活周刊》,加上雪球 App 每个月给我寄月刊。所以整个 2025 年的阅读量还是不小的。 感悟 今年思考人生,总结了 4 篇文章: 构建你的“多巴胺”系统 真相不重要 请为信息付费 我理解的保险产品 其中“多巴胺”系统那篇我自认为对我自己帮助最大,有效指导了我如何择友,如何工作,如何培养兴趣。 保险那篇文章,也让我理清了保险产品的购买思路,不再纠结要不要买保险,怎么买保险。 编程教学 今年继续在教小朋友编程,关于编程竞赛,今年又总结了如下文章: CSPJ 教学思考:贪心算法 CSPJ 教学思考:动态规划 CSPJ 教学思考:二分查找 CSPJ 教学思考:并查集 CSPJ 教学思考:模拟 CSPJ 教学思考:枚举 CSPJ 教学思考:数学题 CSPJ 教学总结:STL CSPJ 教学总结:深度优先搜索(DFS) CSPJ 教学总结:树状数组 GESP 核心考点 加上去年写的三篇,内容越来越丰富了: 五分钟弄懂 CSP-J CSPJ 教学思考:for 循环 CSPJ 教学思考:宽度优先搜索 财务 2025 是财务收获的一年。 首先是年初公司允许大家出让期权,于是我卖了一些期权,补充了一些现金流。 2025 年初赶个 Deepseek 横空出世,整个港股大涨,我刚好又在那个时候有一些港币,配置了一些港股资产,于是买中了恒生高股息率,比亚迪,工商银行等股票,原来持有的腾讯也大涨。 A 股这边,之前配置的建平从亏损变成浮盈;之前配置的 500 指数增强产品一下子也有 40% 的涨幅。 美股也不用说,标普和纳指继续涨,考虑到已经浮盈很多,我在 12 月将仓位全部清空了。 2025 年一直在定投黄金,最后也成功在一个相对低位(均价 700 多)配置了一定比例的黄金,现在也有 20% 的涨幅。 当然,2025 年配置的新能源组合也亏很多,特别是理想汽车,有 -30% 多的亏损。 更详细的业绩总结如下: 港股: 腾讯(00700),成本 374,现价 599,+60% 恒生高股息率(03110) 第一笔:成本 23,现价 30,+30% 第二笔:成本 31,现价 30,-2% 工商银行,买入 5.4,卖出 6.07,含分红赢利约 15% 新能源组合: 理想汽车,-30% 小鹏汽车,-4% 小米,1% 比亚迪,7% 美股: 标普和纳指均清仓,整体收益约 10% 私募 500 指增,+44% 建平远航,+72% A 股 招商银行,+11% 红利 ETF 组合,+2% 恒生科技 ETF,-6% 黄金: 成本 795,现价 1071,+34% 尝鲜 24 小时血糖仪 今年戴了两次 24 小时的血糖仪,每次佩戴 14 天。我整体感觉很受用,它可以监测到不同食物和运动对血糖的影响。 然后我理解了两个事情: 一个是我之前饭后犯困,我大概也猜到是晕碳,现在戴上之后明显就能确认是这个原因。而且我尝试调整饮食之后,因为血糖上升慢,就不犯困了。 第二就是理解了饭后散步和遛弯为什么重要,对比饭后坐着和散步的血糖走势,我一下子就发现散步对降血糖的意义了。所以饭后走个 10 来分钟其实对身体的帮助是很大的。 另外这东西还有一个好处,就是会促进我轻度运动。我现在天气好的时候会尽量骑车上下班,因为这样我的血糖走势会很漂亮。 另外,我对比不同时段吃同样的水果对血糖的影响,明显餐后的影响更小,如下图: 我也总结了一些吃饭顺序的经验: 同样吃一颗橙子,餐前和餐后血糖上升速度和峰值差异很大。优先餐后吃水果,不要空腹吃。 同样是吃饭,先吃蔬菜和后吃蔬菜升血糖速度也差别很大。先吃蔬菜,最后吃碳水。 面食对血糖的提升是迅速的,少吃为妙。 3D打印和建模 公司因为需要,采购了拓竹的 3D 打印机,于是我有一段时间就很痴迷它。拓竹真的是国货之光,把原来只能极客使用的 3D 打印机做成了可以普及的消费品的品质,一些配平等复杂设置都不需要人工介入,机器自动就能完成。 我顺便还在Tinkercad 上学习了建模,然后利用自己学到的建模知识,帮老婆做了一个定制款的精油瓶。它主要可以完美适配我们家的桌面,并且做得很紧凑,可以放更多的瓶子。最后我买了一种松木质感的耗材,这样放在桌子上也很搭。 以下是设计稿截图: 以下是效果: 骑车 今年狠狠地试了一下骑行,但是没有花钱买装备。一方面是因为我的运动量不大,频率不高;另一方面也是希望想骑的时候可以随时骑,想放弃的时候就可以放弃,没有任何压力,共享单车月卡在这方面还是挺方便的。 最狠的一次,我花了周末一个下午绕三环骑了一圈(下图,一共 50 公里)。不过冬天骑行的体验不佳,已经断了有一个月了,希望天气暖和之后可以继续骑起来。 旅行 新疆 五一去了新疆,很值得去的一个地方。我们租了一辆理想 L8,驰骋在满是风力发电站的草原上,感觉非常放松。 希腊 暑假去了希腊。 欧洲的衰败让人感叹时代和周期的变化,当地有很多没人住的空置房(下图:我们下榻酒店附近的一处空置房)。 猫咪在希腊很受人待见,大街和公园上有很多猫咪。 在希腊吃了几天各种西餐之后,最后父母还是忍不住选择中餐,结果我们就把希腊的中餐馆吃了个遍。希腊的物价在欧洲相对低,一个菜大概 80 元人民币左右。 在希腊我也租了一辆车,是一辆 9 座的现代混动 Staria MPV,也非常好开。我们还乘坐渡轮(下图)把车运到了扎金索斯岛,在岛上放松了好几天。 沈阳 十一去了沈阳,就一个感受,物价实在太低太低,锅包肉大概 20 多块钱。租了一辆比亚迪的宋 Pro 混动,油耗特别低,每百公里油耗大概只有 4L。下图是我实际驾驶的数据,因为这辆车只能慢充,所以我一直在亏电情况下行驶,并没有充过电。 这也让我明白了比亚迪为什么出海那么厉害,这么低的油耗在能源较贵的欧洲,其实很有市场。 25 年的目标回顾 工作:硬件稳中有增,图书赢亏打正。带好图书业务。 ❎ 硬件既不稳,也不增。 ✅ 图书实际达成 60 分吧。图书赢亏基本打正了,但是要说有多好,还完全谈不上。 理财:做好配置,找到能拿 10 年的标的,并能坚定持有。 ✅ 配置工作基本完成。先拿两年看看,看自己拿不拿得住。 个人:读 6 本书。编程教学继续累进。 ✅ 最后读了 8 本书。编程教学写了 11 篇总结。 26 年的目标 工作: 产品:今年种下的花,期待 26 年结果。AI 硬件拿到一些认知。 运营:实践《广告的没落,公关的崛起》。 理财: 不折腾,配置+再平衡。 个人: 读 6 本书。 俯卧撑能连续做 20 个。 骑车绕四环一圈。 个人 Milestone 好象没啥特别能说的,如果非要有什么,就是产出了 《构建你的“多巴胺”系统》 这篇文章吧。

2026/1/1
articleCard.readMore

读《疯狂的尿酸》

《疯狂的尿酸》是一本关于健康的科普书,来自于美国医学博士:戴维·珀尔马特,他是一位畅销书作家,写过《谷物大脑》和《菌群大脑》。 什么是尿酸 正常人体中的尿酸,2/3 是内源性的。尿酸是嘌呤的代谢产物,而嘌呤是细胞的重要组成部分,可以用来合成 DNA 和 RNA,人类的细胞因为不停地在分裂和衰老,死亡的细胞在被处理的时候就会产生尿酸。 另外 1/3 的尿酸来自于外部摄入的食物,包括动物内脏,海鲜,啤酒等。 果糖是一种特别的糖,它虽然不会造成血糖上升,但是会在代谢的时候产生尿酸。 尿酸会促进脂肪的产生 因为高尿酸与肥胖相关性很高,为了研究他们之间的因果关系,人们发现了“尿酸氧化酶”。这是一种存在于大多数动物体内的酶,能够迅速将尿酸排出体外,但是我们的人类祖先在几百万年的进化过程中,产生这个酶的基因被破坏了,变成了“假基因”。这就使得我们人类血液中的尿酸含量是其他哺乳动物的 3-10 倍。 当远古时代的人类吃下果糖后,果糖会在代谢过程中产生尿酸,而尿酸会打开人体的“脂肪开关”,帮助人体把果糖转化为脂肪。“从水果到脂肪”的生理机制帮助古代的灵长类动物能够度过漫长的、食物匮乏的冬天。 果糖 果糖是所有天然的碳水化合物中最甜的一种,天然的果糖只存在于水果和蜂蜜中,所以人类摄入得很少。而且水果中富含膳食纤维,可以延缓果糖被吸收的速度;而水果中富含的维生素 C 还有降低尿酸及促进尿酸排出的功能,所以吃水果对果糖的提升是很低的,代谢产生的尿酸也很少。 纯葡萄糖和果糖都是单糖(糖的最简单形式),而蔗糖是葡萄糖和果糖的组合,是一种双糖(两个分子连接在一起)。蔗糖进入人体后在小肠被分解,释放果糖和葡萄糖,然后被吸收。 果葡糖浆是一种以果糖为主的糖浆制品,果糖占比约 55%,葡萄糖占比 42%。最早是 1957 年由美国生物化学家 理查德·O 马歇尔 和 厄尔·R 科伊 生产出来,他们创造了一种酶,可以通过化学方法使玉米糖浆中的葡萄糖的结构重新排列,将其转化为果糖。 果葡糖浆从 20 世纪 70 年代开始流行,主要是因为其甜度比蔗糖高,价格又比蔗糖低,所以逐渐取代了蔗糖。到了 1984 年,可口可乐和百事可乐也都把各自品牌的饮料从添加蔗糖改为添加果葡糖浆。 果糖的升糖指数是所有天然糖中最低的,这意味着它不会直接导致血糖升高,也就不会刺激胰岛素的分泌,所以在一段时间内,人们把果糖视为一种“更安全”和“健康”的糖。但后来人们发现,相比于葡萄糖参与能量生成,果糖则参与能量储存,所以更容易让人肥胖。 果糖的代谢过程 果糖和葡萄糖除了一些化学键不同,其他结构几乎完全一样。然后,正是这微小的差异使得它们的代谢过程完全不同。 葡萄糖代谢的第一步(葡萄糖的磷酸化)是在葡萄糖激酶催化下分解,分解所释放的 ATP 也会在细胞中维持稳定的水平。ATP(三磷酸腺苷)是人体能量的来源。 果糖的代谢与葡萄糖完全不同。果糖在进入人体后,会迅速被血液吸收,然后被运输到肝脏中进行代谢。在肝细胞内,果糖激酶会开始工作,做出包括消耗 ATP 在内的一系列事情。果糖会消耗 ATP 的过程会带来一些下游效应,它会导致血液中的尿酸水平快速上升。由于果糖消耗了 ATP,细胞会发出信号:我们的能量快用完了。这会促使身体减缓新陈代谢以减少静息能量消耗。 除了消耗能量外,果糖还会触发脂肪的生成过程:肝脏中的果糖代谢会直接导致脂肪的产生:主要是以甘油三酯的形式存在,这是人体中最常见的脂肪存在形式。 AMP 活化蛋白激酶 AMP 活化蛋白激酶被激活时,它会向你的身体发出“狩猎状况良好”(即食物充足)的信号,你的身体就会让自己从储存脂肪转换为燃烧脂肪,帮助身体保持良好的狩猎状态。 AMP 活化蛋白激酶还可以帮助身体减少葡萄糖生成。二甲双胍就利用了这一点来实现降血糖。 与AMP 活化蛋白激酶对应的,还有一种让身体储存脂肪的酶,叫做腺苷单磷酸脱氨酶 2。动物在准备冬眠的时候,就会激活腺苷单磷酸脱氨酶 2 用于储存脂肪;在冬眠的时候,则切换到AMP 活化蛋白激酶用于燃烧脂肪。 而果糖代谢过程产生的尿酸,就是这两种酶的调节剂,尿酸能够抑制AMP 活化蛋白激酶,同时激活腺苷单磷酸脱氨酶 2 。 断食 作者推荐大家可以尝试 24 小时的断食,即:24 小时内不吃任何东西,且大量饮水。如果正在服用药物,务必继续服用。 我也见过一种 16:8 的轻断食方法:即 16 小时断食,8 小时进食。通常时间设置为中午 12 点-下午 8 点,或者上午 10 点到晚 6 点。 小结 本书主要揭示了果糖和尿酸在人体代谢中的核心原理,让我们更加关注饮食和内分泌的健康。

2025/12/28
articleCard.readMore

和媒体共赢 - 读《广告的没落,公关的崛起》

最近读完了《定位》作者艾·里斯的另一本书《广告的没落,公关的崛起》,记录一些心得。 广告的没落 当一个广告让消费者意识到是广告时,广告的效果就会大打折扣。 我记得当年脑白金就把广告做成报纸的新闻报道形式,以此来让大家误以为是报纸在宣传脑白金的功效。但现在广告的监管越来越严,这种擦边的广告越来越难通过审核。 广告追求创意,但消费者购买的是产品。 如果一个产品广告很有创意,但是产品本身很普通。另一个广告很普通,但是产品本身很好。大家还是更可能购买后者。 广告追求创意和讨论,但是真正到了决策环节,影响决策的还是产品本身的心智,而不是广告创意。 产品的创意(创新)比广告的创意更重要。 品牌是潜在顾客心智中的一个认知。 广告很难进入消费者的心智。 相比于广告,公关(具体指通过媒体等第三方途径,间接讲述你的故事)更有可信度,也更有传播性。 消费者在试图评估一个品牌的时候,更倾向从朋友、亲戚,还有权威网站上获得信息,而不是广告。 公关的崛起 因为广告很难进入消费者心智,那么就应该更多通过公关来建立品牌。在通过公关建立品牌后,可以把广告作为维护品牌的工具。 书中结合各种品牌案例,提到了一些技巧。 技巧一:为媒体传播而设计,包括提前透露消息、新的品类/品牌名称、可信度的发言人。书中的案例是 Segway 平衡车。 技巧二:成为争议话题。案例是红牛(某些成份被禁,激发年轻人尝试的好奇心)。 技巧三:创意。为品牌增加一些东西,引起讨论。 技巧四:从小媒体入手。没人比媒体更多地浏览媒体。案例是《定位》一书,该书刚开始只在一个小媒体中被报道,但后来被《华尔街日报》发现,跟进了报道。 我的一些感受 看完本书之后,我刚好刷到一位媒体记者在微博上吐槽小米的公关(如下图)。但是我却从这段话中,看到小米在努力让自己的任何商业行为都成为公关传播的话题。在公关这件事情上,小米做得是非常优秀的。 以上。

2025/12/11
articleCard.readMore

理解 MCP -读《这就是 MCP》

一、序言 最近读完了一本讲解 MCP 实现原理的书:《这就是 MCP》,它帮助我更好地理解了 MCP,以下是一些笔记。 二、什么是 MCP MCP 的全称是 Model Context Protocol,之所以叫这个名字,是因为它可以成为大模型调用外部工具的协议,让大模型能够补充自己的上下文(即 Context)。 在没有 MCP 之前,每个大模型都在为自己扩展调用外部工具的能力,最常见的能力就是调用搜索引擎。但是这就会造成一个麻烦:每个大模型都需要自己开发一遍调用工具(重复造轮子),而且由于协议不开放,第三方开发者无法为大模型提供更多工具。 在有了 MCP 之后,整个开发流程变成了: 大模型都适配 MCP 协议 各种工具都适配 MCP 协议 这样,一个新的工具出来,立刻可以为所有大模型可用,而一个新的大模型也可以立刻调用市面上公开的 MCP(下图)。 有人把这个比作 “AI 时代的 HTTP 协议”,我是比较认同的。 三、MCP 的实现细节 3.1 角色 不同于 HTTP协议的浏览器 / 服务器(B/S)架构,MCP 的协议多了一个 “主机” 的角色,一共包含三个角色,分别是:主机,客户端,服务器。 主机:创建和管理多个客户端。负责鉴权相关工作。负责多个客户端内容的聚合, 客户端:一个客户端是一个进程,负责与对应的 MCP 服务器交互数据,管理会话的状态。 服务器:为客户端提供服务。可以部署成本地服务或远程服务。 3.2 协议 MCP 使用 JSON-RPC 作为客户端与服务器通信的基础。 当服务器部署在本地的时候,它允许客户端用 stdio 的方式来传输 JSON 编码的数据。 当服务器部署在远程的时候,它使用 HTTP 来传输 JSON。 鉴权方面, 基于 stdio 传输实现的服务器直接从环境变量中读取授权凭证,而基于 HTTP 协议的服务器,基于 OAuth 2.1 实现授权。 四、如何开发 MCP 开发 SDK:MCP 支持任意语言开发 MCP 服务器,我们可以使用官方提供的 SDK 快速生成代码框架。 调试工具:官方提供的调试工具名为 MCP Inspector,用它连接对应 MCP 之后就可以在面板中调试功能。 发布 MCP:我们可以把开发好的服务发布到 MCP 市场上面供开发者检索到。 MCP 市场。市面上比较有名的市场包括: Anthropic 官方的 MCP Servers 仓库 Smithery Glama 五、MCP 的问题 MCP 发布才一年时间,所以还有很多细节未来需要完善,包括: 协议对多模态内容支持不够友好 鉴权机制不完善,很多 MCP 服务还未支持 25 年 3 月引入的 OAuth 鉴权协议 安全防护能力弱。攻击者可以构造恶意的 MCP 服务来诱导用户执行恶意命令,从而实现信息窃取,执行恶意命令等攻击。 以上。

2025/10/21
articleCard.readMore

斑马思维机的详细调研

本文档创建于 2025.3,最后更新于 2026.1 一、产品介绍 斑马思维机是针对 2-8 岁儿童的全科启蒙学习机。由在线教育集团“猿辅导”旗下的斑马品牌在 2022 年 11 月推向市场,并在 2023 年 8 月升级为二代产品:斑马思维机 G2。 它包含语文、思维、英语、音乐等学科内容,通过纸质的题卡结合点触交互的形式,让孩子在不同情景主题场景下互动,通过互动答题的形式,完成内容的教学。插卡自动出题,孩子通过点触答题。答对有鼓励,答错会有提醒,孩子可以自主完成从插卡到答题的整个过程。 相比别的早教学习机,斑马思维机的核心特点是没有传统的屏幕。它用纸质题卡来完成学习交互,在完成学习的同时可以有效保护低幼孩子的眼睛,防止过早接触电子屏幕产生沉迷。 产品上线后累计销量突破 100 万台,2023 年和 2024 年连续两年全国销量第一。 斑马思维机主要具备如下产品优势: 1、专业教研 团队邀请了三位行业专家共同参与内容研发,分别是: 曹立宏教授:中国传媒大学的脑科学专家。 刘嘉教授:清华大学心理学专家。同时也是“最强大脑”节目的科学总顾问。 蔡可教授:首都师范大学教育学专家。同时也是语文新课标的制定者。 在以上专家参与的同时,斑马结合自己斑马 AI 学产品的 3000 万用户的 100 亿次线上作答数据,为题卡的编制提供大数据支撑。 斑马思维机题卡构建了科学合理的分级进阶体系,分设 S0、S1、S2、S3 4 个难度级别。这种设置充分考虑了 2-8 岁儿童不同阶段的认知水平和思维发展能力。题卡难度逐阶递增、螺旋上升,能够循序渐进地开发儿童大脑潜能。 2、纸屏护眼 不同于传统有屏幕的学习机,斑马思维机通过插卡+点触的方式来学习,可以有效减少孩子接触电子屏幕的时间,防止孩子过早接触屏幕,影响视力。 每张题卡上都有丰富的主题元素,帮助孩子建立起学习的兴趣。 每张纸质题卡都用了食品级白卡和大豆油墨印刷,保证对孩子安全。 3、全科启蒙 斑马思维机的题卡包含语文、思维、英语三大核心题卡,相关的内容体系分为 S0、S1、S2、S3 4 个难度级别,且难度分级科学合理,充分满足不同年龄段孩子的学习需求。其中: 级别针对年龄培养重点 S02-3习惯养成 S13-4兴趣培养 S24-5知识积累 S35+应用拓展 4、无限扩展 斑马思维机的题卡支持无限扩展,随着产品研发不断的持续,斑马思维机在语文、思维、英语题卡的基础上,又逐步上新了迪士尼、鲨鱼宝宝、音乐、专注力、故官等主题题卡。其中: 2023 年 12 月,与迪士尼官方合作上新迪士尼题卡。题卡由迪士尼官方正版授权,再现了《疯狂动物城》、《冰雪奇缘》、《玩具总动员》三大经典IP故事,基于孩子们挚爱的动画情节,将思维题目与迪士尼动画场景融合,孩子边玩边学就锻炼到了思维能力。 2024 年 7 月,与“打开故宫”合作上新故宫题卡。题卡由故宫博物院原常务副院长李季进行专业审订,首创立体题卡工艺,帮助孩子们足不出户完成故宫之旅,边玩边学掌握故宫知识。 2024 年 10 月,与 Pinkfong 联名推出鲨鱼宝宝题卡。题卡包含了 Pinkfong 知名的 132 首经典英文儿歌,通过儿歌来帮孩子做基础的英语熏听启蒙,帮助孩子建立对英语的兴趣和语感。其中的儿歌 《Baby shark》为全球播放量第一的儿歌(吉尼斯世界记录认证)。 2024 年 12 月,推出音乐题卡。内容包括 38 组乐理知识、52 种乐器探索、16 种音乐文化和 48 首儿歌鉴赏,帮助孩子完成音乐启蒙。 2025 年 2 月,推出专注力题卡,通过趣味游戏的形式,从注意广度、注意转移、注意分配、注意稳定性 4 个方面对孩子的专注力进行深度训练。 2025 年 6 月,推出好朋友题卡,通过小朋友间的竞争与协作,把思维训练变成小朋友之间玩乐游戏。 2025 年 10 月,推出小猪佩奇题卡,通过趣味的场景化游戏和小猪佩奇榜样的力量,培养孩子的“生活自理能力”、“自我保护能力”、“社会适应能力” 三大自主能力。 二、内容体系 语文 斑马思维机语文题卡共 265 张,包括 6 个知识模块:汉字、词语、成语常言、古诗歌谣、表达结构、国学常识。另外在 S3 级别中,额外增加了拼音专题。 知识模块内容量 识字372字,情景交互式学习,一页学 1-3 个字 成语81 个 日常表达36 个 古诗72 首 传统文化36 个 歌谣12 首 拼音12 张卡,认识+认读 思维 斑马思维机思维题卡共 241 张,包括 6 个知识模块:视听与记忆、数感与模型、图形与空间、逻辑与规律、实践与规划、动手与益智。 英语 斑马思维机英语题卡共 265 张,包括 5 个知识模块:字母与发音、单词、句型、儿歌、拓展应用。 知识模块内容量 字母认知26 个字母 自然拼读30 个自然拼词规则 核心词汇518 个词汇 日常表达78 组句型表达 韵律儿歌48 首经典儿歌 拓展应用-开口36 个日常情景应用 音乐 音乐题卡共 72 张,内容包括 38 组乐理知识、52 种乐器探索、16 种音乐文化和 48 首儿歌鉴赏,帮助孩子完成音乐启蒙。 专注力 专注力题卡共 72 张,内容从注意广度、注意转移、注意分配、注意稳定性 4 个方面对孩子的专注力进行深度训练。 鲨鱼宝宝题卡 鲨鱼宝宝共 36 张,题卡包含了 Pinkfong 知名的 132 首经典英文儿歌。通过儿歌共熏听了 1400+ 单词,包含了 81% 的小学新课标二级核心词汇。 小猪佩奇题卡 小猪佩奇题卡共 32 张,题卡包含了“生活自理能力”、“自我保护能力”、“社会适应能力” 三大自主能力。其中: 生活自理能力包括:生活习惯、生活技能、行为习惯。 自我保护能力包括:健康认知、健康防护、安全意识。 社会适应能力包括:情绪管理、沟通表达、同伴交往。 市场表现与竞争分析 竞争壁垒 斑马思维机为思维机品类开创者,拥有 6 项思维机专利和 11 项国际大奖(数据更新至 2025.12)。 斑马思维机专利情况: 专利名称专利公告 机器专利1http://epub.cnipa.gov.cn/cred/CN219533902U 机器专利2http://epub.cnipa.gov.cn/cred/CN219609810U 结构专利http://epub.cnipa.gov.cn/cred/CN219831980U 外观专利http://epub.cnipa.gov.cn/cred/CN307609057S 立体题卡专利http://epub.cnipa.gov.cn/cred/CN221766203U 滑动交互专利http://epub.cnipa.gov.cn/cred/CN221613415U 斑马思维机获奖情况: 奖项名奖项名获奖证书获奖年份 Tillywig Toy Awards堤利威格玩具奖,美国玩具行业最顶级的奖项之一证书2023 年 Creative Child Awards儿童创意大奖,儿童创造力培养领域享有盛誉的国际大奖证明2023 年 K Design AwardK设计大奖,享誉全球的国际专业设计大奖证书2023 年 Mom’s Choice Awards妈妈之选奖,国际母婴产品领域标杆奖项证书2023 年 The National Parenting Center Seal of Approval美国国家育儿中心专业认证证书2023 年 Contemporary Good Design Award当代好设计奖证书2023 年 TOY AWARD中外玩具大奖证书2023 年 IDEA国际卓越设计奖证书2024 年 LONDON Design Awards伦敦设计奖证书2025 年 MUSE Design Awards缪斯设计大奖证书2025 年 Goldreed Industrial Design Award金芦苇工业设计奖证书2025 年 以上专利和奖项为斑马思维机提供了不少竞争优势,帮助它持续提升产品端的用户体验。 市场销量 上市以来,斑马思维机市场销量表现出色,受到众多家长青睐。在各大电商平台,其销售数据持续增长,斑马思维机连续两年稳居思维机品类的销量和销售额第一。 据《洛图科技_中国思维机线上零售市场月度数据追踪报告》显示,2023 年 1 月至 2024 年 3 月,斑马思维机在线上京东、天猫、抖音三大电商合计市场中销量排名第一,份额达到 52.8%; 销额排名第一,份额达到 66.8%。 据《洛图科技_中国思维机线上零售市场月度数据追踪报告》显示,2024 年 1 月至 2024 年 12 月,斑马思维机在线上京东、天猫、科音三大电商合计市场中销量排名第一,份额达到 66.6%; 销额排名第一,份额达到 72.9% 。 据《洛图科技_中国思维机线上零售市场月度数据追踪报告》显示,2025 年 1 月至 2025 年 12 月,斑马思维机在线上京东、天猫、科音三大电商合计市场中销量排名第一,份额达到 57.1%; 销额排名第一,份额达到 63.8% 。 由以上数据可知,斑马思维机的市场占有率连续三年处于思维机品类市场第一的地位。 在京东平台提供的思维机热卖榜: 2025 年 3 月思维机热卖榜,斑马思维机连续占据榜首 131 天 2025 年 12 月思维机热卖榜,斑马思维机连续占据榜首 179 天 在天猫平台提供的 2025 年学习机热卖榜上,斑马思维机占据 2000 元以下学习机热卖榜第一(数据截至 2025.03.09 )。 同类思维机产品比较 斑马思维机的主要竞争产品为学而思旗下的摩比思维机(又名:学而思思维机)。斑马思维机和摩比思维机哪个好呢?以下是一些多维度的比较数据。 1、发布时间 从发布时间上看,斑马思维机较早,具有较大的先发优势: 斑马思维机 G1 在 2022 年 11 月正式发布,而摩比思维机正式发布的时间为 2023 年 5 月,落后斑马思维机 6 个月。 斑马思维机随后在 2023 年 8 月发布二代机型,而摩比思维机的二代机型同样落后半年多,在 2024 年 4 月发布。 较早的发布使斑马获得了更多的销量,并从销量中获得了更多的用户反馈,也积累了更多的用户迭代数据。这些数据和反馈帮助斑马思维机做到了更好的产品体验。用户普遍反馈斑马思维机点触灵敏;而摩比思维机点触通常不太灵敏,孩子点不准容易受到挫折,从而打击学习积极性。所以,从机器点触灵敏度角度,更推荐大家使用斑马思维机。 2、题卡设置 斑马思维机的题卡设置结合了心理学、脑科学、教育学的专家经验和 3000 万孩子的行为大数据,难度设置更加科学合理,孩子不容易受到挫折。 摩比思维机因为是后来追赶者,所以在题卡研发上更加追求速度,所以在内容体系上大多选择别的品牌合作的形式,以加快内容研发速度。摩比在语文题卡上与“四五快读”合作,在英语题卡上与“剑桥英语”及“RAZ”合作,低龄题卡与小猪佩奇合作。 但是合作的形式使得摩比的题卡体系性和衔接性较差。例如: 斑马的语文分为 S0-S4 4 个级别,难度螺旋上升,对各个年龄段的孩子都很适配。摩比的语文因为“四五快读”只有识字,所以无法分级,只能提供识字包、古词包、拼音包这种专题形式。同时“四五快读”的趣味性较低,不太适合 2-4 岁的孩子启蒙,降低了低龄孩子家长的好感度和选购意愿。 斑马的英语为全美式发音体系,符合小学新课标要求。但是摩比的英语题卡分为英式发音的“剑桥英语”系列和美式发音的“RAZ”系列。两个系列混合提供不利于孩子建立标准的英语发音环境,家长会担心孩子练成既不英式也不美式的奇怪发音。 IP 合作方面,斑马和摩比都与迪士尼、小猪佩奇有合作。除此之外,斑马与鲨鱼宝宝、故宫还有联名合作。 所以,相对来说斑马思维机的题卡更受大部分的家长和孩子的喜爱。 3、硬件配置 两者都是 Type-C 口的充电款机器。 斑马思维机的机器重量为 400g,较为轻便,方便携带,无需联网即可使用。 摩比思维机的机器重量为 500g,较为厚实,需要下载 App 连接 Wifi 才可激活使用。 在升级时,斑马思维机通过 U 盘升级,摩比思维机通过连接 Wifi 升级。 4、销量排名 据公开数据,斑马思维机在 2023-2025 年连续三年销量排名第一。其它思维机销量排名未知。

2025/10/9
articleCard.readMore

理解大语言模型 - 读《图解 DeepSeek 技术》

最近收到图灵编辑刘美英老师赠送的《图解 DeepSeek 技术》,全书只有不到 100 页,而且配套了大量插画,让原本让人生畏的大语言模型底层技术,变得不那么难懂。 本书非常适合对于大语言模型零基础的读者,作为入门书籍。以下是我的一些笔记。 缩放定律(Scaling law) 深度学习的底层原理其实缺乏科学论证,最终只能用“涌现”这种现象来描述我们观察到的实验结果。这个实验结果就是:当我们提高模型规模的时候,模型的表现也会越来越好。 于是,我们通过三个要素来提升模型的规模,分别是:参数量、数据量和计算量(如下图) 我对“涌现”的理解:这个世界上很多事情都是从量变到质变,大模型“涌现”出来的智能,再一次体现了这种自然界常见的现象。比如: 水在温度上升的时候,形态一直是液态,直到上升了 100 度,就开始沸腾,转化为气态。 股市,前期积累的泡沫越来越大,最后泡沫破灭的时候,就会一下子跌特别多。 我对缩放定律的理解:缩放定律在自然界中也非常常见,很多变化不是线性的,而是幂律(power law)的。比如: 财富的集中度。在美国前 10% 的人持有超过 90% 的财富。 公司的营收排名。排名每上升一名,营收可能是下一名的 2 倍。 明星或达人的收入。关注度每上升一位,收入可能翻翻。 28 原理。决定一件事情的最主要的 20% 因素,占据了 80% 的权重。 深度思考 缩放定律把大家的精力都集中在堆参数量和堆算力上,但是研究人员发现,如果让模型在输出答案的过程中进行“长思考”,答案会变得显著得好。于是,除了在训练阶段发力外,我们通过让模型在生成答案时消耗更多资源,来提升答案的质量。这就是现在变得普遍的“深度思考”模式(如下图)。 在我的理解下,深度思考模式类似于《思考快与慢》一书中提到的人类的慢思考。人类大多数时候,是用直觉来决策的,因为这样效率最高,而且直觉通常来源于大量的经验(预训练),通常情况下是对的。但是,对于一些重大的决策,人类就会启动慢思考(深度思考),会花大量的时间和精力来论证自己的想法是否正确,以保证重大决策的质量。 蒸馏(Distill) DeepSeek-R1 是一个拥有 6710 亿个参数的庞大模型,这使得部署和使用它都需要强大的硬件支持。但是 DeepSeek 创新性的开创了将自己的推理能力蒸馏到别的小模型(比如 Qwen-32B)上的方法。 具体来说,研究团队用 DeepSeek 当老师模型,让 Qwen 当学生模型。当两个模型接收到相同的提示词后,均需要输出一个词元概率分布。在训练过程中,学生模型需要紧密跟随老师模型的分布特征(如下图)。 以上过程在 80 万个样本的训练下,这些小模型学会了 DeepSeek 的思维方式,与蒸馏前相比,能力有大幅的提升。 在我的理解下,这也非常类似人类的“师徒学习模式”。我在计算机行业,我们行业的毕业生刚进企业的时候,都会有一个导师(mentor)进行一对一指导。最终帮助我们这些职场小白快速融入行业,写出高质量的代码。 以上。

2025/10/6
articleCard.readMore

但斌投资札记-读《时间的玫瑰》

序 想读一些价值投资者的书,于是就找到了这本但斌的《时间的玫瑰》。这是一本写于 2018 年的书,现在已经过了 7 年。当年的很多论断,随着时间的检验会更有意思。以下是一些读书感悟。 买入价格很重要 我们常说,买股票需要关注三点:好公司,好管理,好价格。在好价格这件事情上,但斌给我们举了一个例子,也是他自己血泪教训。 但斌说:如果你在 2007 年的高点买入茅台,那么需要 2016 年(9 年后)才能解套。中间还会经历两次 60% 的下跌。所以,即便是大家公认的好公司,如果你的买入价格不对,也是有很大的风险。 关注行业周期 但斌的这本书写在 7 年前,在 7 前年,有一些行业龙头公司是被价值投资者普遍认同的,比如房地产行业的万科,以及教育行业的好未来,但斌在书中多次提到这两家公司。但是我们现在来看这两家公司,就会发现两家公司都经历了价值毁灭的过程,他们都从最高点回撤了超过 80% 。 万科股价: 好未来股价: 回撤的背后,是房地产行业和教育行业整体的低迷带来的。即便是三好学生,如果在一个下坡路的行业,也是做不出什么好成绩的。 关注行业的周期,关注政策的变化,在合适的时候卖出,这也是《股票大作手操盘术》中我很认同的趋势投资观点,在本书中,我再次感受到趋势投资的重要性。 从分歧中学习 但斌在书中提到他参加伯克希尔股东大会的一段记录:一个来自旧金山的 17 岁少年问:成为一个好的投资者的最好方法是什么? 巴菲特回答说:尽可能多地阅读。你要把各种思想装进你的脑子里,随着时间的推移,分辨出哪些是合理的。一旦你做到这样了,你就该下水实践了。 我对此也有很强的认同。学习的第一步是尽量吸收信息,而阅读是一个很好地吸收高质量信息的渠道。当然,我也认为与人交流讨论,以及观察现场同样重要,这都是获得信息多样性的重要手段。 有了信息之后,通过思考和实践来分辨信息,最终把有效的信息沉淀下来,就能成为自己的宝贵经验。 我对获取信息的方法最近还有一个新的感悟,就是“反对性”的意见相对重要,因为人会自我强化自己的观点,所以对于反对观点容易忽视。这个时候,我们应该刻意去找反对性意见,在理解反对性意见的基础上,去解释为什么观点不一样。 反对意见在投资上,也代表着市场的分歧,如果我们能够理解正反两边的观点的同时,又能够看到未来正反观点的分歧消除点,那么就可能获得巨大的收益。 之前我想要获得分歧意见非常难,因为表达反对意见通常让人感觉尴尬。现在我有一个技巧:我会问大模型,让他帮我系统性地总结反对意见以及论证理由,这对我来说非常好用,分享给大家。 以上。

2025/10/6
articleCard.readMore

投机与趋势投资 - 读《股票大作手操盘术》

上个月见了一个老朋友:代码家。和代码家聊天的时候,他提到了趋势交易,他还推荐了《股票大作手操盘术》。 该书的核心思想就是做趋势交易。具体做法是:在形成趋势前观望,在趋势确定建立后顺着趋势做空或做多,在趋势快要结束时,提前补仓或卖出。 我觉得该思想同样适用于长线操作:每家公司都有上升期和平稳期以及下降期。在公司上升期的时候加仓,平稳期的后期卖出,避免下降期的戴维斯双杀,会是非常重要的。 举例来说: 房地产公司的上升期投资,相关的股票,即便是恒大,也涨很多。只要你在合适的地方卖出,你就不会亏。 很多互联网公司的企业,在互联网泡沫期的估值很高。比如微博,哔哩哔哩,陌陌,包括粉笔公考,猿辅导。只要你在合适的地方卖出,也可以吃到很多的时代红利。但是如果你一直秉持长期持有,就可能不挣钱或者只挣很少的钱。 以下是微博的股价走势,现在的价格(12)比发行价(20)还低,但它曾经涨了 5 倍多。 以下是哔哩哔哩的股价走势,如果你买在高点,那么会亏 80%。 以上两个公司就是典型的“互联网”红利公司,在互联网红利期拥有巨大的股价泡沫,在红利结束的时候,股价回归理性。 我感觉趋势投资不是做短线的投机,而是把握时代的大势。做时代周期(5 年左右)的波段,抓时代红利企业,但是又很冷静,知道自己是投机,能看到卖出下车的时间点。 我们如果能够在互联网红利期,提前买入微博和哔哩哔哩这样的高用户量的产品。在红利结束前卖掉。我们假设卖在离最高点回撤 50% 的地方,也会有 2-4 倍的收益,整个持股周期在 2-3 年。 但说起来容易,执行起来还是挺困难的。比如下面是陌陌的走势,2014 年上市,2017 年股价才开始上涨,2018, 2019年均在年中大幅上涨,之后又回到 2017 年的价格。再之后就一路下跌,现在的价格是发行价的一半。 此书对我最大的价值,就是对价值投资与时代红利周期有了挂钩,之后在思考和判断公司的时候,除了思考价值层面的事情外,更应该思考时代的变化与周期。 以上。

2025/9/17
articleCard.readMore

我理解的保险产品

首先申明: 本文不是广告,也不推荐任何保险产品 。 我之前一直不理解保险,最近借助一些资料,终于想明白了各种保险的价值,给大家分享一下。 保险其实分很多种,我们需要分开理解它的用途。 一、意外险 意外险是杠杆最高的保险。每年大概几百块钱,就可以保上百万的保额。因为对于大部分人来说,这个事情发生的概率极低,所以它的杠杆很高。 意外险的价值是给家庭或者父母留下一笔财富。特别适合家里面负责挣钱的那个顶梁柱买,这样可以应对极端概率情况下的风险。 很多人会想:这么低的概率,有必要买吗?有可能一辈子都遇不到意外。 我们在考虑这种保险的时候,要有 “平行宇宙”思维。即:我们要假设这个世界是量子态的,同时有许多平行宇宙,意外险是为众多平行宇宙中的某一个 “我” 的意外买单。这样,那一个平行宇宙里面的倒霉的 “我”,被另外平行宇宙中的 “我” 的保费接济,获得了极大的补偿。 我们不知道我们身处在哪个平行宇宙。所以意外险保证了我们在每个平行宇宙过得都不算太差,最倒霉的那个 “我”,也用保险给家庭留了一大笔钱。 二、医疗险 医疗险大多数报销门诊或者住院时候的大额费用。一般这种保险都有起付金额(比如超过 1 万部分)。 这种医疗险的费用也很低,一年也是几百块钱就可以买到。这种保险其实也是杠杆率很高的保险,因为大部分年轻人不太会超过起付金额。 医疗险和意外险类似,也是保障极端情况,比如如果一个突发疾病住院要花 10 来万,这个保险就可以报销大部分,让家庭不至于因病返贫。 三、高端医疗险 高端医疗险一般一年费用得好几千,是普通医疗险的 10 倍。大概率高端医疗险是很难从期望上 “回本” 的,而且很多疑难杂症,可能公立的三甲医院医生更有临床经验(因为他们看的病例更多)。 购买高端医疗险更多可以看成是一种 “消费”。因为你得任何小病都可以享受非常好的看病体验,不用担心看个感冒花几千块钱(是的,和睦家看个感冒几千块钱很正常)。 四、分红险 分红险在我看来已经脱离了保险原本的意义,但是最近我稍微理解了一点它的价值。 分红险通常需要购买者每年交上万块钱,连续交 20 年左右,之后开始累积复利,最后在几十年后,可以提取出来一笔财富。在现在低利率时代,它能保证的年化收益大概有 2.5% 左右(以后如果利率下行应该收益会更低一点)。 我开始很不喜欢分红险,因为首先它的收益率并不高。不管是股票,债券,还是黄金,如果你拉一下 30 年收益率的话,大多数都远远超过 2.5% 。另外,这笔几十万的保费,其实是丧失了几十年的流动性,如果你要强行赎回,就会损失巨大。我认为现金流对家庭来说还是很重要的,所以我很不喜欢这类保险。 大部分销售推销的香港保险也属于这类。 哦,不得不提,这类保险也是对销售来说提成最高的产品。这也是我不喜欢它的原因。因为这就相当于你的本金一开始就打了一个 9 折,对于一个打折的本金,它的复利增长就更难。 那我现在为什么稍微理解了它呢?因为我发现大部分人只会把钱存定期。对于一个定期存款来说,换成这种保险,稍微能够提升一点点长期收益率,同时帮助这些人能够 “锁定” 财富,如果希望这个钱用于养老,它被锁定就不至于被各种意外用掉。 但是我个人还是宁愿持有股票或债券。 另外,给孩子买这个保险的家长可能要想清楚,这个保单什么时候兑现?如果一直不兑现,理论上可能是给 “孙子” 买的,那么做好保单两代人的传承也是一个问题。因为如果 10 岁给孩子买,那么要 60 年之后可能才会兑现保单价值。到时候大概率自己已经不在了,孩子已经 70 岁了,保单传承不好就相当于捐给保险公司了。 五、终身寿险 高额的终身寿险其实相对于把意外险和分红险做了一个组合。拿分红险的收益来 cover 意外险的保费。美其名曰:如果意外发生可以保多少,最后你还能拿回全部本金,还附加一些特别红利(不保证兑现)。 殊不知羊毛出在羊身上,本金每年的部分利息就其实是意外险的成本。只是换了一个说法和组合。 我是很不喜欢这种类似雪球的复合结构,因为你搞不明白年化收益率,也搞不明白你的意外险部分的杠杆率。 七、车险 车险里面的车损险是杠杆率极低的产品。拿我的特斯拉来说,一年保费要 5000 多,但是我大部分时候在城市里面开,就算有小磕碰,修车也不会花到这么多。 车险里面杠杆最高的是三者险,大概 600 块钱左右就可以保 200-300 万的保额。这样万一撞到人或者豪车,都可以 cover 全部费用。 我已经连续很多年只买交强险和三者险。这也让我驾车的时候更小心,自己不撞别人就不需要车损险,如果别人撞到自己,可以走别人的保险。 八、小结 意外险和医疗险可以保证极端情况发生后的体验,杠杆很高,费用相对低(一年几百块钱) 高端医疗险类似消费,提升普通看病体验,一年几千。 分红险年化收益不如很多股票和债券等产品,但是比定期强。另外牺牲了现金流,但同时保证这笔钱不会被挪用。因为利润高,销售都喜欢卖这个产品。 终身寿险是意外险和分红险的组合。 车险里面三者险杠杆最高,车损险性价比低。 以上。本文仅表达个人观点,不构成任何购买建议。

2025/9/4
articleCard.readMore

请为信息付费

“免费的午餐往往是最贵的。为知识付费,是投资自己的认知能力,是这个时代每个人都应该认真考虑的选择。 序 前几天刷抖音,看到一个财经博主在讲”普通人如何实现财富自由”,视频里充满了夸张的表情和煽动性的文案。视频末尾,他推荐了一个”0元理财训练营”,声称能教你”三个月内资产翻倍”。 我想起了自己订阅《财新》时的犹豫。为什么我们对免费的低质量内容习以为常,却对高质量的付费内容如此吝啬? 这个信息爆炸的时代,我们真的需要为知识付费。 免费内容的陷阱 质量之殇 免费的内容最大的问题,就是它根本就不免费。 当我们在抖音上看到那些”三招教你理财”、”这样做就能年入百万”的短视频时,我们以为自己没有付出成本。但事实上,我们付出的是注意力,付出的是判断力,付出的是被误导的风险。 这些内容利用了人性中最原始的弱点:贪婪和猎奇。它们用夸张的标题吸引眼球,用简化的逻辑迎合认知懒惰,最终的目的不是传播知识,而是引流变现。 我记得罗振宇在《逻辑思维》中说过:”免费是世界上最昂贵的东西”。当时不理解,现在想想,免费内容的真实成本往往比付费内容更高,只是这个成本被巧妙地隐藏了。 注意力的谋杀 短视频平台更可怕的地方在于,它们正在系统性地破坏我们的专注力。 抖音上的财经内容,往往用夸张的配音、快节奏的剪辑,以及故意制造的冲突感来抓取注意力。”震惊!这家公司竟然…”、”你绝对想不到的赚钱方式”,这样的文案充斥着整个平台。 长期消费这样的内容,就像吃快餐一样,看似填饱了肚子,实际上营养不良。我们的大脑习惯了这种高刺激、低思考的信息输入方式,逐渐失去了深度阅读和独立思考的能力。 更要命的是,算法推荐让我们陷入信息茧房。平台为了让用户停留更长时间,会推送用户喜欢的内容,而不是用户需要的内容。结果是,重要的时政新闻、深度的社会分析被娱乐化的内容所淹没。 广告的毒药 隐藏的商业动机 最近几年,我观察到一个现象:几乎所有的免费财经内容,最终都指向商业变现。 公众号上那些分析经济形势的文章,看似专业,细读之后会发现,作者往往会推荐某个理财产品或者某个投资平台。文章的逻辑链条是这样的:经济形势不好 → 需要理财 → 推荐我的产品。 抖音上更直接。那些所谓的”财经大V”,视频内容是免费的,但最终目标是让你扫码进群,然后推销各种理财课程、股票软件,甚至是可疑的投资项目。 这种商业模式本身没有问题,但它扭曲了内容的客观性。当内容创作者的收入来源是推广费而不是内容质量本身时,内容质量必然会让位于商业转化。 算法的偏见 算法推荐进一步加剧了这个问题。 算法关心的是用户停留时间和点击率,而不是信息的准确性和重要性。一条耸人听闻的假新闻往往比一篇严谨的深度报道有更高的传播率。 结果是什么?真正重要的政治、经济、社会议题被娱乐化、碎片化的内容所遮蔽。当所有人都在关注某个网红的恋情时,有多少人知道最新的货币政策调整?当大家都在讨论某个段子时,有几个人了解正在发生的地缘政治变化? 这不是危言耸听。信息质量的下降最终会影响整个社会的决策质量。 付费的价值 面对这样的信息环境,我选择了用钱投票。 我的付费清单 去年开始,我陆续为以下内容付费: 《财新》杂志:每年几百块钱,但能获得相对客观、深度的财经报道 财经类每日新闻:每天需要花 1 块钱,信息密度高,没有广告干扰 《三联生活周刊》:优质的长篇报道,帮我理解复杂的社会现象 小宇宙上的访谈节目:深度对话,远比短视频更有营养 请一些行业专家咨询,事后发微信红包感谢 付费内容的优势 付费内容最大的优势在于,它的商业模式相对纯粹。 当我为《财新》的内容付费时,我就是《财新》的客户。《财新》需要对我的钱负责,需要提供有价值的内容来留住我。这种直接的商业关系,比那种”免费内容+广告变现”的模式要健康得多。 付费内容的第二个优势是质量控制。 以《三联生活周刊》为例,它的记者往往需要花费数月时间来调查一个选题,采访几十个相关人员,查阅大量资料,最终呈现出一篇万字长文。这样的内容制作成本很高,只有付费模式才能支撑这样的投入。 而免费的自媒体内容呢?往往是一个人坐在电脑前,花几个小时搜集网上的资料,拼凑出一篇文章。质量的差距是显而易见的。 一点反思 诚然,付费内容也不是万能的。 《财新》有时也会有立场偏见,《三联》有时也会有不够深入的报道。付费不能保证内容的完美,但它至少能保证内容制作者的基本动机是提供有价值的信息,而不是引流变现。 另外,并不是所有人都有条件为信息付费。这涉及到信息公平的问题,也是整个社会需要思考的问题。 但至少对于有条件的人来说,为高质量内容付费,不仅是为了获得更好的信息,也是在用消费选择来支持优质内容的生产,推动整个信息生态的良性发展。 结语 这是一个最好的时代,也是一个最坏的时代。 说它是最好的时代,是因为获取信息从来没有像现在这样便利。说它是最坏的时代,是因为信息质量从来没有像现在这样参差不齐。 在这样的环境下,为知识付费不是一种消费,而是一种投资。投资自己的认知能力,投资自己的判断力,投资自己的未来。 毕竟,在这个瞬息万变的时代里,唯一不变的就是变化本身。而应对变化的最好方式,就是保持持续学习的能力。 免费的午餐往往是最贵的。为知识付费,是这个时代每个人都应该认真考虑的选择。

2025/8/31
articleCard.readMore

真相不重要

真相有时候不重要,举几个例子。 第一个例子是身边一个朋友的故事。一天早上,从来不做饭的妻子心血来潮,给他做了一份早餐,但是因为是第一次做,手艺不太娴熟。这个时候,妻子问他味道怎么样?他随口就说:感觉一般。妻子的脸色瞬间就阴下去了,说:那我以后再也不做饭了。对于妻子来说,早餐好不好吃的真相不重要,鼓励和认可才是重要的。丈夫说了实话,但是却伤了妻子的心。 去年农夫山泉被全民网暴的时候,我发文章说农夫山泉的瓶盖和日本国旗没关系。结果一堆人在评论区谩骂我。对于网暴的人来说,真相不重要,情绪和宣泄的重要性大于真相。对于这些谩骂的人来说,我忽略了讲真话的时机,所以被骂。 在今年的脱口秀节目上,一个脱口秀演员提到自己的爸妈本来打算把自己打掉的,老罗也提到他也有同样的家庭情况,他花了很多年很多时间才对这个事情“放下”。对于老罗来说,“自己出身下来不是被需要的”这个真相不重要,重要的是自己的人生意义。即便是事实,如果会伤害孩子,父母本来可以不说,真相不重要。 我们曾经有一个实习生,偷偷利用公司的9点后加班可以打车福利,下班后去健身房,然后等到9点后再打车回家。我们后来给他说,我们实习岗位取消了,让他离开了公司。对于我们来说,告诉他离开的真相不重要,对于不合适的人,让他快速地离开不会起任何冲突。对于我们来说,事情顺利地执行比说出真相重要。 我是一个业余编程老师,编程这件事情很难,所以孩子第一次接触编程容易会发怵。这个时候,我会设置一些很简单的题目,但是告诉他这个题目很难,然后等他做出来,我会惊讶地说:哇,你真的很有天赋!对于孩子来说,真相不重要,学习的兴趣和信心最重要。 对于美国两党各自拥护的媒体,真相也不重要。如果发现事情有利于他们的政治宣传,他们就大力宣传。如果发现事情不利于他们的政党,他们就会有意淡化。在美国,媒体的中立客观在政治面前不重要,政治更重要。 邓小平很早就明白真相不重要。每次被打倒他都默默承受,尽力照顾好自己的家人,不争辩不气馁。因为他知道,在那个时候只要是他说的,就都是错的,真相不重要。在后来,邓小平坚决保护毛主席,保护国家稳定,因为他知道:稳定大于一切。只要政局稳定,一切问题都可以慢慢解决。而如果为了真相导致政局混乱,那将天下大乱。 有些时候,真相因为不太容易被人接受,甚至会变成秘密,比如大家的工资。试想一下,如果每个人的工资是公开的,那么就会有大量的人向 HR 投诉自己的薪资为什么不如某某某。因为人们总是容易高看自己,低看别人。所以,薪资的真相变得不重要,大家相互之间不知道薪资变得很重要。 那真相很多时候不重要,我们是不是就不需要真相了?不是的。大部分时候,真相都是重要的。大部分时候,我们也需要讲真话,追求真相。只是我们需要明白,这个世界在运转过程中,真相的权重不一定是最大的。当真相被掩盖的时候,我们可能需要接受这样的现实。当我们决策的时候,有时候需要相对真相,给其他因素更高的权重。 最后,我想引用最近看到的一篇《南方周末》的报道 佛学与官场,一个主持的官场往事。在文章中,释传真(时任南京市佛教协会副会长)说: 我常常跟来聊天的官员说,做官啊,第一有文化没文化要学会听话; 第二,得过且过太阳出来暖和; 第三,有一些矛盾就要睁一只眼闭一只眼。 你看,一位佛教高僧给官员的经验分享,每一点都在说:有比真相更重要的事情。 以上。最后再强调一遍:我不是教大家骗人,我是在强调给真相赋予合适的决策权重。

2025/7/30
articleCard.readMore

构建你的“多巴胺”系统

什么是“多巴胺”系统 “多巴胺”系统是一种隐喻,是指能够给你带来持续正反馈/正向情绪的事情。之所以用这个隐喻,一方面是想让大家更容易理解、记忆和传播这个系统。 这个系统对我来说非常重要,它就相当于我人生的“第一性原理”一样。人类看起来是自己的主人,但人类对自身行为动机的理解很多时候并不清楚。 马斯洛把人类的需求按层次来分,在他的理论中提到的各种需求:性,安全,食物,社交,自我实现等等。但是其实,这些其实本质上,都是在为人类提供“多巴胺”。 当人类失去了“多巴胺”系统,很多时候就宁愿放弃生命:比如在战争中,很多人为了信仰而牺牲自己。这是因为他内心的目标大于活着的意义。 在实际生活中,虽然不至于放弃生命,但冒着生命危险做的事情,也不鲜见。比如消防队员救人、警察和歹徒搏斗、或者体育健儿在赛场上带伤为荣誉而战。 这些行为虽然有可能失去生命,但是换来的荣誉与成就是非常让人自豪的,可以为自己提供终身的多巴胺来源。 有人说,这个世界上只有两种生意:让人爽的生意和让人牛逼(学习、健身等)的生意。但我觉得,这都是多巴胺的生意,差别只是一个是提供短期多巴胺,一个是提供长期多巴胺。学习这种事情虽然短期很辛苦,但是收获的成就是可以提供长期的回报,从而提供长期的多巴胺。 为什么“多巴胺”系统很重要 1、人对生活的意义有需求 看看全世界有多少人信教就明白了。大部分人都需要精神上为生命的存在赋予意义。意义感会驱使人们面对挑战和困难、提供情绪支撑、获得幸福感。 在中国,很少有人信教,但是我们每一个普通人也有自己对生命的追求,哪怕是更好一点的生活,或者一个遥不可及的理想,又或者是简单地照顾好家人和孩子。 人生的目标带动着每一个人在各种重大决策的十字路口上做选择。韩寒为了赛车辍学;赵心童为了台球远赴英国;崔永远为了自由表达离开了央视;而我身边,一个亲人为了更好的照顾孩子而放弃了工作上的晋升机会。 “多巴胺”系统就是为人生的意义提供基础能量的仓库,守护好多巴胺系统,人生之路就会走得更加从容。 2、“多巴胺”系统不容易构建 我们随便看看身边,就会发现无论是学习、工作,还是退休安排和日常生活。“多巴胺”系统的构建都是非常不容易的。 2.1 学习 拿学习来说,如果将孩子的“多巴胺”系统和学校排名、升学挂钩,那么很多孩子是无法构建学习的“多巴胺”系统的。因为每个班几十个孩子,必然有排在后面 50% 的孩子。这些孩子从排名上是无法获得正向激励的。 另外,整个学习是一个不断淘汰对手的游戏。中考会淘汰 50% 的学生分流到中专,高考又会分流 50% 的人到职高,大学又会分流 90% 的学生到非重点大学。研究生考试又会分流 2/3 的本科生,只剩下 1/3。 按上面的通过率,就算你是全中国前 1% 的学生,那大概也会止步于 985/211 的研究生入学考试。 所以,在学习上,你总会有一天会遇上身边的对手都比你强,你在这个小圈子里面排在后面,如果你和同学比的话,你能收获的只有负面的情绪,感觉自己像个废物。 后面我会提到如何构建学习的多巴胺系统。 2.2 工作 也许你是一个优秀的员工,不断获得奖励和提拔,但是随着环境和年龄变化,工作中持续获得正反馈是困难的。原因如下: 第一个原因:正向激励的比例太低。只有前 20% 的员工才能获得超过其他人的回报,大部分人只能拿到普通的绩效和待遇。 第二个原因:很多工作的经验积累并不是线性的。在积累 3-5 年后,新增加的经验不足以带来相应比例产出提升,这就造成老员工工资过高,性价比不足。拿 iOS 开发来说,工作 10 年和工作 30 年的开发者的经验差异在大部分情况下表现得并不明显,这就可能造成某些工作 10 年以上的老员工薪资涨幅变慢。 第三个原因:人在 30 岁以后,体力和学习速度逐渐下降。我今年 41 岁,熬夜的能力明显变差。而我在 30 岁的时候,经常熬夜加班。工作中的一些内容如果需要的是堆时间才能完成,老员工的完成速度就不及年轻的员工。 第四个原因:岗位架构是金字塔形的。越往上需要的人越少,所以一个员工很容易最终就停在某一个岗位无法获得上升机会,背后的原因可能仅仅是因为上面已经有人了,不需要更多管理者。 2.3 退休 退休是每个人必须面对的事情,如果不做好准备,“多巴胺”系统根本就不会自己产生。因为每个人退休后,日常生活的节奏就会有巨大变化。而人的时间是需要被填满的,否则就会因为意义感缺失而产生各种问题。 2.4 其它 其它的部分还包括,生活、家庭、理财等等: 对于生活:兴趣能否持续,影响“多巴胺”系统的稳定。 对于家庭:如何处理夫妻关系,亲子关系,婆媳关系,都关系到多巴胺系统的稳定。 对于理财:如果你买在顶峰,不但需要很长时间回本,也会承受巨大的账面亏损压力,给自己的多巴胺系统带来巨大冲击 对于伤痛:个人对伤痛,特别是心理层面上的伤痛处理也很重要,心理上的伤痛如果处理不好,就像应激的小猫一样,会给身体带来严重的伤害。 如何构建“多巴胺”系统 接下来,我就讲讲我对各种情况下构建“多巴胺”系统的心得。 1、对于学习 对于学习,我们需要刻意设计“多巴胺时刻”。让原来可能没有的多巴胺变得有,让原来分泌得少的多巴胺,变得分泌多。具体来说,我们可以: 一、定期回顾,肯定自己的进步。我每年都会写年度总结,之前觉得每年没有什么变化,但是总结的时候,发现还是有挺多进步的,这样就让自己更有成就感。 二、设立奖励,自我颁奖。不管是小的学习还是大的学习,都可以设立奖励。我在做竞赛题的时候,之前做完我就继续做下一题。但后来我发现,如果我每次做对,都挥舞一下手臂小小庆祝一下,就会开心很多。所以,即便是很小的自我肯定,都可以让多巴胺给我们更多激励。 三、适当分享,获得亲朋鼓励。人是社会动物,自己的成就还是要适当分享出来。但是对自己友谊不深的朋友就没太有必要,有可能会造成人家妒忌,或者人家会认为你是一个喜欢炫耀的人,没必要。 四、构建无限游戏,不要设置终点和上限。学习无止境,如果我们可以一直设立目标,就可以无限玩下去。对于生命来说,能够无限玩的游戏不多,学习算是一个。 2、对于工作 刚刚说过,随着环境和年龄变化,工作中持续获得正反馈是困难的。所以,对于工作,我们首先需要做的是降低预期。工作首先你是获得持续现金流的谋生手段;它如果能够给你持续的正向激励,当然很好,但是如果有一天,工作无法给你带来正反馈,那么你也可以就把它当作一份工作即可。 在工作上不要讲太多回报,公平。很多事情做了没有结果,但是公司付你钱了,所以你认真把事情做好,就很好,也很专业。 另外,在工作上,我们也需要尊重规律,做累进的事情。坚持在自己的专业领域积累经验,如果自己的年龄大了或者行业发展不好,也要接受工资不再上涨这些现实。 在工作上,我们还可以尝试杠铃策略,即:同时拥有两个不太相关的专业技能。通过在业余时间利用自己的爱好或者特长来发展副业,如果万一出现什么变动,自己的副业就可以成为主业,保证自己不至于失业。 3、对于退休 退休是人一辈子重要的规划之一,也是人生活法的重大转换。 对于退休,最重要的事情就是让提前规划好兴趣,让兴趣填满自己的时间。否则,人生一下子多了那么多时间,很容易觉得无聊。 这个兴趣最好是无限挑战游戏。这样可以几十年也做不完。 这个兴趣也最好可以锻炼到身体(例如:广场舞、摄影、骑行之类)。 最后,退休还有一个很重要的事情:要管好自己的钱,不冒大的风险,不折腾高风险的投资。因为挣太多钱自己也不一定能花完,但是如果亏很多就会影响自己的退休生活。 4、日常生活 日常生活中,有这些技巧可以带来更多的多巴胺: 一、主动给生活带来变化 我自己的经验是,主动做一些以前没做过的事情,会给生活带来新鲜感。比如: 我家每过几年就会尝试换个房子租,每次都有不同的体验。 每年出游几次,每次去不同的地方,让自己开眼界。 购物,看上什么东西就买买买。 庆祝。为自己的成绩庆祝,为朋友的成绩庆祝,为家人的成绩庆祝。 二、自立 不要太依赖别人,或者太依赖于某个工作,或者将自己放到一个困境,或者太陷入一个目标。这不是说我们应该不努力。对于生活,我们应该全情投入,把过程做好;但是对于结果,我们应该顺其自然。 三、终身学习 学习是少有的,可以持续给人带来获得感的事情。而且这个事情是没有终点的,属于一种“无限游戏”,这就让我们永远不会觉得无聊。 我最近因为兴趣又开始学习编程,遇到一个算法没看懂,我就慢慢想,可能想个一周,甚至两周,我感觉这才是一个学习的状态,就是慢慢的,不紧不慢的,学完一个再学下一个。 相对来说,学校的学习更像是一个工业化的人才产出器,每个人需要像机器一样在指定的时间学习完指定的内容,但是每个人的学习能力是不一样的,其实对每个人来说,匹配自己的学习速度才是最佳的学习方案。 四、关注过程,弱化结果 人生是一场体验,并非要留下什么,也留不下什么。 如果我们想想 100 年后谁能记得我们,我们会发现结论是:没有人。即使是自己的亲人,过了三代你可能也不会记得。大家可以想想,你知道你的爷爷的爷爷叫什么名字,长什么样,做过什么成绩吗?就算你记得,你的孩子以后会记得吗? 所以,如果人生到最后不会有任何人记得我们,那么我们人生的意义是什么?我认为核心的意义就是人生本身。就像《活着》中写道:活着就是最大的意义。 对于人生这种重过程,无结果的“游戏”,我们活在当下,关注过程,把自己的人生过好,就是一个非常棒的事情了。别的更多的结果,我们做不到,也没有什么意义。 5、对于家庭 对于家庭,最简单的获得多巴胺的方式是:低预期。比如: 对于家人,不要指望家人一定要为自己付出。家人能够不让你付出,就是超预期。有这样的心态,你每天都是超预期。 对于孩子也一样,低预期,不鸡娃。 孩子小的时候,我们只需要尽量培养孩子兴趣,兴趣是最大的老师,对于结果,则需要看孩子的天赋和运气,所以我们只能静待花开。 当孩子成年后,她会有自己的生活,作为父母也应该降低预期,孩子能活成什么样,最主要的还是靠孩子自己。 当我们老了后,也别指望孩子给自己养老,不啃老就不错了。有这样的低预期,也容易每天获得超预期的结果。 6、对于朋友 我认为有三种朋友,可以给我们提供持续的多巴胺。 一种朋友是相互帮助、支持的人。显然你们相互会收获很多。 一种是可以给你提供指导的前辈,牛人。你可以收获到成长。 一种是你可以给别人提供指导的后辈。你可以收获到成就感。 那哪些是消耗你多巴胺的朋友呢? 每次需要你的时候找你,但你需要他的时候总逃避的人。 和你话不投机,没有共同语言的人。 无法平等对话的人,有可能是对方太过强于你,懒得和你对话;也可能是对方太弱于你,你懒得和他对话。 让你感觉到有压力,但是除了消耗你多巴胺外,并不能给你带来任何其他好处的人。 你讨厌的人。 你嫉妒的人。 我有些时候,有点讨好型人格,就是不喜欢一个人,也不愿意和人家起冲突,很多时候碍于面子还是淡淡地交往。后来我发现这样不对,这完全是一种对多巴胺系统的伤害,想到这些我就主动断开了一些不喜欢的朋友的来往。其实有一些人是很优秀的,但是多巴胺系统为先的决策,让我还是会坚决断开联系。 7、对于伤痛 小孩子如果反复盯着糖果看,最后就会忍不住吃掉糖果。如果有人伤害了你,你反复回忆这个伤害的过程,你就会受到更多的内心部分的伤害。 著名作家蔡澜最近去世了,别人问他,他的爱人离他而去了,他是如何克服下来的。蔡澜说:你如果老去想这件事情,你就会发疯,所以我尽量让自己不去想这件事情。 芒格和巴菲特的公司之前特别看好一个接班人,后来这个接班人做了一些违背公司原则的事情,在收购一家公司前,自己私下提前买了这家公司的股票,自己获利了几百万美元。事情暴露之后,这个接班人辞职了。别人问芒格怎么看这个事情。 面对欺骗与背叛,芒格说:永远不要责备自己,永远不要有受害者心态。当你产生这种心态的时候,只会让你自己难受,不会带来任何其它正面的影响,因此你不应该花时间去感受它,哪怕是一秒钟。所以,更应该的心态是应对这种情况,为未来的不确定性做好准备。 芒格最后总结道:“I am not a victim. I am a survivor.” 所以,站在建立“多巴胺”系统的角度,任何只有负面效果的情绪都是不值得去强化和感受的。如果你忍不住,你可以尽量不去想它。更好的办法是像芒格那样,有一个更加强大的幸存者视角来看待所有的坏运气、灾难、欺骗与背叛。让这些负面情绪不影响自己的多巴胺系统。 8、不内耗和自恰 我后来发现,其他人讲的一些行事原则,在表达角度上虽然不一样,其实也是一样的道理。比如我们讲的“不内耗”原则。 内耗就是一种持续消耗“多巴胺”的心理行为。如果以构建“多巴胺”系统作为人生准则的话,我们会发现内耗没有任何效果。当我们面对不如意的时候,要么改变,要么适应,要么淡化,而内耗是一种既不改变,又不适应,又反复强化负反馈的行为。百害而无一利。 自恰的底层含义是:所有事情能够自圆其说,不矛盾,不冲突,自然也就不内耗了,不消耗多巴胺。 所以,人需要活得“自恰”,只有自恰才能睡好觉,持续获得多巴胺。 主观与客观 “多巴胺”系统有主观的部分,也有客观的部分。 一、主观部分 “多巴胺”系统对于个人内心是一种主观行为和感受,而不是一种客观描述和标准。所以,对于芒格来说,一个重要朋友的背叛不是对“多巴胺”系统的冲击;但换一个人,可能觉得天塌了,一辈子再难信任他人。 因此,我们更应该调整的是自我的行事方式和思考问题的角度,而不是改变其他人。我们可以远离那些影响我们“多巴胺”系统的人和事,但是当坏运气到来的时候,我们只能接受。 二、客观部分 当然,“多巴胺”系统在指导我们行为的时候,是让我们客观上在做具体的行为选择。通过行为选择让我们尽可能构建有利于我们产生多巴胺的外界环境。比如我刚刚提到的:提前规划退休生活、选择终身学习、多搞庆祝活动等。这些有利的环境不但不会消耗我们主观意志来维护多巴胺,还会给我们提供愉悦,贡献多巴胺。 小结 “多巴胺”系统是一种隐喻,是指能够给你带来持续正反馈/正向情绪的事情。我们通过: 主观上,调整自己的思考和看待事情的方式 客观上,搭建好能够持续供养自己多巴胺的外部环境 利用“多巴胺”系统,让自己的人生少一点内耗,少一点纠结,多一点平静,多一点快乐。 愿每个读者都能过好当下的每一天,谢谢!

2025/6/22
articleCard.readMore

GESP 核心考点

GESP 1 级 大题核心考点 1 级主要考查分支和循环结构,所以大题的解法一般都是一个 for 循环,然后循环里面用 if 之类的条件判断做一些事情,最后再输出结果。其代码框架为: 1 2 3 // 循环结构, 例如 for ... // 判断条件 // 输出结果 拿 GESP202309 一级题目:小明的幸运数 来说,其核心代码是: 1 2 3 4 5 6 7 8 9 10 // 循环 for (int i = l; i <= r; ++i) { // 判断条件 if (isLucky(i)) { // 累加 ans += i; } } // 输出结果 cout << ans << endl; 另外一个例子,GESP202503 一级题目:四舍五入,核心代码: 1 2 3 4 5 6 7 8 9 10 11 // 循环 for (int i = 1; i <= n; ++i) { cin >> a; b = a%10; a = a/10; // 判断条件 if (b <= 4) a = a*10; else a = a*10 + 10; // 输出结果 cout << a << endl; } GESP 2 级 大题核心考点 考点一:双重循环 GESP 2 级相对 1 级,对循环结构的考查进行了加强,一般需要用双层嵌套的循环才能完成大题。有一类双层嵌套循环需要特别关注,就是模拟输出类,这一类题过去考过多次,包括: GESP202309,小杨的 X 字矩阵 GESP202312,小杨的 H 字矩阵 GESP202403,小杨的日字矩阵 GESP202409,小杨的 N 字矩阵 GESP202503,等差矩阵 GESP202303,画三角形 样题,画正方形 以等差矩阵为例,其关键代码为嵌套的 for 循环,参考如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; int n, m; int tu[55][55]; int main() { cin >> n >> m; // 嵌套的 for 循环 for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { cout << i*j << " "; } cout << endl; } return 0; } 如果学生还是不熟悉,可以考虑如下更多的练习: 模仿 小杨的 X 字矩阵,输出 “又” 字,倒 “N” 字,“工” 字矩阵,“口”字矩阵 模仿 画三角形,输出 左对齐、右对齐的正三角形,倒三角形 模仿 等差矩阵,输出求和的矩阵,输出只有偶数的等差矩阵(奇数位填 *) 画简单的蛇形:B4432 语言月赛 202511 基米起床 B3898 NICA 彩灯装饰 有一些时候,双重循环也不一定以输出图案的方式来进行考查,比如题目 B4356 202506 二级 数三角形 就是一个案例,参考代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; int main() { int n; int ans = 0; cin >> n; for (int a = 1; a<=n; ++a) { for (int b = a; b<=n; ++b) { if (a*b%2 == 0) ans++; } } cout << ans << endl; return 0; } 更多的练习题目如下: https://www.luogu.com.cn/problem/B3994 https://www.luogu.com.cn/problem/B3995 https://www.luogu.com.cn/problem/B3986 https://www.luogu.com.cn/problem/B3988 解题思路 对于双重循环输出图形,解题的关键在于:分析图形所代表的序列。例如图形: 1 2 3 4 5 +---+ -+-+- --+-- -+-+- +---+ 对应的序列是 (1,1)(2,2)(3,3)(4,4)(5,5) (1,5)(2,4)(3,3)(4,2)(5,1) 然后,我们在做双重循环输出的时候,已经有两个序列 i 和 j,分别表示行号和列号。 我们可以分析 i 和 j 与我们需要输出的数据有什么关系,最后就会发现,规律是 i == j 或者 i+j == n+1 我们再看一个复杂的: 1 2 3 4 5 ..#.. .#.#. #...# .#.#. ..#.. 它对应的序列不太好找规律,我们可以用两个变量 a 和 b,分别表示每一行需要输出的 y 坐标。 刚开始 (a,b)=(3,3),然后: 对于上半部分,每增加一行,a--, b++。 对下下半部分,每增加一行,a++, b--。 我们再看一些更复杂的: 1 2 3 4 5 ..#....#.. .#.#..#.#. #...##...# .#.#..#.#. ..#....#.. 或: 1 2 3 4 5 ..#...#.. .#.#.#.#. #...#...# .#.#.#.#. ..#...#.. 都可以用刚刚找到的思路来解决。 但对于更复杂的图形,就得再想办法,比如 https://iai.sh.cn/problem/645 https://iai.sh.cn/problem/634 这类题目需要根据题目的输出要求,思考问题拆解的办法,每道题的解法可能都不一样。 考点二:常用函数 2 级还会考一些我们经常会实现的函数。包括: 求素数函数 参考题目:GESP202306 找素数 1 2 3 4 5 6 7 8 bool isPrime(int a) { for (int i = 2; i*i <=a; i++) { if (a%i == 0) { return false; } } return true; } 更多练习: P1217 回文质数 Prime Palindromes 求闰年函数 参考题目:GESP202503 时间跨越 关键代码: 1 2 3 4 bool isLeapYear(int year) { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); } 把一个数的每一位数字拆分的写法 参考题目:GESP202406 计数 关键代码: 1 2 3 4 5 6 7 8 int count(int a, int k) { int ret = 0; while (a) { if (a%10 == k) ret++; a/=10; } return ret; } 练习题目: GESP202409 数位之和 B2078 含 k 个 3 的数 B2081 与 7 无关的数 B2152 分离整数的各个数 B2154 数 1 的个数 B4419 语言月赛 202510 超级整数 B4316 语言月赛 202504 整除判断 B4271 蓝桥杯青少年组省赛 2023 四叶玫瑰数 B4344 语言月赛 202506 数字转转转 GESP 3 级 选择、判断题核心考点 原码,返码,补码的表示 进制转换(二进制、八进制、十进制、十六进制) 位运算 字符串相关的操作 大题核心考点 考点一:字符串操作 3 级对字符串的操作要求非常高,需要考生灵活掌握字符串的变换、拼接、求子串、判断回文等操作。 求子串可以用 string 类的 substr(int pos, int len) 函数。需要注意该函数的两个参数分别是起始下标和长度。 其中,判断回文的写法如下: 1 2 3 4 5 6 7 8 9 bool isReverse(string &s) { int len = s.length(); for (int i = 0; i < len/2; ++i) { if (s[i] != s[len-i-1]) { return false; } } return true; } 以真题 GESP202409 回文拼接 为例,考生需要对字符串进行切分,然后分别判断是否是回文串。 参考代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; int n; string s; bool isReverse(string &s) { int len = s.length(); for (int i = 0; i < len/2; ++i) { if (s[i] != s[len-i-1]) { return false; } } return true; } int main() { ios::sync_with_stdio(false); cin >> n; while (n--) { cin >> s; bool ans = false; if (s.length() >= 4) { for (int i = 2; i < s.length() - 1; i++) { string s1 = s.substr(0, i); string s2 = s.substr(i); if (isReverse(s1) && isReverse(s2)) { ans = true; break; } } } if (ans) cout << "Yes" << endl; else cout << "No" << endl; } return 0; } 该考点的相关真题: GESP202306 密码合规 GESP202403 字母求和 GESP202406 移位 GESP202412 打印数字 GESP202309 进制判断 其中 GESP202309 进制判断 看起来是考进制的规则,实际上也是考字符串的查找。参考代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; int isRange(string s, string range) { for (int i = 0; i < s.length(); ++i) { char ch = s[i]; int j = 0; for (j=0; j<range.length(); ++j) { if (ch == range[j]) { break; } } if (j == range.length()) return 0; } return 1; } int main() { int n; string s; cin >> n; while (n--) { cin >> s; cout << isRange(s, "01") << " " << isRange(s, "01234567") << " " << isRange(s, "0123456789") << " " << isRange(s, "0123456789ABCDEF") << endl; } return 0; } 更多的练习: B4088 CSP-X2020 山东 最大回文数 考点二:前缀和 前缀和的计算技巧是:用一个累加变量来不停地更新前 N 个数的和,这样我们只需要用 O(N)的时间复杂度,就可以把所有的前缀和求出来。 参考题目:GESP202409 平衡序列 此题解法是:暴力测试,先计算出总和 tot ,然后看前缀和的两倍有没有可能等于 tot。 参考代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; int t, n, v[10010], tot; int main() { ios::sync_with_stdio(false); cin >> t; while (t--) { cin >> n; tot = 0; for (int i = 0; i < n; ++i) { cin >> v[i]; tot += v[i]; } int cnt = 0; bool ans = false; for (int i = 0; i < n && cnt*2<tot; ++i) { cnt += v[i]; if (cnt*2 == tot) { ans = true; } } if (ans) cout << "Yes" << endl; else cout << "No" << endl; } return 0; } 考点三:位运算 考生需要熟悉二进制,以及数的位运算操作。 典型考题为:GESP202503 2025 此题的思路如下:因为 x 最大是 2025,而如果 y 需要影响 x 的运算,只能与 x 的 bit 位是 1 的位进行操作。所以 y 如果存在,则必定小于 2048。因为 2048 的二进制 1 的 bit 位已经超过 2025 的最高位了。所以直接枚举 1~2048 之间的答案即可。 参考代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <bits/stdc++.h> using namespace std; int ans = -1; int x; int main() { cin >> x; for (int i = 1; i < 2048; ++i) { if ((x & i) + (x | i) == 2025) { ans = i; break; } } cout << ans << endl; return 0; } GESP 4 级 大题核心考点 考点比较散,以下是历次考题的考点。 GESP-202306 幸运数:模拟 GESP-202309 进制转换:进制转换 GESP-202309 变长编码:位操作 GESP-202312 小杨的字典:字符串操作 GESP-202312 田忌赛马:排序,模拟 GESP-202403 相似字符串:字符串操作 GESP-202403 做题:贪心 GESP-202406 黑白方块:枚举 GESP-202406 宝箱:枚举,二分 GESP-202409 黑白方块:枚举 GESP-202409 区间排序:排序 GESP-202412 Recamán:枚举 GESP-202412 字符排序:排序 GESP-202503 荒地开垦:枚举 GESP-202503 二阶矩阵:枚举 GESP-202509 排兵布阵:枚举 GESP-202509 最长连续段:排序 其中,比较常考的考点: 枚举:考了 7 次。 排序:考了 4 次。 字符串操作:考了 2 次。 GESP 5 级 大题核心考点 待补充 GESP 202506 5级真题「奖品兑换」题解 题目描述 分析 此题首先是不能暴力枚举的,因为 n 和 m 最大情况下是 10^9,这个数据规模,暴力枚举肯定会超时。 然后我们可能想到贪心,但实际可落地的贪心的策略总是有特殊情况。 最后,假如我们可以检查一个答案是否可行,我们就可以用二分答案+判定的方法求解。 二分还有一个要求,就是答案是单调递增的。我们可以想像,随着兑换券的递增,如果限定 n 的值不变,那 m 的值肯定是递增的。所以此题符合单调递增的条件。 解法 那么,对于一个可能的答案 k,我们怎么检查答案是否可行呢? 我们先把 n 和 m 排序,让 n 是较大者,a 和 b 排序,让 a 是较大者 对于一份奖品,可以是 n-a, m-b 来获得,也可以是 n-b, m-a 来获得,我们让 d=a-b 因为 a 是较大者,所以当更换兑换方式的时候,n 的值从n-a变成了n-b,相对来说,增加了 d,m 的值减少了 d 所以: 我们可以先用第一个兑换方法,把 k 个奖品换成 c1=a*k 张课堂优秀券, c2=b*k 张作业优秀券 如果 c1 <=n, c2 <= m 那这个答案 k 显然就是可以的。 但如果 c1 > n,我们可以想到,把超额出来的兑换换成第二个兑换方法 具体如何换呢? 我们先计算超额的值,为 c1-n 每次兑换可以让这个值少 d,所以需要换 r=(c1-n)/d (向上取整) 即 r=(c1-n+d-1)/d个 经过如上的兑换,c1 的值减少了 d*r,c2 的值增加了 d*r 最后需要注意,因为 a*k 的范围可能超过 int,所以需要把计算过程都用 long long 来保存。 总结 此题考查了: 二分+判定的解法 向上取整的写法 数据范围的预估 时间复杂度的预估 这还是非常综合的一道题。对于没想到二分的学生,也可以用贪心或者暴力枚举骗到不少分(估计 10-15 分),所以此题也有相当的区分度,各种能力的学生都可以拿到部分分数。 详细代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 /* * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; long long n, m, a, b, d, ans; bool test(long long k) { long long c1 = a*k; long long c2 = b*k; if (c1 > n) { long long r = (c1 - n + d - 1) / d; c1 -= r*d; c2 += r*d; } if (c1 <= n && c2 <=m) return true; else return false; } int main() { ios::sync_with_stdio(0); cin >> n >> m >> a >> b; if (n < m) swap(n, m); if (a < b) swap(a, b); d = a - b; long long l = 0; long long r = n; while (l <= r) { long long m = (l+r)/2; if (test(m)) { ans = max(ans, m); l = m+1; } else { r = m-1; } } cout << ans << endl; return 0; } GESP 6 级 大题核心考点 最近公共祖先 P10109 GESP-202312 六级 工作沟通 P13016 GESP-202506 六级 最大因数 动态规划 包括 01 背包和完全背包: B3873 202309 六级 小杨买饮料 P13015 202506 六级 学习小组 P10721 202406 六级 计算得分 基础动态规划: P10108 202312 六级 闯关游戏 P10376 202403 六级 游戏 P11246 202409 六级 小杨和整数拆分 记忆化搜索: P10250 GESP样题 六级 下楼梯 复杂贪心: P11247 202409 六级 算法学习 其它 树状数组: B3874 202309 六级 小杨的握手问题 暴力枚举: P10377 202403 六级 好斗的牛 模拟+高精度: P11375 202412 六级 树上游走 相关练习题目 背包 从 这儿 可以获得洛谷上所有的背包相关题目,推荐练习的如下: P1734 最大约数和 P1507 NASA的食物计划 P1164 小A点菜 P1060 NOIP 2006 普及组 开心的金明 P1358 扑克牌 P1877 HAOI2012 音量调节 P1910 L 国的战斗之间谍 P1926 小书童——刷题大军 P1855 榨取kkksc03 P2430 严酷的训练 P1802 5 倍经验日 P1757 通天之分组背包 P2563 AHOI2001 质数和分解 GESP 7 级 大题核心考点 注:名称中的 A 表示第 1 题,B 表示第 2 题。 动态规划 背包: P11377 202412-A 七级 武器购买 P13018 202506-B 七级 调味平衡 动态规划: P10111 202312-B 七级 纸牌游戏 P11248 202409-A 七级 矩阵移动 并查集 P10378 202403-A 七级 交流问题 枚举/DFS/BFS(在树或者图上) P10110 202312-A 七级 商品交易 P10379 202403-B 七级 俄罗斯方块 P11249 202409-B 七级 小杨寻宝 P11378 202412-B 七级 燃烧 P11964 202503-A 七级 图上移动 P14077 202509-A 七级 连通图 树状数组 P14078 202509-B 七级 金币收集

2025/6/6
articleCard.readMore

CSPJ 教学总结:树状数组

引言 有些时候,题目给我们 N 个元素的序列,然后让我们求前缀和或者区间和。并且,题目还会动态地修改这个序列的值。如果我们每次暴力求解前缀和,时间复杂度会是 O(N),而使用树状数组,可以将查询前缀和的复杂度降低到 O(LogN)。 树状数组是挺不好教学的一个知识点。它需要以下前置知识: 二进制表示法及熟练的位操作 前缀和的知识 树的基础知识 时间复杂度的估算 在教学的时候,我们的教学顺序如下: 先引入问题 lowbit 函数讲解 树状数组的结构特点 利用树状数组求前缀和的方法 怎么修改树状数组的值 如何初始化树状数组 增加值或替换值 二维的树状数组 那么让我们来开始。 问题的引入 P3374 树状数组 1 是一道标准的树状数组问题:该题目给我们了一个数列,我们需要解决以下两个问题: 数列的区间求和 更新某一个数(加上 x) 我们很容易想到用暴力的方法来做此题。于是我们可以估计一下暴力的时间复杂度: 数列的区间求和,时间复杂度 O(N) 更新某一个数,时间复杂度 O(1) 题目中提到,求和的次数最多为 M 次,所以最坏情况下,时间复杂度为 O(M*N)。而由于 M 和 N 的最大范围为 5*10^5,所以最大运算次数高达 (5*10^5) * (5*10^5) = 2500亿次,而竞赛中估算 1000 万次的运算时间就接近 1 秒了,这个时间肯定会超时。 数列的区间求和有一个 O(1)的办法,就是提前求出前缀和。假如 Sum(i) 表示前 i 个数的和,那么区间 (i,j] 的和就可以通过 Sum(j) - Sum(i) 来得出。可惜的是,本题还有一个操作是更新某一个数。如果更新的是第一个数,那么整个前缀和数组 Sum 都需要更新,这样更新的时间复杂度会变成 O(N),最坏情况下会有 O(M*N)次更新,造成运算同样超时。 由此,我们需要一个更优秀的数据结构来解决这类问题,这就是树状数组。 lowbit 函数 在讲解树状数组前,我们先学习一下 lowbit 函数。 lowbit 函数实现的功能是:求 x 的二进制最低位 1 以及后面的 0 组成的数。例如: 8 (10 进制) = 1000 (2 进制) ,则 lowbit(8) = 8 9 (10进制)= 1001(2 进制),则 lowbit(9) = 1 10(10 进制)= 1010(2 进制),则 lowbit(10) = 2 所以,我们需要找到目标数的二进制中的最后那个 1 的位置。有两种实现方式: 方法一:x^(x-1) & x 方法一相对比较好理解,我拿二进制数 1100 举例解释如下: (x-1)的效果,相当于把二进制的最后一个1变成 0,比如某数 1100 减 1之后,就变成了 1011 这个时候,如果我用 x^(x-1),就会得到 1100^1011=0111 最后,用 x& 刚刚的 x^(x-1),就相当于把x的最后一个1留下来了,前面的1都抹掉了:1100 & 0111 = 0100 方法二:x&-x 我们还是拿二进制数 1100 举例,由于负数是用补码表示,所以对于 1100,它的负数: 原码为:11100(最高为 1 为符号位) 反码为:10011(反码符号位不变,其余位取反) 补码为:10100(补码=反码+1) 这样一操作,x&-x 就等于 01100 & 10100 = 0100,同样把最后的 1 取出来了。 在实现中,我们用方法二的更多,因为更短。参考代码如下: 1 2 3 int lowbit(int x) { return x & -x; } 树状数组的定义 对于一个长度为 N 的序列,为了满足上面提到的更快的区间求和和更新的需求,我们可以构造一个树状数组。 树状数组(Binary Index Tree,简称 BIT)通过构造另一个长度为 N 的数组,来做到: 区间求和,时间复杂度 O(log N) 更新某一个数,时间复杂度 O(log N) 因为树状数组需要另外创建一个长度为 N 的数组,所以它的空间复杂度为O(N)。 我们先创建出这个数组 b ,然后再引入它的元素间的树状逻辑关系。 我们有了数组 b,我们让数组 b 相对于原始序列 a,按如下的关系来保存范围和: b[1] 保存 a[1]的值 b[2] 保存区间 [a[1], a[2]] 的和 b[3] 保存 a[3]的值 ….省略若干行 b[8] 保存区间 [a[1], a[8]] 的和 我们先不管如何做到的,先假设我们按上面的逻辑,初始化好了这个数组,那么它怎么能快速求出前缀和呢? 树状数组求和 我们假设要求 a[1] ~ a[7]的和,如下图所示,我们知道这段和满足:Sum(7) = b[4] + b[6] + b[7] 那么,我们观察一下 b[4],b[6],b[7] 这几个下标有什么特点: 4 的二进制:0100 6 的二进制:0110 7 的二进制:0111 如果结合上我们刚刚教的 lowbit 函数,我们就可以发现如下规律: 4 的二进制:0100,4 = 6 - lowbit(6) 6 的二进制:0110,6 = 7 - lowbit(7) 7 的二进制:0111 于是,如果我们要求 Sum(7),就可以用 b[7] 开始累加,然后用 7 - lowbit(7) 得到 6,再用 6 - lowbit(6) 得到 4,最后 4 - lowbit(4) = 0,就结束整个求和累加过程。 把以上逻辑转换成代码,是这样的: 1 2 3 4 5 6 7 8 int query(int range) { int ret = 0; while (range > 0) { ret += b[range]; range -= lowbit(range); } return ret; } 有人可能要问了,这个求和都是从序列开头开始的,如果我们想求序列中间一段,比如从 x 到 y 的区间和,应该怎么办呢?这种情况,我们可以: 用 query(y) 把从头到 y 位置的和求出来 用 query(x-1) 把从头到 x-1 位置的和求出来 然后相减 query(y) - query(x-1) 得到区间 [x,y] 的和 更新数据 树状数组也支持更新数据,像P3374 树状数组 1题目中要求的那样,我们可以将某个数加上 x,这种情况应该如何更新数组呢? 我们以更新 a[1]为例,通过观察,我们发现涉及 a[1] 的数组有:b[1],b[2],b[4],b[8],如下图所示: 你有观察出来规律吗?这刚好是我们构建的这个树从叶子结点到根结点的一条路径。 那同样的问题来了,我们如何求解出b[1],b[2],b[4],b[8]这个路径呢?我们来观察一下: 1 的二进制是:0001 2 的二进制是:0010, 2 = 1 + lowbit(1) 4 的二进制是:0100, 4 = 2 + lowbit(2) 8 的二进制是:1000, 8 = 4 + lowbit(4) 我们再验证一个中间结点的更新,比如更新 a[5],如下图所示: 我们看看规则是不是一样: 5 的二进制是 0101, 6 的二进制是 0110,6 = 5 + lowbit(5) 8 的二进制是 1000,8 = 6 + lowbit(6) 至此,我们总结出更新方法:从数列的下标 idx 开始,不停地更新,并且用 idx += lowbit(idx) 获得下一个更新的下标,直到更新到下标超过上界(N)为止。 1 2 3 4 5 6 void add(int idx, int val) { while (idx <= n) { b[idx] += val; idx += lowbit(idx); } } 初始化 最暴力的初始化方法是:我们假设原序列全是 0,这样树状数组的初始状态也全是 0 即可正常表达上面的树型关系。然后,我们把每一个 a 序列中的数用更新的方式来放入树状数组中。 至此,我们完成了例题P3374 树状数组 1中的所有细节讨论,完整的代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; #define MAXN (int)(500000+10) int n, m; int a[MAXN], b[MAXN]; int lowbit(int x) { return x & -x; } void add(int idx, int val) { while (idx <= n) { b[idx] += val; idx += lowbit(idx); } } int query(int range) { int ret = 0; while (range > 0) { ret += b[range]; range -= lowbit(range); } return ret; } int main() { ios::sync_with_stdio(false); cin >> n >> m; for (int i = 1; i <=n; ++i) { cin >> a[i]; add(i, a[i]); } for (int i = 1; i <= m; ++i) { int op, x, y; cin >> op >> x >> y; if (op == 1) { add(x, y); } else { cout << query(y) - query(x-1) << endl; } } return 0; } 但是,以上的这种初使化方法,时间复杂度为 O(N*logN),如果数据刚好卡在初始化中,我们可以用以下这种方法来将初始化时间复杂度优化到 O(N)。 初始化(优化) 为了讲明白这种初始化,我们需要观察树状数组 b 中的每个元素代表的数据范围有什么规律。为什么 b[5] 只代表 a[5] 一个元素,但是 b[8]代表的是[a[1],a[8]] 区间的 8 个元素的和 ? 最终我们可以发现,一个数组元素代表的区间范围大小就是它的 lowbit 函数求出来的值。 例如: lowbit(5) = 1,所以它只代表 a[5] 一个元素 lowbit(8) = 8,所以它代表 [a[1],a[8]] 共 8 个元素 一个十进制数 88,其二进制为 01011000,lowbit(88)=8,所以它代表的区间为 8 个元素。 进一步的,我们可以观察出,对于一个 b[x],它代表的区间为[x-lowbit(x)+1, x]。 这对初始化有什么用呢? 我们如果构建了数组 a 的前缀和数组 s,s[i]表示前 i 个数的和。 那么,我们就可以用前缀和数组 s 来初始化 b[x]。 因为 b[x] 代表的区间和是[x-lowbit(x)+1, x],所以:b[i] = s[i] - s[i-lowbit(i)] 至此,我们可以将例题P3374 树状数组 1的代码更新如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; #define MAXN (int)(500000+10) int n, m; int a[MAXN], b[MAXN], s[MAXN]; int lowbit(int x) { return x & -x; } void add(int idx, int val) { while (idx <= n) { b[idx] += val; idx += lowbit(idx); } } int query(int range) { int ret = 0; while (range > 0) { ret += b[range]; range -= lowbit(range); } return ret; } int main() { ios::sync_with_stdio(false); cin >> n >> m; for (int i = 1; i <=n; ++i) { cin >> a[i]; s[i] = s[i-1] + a[i]; } // 初始化 for (int i = 1; i<=n; ++i) { b[i] = s[i] - s[i-lowbit(i)]; } for (int i = 1; i <= m; ++i) { int op, x, y; cin >> op >> x >> y; if (op == 1) { add(x, y); } else { cout << query(y) - query(x-1) << endl; } } return 0; } 管辖区间 上面讲到,树状数组中的元素 b[x] 管辖的区间和是[x-lowbit(x)+1, x],因此,我们更能理解树状数组的更新逻辑: 所谓的更新a[x],就是把管辖区间涵盖 a[x] 的所有 b[x]都更新一遍。 那哪些 b[x]的管辖区间涵盖 a[x]呢?就是从二进制看,就是范围中有 lowbit(x) 的数。 举例来说,如果我们要更新 a[2] 的值,lowbit(2) 的值是 0010,所以,我们要更新: b[2], 因为 2 的二进制是 0010,管辖区间是 [1, 2],宽度是 2 b[4], 因为 4 的二进制是 0100,管辖区间是 [1, 4],宽度是 4 b[8], 因为 8 的二进制是 1000,管辖区间是 [1, 8],宽度是 8 再举一个例子,如果我们要更新 a[5] 的值,lowbit(5) 的值是 0001,所以我们要更新: b[5],因为 5 的二进制是 0101,管辖区间是 [5, 5],宽度是 1 b[6],因为 6 的二进制是 0110,管辖区间是 [5, 6],宽度是 2 b[8],因为 8 的二进制是 1000,管辖区间是 [1, 8],宽度是 8 再举一个例子,如果我们要更新 a[7] 的值,lowbit(7) 的值是 0001,所以我们要更新: b[7],因为 7 的二进制是 0111,管辖区间是 [7, 7],宽度是 1 b[8],因为 8 的二进制是 1000,管辖区间是 [1, 8],宽度是 8 通过上面的例子,我们可以看到,管辖区间在更新的过程中宽度是不断扩大的。不同的数,宽度扩大的倍数不同。但至少是每次翻倍的方式来扩大。 我们再从另一个角度来看管辖区间:我们把数状数组的第 1 个到第 56 个元素的二进制列出来,如下所示: 我们可以观察到:bit 为 1 的位置越低,管辖的区域越小,所以: 有一半管辖区域大小为 1 的数(图中为粉色) 剩下的一半,有一半管辖区域大小为 2 的数(图中为绿色) 再剩的一半,有一半管辖区域大小为 4 的数(图中为紫色) 再剩的一半,有一半管辖区域大小为 8 的数(图中为黄色) 再看这些数的间隔: 粉色的间隔是 2-1,每 2 个出现一次 绿色的间隔是 4-1,每 4 个出现一次 紫色的间隔是 8-1,每 8 个出现一次 黄色的间隔是 16-1,每 16 个出现一次 所以,其实树状数组是把区间和数据按分治的思想进行了切分,这样可以快速求和。 另外,从管辖区域的角度考虑,每一个数在进行 lowbit 减运算的时候,得到的新数,一定和之前的区间不是重叠的。我们可以这样证明: 每个元素 b[x] 管辖的区间和是[x-lowbit(x)+1, x] 我们令 y = x - lowbit(x), 则 b[y] 的管辖区间就是:[y-lowbit(y)+1, y],即:[y-lowbit(y)+1, x - lowbit(x)] 可以看到,这两个区间 [y-lowbit(y)+1, x - lowbit(x)] 和 [x-lowbit(x)+1, x]其实是相邻的。 所以,每次减 lowbit(x) 的运算,其实是获得了其左侧相邻的一块区间的和。 我们来看一个查询和的例子,如果我们要求前缀和 sum(7): 我们先计算 b[7],7 的二进制是 0111,管辖区间是 [7, 7],宽度是 1 我们再计算 b[6],6 的二进制是 0110,管辖区间是 [5, 6],宽度是 2 我们再计算 b[4],4 的二进制是 0100,管辖区间是 [1, 4],宽度是 4 我们从上面的例子可以看到:由于每次减掉的都是最小的一个 lowbit 位,所以左侧相邻的新区间一定更宽。所以求和过程中, b[7],b[6],b[4] 对应的管辖宽度从 1 到 2 再到 4. 我们再看一个前缀和 sum(9) 的例子: 我们先计算 b[9], 9 的二进制是 1001,管辖区间是 [9, 9],宽度是 1 我们再计算 b[8], 9 的二进制是 1000,管辖区间是 [1, 8],宽度是 8 和我们刚刚得到的结论相同:求和过程中,随着不断地减 lowbit(x),获得的新区间更宽。 小结: 树状数组中的元素 b[x] 管辖的区间和是[x-lowbit(x)+1, x] 每次加 lowbit(x) 的过程,相当于在不断扩展管辖区间。不同的数,宽度扩大的倍数不同。但至少是每次翻倍的方式来扩大。 每次减 lowbit(x) 的过程,相当于在查找紧临 b[x] 管辖区间的一块新区间。这个新区间,宽度也是不断扩大的。不同的数,宽度扩大的倍数不同。但至少是每次翻倍的方式来扩大。 差分数组 有些时候,题目会让我们一次更新一段区间,这个时候,我们可以引入差分数组来替代原数组。 差分数组中的每一个元素,是原数组相邻两个数的差。 例如: 原数组: 1,2,3,4,5,6 差分数组:1,1,1,1,1,1 我们对差分数组求前缀和,就可以还原出原数组。 这个时候,如果我们把原数组的第 3 个数到第 5 个数都加上 2,我们看看效果: 原数组: 1,2,5,6,7,6 差分数组:1,1,3,1,1,-1 我们观察到,原数组的一个区间都加上 2 之后,在差分数组那里,只有第 3 个数和第 6 个数有变化,其它都没有变化。所以,如果我们用差分数组来代替原数组,就可以只更新两个数值来代表原来的范围更新。 P3368 【模板】树状数组 2此题可以很好地练习差分数组与数状数组的结合运用,相关代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 /** * 差分: * - 假设 A 序列为原序列 * - 差分数列 C 为原序列每两个数之间的差 * - 即:c[i] = a[i] - a[i-1] * c[1] = a[1] * c[2] = a[2] - a[1] * c[3] = a[3] - a[2] * - 所以: * - a[i] = sum(c[1]+c[2]+...c[i]) * * 对于本题,如果把数组变成差分数组: * - [x,y] 每个数加上 k,等价于: * - c[x] += k * - c[y+1] -= k * - 求第 a[x] 的值,等价于: * - sum(c[1]+c[2]+...c[x]) * - 即求前缀和 * * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; #define MAXN (int)(500000+10) int n, m; int a[MAXN], c[MAXN], b[MAXN]; int lowbit(int x) { return x&-x; } void add(int idx, int v) { while (idx <= n) { b[idx] += v; idx += lowbit(idx); } } int query(int range) { int ret = 0; while (range) { ret += b[range]; range -= lowbit(range); } return ret; } int main() { ios::sync_with_stdio(false); cin >> n >> m; for (int i = 1; i <= n; ++i) { cin >> a[i]; c[i] = a[i] - a[i-1]; add(i, c[i]); } while (m--) { int op, x, y, k; cin >> op; if (op == 1) { cin >> x >> y >> k; add(x, k); add(y+1, -k); } else { cin >> x; cout << query(x) << endl; } } return 0; } 二维的树状数组 刚刚讲到,对于一个 b[x],它代表的区间为[x-lowbit(x)+1, x] 那么对于一个二维的树状数组 b[x, y],它代表的区间就是 a(x-lowbit(x)+1, y-lowbit(y)+1) - a(x, y) 形成的矩阵的总和。如下图所示: 对于二维的树状数组,更新就需要用两层的循环了。示例代码如下: 1 2 3 4 5 6 7 void add(int x, int y, int v) { for (int i = x; i <= n; i += lowbit(i)) { for (int j = y; j <= m; j += lowbit(j)) { c[i][j] += v; } } } 查询前缀和同样需要用循环,示例代码如下: 1 2 3 4 5 6 7 8 9 int query(int x, int y) { int res = 0; for (int i = x; i > 0; i -= lowbit(i)) { for (int j = y; j > 0; j -= lowbit(j)) { res += c[i][j]; } } return res; } 如果题目要求区间和,则需要用容斥原理来求解,这里不再展开介绍。 用树状数组求逆序对 什么是逆序对?逆序对是指一个序列中,a[i] > a[j] 且 i < j 的有序对。 比如一个序列是 3 2 1,它的逆序对就有:3 2,3 1,2 1 三组。 树状数组如何和逆序对的数量扯上关系呢? 拿序列 3 2 1 举例,我们知道,树状数组是可以用前缀和的。如果我们: 假设序列初始情况下为全 0 当处理第一个数 3 的时候,我们让树状数组的下标 3 加 1:update(3, 1),同时记录插入了 1 个数 当处理第二个数 2 的时候,我们统计小于等于 2 的前缀和:query(2),然后拿总数减 query(2),得到大于 2 的数字数量 这个数量,就是当 2 被处理的时候,前面有一共多少个数大于 2,即与 2 能够组成逆序对的数量 例题:P1908 逆序对 在此题中,我们先要解决两个问题,才能借用上面的思想: 问题1、题中的数据范围太大,我们如何解决? 答案:我们可以用离散化的思想,把 2 10000 1 变成 2 3 1,因为逆序对是统计相对大小,所以这样更改之后,逆序对的数量是不变的。 具体如何离散化呢?我们可以将数据依次标记上编号,然后排序。例如: 原始序列为 100 200 50, 我们把它分别标上编号 (100,1), (200,2), (50,3) 然后我们将数值排序,得到:(50,3), (100,1), (200,2) 然后,我们再将新的序列赋上从 1 开始的编号:(50,3,1), (100,1,2), (200,2,3) 然后,我们再将序列按原来的编号(第 2 个数字)排序,得到 (100,1,2), (200,2,3), (50, 3, 1) 至此,我们转换得到了新的编号 2,3,1 因为 N 最多是 5*10^5,所以离散化之后,树状数组的大小也缩减到了 5*10^5 在实现的时候,我们可以用结构体来保存上面的三元组。 1 2 3 4 5 struct Node { int v; int origin_idx; int next_idx; }; 问题2、如果有两个相等的元素,会不会计算错误? 我们假设元素是 200 300 200,按我们刚刚的操作: 先标号,得到 (200,1) (300,2) (200,3) 再排序,得到 (200,1) (200,3) (300,2) 再标号,得到 (200,1,1) (200,3,2) (300,2,3) 再排序,得到 (200,1,1) (300,2,3) (200,3,2) 最后序列是 1,3,2 这种是没问题的,但是,如果我们排序的时候不是用的稳定排序,把第二个 200 排到了前面,就会得到 2,3,1,这样逆序对就会多一个 2 1,而这本来是不存在的。 所以,为了解决这个问题,我们可以用稳定排序stable_sort,或者保证排序的时候,值相同的情况下,标号大的在后面。 以下是完整的参考程序: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; #define MAXN (int)(5*1e5+10) struct Node { int v; int origin_idx; int next_idx; }; Node a[MAXN]; int n,c[MAXN]; long long ans; bool comp1(const Node &a, const Node &b) { return a.v < b.v; } bool comp2(const Node &a, const Node &b) { return a.origin_idx < b.origin_idx; } int lowbit(int x) { return x&-x; } void add(int a, int v) { while (a<=n) { c[a]+=v; a+=lowbit(a); } } int query(int a) { int ret = 0; while(a) { ret += c[a]; a -= lowbit(a); } return ret; } int main() { cin >> n; for (int i = 1; i <=n; ++i) { cin >> a[i].v; a[i].origin_idx = i; } stable_sort(a+1, a+1+n, comp1); for (int i = 1; i<=n; ++i) a[i].next_idx = i; stable_sort(a+1, a+1+n, comp2); for (int i = 1; i <=n; ++i) { add(a[i].next_idx, 1); ans += i - query(a[i].next_idx); } cout << ans << endl; return 0; } 相关练习题目 文章中涉及的例题: P3374 树状数组 1 P3368 树状数组 2 P1908 逆序对 练习题: 题目描述 B3874 小杨的握手问题GESP 202309 六级真题 -- B3874 小杨的握手问题 解题思路: 把学号为 a 的学生进入教室的行为,转化为第 a 个序列元素的值加 1。 这样,找出小于 a 的学生数量,就等价于求序列前 a-1 个元素的前缀和。 利用数状数组,就可以快速求前缀和了。 参考代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 /** * 数状数组求逆序对。 * * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; #define MAXN int(3e5+10) int n, b[MAXN]; long long ans; int lowbit(int a) { return a&-a; } void add(int idx, int v) { while (idx <= n) { b[idx] += v; idx += lowbit(idx); } } int query(int range) { int ret = 0; while (range) { ret += b[range]; range -= lowbit(range); } return ret; } int main() { ios::sync_with_stdio(0); cin >> n; for (int i = 0; i < n; ++i) { int a; cin >> a; // 将学号下标从 0 开始改到 1 开始 a = a + 1; ans += query(a - 1); add(a, 1); } cout << ans << endl; return 0; }

2025/4/26
articleCard.readMore

CSPJ 教学总结:深度优先搜索(DFS)

深度优先搜索(DFS)是学生学习算法的第一道门槛,因为它的主要形式是递归。而递归中需要将搜索的相关信息通过参数传递,这一点需要学生深刻理解 DFS。 模版 DFS 有比较标准的模版,如下所示: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void dfs(int pt) // pt 表示层数 { if (终止条件) { // 处理 return; } for (枚举这个层的所有可选项) { if(这个选项是合法的){ 标记这个选项(保存现场) dfs(pt+1); 取消标记(恢复现场) } } } 我们将运用该模版,完成后面的题目。 递归的深度 递归的时候,程序会占用栈空间来保存函数的环境变量。根据编译器以及编辑参数的不同,栈空间的大小也不同。通常情况下,竞赛中的编译器设定的栈空间为 8M 或者 16M。 假如,我们在一个递归函数中使用了 10 个 int 变量,那么每个递归函数就需要 4*10=40字节的栈空间。8M 一共可以支持 8*1000*1000/40=200000层调用。考虑到栈空间还需要保存当前函数执行的地址等变量,可供支持的调用层数会更小一点。 同学们也可以做如下的递归函数栈空间的测试: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; int dfs(int x) { int test[9] = {0}; cout << x << endl; dfs(x + 1); return 0; } int main() { dfs(0); return 0; } 在我的本地,以上程序调用了约 13 万次后栈溢出。为了保险,我们在比赛中如果调用深度小于 1 万层,那应该是稳妥的;否则我们需要考虑是否用别的解法来解题。 教学和练习题目 题目名说明 P1036 选数NOIP 2002 普及组 P1219 八皇后 Checker ChallengeUSACO 1.5 P1596 Lake Counting SUSACO10OCT P2036 PERKETCOCI 2008/2009 #2 P12139 黑白棋蓝桥杯 2025 省 A,写起来较繁琐 P1605 迷宫标准的 DFS P2404 自然数的拆分问题 P1019 单词接龙NOIP 2000 提高组 P7200 P10483 P1219 八皇后 Checker Challenge 这是八皇后的变种,N 皇后问题。可以作为基础练习。具体解法是: 我们用变量 v[15] 表示每个皇后的列值。 对于新放入的皇后,我们依次检查它与前面的皇后是否在一条斜线上。检查方法是看其“横坐标差”与“纵坐标差”是否相同。检查函数如下: 1 2 3 4 5 6 bool check(int pt) { for (int i = 0; i < pt; i++) { if (abs(v[i] - v[pt]) == abs(i - pt)) return false; } return true; } 完整的代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; int n; int v[15], ans; bool flag[15]; bool check(int pt) { for (int i = 0; i < pt; i++) { if (abs(v[i] - v[pt]) == abs(i - pt)) return false; } return true; } void dfs(int pt) { if (pt == n) { ans++; if (ans <= 3) { for (int i = 0; i < n; i++) { cout << v[i] << " "; } cout << endl; } return; } for (int i = 1; i <= n; i++) { if (flag[i]==false) { flag[i] = true; v[pt] = i; if (check(pt)) dfs(pt + 1); flag[i] = false; } } } int main() { cin >> n; dfs(0); cout << ans << endl; return 0; } P1036 选数 此题需要从小到大取数求和,然后再判断是否是素数。用递归的方式来进行枚举。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; int n, k, tot, ans; int a[22], p[22]; bool isPrime(int v) { for (int i = 2; i*i <= v; ++i) { if (v%i == 0) { return false; } } return true; } void dfs(int pt) { if (pt == k+1) { if (isPrime(tot)) { ans++; } } else { // 每一层都必须取比前一层更大的下标,防止重复取 for (int i = p[pt-1]+1; i <= n; ++i) { p[pt] = i; tot += a[i]; dfs(pt+1); tot -= a[i]; } } } int main() { cin >> n >> k; for (int i = 1; i <= n; ++i) { cin >> a[i]; } dfs(1); cout << ans << endl; return 0; } P1596 Lake Counting S 此题既可以用 DFS,也可以用 BFS。考虑到 N 和 M 最大值为 100,所以递归的层次最多为 1 万层,所以我们可以试试 DFS。 以下是参考代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; int n, m; char tu[105][105]; int ans; int movex[8] = {0, 0, 1, -1, 1, 1, -1, -1}; int movey[8] = {1, -1, 0, 0, 1, -1, 1, -1}; void dfs(int x, int y) { tu[x][y] = '.'; for (int i = 0; i < 8; i++) { int nx = x + movex[i]; int ny = y + movey[i]; if (nx < 0 || nx >= n || ny < 0 || ny >= m || tu[nx][ny] != 'W') continue; dfs(nx, ny); } } int main() { cin >> n >> m; for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { cin >> tu[i][j]; } } for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { if (tu[i][j] == 'W') { dfs(i, j); ans++; } } } cout << ans << endl; return 0; } P2036 PERKET 因为 N 最多为 10,每种食材可以选或者不选两种情况,所以最多情况数为 2^10=1024 种。搜索时间满足要求。 所以,此题用 DFS 可以非常方便解决。在搜索的时候,我们可以将食材的相关信息带到 DFS 函数的参数中,方便最后答案的求解。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; int n; int s[11], b[11], v[11]; int ans = INT_MAX; /** * pt: 当前处理到的食材 * cnt: 当前选中的食材数量 * ss: 当前选中的食材的总酸度 * bb: 当前选中的食材的总甜度 */ void dfs(int pt, int cnt, int ss, int bb) { if (pt == n) { if (cnt > 0) { ans = min(ans, abs(ss - bb)); } return; } v[pt] = 1; dfs(pt + 1, cnt + 1, ss * s[pt], bb + b[pt]); v[pt] = 0; dfs(pt + 1, cnt, ss, bb); } int main() { cin >> n; for (int i = 0; i < n; i++) { cin >> s[i] >> b[i]; } dfs(0, 0, 1, 0); cout << ans << endl; return 0; } P12139 黑白棋 此题是搜索题,需要在中间尽可能检查状态来剪枝,以节省搜索次数。 题目有三类限制,分别可以用在不同的剪枝环节。 限制一:在每一行和每一列中,黑色棋子和白色棋子的数量必须相等(即为 3)。 我们可以对每一行记录黑子和白子的数量,如果某一行或某一列的一种颜色达到 3,后面则不能用这个颜色。 限制二:不能有超过两个相同颜色的棋子连续排列。 我们可以在当前落子的时候,检查它的左边和上面连续的几个格子,看是否有 3 个相同的子。 限制三:行列唯一性 可以放到最后检查。 另外,这个棋盘有几个位置已经设定了值,我们需要标记下来,搜索的时候跳过对这些位置的尝试,但需要在这些位置做合法性检查。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; int row_cnt[6][2], col_cnt[6][2]; char tu[7][7], mark[7][7]; bool check(int r, int c) { // 在每一行和每一列中,黑色棋子和白色棋子的数量必须相等(即为 3) if (row_cnt[r][1] > 3 || row_cnt[r][0] > 3 || col_cnt[c][1] > 3 || col_cnt[c][0] > 3) return false; // 不能有超过两个相同颜色的棋子连续排列 if (r >= 2) { if (tu[r][c] == '1' && tu[r-1][c] == '1' && tu[r-2][c] == '1') return false; if (tu[r][c] == '0' && tu[r-1][c] == '0' && tu[r-2][c] == '0') return false; } if (c >= 2) { if (tu[r][c] == '1' && tu[r][c-1] == '1' && tu[r][c-2] == '1') return false; if (tu[r][c] == '0' && tu[r][c-1] == '0' && tu[r][c-2] == '0') return false; } return true; } // 行列唯一性检查 bool final_check() { set<int> row_set, col_set; for (int i = 0; i < 6; i++) { int v = 0; for (int j = 0; j < 6; ++j) { v = v * 10 + (tu[i][j] - '0'); } row_set.insert(v); } if (row_set.size() != 6) return false; for (int j = 0; j < 6; ++j) { int v = 0; for (int i = 0; i < 6; ++i) { v = v * 10 + (tu[i][j] - '0'); } col_set.insert(v); } if (col_set.size() != 6) return false; return true; } void dfs(int r, int c); void try_dfs(int r, int c) { char ch = tu[r][c]; row_cnt[r][ch - '0']++; col_cnt[c][ch - '0']++; if (check(r, c)) { int nr = r; int nc = c + 1; if (nc == 6) { nr++; nc = 0; } dfs(nr, nc); } row_cnt[r][ch - '0']--; col_cnt[c][ch - '0']--; } void dfs(int r, int c) { if (r == 6) { if (final_check()) { for (int i = 0; i < 6; i++) { for (int j = 0; j < 6; j++) { cout << tu[i][j]; } } cout << endl; // 因为只有一个合法解,所以找到答案就退出 exit(0); } return; } if (mark[r][c] == 0) { tu[r][c] = '1'; try_dfs(r, c); tu[r][c] = '0'; try_dfs(r, c); } else { tu[r][c] = mark[r][c]; try_dfs(r, c); } } void init() { memset(mark, 0, sizeof(mark)); mark[0][0] = '1'; mark[0][1] = '0'; mark[0][3] = '0'; mark[1][3] = '0'; mark[2][4] = '0'; mark[2][5] = '0'; mark[4][2] = '1'; mark[4][5] = '1'; mark[5][1] = '0'; mark[5][4] = '1'; } int main() { init(); dfs(0, 0); return 0; } P1605 迷宫 用 DFS 来枚举,但需要标记走过的路。 因为最多只有 5x5=25 个格子,所以递归的深度最大只有 25,不存在溢出情况。 因为有陷阱(不能走)和起点终点(不能重复走),所以我们假设平均每次有 2 条支路, 整个的最坏情况估计只有 2^23=8388608 次,所以也不会超时。 一些陷阱: 终点可能也有障碍物,这个时候始终就到不了。 起点在走之前需要标记,否则会重复走。 参考代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include <bits/stdc++.h> using namespace std; // 0 - 空地 // 1 - 障碍物 int tu[6][6], n, m, t, sx, sy, ex, ey, ans; int movex[]={1,-1,0,0}; int movey[]={0,0,-1,1}; void dfs(int x, int y) { if (x == ex && y == ey) { ans++; return; } for (int i = 0; i < 4; ++i) { int tox = x + movex[i]; int toy = y + movey[i]; if (tox >=1 && tox<=n && toy>=1 && toy<=m && tu[tox][toy]!=1){ tu[tox][toy]=1; dfs(tox, toy); tu[tox][toy]=0; } } } int main() { ios::sync_with_stdio(false); cin >> n >> m >> t; cin >> sx >> sy >> ex >> ey; while (t--) { int x, y; cin >> x >> y; tu[x][y] = 1; } tu[sx][sy] = 1; dfs(sx, sy); cout << ans << endl; return 0; } P2404 自然数的拆分问题 DFS,有两个技巧: 保证后面的数 >= 前面的数。 让每个数必须小于 n,这样不会出现 n=n 这种等式。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; int n, tot, v[10]; void dfs(int pt) { if (tot == n) { cout << v[1]; for (int i = 2; i < pt; ++i) { cout << "+" << v[i]; } cout << endl; return; } for (int i = v[pt-1]; tot + i <=n && i < n ; ++i) { tot += i; v[pt] = i; dfs(pt+1); tot -= i; } } int main() { ios::sync_with_stdio(false); cin >> n; v[0] = 1; dfs(1); return 0; }

2025/4/13
articleCard.readMore

CSPJ 教学总结:STL

STL 库是 C++ 语言的标准库,我们在比赛中主要用到的有如下内容。 string 类 substr find replace insert erase c_str 容器 pair vector deque list stack queue priority_queue map unordered_map set unordered_set 算法库 函数调用示意说明 sortsort(v.begin(), v.end())快速排序 stable_sortstable_sort(v.begin(), v.end())稳定排序 uniqueunique(v.begin(), v.end())去重,返回的是去重后的元素末地址。可以结合 erase 函数来把多余数据删除。参考代码:v.erase(unique(v.begin(), v.end()), v.end()); next_permutationnext_permutation(v, v+n)返回全排列的下一个值,当没有下一个排列时,函数返回 false prev_permutationprev_permutation(v, v+n)返回全排列的上一个值,当没有上一个排列时,函数返回 false nth_elementnth_element(v.begin(), v.begin() + k, v.end()),函数执行后,v.begin()+k 位置的数为排序后的最终位置,即左边的数都小于它,后面的数都大于它 lower_boundslower_bounds(v, v+n, a)查找大于或等于 a 的第一个位置,如果没找到则返回 end() upper_boundsupper_bounds(v, v+n, a)查找大于 a 第一个位置,如果没找到则返回 end() equal_rangeequal_range(v, v+n, a)equal_range 返回一个 pair,first 元素是查找到的匹配 a 值的左边界,second 元素是匹配到的 a 值的右边界,边界为左闭右开原则。当 first == second 的时候,相当于没找到目标值 __gcd__gcd(a, b)返回 a 和 b 的最大公约数 reversereverse(v.begin(), v.end())将原序列逆序 min_elementmin_element(v.begin(), v.end())返回的是地址,如果想要值,可以用 * 获得对应下标的值,如果想获得下标,可以让它减去 v.begin() max_elementmax_element(v.begin(), v.end())返回的是地址,如果想要值,可以用 * 获得对应下标的值,如果想获得下标,可以让它减去 v.begin() accumulateaccumulate(v.begin(), v.end(), 0);第三个参数是初始值 练习 题号说明 P1996 约瑟夫问题适合用 list P3613 寄包柜适合用 map 和 pair P4387 验证栈序列适合用 stack P1540 机器翻译NOIP 2010 提高组,适合用 vector 以及 STL 的 find 算法 P1449 后缀表达式适合练习 stack P2058 海港NOIP 2016 普及组,练习桶和队列 P2234 营业额统计练习 set 和 lower_bound 函数 P4305 不重复数字可以练习 unordered_map 以及对比 cin 和 scanf 的速度差别 P1571 眼红的Medusa练习 map 或 set P4387 验证栈序列 解法:把 A 数组中的元素住栈里面 push,然后如果栈顶元素和 B 数组的当前元素相同,就 pop,同时 B 数组的当前元素后移。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; int t, n, a[100010], b[100010]; int main() { ios::sync_with_stdio(false); cin >> t; while (t--) { cin >> n; for (int i = 0; i < n; ++i) cin >> a[i]; for (int i = 0; i < n; ++i) cin >> b[i]; stack<int> q; int idx = 0; for (int i = 0; i < n; ++i) { q.push(a[i]); while (!q.empty() && q.top() == b[idx]) { q.pop(); idx++; } } if (q.empty()) cout << "Yes" << endl; else cout << "No" << endl; } return 0; } P1540 机器翻译 参考代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); int m, n, t, ans = 0; cin >> m >> n; vector<int> v; while (cin >> t) { if (find(v.begin(), v.end(), t) == v.end()) { // 如果不在内存中 v.push_back(t); ++ans; } if (v.size() > m) v.erase(v.begin()); } cout << ans << endl; } P1449 后缀表达式 表达式计算: 不停读入。 如果读到数字,就和之前的数字拼接:a = a * 10 + ch - '0' 如果读到 . 就压栈 如果读到运算符,就出栈两个数进行运算,结果再压栈 如果读到 @ 结束 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; stack<int> s; int a, v1, v2; int main() { char ch; while (cin >> ch) { if (ch == '@') break; if (ch >= '0' && ch <='9') { a = a*10 + ch - '0'; } else if (ch == '.') { s.push(a); a = 0; } else if (ch == '+') { v1 = s.top(); s.pop(); v2 = s.top(); s.pop(); s.push(v1 + v2); } else if (ch == '-') { v1 = s.top(); s.pop(); v2 = s.top(); s.pop(); s.push(v2 - v1); } else if (ch == '*') { v1 = s.top(); s.pop(); v2 = s.top(); s.pop(); s.push(v1 * v2); } else if (ch == '/') { v1 = s.top(); s.pop(); v2 = s.top(); s.pop(); s.push(v2 / v1); } } cout << s.top() << endl; return 0; } P2058 海港 解法:用一个队列记录所有 24 小时内的船。用一个桶记录每个国家的乘客数量。 每次有新船入队列的时候,更新桶。如果桶更新前是 0,则 ans++ 每次新船入队列后,检查最早的队列,如果超24 小时,则出队 出队的时候,更新桶,如果桶的数量减为 0,则 ans-- 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; struct Node { int t; int len; vector<int> v; }; // 桶,记录每个国家的乘客数量 int cnt[100010], n, t, ans; // 队列 queue<Node> q; int main() { ios::sync_with_stdio(false); cin >> n; for (int i = 0; i < n; ++i) { Node a; cin >> a.t >> a.len; a.v.resize(a.len); for (int j = 0; j < a.len; ++j) { cin >> a.v[j]; if (cnt[a.v[j]] == 0) ans++; cnt[a.v[j]]++; } q.push(a); int min_t = a.t - 86400; // 检查出列 a = q.front(); while (a.t <= min_t) { for (int j = 0; j < a.len; ++j) { cnt[a.v[j]]--; if (cnt[a.v[j]] == 0) ans--; } q.pop(); a = q.front(); } cout << ans << endl; } return 0; } P2234 营业额统计 把营业额往 set 里面放,这样数据就是有序的。然后用 lower_bound 查找大于等于 x 的值。 如果找到了,那么波动就是 0 如果没找到,比较当前位置和上一个位置与 x 的差,取较小那个;同时插入 x 取上一个位置的时候要处理一下边界,如果是在 s.begin()位置的话就不用处理了。 取当前位置的时候要处理一下,看看是不是在 s.end()。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 /** * Author: Tang Qiao */ #include <bits/stdc++.h> using namespace std; set<int> s; int n, x, ans; bool debug = false; int main() { ios::sync_with_stdio(false); cin >> n; cin >> x; ans = x; s.insert(x); for (int i = 1; i < n; ++i) { cin >> x; set<int>::iterator it; it = s.lower_bound(x); if (it != s.end() && *it == x) { continue; } else { int diff = INT_MAX; if (it != s.end()) { diff = min(diff, abs(*it-x)); } if (it != s.begin()) { it--; diff = min(diff, abs(*it-x)); } ans += diff; s.insert(x); } } cout << ans << endl; return 0; }

2025/4/12
articleCard.readMore