普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月16日唐巧的博客

CSPJ 教学思考:背包问题

作者 唐巧
2026年1月11日 22:41

引言

背包问题是动态规划中的经典问题,也是 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 疯狂的采药 完全背包问题

多重背包

多重背包描述了这样一种场景,一个物品将同时受两个限制条件的制约,例如:一个背包,即有体积限制,又有重量限制,让你往里放物品,求最大化物品价值的放法。

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 背包和完全背包想像成填一个一维的表格,那么多重背包就在填一个二维的表格。我们需要保证表格的填写过程符合动态规划的阶段性,表格总是从一个方向往另一个方向填,填过的数字不会再次被修改(在没压维的情况下),这样才能保证状态无后效性。

动态规划题目能够划分出清晰的阶段,后一个阶段只依赖于前面的阶段,问题就解决了一大部分。

背包变型一:物品的相互依赖

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;
}

相关练习题目

推荐练习:

题目名 说明
P2871 Charm Bracelet S 01 背包, USACO 07 DEC
P1802 5 倍经验日 01 背包
P1060 开心的金明 01 背包,NOIP 2006 普及组第二题
P1049 装箱问题 01 背包,NOIP2001 普及组
P1064 金明的预算方案 01 背包变型,NOIP2006 提高组第二题
P2392 考前临时抱佛脚 01 背包变型
P2639 Bessie’s Weight Problem G 01 背包变型,容量与价值相同
B3873 小杨买饮料 01 背包变型, GESP202309 六级
P12207 划分 01 背包的变型,蓝桥杯 2023 国
P1510 精卫填海 01 背包,但是输出要求有变化
P2430 严酷的训练 01 背包,题目较长
P11377 武器购买 01 背包的变型,GESP202412 七级
P13018 调味平衡 01 背包的变型,GESP202506 七级
P1926 小书童——刷题大军 01 背包,需拆成两个子问题
P13015 学习小组 完全背包,GESP 202506 六级
P1679 神奇的四次方数 完全背包,需要求最小值
P1832 A+B Problem 完全背包变型,计数
P10721 计算得分 背包问题变种,GESP 202406 六级
P2918 Buying Hay S USACO08NOV, 求最小值的完全背包
P1794 装备运输 多重背包
P1910 L 国的战斗之间谍 多重背包
P1855 榨取kkksc03 多重背包
P2663 越越的组队 非多重背包的 DP
昨天以前唐巧的博客

2025 年个人总结

作者 唐巧
2026年1月1日 12:02

工作

2025 年是艰难的一年,上半年玩教具业务同比下滑,下半年尝试了一些营销推广,业务量有所稳定,但是赢亏承压。最后一年下来,虽然没亏钱,但是也没挣到钱。好在团队在持续成长,每个岗位的人都比去年有了长足的进步和成长。

2025 年也立项了好几个新项目,这些项目会陆续在 2026 年上线。前面的播种会积累到 2026 年的收获,所以未来怎么样,还是挺值得期待的。

读书

25 年一共读了 8 本书,以下是读书笔记:

其中 《不落俗套的成功》《广告的没落,公关的崛起》 是我今年最喜欢的两本书,一本书指导我开始更多关注配置,另一本书指导我如何做品牌。

25 年还开始订阅了纸质版本的《三联生活周刊》,加上雪球 App 每个月给我寄月刊。所以整个 2025 年的阅读量还是不小的。

感悟

今年思考人生,总结了 4 篇文章:

其中“多巴胺”系统那篇我自认为对我自己帮助最大,有效指导了我如何择友,如何工作,如何培养兴趣。

保险那篇文章,也让我理清了保险产品的购买思路,不再纠结要不要买保险,怎么买保险。

编程教学

今年继续在教小朋友编程,关于编程竞赛,今年又总结了如下文章:

加上去年写的三篇,内容越来越丰富了:

财务

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 年的目标

  • 工作:
  • 理财:
    • 不折腾,配置+再平衡。
  • 个人:
    • 读 6 本书。
    • 俯卧撑能连续做 20 个。
    • 骑车绕四环一圈。

个人 Milestone

好象没啥特别能说的,如果非要有什么,就是产出了 《构建你的“多巴胺”系统》 这篇文章吧。

读《疯狂的尿酸》

作者 唐巧
2025年12月28日 14:14

《疯狂的尿酸》是一本关于健康的科普书,来自于美国医学博士:戴维·珀尔马特,他是一位畅销书作家,写过《谷物大脑》和《菌群大脑》。

什么是尿酸

正常人体中的尿酸,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月11日 21:20

最近读完了《定位》作者艾·里斯的另一本书《广告的没落,公关的崛起》,记录一些心得。

广告的没落

当一个广告让消费者意识到是广告时,广告的效果就会大打折扣。

我记得当年脑白金就把广告做成报纸的新闻报道形式,以此来让大家误以为是报纸在宣传脑白金的功效。但现在广告的监管越来越严,这种擦边的广告越来越难通过审核。

广告追求创意,但消费者购买的是产品。

如果一个产品广告很有创意,但是产品本身很普通。另一个广告很普通,但是产品本身很好。大家还是更可能购买后者。

广告追求创意和讨论,但是真正到了决策环节,影响决策的还是产品本身的心智,而不是广告创意。

产品的创意(创新)比广告的创意更重要。

品牌是潜在顾客心智中的一个认知。

广告很难进入消费者的心智。

相比于广告,公关(具体指通过媒体等第三方途径,间接讲述你的故事)更有可信度,也更有传播性。

消费者在试图评估一个品牌的时候,更倾向从朋友、亲戚,还有权威网站上获得信息,而不是广告。

公关的崛起

因为广告很难进入消费者心智,那么就应该更多通过公关来建立品牌。在通过公关建立品牌后,可以把广告作为维护品牌的工具。

书中结合各种品牌案例,提到了一些技巧。

技巧一:为媒体传播而设计,包括提前透露消息、新的品类/品牌名称、可信度的发言人。书中的案例是 Segway 平衡车。

技巧二:成为争议话题。案例是红牛(某些成份被禁,激发年轻人尝试的好奇心)。

技巧三:创意。为品牌增加一些东西,引起讨论。

技巧四:从小媒体入手。没人比媒体更多地浏览媒体。案例是《定位》一书,该书刚开始只在一个小媒体中被报道,但后来被《华尔街日报》发现,跟进了报道。

我的一些感受

看完本书之后,我刚好刷到一位媒体记者在微博上吐槽小米的公关(如下图)。但是我却从这段话中,看到小米在努力让自己的任何商业行为都成为公关传播的话题。在公关这件事情上,小米做得是非常优秀的。

以上。

我理解的保险产品

作者 唐巧
2025年9月4日 22:45

首先申明: 本文不是广告,也不推荐任何保险产品

我之前一直不理解保险,最近借助一些资料,终于想明白了各种保险的价值,给大家分享一下。

保险其实分很多种,我们需要分开理解它的用途。

一、意外险

意外险是杠杆最高的保险。每年大概几百块钱,就可以保上百万的保额。因为对于大部分人来说,这个事情发生的概率极低,所以它的杠杆很高。

意外险的价值是给家庭或者父母留下一笔财富。特别适合家里面负责挣钱的那个顶梁柱买,这样可以应对极端概率情况下的风险。

很多人会想:这么低的概率,有必要买吗?有可能一辈子都遇不到意外。

我们在考虑这种保险的时候,要有 “平行宇宙”思维。即:我们要假设这个世界是量子态的,同时有许多平行宇宙,意外险是为众多平行宇宙中的某一个 “我” 的意外买单。这样,那一个平行宇宙里面的倒霉的 “我”,被另外平行宇宙中的 “我” 的保费接济,获得了极大的补偿。

我们不知道我们身处在哪个平行宇宙。所以意外险保证了我们在每个平行宇宙过得都不算太差,最倒霉的那个 “我”,也用保险给家庭留了一大笔钱。

二、医疗险

医疗险大多数报销门诊或者住院时候的大额费用。一般这种保险都有起付金额(比如超过 1 万部分)。

这种医疗险的费用也很低,一年也是几百块钱就可以买到。这种保险其实也是杠杆率很高的保险,因为大部分年轻人不太会超过起付金额。

医疗险和意外险类似,也是保障极端情况,比如如果一个突发疾病住院要花 10 来万,这个保险就可以报销大部分,让家庭不至于因病返贫。

三、高端医疗险

高端医疗险一般一年费用得好几千,是普通医疗险的 10 倍。大概率高端医疗险是很难从期望上 “回本” 的,而且很多疑难杂症,可能公立的三甲医院医生更有临床经验(因为他们看的病例更多)。

购买高端医疗险更多可以看成是一种 “消费”。因为你得任何小病都可以享受非常好的看病体验,不用担心看个感冒花几千块钱(是的,和睦家看个感冒几千块钱很正常)。

四、分红险

分红险在我看来已经脱离了保险原本的意义,但是最近我稍微理解了一点它的价值。

分红险通常需要购买者每年交上万块钱,连续交 20 年左右,之后开始累积复利,最后在几十年后,可以提取出来一笔财富。在现在低利率时代,它能保证的年化收益大概有 2.5% 左右(以后如果利率下行应该收益会更低一点)。

我开始很不喜欢分红险,因为首先它的收益率并不高。不管是股票,债券,还是黄金,如果你拉一下 30 年收益率的话,大多数都远远超过 2.5% 。另外,这笔几十万的保费,其实是丧失了几十年的流动性,如果你要强行赎回,就会损失巨大。我认为现金流对家庭来说还是很重要的,所以我很不喜欢这类保险。

大部分销售推销的香港保险也属于这类。

哦,不得不提,这类保险也是对销售来说提成最高的产品。这也是我不喜欢它的原因。因为这就相当于你的本金一开始就打了一个 9 折,对于一个打折的本金,它的复利增长就更难。

那我现在为什么稍微理解了它呢?因为我发现大部分人只会把钱存定期。对于一个定期存款来说,换成这种保险,稍微能够提升一点点长期收益率,同时帮助这些人能够 “锁定” 财富,如果希望这个钱用于养老,它被锁定就不至于被各种意外用掉。

但是我个人还是宁愿持有股票或债券。

另外,给孩子买这个保险的家长可能要想清楚,这个保单什么时候兑现?如果一直不兑现,理论上可能是给 “孙子” 买的,那么做好保单两代人的传承也是一个问题。因为如果 10 岁给孩子买,那么要 60 年之后可能才会兑现保单价值。到时候大概率自己已经不在了,孩子已经 70 岁了,保单传承不好就相当于捐给保险公司了。

五、终身寿险

高额的终身寿险其实相对于把意外险和分红险做了一个组合。拿分红险的收益来 cover 意外险的保费。美其名曰:如果意外发生可以保多少,最后你还能拿回全部本金,还附加一些特别红利(不保证兑现)。

殊不知羊毛出在羊身上,本金每年的部分利息就其实是意外险的成本。只是换了一个说法和组合。

我是很不喜欢这种类似雪球的复合结构,因为你搞不明白年化收益率,也搞不明白你的意外险部分的杠杆率。

七、车险

车险里面的车损险是杠杆率极低的产品。拿我的特斯拉来说,一年保费要 5000 多,但是我大部分时候在城市里面开,就算有小磕碰,修车也不会花到这么多。

车险里面杠杆最高的是三者险,大概 600 块钱左右就可以保 200-300 万的保额。这样万一撞到人或者豪车,都可以 cover 全部费用。

我已经连续很多年只买交强险和三者险。这也让我驾车的时候更小心,自己不撞别人就不需要车损险,如果别人撞到自己,可以走别人的保险。

八、小结

  • 意外险和医疗险可以保证极端情况发生后的体验,杠杆很高,费用相对低(一年几百块钱)
  • 高端医疗险类似消费,提升普通看病体验,一年几千。
  • 分红险年化收益不如很多股票和债券等产品,但是比定期强。另外牺牲了现金流,但同时保证这笔钱不会被挪用。因为利润高,销售都喜欢卖这个产品。
  • 终身寿险是意外险和分红险的组合。
  • 车险里面三者险杠杆最高,车损险性价比低。

以上。本文仅表达个人观点,不构成任何购买建议。

真相不重要

作者 唐巧
2025年7月30日 22:57

真相有时候不重要,举几个例子。

第一个例子是身边一个朋友的故事。一天早上,从来不做饭的妻子心血来潮,给他做了一份早餐,但是因为是第一次做,手艺不太娴熟。这个时候,妻子问他味道怎么样?他随口就说:感觉一般。妻子的脸色瞬间就阴下去了,说:那我以后再也不做饭了。对于妻子来说,早餐好不好吃的真相不重要,鼓励和认可才是重要的。丈夫说了实话,但是却伤了妻子的心。

去年农夫山泉被全民网暴的时候,我发文章说农夫山泉的瓶盖和日本国旗没关系。结果一堆人在评论区谩骂我。对于网暴的人来说,真相不重要,情绪和宣泄的重要性大于真相。对于这些谩骂的人来说,我忽略了讲真话的时机,所以被骂。

在今年的脱口秀节目上,一个脱口秀演员提到自己的爸妈本来打算把自己打掉的,老罗也提到他也有同样的家庭情况,他花了很多年很多时间才对这个事情“放下”。对于老罗来说,“自己出身下来不是被需要的”这个真相不重要,重要的是自己的人生意义。即便是事实,如果会伤害孩子,父母本来可以不说,真相不重要。

我们曾经有一个实习生,偷偷利用公司的9点后加班可以打车福利,下班后去健身房,然后等到9点后再打车回家。我们后来给他说,我们实习岗位取消了,让他离开了公司。对于我们来说,告诉他离开的真相不重要,对于不合适的人,让他快速地离开不会起任何冲突。对于我们来说,事情顺利地执行比说出真相重要。

我是一个业余编程老师,编程这件事情很难,所以孩子第一次接触编程容易会发怵。这个时候,我会设置一些很简单的题目,但是告诉他这个题目很难,然后等他做出来,我会惊讶地说:哇,你真的很有天赋!对于孩子来说,真相不重要,学习的兴趣和信心最重要。

对于美国两党各自拥护的媒体,真相也不重要。如果发现事情有利于他们的政治宣传,他们就大力宣传。如果发现事情不利于他们的政党,他们就会有意淡化。在美国,媒体的中立客观在政治面前不重要,政治更重要。

邓小平很早就明白真相不重要。每次被打倒他都默默承受,尽力照顾好自己的家人,不争辩不气馁。因为他知道,在那个时候只要是他说的,就都是错的,真相不重要。在后来,邓小平坚决保护毛主席,保护国家稳定,因为他知道:稳定大于一切。只要政局稳定,一切问题都可以慢慢解决。而如果为了真相导致政局混乱,那将天下大乱。

有些时候,真相因为不太容易被人接受,甚至会变成秘密,比如大家的工资。试想一下,如果每个人的工资是公开的,那么就会有大量的人向 HR 投诉自己的薪资为什么不如某某某。因为人们总是容易高看自己,低看别人。所以,薪资的真相变得不重要,大家相互之间不知道薪资变得很重要

那真相很多时候不重要,我们是不是就不需要真相了?不是的。大部分时候,真相都是重要的。大部分时候,我们也需要讲真话,追求真相。只是我们需要明白,这个世界在运转过程中,真相的权重不一定是最大的。当真相被掩盖的时候,我们可能需要接受这样的现实。当我们决策的时候,有时候需要相对真相,给其他因素更高的权重。

最后,我想引用最近看到的一篇《南方周末》的报道 佛学与官场,一个主持的官场往事。在文章中,释传真(时任南京市佛教协会副会长)说:

我常常跟来聊天的官员说,做官啊,第一有文化没文化要学会听话;
第二,得过且过太阳出来暖和;
第三,有一些矛盾就要睁一只眼闭一只眼。

你看,一位佛教高僧给官员的经验分享,每一点都在说:有比真相更重要的事情。

以上。最后再强调一遍:我不是教大家骗人,我是在强调给真相赋予合适的决策权重

斑马思维机的详细调研

作者 唐巧
2025年10月9日 17:39

本文档创建于 2025.3,最后更新于 2025.12

一、产品介绍

斑马思维机是针对 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 个难度级别,且难度分级科学合理,充分满足不同年龄段孩子的学习需求。其中:

级别 针对年龄 培养重点
S0 2-3 习惯养成
S1 3-4 兴趣培养
S2 4-5 知识积累
S3 5+ 应用拓展

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)。

斑马思维机专利情况:

专利名称 专利公告
机器专利1 http://epub.cnipa.gov.cn/cred/CN219533902U
机器专利2 http://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 Award K设计大奖,享誉全球的国际专业设计大奖 证书 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 年

以上专利和奖项为斑马思维机提供了不少竞争优势,帮助它持续提升产品端的用户体验。

市场销量

上市以来,斑马思维机市场销量表现出色,受到众多家长青睐。在各大电商平台,其销售数据持续增长,斑马思维机连续两年稳居思维机品类的销量和销售额第一。

由以上数据可知,斑马思维机的市场占有率进一步扩大,从 2024 年初的 52.8% 上升到 2025 年初的 66.6%,进一步巩固了市场第一的地位。

  • 在京东平台提供的思维机热卖榜:
  • 在天猫平台提供的 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、销量排名

公开数据,斑马思维机销量排名第一。其它思维机销量排名未知。

理解 MCP -读《这就是 MCP》

作者 唐巧
2025年10月21日 08:36

一、序言

最近读完了一本讲解 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 市场。市面上比较有名的市场包括:

五、MCP 的问题

MCP 发布才一年时间,所以还有很多细节未来需要完善,包括:

  • 协议对多模态内容支持不够友好
  • 鉴权机制不完善,很多 MCP 服务还未支持 25 年 3 月引入的 OAuth 鉴权协议
  • 安全防护能力弱。攻击者可以构造恶意的 MCP 服务来诱导用户执行恶意命令,从而实现信息窃取,执行恶意命令等攻击。

以上。

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

作者 唐巧
2025年10月6日 22:06

最近收到图灵编辑刘美英老师赠送的《图解 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日 20:00

想读一些价值投资者的书,于是就找到了这本但斌的《时间的玫瑰》。这是一本写于 2018 年的书,现在已经过了 7 年。当年的很多论断,随着时间的检验会更有意思。以下是一些读书感悟。

买入价格很重要

我们常说,买股票需要关注三点:好公司,好管理,好价格。在好价格这件事情上,但斌给我们举了一个例子,也是他自己血泪教训。

但斌说:如果你在 2007 年的高点买入茅台,那么需要 2016 年(9 年后)才能解套。中间还会经历两次 60% 的下跌。所以,即便是大家公认的好公司,如果你的买入价格不对,也是有很大的风险。

关注行业周期

但斌的这本书写在 7 年前,在 7 前年,有一些行业龙头公司是被价值投资者普遍认同的,比如房地产行业的万科,以及教育行业的好未来,但斌在书中多次提到这两家公司。但是我们现在来看这两家公司,就会发现两家公司都经历了价值毁灭的过程,他们都从最高点回撤了超过 80% 。

万科股价:

好未来股价:

回撤的背后,是房地产行业和教育行业整体的低迷带来的。即便是三好学生,如果在一个下坡路的行业,也是做不出什么好成绩的。

关注行业的周期,关注政策的变化,在合适的时候卖出,这也是《股票大作手操盘术》中我很认同的趋势投资观点,在本书中,我再次感受到趋势投资的重要性。

从分歧中学习

但斌在书中提到他参加伯克希尔股东大会的一段记录:一个来自旧金山的 17 岁少年问:成为一个好的投资者的最好方法是什么?

巴菲特回答说:尽可能多地阅读。你要把各种思想装进你的脑子里,随着时间的推移,分辨出哪些是合理的。一旦你做到这样了,你就该下水实践了。

我对此也有很强的认同。学习的第一步是尽量吸收信息,而阅读是一个很好地吸收高质量信息的渠道。当然,我也认为与人交流讨论,以及观察现场同样重要,这都是获得信息多样性的重要手段。

有了信息之后,通过思考和实践来分辨信息,最终把有效的信息沉淀下来,就能成为自己的宝贵经验。

我对获取信息的方法最近还有一个新的感悟,就是“反对性”的意见相对重要,因为人会自我强化自己的观点,所以对于反对观点容易忽视。这个时候,我们应该刻意去找反对性意见,在理解反对性意见的基础上,去解释为什么观点不一样。

反对意见在投资上,也代表着市场的分歧,如果我们能够理解正反两边的观点的同时,又能够看到未来正反观点的分歧消除点,那么就可能获得巨大的收益。

之前我想要获得分歧意见非常难,因为表达反对意见通常让人感觉尴尬。现在我有一个技巧:我会问大模型,让他帮我系统性地总结反对意见以及论证理由,这对我来说非常好用,分享给大家。

以上。

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

作者 唐巧
2025年9月17日 21:27

上个月见了一个老朋友:代码家。和代码家聊天的时候,他提到了趋势交易,他还推荐了《股票大作手操盘术》

该书的核心思想就是做趋势交易。具体做法是:在形成趋势前观望,在趋势确定建立后顺着趋势做空或做多,在趋势快要结束时,提前补仓或卖出。

我觉得该思想同样适用于长线操作:每家公司都有上升期和平稳期以及下降期。在公司上升期的时候加仓,平稳期的后期卖出,避免下降期的戴维斯双杀,会是非常重要的。

举例来说:

  • 房地产公司的上升期投资,相关的股票,即便是恒大,也涨很多。只要你在合适的地方卖出,你就不会亏。

  • 很多互联网公司的企业,在互联网泡沫期的估值很高。比如微博,哔哩哔哩,陌陌,包括粉笔公考,猿辅导。只要你在合适的地方卖出,也可以吃到很多的时代红利。但是如果你一直秉持长期持有,就可能不挣钱或者只挣很少的钱。

以下是微博的股价走势,现在的价格(12)比发行价(20)还低,但它曾经涨了 5 倍多。

以下是哔哩哔哩的股价走势,如果你买在高点,那么会亏 80%。

以上两个公司就是典型的“互联网”红利公司,在互联网红利期拥有巨大的股价泡沫,在红利结束的时候,股价回归理性。

我感觉趋势投资不是做短线的投机,而是把握时代的大势。做时代周期(5 年左右)的波段,抓时代红利企业,但是又很冷静,知道自己是投机,能看到卖出下车的时间点。

我们如果能够在互联网红利期,提前买入微博和哔哩哔哩这样的高用户量的产品。在红利结束前卖掉。我们假设卖在离最高点回撤 50% 的地方,也会有 2-4 倍的收益,整个持股周期在 2-3 年。

但说起来容易,执行起来还是挺困难的。比如下面是陌陌的走势,2014 年上市,2017 年股价才开始上涨,2018, 2019年均在年中大幅上涨,之后又回到 2017 年的价格。再之后就一路下跌,现在的价格是发行价的一半。

此书对我最大的价值,就是对价值投资与时代红利周期有了挂钩,之后在思考和判断公司的时候,除了思考价值层面的事情外,更应该思考时代的变化与周期。

以上。

读《真需求》

作者 唐巧
2025年3月8日 21:22

一、序言

最近读完了梁宁的《真需求》,在我看来,梁宁的角色更像是一个老师,因为老师喜欢给学生结论。可能她最有名的作品就是得到 App 上的《产品思维 30 讲》,所以她喜欢给解决方案,给框架。

什么是解决方案?就是给你说某某成功的核心原因是什么,再围绕一系列核心原因建立一个理论上的框架,于是所有的成功就来自于这个框架。学生掌握了这个框架,就理解了所有的生意。

这,确实很符合很多人的需求。

在这本书中,梁宁的解决方案是:价值-共识-模式框架。

但是说实话,我不太喜欢将创业之路极简化的叙事。这种形式虽然易于理解,但是不解决实际问题。真实的企业经营每天面对各种复杂的决策和执行,不是有一个好的生意框架就能当银弹的。极简叙事也简化了成功企业的归因,容易误导读者。

我更喜欢的是能够落地的思维。比如段永平的“不为清单”,“长期主义”,“做正确的事情”,虽然有点像什么都没说,但是更易于落地。

所以,本书的大部分内容对我来说帮助不大,但是我从另外的视角也从书中得到了一些启发,分享如下。

二、情绪价值的产品很重要

梁宁把产品价值分为功能价值+情绪价值+资产价值。我不同意这样的分法,因为这么分不太 MECE( 金字塔原理中的 MECE 原则,即 Mutually Exclusive Collectively Exhaustive)。

但是,我认为情绪价值是重要的商业产品。我的老板把这个叫做“无用之物”的生意。未来消费者会越来越关注自己,做悦己的选择,这方面的商业价值非常大。

三、从历史中思考

梁宁在书中问:如果你在 2012 年同时拿到当时的几个 offer,你应该如何选择?这几个 offer 是新浪微博,虎嗅,搜狐,微信,今日头条。

这是一个很有意思的问题,因为当时没几个人看得懂今日头条。就连投资机构都不投今日头条,更别说一个应届生会选择头条了。

但是这种思考角度让我意识到,其实这个世界的未知性是极强的,就算你是这个世界上最聪明的人,你也可能判断失误。

面对不确定性,构建好自己的反脆弱系统才是合理的应对方式。这事就像做资产配置一样,是我们应对变化和风险必须学会的生存技能。

以上。

个人投资的最佳实践 - 读《不落俗套的成功》

作者 唐巧
2025年2月23日 21:12

序言

本书的作者是耶鲁大学的投资总监大卫·F·斯文森,他管理着耶鲁大学140多亿美元的捐赠资产,并让耶鲁大学在过去的20年里的年收益率达到16.1%。

书中的内容不是很好消化,所以我断断续续读了将近一年时间,里面的很多道理对我在投资领域的成长帮助很大。

我主要的收获有:

  • 资产配置和再平衡的重要性
  • 高费率基金的长期收益问题

下面就这两点结合我自己的个人经历做个分享。

资产配置和再平衡

资产配置可能是每个人学习投资的第一课。这一点很多人都能理解。把自己的钱分为两大部分,一部分保证自己和家人的生活质量不受影响,另外一部分长期不用的闲钱再拿来投资。

对于投资的钱,也应该做好配置。有人把它分成股票,债券,黄金,现金四大类。在书中,作者将核心资产分成了 6 种,分别是:

  • 国内股票
  • 国外发达市场股票
  • 国外新兴市场股票
  • 房地产
  • 美国长期国库券
  • 美国通胀保值国债

但作者在美国,以上核心资产很多在中国并没有有效对标的标的。能对标的可以是 A 股和港股通股票,房地产,债券,QDII 基金等。

我习惯拿石墨表格把自己的资产分类,然后再看各类型的比例。一些简单有用的原则是:任何单一资产不要让它的占比超过可投资资产的 30%。

我在这方面犯过一个巨大的教训,曾经有一个资产在一段时间涨幅特别猛。有一段时间它的占比超过了 30%,这个时候我不但没有减仓,还额外追加了一笔投资。追高操作最终使得这笔追加款后来跌幅将近 50%。整体的盈亏虽然不大,但是追加款的损失把之前积累的利润都抵消了。这本应该避免。

正确的做法是做资产的“再平衡”。

对于每一个资产,定下计划的投资占比。当某个资产涨幅超过了占比一定幅度,就应该卖出一部分,让它恢复到原始占比。

同样的,如果某一笔资产它的价格大幅缩水,那么我们应该补仓,让它的占比恢复到之前的比例。

但是,以上两种操作非常反人性。人性是追高杀跌,而不是追跌杀高。所以我一直在试图修炼自己在这方面的心智。

之前巨大的亏损对我来说也是一个宝贵的经验教训,让我谨记资产配置的重要性。

高费率基金的问题

我之前持有了一些私募基金,有一些亏钱有一些挣钱,我也不知道应该怎么评估这些投资行为。

本书系统性的将美国市场的共同基金做了长达几十年的收益分析和解读,最终让我意识到:高费率的基金是不值得长期持有的。

这类基金的主要问题是:

  • 管理费和超额提成吃掉了一大部分收益
  • 在市场整体大幅上涨的时候,收益提成也会吃掉一大部分 beta 收益
  • 频繁通过高水位法提成,基金的波动就会让基金管理者挣钱,但是遇到深度回调的时候,这部分提成就会变成投资人的亏损
  • 基金管理者旱涝保收,即便基金下跌,管理费也不会少。即便基金规模扩大很多,管理费也不会打折。

另外,很多人其实不知道,大部分基金用 份额缩减法 来收取管理费和提成。这样在产品业绩图上,投资人其实一眼看不到费后收益。

以下是我的一个真实案例:

我持有的名字为金锝睿知 1 期(T18145)的产品显示,从它的发行开始日 22.2.22 开始到 24.12.27,这段时间的收益率是 22.82%(如下图)

但是如果我查看我的资金流水,我的两年持有收益率只有 16.5%,差了 6.3%(如下图)

我不是说这只基金不好,实际上它在过去两年的收益还是远高于 A 股沪深 300 指数的。但是你确实没办法一眼在产品资料里面看到真实的年化费后收益率。当然,问题不是针对这一只基金,大部分私募基金都是采用份额缩减法。

意识到以上这些之后,我赎回了几乎所有整体费率大于 1.5% 的基金。转而更多持有低费率的指数基金。

另外我也买入了一些我觉得不错的个股,比如腾讯,招商银行,比亚迪。对这些个股的生意的思考也让我的商业思维得到进步。

小结

《不落俗套的成功》是一本面向个人投资者的启蒙读物,作者通过大量详细的数据说明资产配置、再平衡的重要性,也让我意识到高费率的基金不值得长期持有。

以上。

请为信息付费

作者 唐巧
2025年8月31日 17:46

“免费的午餐往往是最贵的。为知识付费,是投资自己的认知能力,是这个时代每个人都应该认真考虑的选择。

前几天刷抖音,看到一个财经博主在讲”普通人如何实现财富自由”,视频里充满了夸张的表情和煽动性的文案。视频末尾,他推荐了一个”0元理财训练营”,声称能教你”三个月内资产翻倍”。

我想起了自己订阅《财新》时的犹豫。为什么我们对免费的低质量内容习以为常,却对高质量的付费内容如此吝啬?

这个信息爆炸的时代,我们真的需要为知识付费。

免费内容的陷阱

质量之殇

免费的内容最大的问题,就是它根本就不免费。

当我们在抖音上看到那些”三招教你理财”、”这样做就能年入百万”的短视频时,我们以为自己没有付出成本。但事实上,我们付出的是注意力,付出的是判断力,付出的是被误导的风险。

这些内容利用了人性中最原始的弱点:贪婪和猎奇。它们用夸张的标题吸引眼球,用简化的逻辑迎合认知懒惰,最终的目的不是传播知识,而是引流变现。

我记得罗振宇在《逻辑思维》中说过:”免费是世界上最昂贵的东西”。当时不理解,现在想想,免费内容的真实成本往往比付费内容更高,只是这个成本被巧妙地隐藏了。

注意力的谋杀

短视频平台更可怕的地方在于,它们正在系统性地破坏我们的专注力。

抖音上的财经内容,往往用夸张的配音、快节奏的剪辑,以及故意制造的冲突感来抓取注意力。”震惊!这家公司竟然…”、”你绝对想不到的赚钱方式”,这样的文案充斥着整个平台。

长期消费这样的内容,就像吃快餐一样,看似填饱了肚子,实际上营养不良。我们的大脑习惯了这种高刺激、低思考的信息输入方式,逐渐失去了深度阅读和独立思考的能力。

更要命的是,算法推荐让我们陷入信息茧房。平台为了让用户停留更长时间,会推送用户喜欢的内容,而不是用户需要的内容。结果是,重要的时政新闻、深度的社会分析被娱乐化的内容所淹没。

广告的毒药

隐藏的商业动机

最近几年,我观察到一个现象:几乎所有的免费财经内容,最终都指向商业变现。

公众号上那些分析经济形势的文章,看似专业,细读之后会发现,作者往往会推荐某个理财产品或者某个投资平台。文章的逻辑链条是这样的:经济形势不好 → 需要理财 → 推荐我的产品。

抖音上更直接。那些所谓的”财经大V”,视频内容是免费的,但最终目标是让你扫码进群,然后推销各种理财课程、股票软件,甚至是可疑的投资项目。

这种商业模式本身没有问题,但它扭曲了内容的客观性。当内容创作者的收入来源是推广费而不是内容质量本身时,内容质量必然会让位于商业转化。

算法的偏见

算法推荐进一步加剧了这个问题。

算法关心的是用户停留时间和点击率,而不是信息的准确性和重要性。一条耸人听闻的假新闻往往比一篇严谨的深度报道有更高的传播率。

结果是什么?真正重要的政治、经济、社会议题被娱乐化、碎片化的内容所遮蔽。当所有人都在关注某个网红的恋情时,有多少人知道最新的货币政策调整?当大家都在讨论某个段子时,有几个人了解正在发生的地缘政治变化?

这不是危言耸听。信息质量的下降最终会影响整个社会的决策质量。

付费的价值

面对这样的信息环境,我选择了用钱投票。

我的付费清单

去年开始,我陆续为以下内容付费:

  • 《财新》杂志:每年几百块钱,但能获得相对客观、深度的财经报道
  • 财经类每日新闻:每天需要花 1 块钱,信息密度高,没有广告干扰
  • 《三联生活周刊》:优质的长篇报道,帮我理解复杂的社会现象
  • 小宇宙上的访谈节目:深度对话,远比短视频更有营养
  • 请一些行业专家咨询,事后发微信红包感谢

付费内容的优势

付费内容最大的优势在于,它的商业模式相对纯粹。

当我为《财新》的内容付费时,我就是《财新》的客户。《财新》需要对我的钱负责,需要提供有价值的内容来留住我。这种直接的商业关系,比那种”免费内容+广告变现”的模式要健康得多。

付费内容的第二个优势是质量控制。

以《三联生活周刊》为例,它的记者往往需要花费数月时间来调查一个选题,采访几十个相关人员,查阅大量资料,最终呈现出一篇万字长文。这样的内容制作成本很高,只有付费模式才能支撑这样的投入。

而免费的自媒体内容呢?往往是一个人坐在电脑前,花几个小时搜集网上的资料,拼凑出一篇文章。质量的差距是显而易见的。

一点反思

诚然,付费内容也不是万能的。

《财新》有时也会有立场偏见,《三联》有时也会有不够深入的报道。付费不能保证内容的完美,但它至少能保证内容制作者的基本动机是提供有价值的信息,而不是引流变现。

另外,并不是所有人都有条件为信息付费。这涉及到信息公平的问题,也是整个社会需要思考的问题。

但至少对于有条件的人来说,为高质量内容付费,不仅是为了获得更好的信息,也是在用消费选择来支持优质内容的生产,推动整个信息生态的良性发展。

结语

这是一个最好的时代,也是一个最坏的时代。

说它是最好的时代,是因为获取信息从来没有像现在这样便利。说它是最坏的时代,是因为信息质量从来没有像现在这样参差不齐。

在这样的环境下,为知识付费不是一种消费,而是一种投资。投资自己的认知能力,投资自己的判断力,投资自己的未来。

毕竟,在这个瞬息万变的时代里,唯一不变的就是变化本身。而应对变化的最好方式,就是保持持续学习的能力。

免费的午餐往往是最贵的。为知识付费,是这个时代每个人都应该认真考虑的选择。

GESP 202506 5级真题「奖品兑换」题解

作者 唐巧
2025年7月1日 22:29

题目描述

分析

此题首先是不能暴力枚举的,因为 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;
}

构建你的“多巴胺”系统

作者 唐巧
2025年6月22日 22:08

什么是“多巴胺”系统

“多巴胺”系统是一种隐喻,是指能够给你带来持续正反馈/正向情绪的事情。之所以用这个隐喻,一方面是想让大家更容易理解、记忆和传播这个系统。

这个系统对我来说非常重要,它就相当于我人生的“第一性原理”一样。人类看起来是自己的主人,但人类对自身行为动机的理解很多时候并不清楚。

马斯洛把人类的需求按层次来分,在他的理论中提到的各种需求:性,安全,食物,社交,自我实现等等。但是其实,这些其实本质上,都是在为人类提供“多巴胺”。

当人类失去了“多巴胺”系统,很多时候就宁愿放弃生命:比如在战争中,很多人为了信仰而牺牲自己。这是因为他内心的目标大于活着的意义。

在实际生活中,虽然不至于放弃生命,但冒着生命危险做的事情,也不鲜见。比如消防队员救人、警察和歹徒搏斗、或者体育健儿在赛场上带伤为荣誉而战。

这些行为虽然有可能失去生命,但是换来的荣誉与成就是非常让人自豪的,可以为自己提供终身的多巴胺来源。

有人说,这个世界上只有两种生意:让人爽的生意和让人牛逼(学习、健身等)的生意。但我觉得,这都是多巴胺的生意,差别只是一个是提供短期多巴胺,一个是提供长期多巴胺。学习这种事情虽然短期很辛苦,但是收获的成就是可以提供长期的回报,从而提供长期的多巴胺。

为什么“多巴胺”系统很重要

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、不内耗和自恰

我后来发现,其他人讲的一些行事原则,在表达角度上虽然不一样,其实也是一样的道理。比如我们讲的“不内耗”原则。

内耗就是一种持续消耗“多巴胺”的心理行为。如果以构建“多巴胺”系统作为人生准则的话,我们会发现内耗没有任何效果。当我们面对不如意的时候,要么改变,要么适应,要么淡化,而内耗是一种既不改变,又不适应,又反复强化负反馈的行为。百害而无一利。

自恰的底层含义是:所有事情能够自圆其说,不矛盾,不冲突,自然也就不内耗了,不消耗多巴胺。

所以,人需要活得“自恰”,只有自恰才能睡好觉,持续获得多巴胺。

主观与客观

“多巴胺”系统有主观的部分,也有客观的部分。

一、主观部分

“多巴胺”系统对于个人内心是一种主观行为和感受,而不是一种客观描述和标准。所以,对于芒格来说,一个重要朋友的背叛不是对“多巴胺”系统的冲击;但换一个人,可能觉得天塌了,一辈子再难信任他人。

因此,我们更应该调整的是自我的行事方式和思考问题的角度,而不是改变其他人。我们可以远离那些影响我们“多巴胺”系统的人和事,但是当坏运气到来的时候,我们只能接受。

二、客观部分

当然,“多巴胺”系统在指导我们行为的时候,是让我们客观上在做具体的行为选择。通过行为选择让我们尽可能构建有利于我们产生多巴胺的外界环境。比如我刚刚提到的:提前规划退休生活、选择终身学习、多搞庆祝活动等。这些有利的环境不但不会消耗我们主观意志来维护多巴胺,还会给我们提供愉悦,贡献多巴胺。

小结

“多巴胺”系统是一种隐喻,是指能够给你带来持续正反馈/正向情绪的事情。我们通过:

  • 主观上,调整自己的思考和看待事情的方式
  • 客观上,搭建好能够持续供养自己多巴胺的外部环境

利用“多巴胺”系统,让自己的人生少一点内耗,少一点纠结,多一点平静,多一点快乐。

愿每个读者都能过好当下的每一天,谢谢!

GESP 核心考点

作者 唐巧
2025年6月6日 22:12

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 级,对循环结构的考查进行了加强,一般需要用双层嵌套的循环才能完成大题。有一类双层嵌套循环需要特别关注,就是模拟输出类,这一类题过去考过多次,包括:

等差矩阵为例,其关键代码为嵌套的 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;
}

如果学生还是不熟悉,可以考虑如下更多的练习:

有一些时候,双重循环也不一定以输出图案的方式来进行考查,比如题目 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;
}

更多的练习题目如下:

解题思路

对于双重循环输出图形,解题的关键在于:分析图形所代表的序列。例如图形:

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
..#...#..
.#.#.#.#.
#...#...#
.#.#.#.#.
..#...#..

都可以用刚刚找到的思路来解决。

但对于更复杂的图形,就得再想办法,比如

这类题目需要根据题目的输出要求,思考问题拆解的办法,每道题的解法可能都不一样。

考点二:常用函数

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;
}

更多练习:

求闰年函数

参考题目: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;
}

练习题目:

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;
}

该考点的相关真题:

其中 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;
}

更多的练习:

考点二:前缀和

前缀和的计算技巧是:用一个累加变量来不停地更新前 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 级

大题核心考点

最近公共祖先

动态规划

包括 01 背包和完全背包:

基础动态规划:

记忆化搜索:

复杂贪心:

其它

树状数组:

暴力枚举:

模拟+高精度:

相关练习题目

背包

这儿 可以获得洛谷上所有的背包相关题目,推荐练习的如下:

GESP 7 级

大题核心考点

注:名称中的 A 表示第 1 题,B 表示第 2 题。

动态规划

背包:

动态规划:

并查集

枚举/DFS/BFS(在树或者图上)

树状数组

CSPJ 教学总结:树状数组

作者 唐巧
2025年4月26日 20:12

引言

有些时候,题目给我们 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,比如某数 11001之后,就变成了 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,其二进制为 01011000lowbit(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;
}

相关练习题目

文章中涉及的例题:

练习题:

题目 描述
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;
}

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

作者 唐巧
2025年4月13日 15:27

深度优先搜索(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 Challenge USACO 1.5
P1596 Lake Counting S USACO10OCT
P2036 PERKET COCI 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;
}
❌
❌