2025-01-25 22:19:44
二分查找的基础逻辑很简单:我们小时候都玩过猜数字游戏,心里想一个数字( 数字范围是 1-100),让对方猜,如果没猜对,就只告诉对方猜大了还是小了,看看最快几次能猜到。
这个游戏的最佳策略就是二分。先猜 50,如果大了,就猜 25。这样最多 7 次就可以猜到答案。
对于猜数字这个游戏来说,二分的模版最简单的就是如下形式:
1 |
// 二分查找 |
以上代码需要注意的有以下几点:
[left, right]
,即 left 和 right 都是闭区间。left <= right
,即当 left == right
时,还需要进行一次测试。mid = left + (right-left) / 2
其实等价于 mid = (left + right) / 2
只是后者可能超界,用前者可以避免。这种思路其实比较简单,写起来基本上不会犯错。但是,如果有多个目标值时,我们可能要多次更新 ans
变量。
P2249 查找就是一道例题,此题需要找到目标值第一次出现的位置,如果用上面的模版,我们需要多次更新 ans
,参考代码如下:
1 |
|
除了刚刚的模版外,我们还可以用另外一种写法来写二分:我们用 [l,r)
来表示目标查找区间,注意这里是左闭右开的区间。然后,我们不停地尝试缩小这个区间:
[mid+1, r)
[l, mid)
[l, mid)
。以上的情况 2 和情况 3 是可以合并的。结果就是只需要写一个 if 就可以了,核心代码如下:
1 |
while (l < r) { |
有同学可能会问:如果只有一个值相等,并且在 mid 位置,那以上做法不是把结果就跳出区间了?其实这种情况下,l 的值会一步步右移,最后的循环结束的结果会是 [mid,mid)
。所以我们还是可以从循环结束的 l 值中读到目标值。
对于这种写法,我们的二分判断会少很多,只需要最后判断一下 l 的值是否是目标值,即可知道是否查找成功。
以下是参考代码(从以前的 32 行缩短为 24 行):
1 |
|
如果记不清楚,就分开写:
另外,以上这种代码其实是不停在[l,mid)
和 [mid+1, r)
之间做选择,所以:
l
只会更新成 mid+1
r
只会更新成 mid
最后答案如果有,则在 l
位置,当然 l
位置也可能不是答案:
l
位置为查找的范围最左侧下标l
位置为最初的 r 的位置(那个位置是最后一个元素的下一个位置,直接读取会数组越界)其实上面那个写法就是 C++ STL 里面的 lower_bound
函数,所以我们可以直接用 lower_bound
函数来实现 P2249 题。
1 |
|
函数 lower_bound
在 [first,last)
中的前闭后开区间进行二分查找,返回大于或等于目标值的第一个元素位置。如果所有元素都小于目标值,则返回 last 的位置。
这种函数行为初看很奇怪,因为它:
这实际上就是它的内部实现所致(可以理解为这种写法的side effect),它内部实现就是我们刚刚提到的写法,所以才会这么返回目标值。
如果我们想把查找结果转换成数组下标,只需要让它减去数组首地址即可,像这样:
1 |
int idx = lower_bound(v, v+n, a) - v; |
除了 lower_bound
函数之外,C++还提供了 upper_bound
函数。lower_bound
在 [first, last)
中的前闭后开区间进行二分查找,返回第一个比目标值大的位置。如果没找到,则返回 last 的位置。
upper_bound
的内部实现逻辑是:
为了方便对比,我把 lower_bound
的逻辑再写一下:
你看出来了吗?只是第一个更新的逻辑不一样。所以,其实两者的代码很像,我自己分别写了二者的一个实现,大家可以对比看一下,实际上二者实现部分只差了一个字符:
1 |
// 如果目标值等于或者小于 mid,则 r = m |
我们 upper_bound
考虑几种情况:
所以 upper_bound
如果没找到,会返回 last。
我们再看 lower_bound
所以,其实这两个函数在没找到目标值的情况下,都有可能返回首地址或末地址的。只是对于 upper_bound
函数来说,首地址是有意义的。
而 lower_bound
函数返回的首地址怎么说呢?有点像 side effect。很少有需求是求这个地址,所以很多时候要特殊处理一下,就像我们刚刚例题里面又判断了一下一样(如下所示)
1 |
if (l < n+1 && v[l] == a) cout << l << " "; |
二分不但能用于查找数值,还可以用来暴力尝试答案。因为即便是 0-20 亿这么大的范围的猜数字游戏,也只需要 30 多次就可以猜到,所以如果某个问题可以像猜大小一样,每次获得答案是大了还是小了,就可以用二分的办法来“二分答案”。
对于二分答案一类的题目,最常见的题目描述特征是求某某值的最大值最小,或者最小值最大。这个特征可以作为我们选择二分解题的小提示。我们在练习题目 P2678 跳石头 和 P1182 数列分段 Section II 中就可以看到这种提示。
题目 | 说明 |
---|---|
P2249 查找 | 可用 lower_bound 函数 |
P1102 A-B 数对 | 也可使用 STL map |
P1873 砍树 | 二分答案 |
P3853 路标设置 | 天津省选,二分答案 |
P1678 烦恼的高考志愿 | 二分查找,可用 upper_bound 函数 |
P2440 木材加工 | 二分答案 |
P2678 跳石头 | 二分答案,NOIP2015 提高组 |
P1182 数列分段 Section II | |
二分答案+判定。
1 |
|
1 |
/** |
1 |
/** |
二分答案:用 mid 去试跳,如果间距小于 mid,则去掉那个石头,如果去掉个数超过 k 个,则失败。
1 |
|
二分答案。对目标答案每 mid 分一段,如果分出来的段数 <= m 即为真。
1 |
|
因为lower_bound
和 upper_bound
的写法相比传统写法还是有点复杂,在教学中还是适合用最初的那个易懂的版本。易懂的版本虽然执行起来多几次判断,但是在比赛中这一点多的时间并不影响整体的时间复杂度,所以不会因此扣分。同时,简单易于理解的代码,在学习和解题时,也更加不容易犯错。
待学生理解基础二分的写法后,再把系统的实现拿出来,作为增强的补充练习题目。这么补充练习并不是要学生一定掌握,而是借由实现系统的函数,学会在比赛中调用 C++ 的 lower_bound
和 upper_bound
库函数,这样可以加速解题的速度。
二分答案的思路很好理解,但是实际写起来还是很容易晕,所以需要多加练习。另外利用题目特征来获得提示,帮助自己快速解题。
lower_bound
和 upper_bound
都是极简二分查找的 C++ 内部实现。lower_bound
赋予了额外的功能:返回第一个大于或等于目标值的位置;如果不存在返回 last。lower_bound
有可能返回的不是目标值,所以最后要判断一下。2025-01-05 18:03:48
动态规划是 CSPJ 拉分的关键知识点。
之所以这样,是因为动态规划不像 DFS、BFS、二分那样有固定的模版格式。学生要在动态规划问题上融汇贯通,需要花费大量的练习,也需要足够的聪明。
笔者自己在高中阶段,也是在动态规划问题上困扰许久。我自己的学习经验是:动态规划还是需要多练,练够 100 道题目,才能够熟悉动态规划的各种变型。之后在比赛中看到新的题目,才会有点似曾相识的感觉,进一步思考出状态转移方程。
所以,我打算写 100 道动态规划方程的题解,希望有志攻破此难关的学生和家长一起加油!
虽然动态规划没有模版可以套,但是动态规划有三个核心问题:
一般思考动态规划就是思考以上三个问题,这三个问题解决了,动态规划的程序也可以写出来了。
推荐的教学题目如下:
题目名 | 说明 |
---|---|
P2842 纸币问题 1 | 基础 DP,记忆化搜索 |
P1216 数字三角形 | 基础 DP,记忆化搜索 【经典 DP】 |
P2840 纸币问题 2 | 基础 DP |
P2834 纸币问题 3 | 基础 DP,有多处可优化的点 |
P1048 采药 | NOIP2005 普及组第三题。01 背包问题。【经典 DP】 |
P1616 疯狂的采药 | 完全背包问题。【经典 DP】 |
P2196 挖地雷 | NOIP1996 提高组第三题。涉及输出路径技巧。 |
P1434 滑雪 | 上海市省队选拔 2002 |
P1115 最大子段和 | 最大子段和。【经典 DP】 |
适合的作业:
题目名 | 说明 |
---|---|
P4017 最大食物链计数 | 记忆化搜索 |
P2871 Charm Bracelet S | USACO 07 DEC,01 背包 |
P1802 5 倍经验日 | 01 背包 |
P1002 过河卒 | NOIP2002 普及组,记忆化搜索 |
P1049 装箱问题 | NOIP2001 普及组,01 背包 |
P1064 金明的预算方案 | 01 背包变型,NOIP2006 提高组第二题 |
P1077 摆花 | NOIP2012 普及组 |
P1164 小A点菜 | 与摆花一题类似 |
P2392 考前临时抱佛脚 | 01 背包变型 |
此题可以带着孩子一步步推导和演进。具体步骤如下。
先引导孩子用最暴力的 DFS 的方式来做此题,建立基础的解题框架,虽然会超时,但是也帮助我们后面引导孩子学会记忆化搜索。代码如下:
1 |
/** |
有了上面的代码,通过分析,发现大部分的超时是因为有重复的计算过程。以下是一个以 10,5,1 为例的示意:
所以,我们可以将重复计算的过程保存下来,以后再次需要计算的时候,直接读取保存的结果即可。在此思想下,我们只需要在上面改动三行,即可将超时的程序改为通过。具体代码如下:
1 |
/** |
有了以上两段代码的尝试,我们能够发现:
那么,我们就可以思考,如果我们用 dp[i] 来表示钱币总额为 i 的结果数。那么,dp[i] 的计算过程(即:状态转移方程)为:dp[i] = min( dp[i-v[j]] )+1
,其中j=0~N
。
这样,我们就可以引导学生写出第一个动态规划程序。
1 |
/** |
P1216 数字三角形同样可以用记忆化搜索引入。先写记忆化搜索的代码有助于我们理解动态规划的状态转移方程。
搜索的代码为:
1 |
/** |
由搜索代码可知,每一个位置的最价结果由它下面两个结点的最价结果构成。于是,我们可以构造出状态转移方程:dp[i][j] = v[i][j] + max(dp[i+1][j], dp[i+1][j+1])
另外,我们可以引导学生:上层的依赖于下层的数据,那应该怎么推导呢?让学生想到用倒着 for 循环的方式来从下往上推导。
最后,我们再引导学生构建一下初始值。由此,我们建立起动态规划解题的三个核心问题:
1 |
/** |
状态转移方程为:dp[i] = sum(dp[i- v[j]]), j = 0~N
,结果需要每次模 1000000007。
1 |
|
此题不能像之前的题目那样,用金钱数为阶段。因为此题是计算的组合数,所以 1,5 和 5,1 是一种答案。如果以金钱数为阶段,就无法方便将这种重复计算的排除掉。
那么,以什么为阶段,可以保证每个阶段可以基于过去的阶段推导出来?可以用不同的钱币种类为阶段!
接下来就是思考这种情况下的状态转移方程。可以得出,状态转移方程如下:
dp[i][j]
表示用前 i 种钱币组成金额 j 的组合数dp[i][j] = dp[i-1][j-v[i]] + dp[i-1][j - v[i]*2] + …. dp[i-1][j-v[i]*n]; (j >= v[i]*n)
dp[1][0] = 1; dp[1][v[1]] = 1; dp[1][v[1]*2] = 1;
参考程序如下:
1 |
|
此题还有另外一种状态转移方程,把阶段分为没有用过 a,和至少用过一张 a。
这样的话,状态转移方程优化为:dp[i][j] = dp[i-1][j] + dp[i][j-v[i]]
这样,代码的复杂度进一步降低,代码如下:
1 |
|
此题还可以进一步简化,因为 dp[i] 那一层算完之后 dp[i-1] 层就没有用了。有没有可能我们将 dp[i]层和 dp[i-1]都合并在一起呢?
答案是可以的。我们可以将关键代码进一步简化如下,把 dp 改成一个一维数组。状态转移方程变为了:dp[j] = dp[j] + dp[j-v[i]]
1 |
|
P1048 采药这题是经典的 01 背包问题。为了方便教学,我们还是从最简单的动态规划思路开始推导。
我们把每个草药是一个阶段,这样:
dp[i][j]
表示前 i 个草药,花费 j 时间可以得到的最大价值dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]])
这样写出来的参考程序如下:
1 |
/** |
与上一题一样,通过分析,我们发现 dp[i][j] 中的 i 一层可以优化掉,变成只有 dp[j]。
这样,状态转移方程被优化成:dp[j]=max(dp[j],dp[j-t[i]]+v[i])
。
但是,因为每一个草药只能用一次,如果我们正着循环 j 的话,会出现多次使用第 i 个草药的情况。所以,我们倒着进行递推,就可以避免这种情况。
最终实现的代码如下:
1 |
/** |
P2196 挖地雷 是 NOIP1996 提高组第三题。这道题的解法有点类似于P1216 数字三角形。
但是,这道题更难的是:它需要我们输出路径。
我们先说状态转移方程:
dp[i] = max(dp[i+1~N]中能够与 dp[i] 连通的地窖) + w[i]
与 dp[i] = w[i]
中的较大者。我们再说说如何输出路径。因为计算之后 dp 数组中保存了每个结点能够挖的最大地雷数。所以,我们从答案 dp[ans]开始,找哪一个地窖与当前相连,同时值又等于 dp[ans] - w[ans],则表示那个地窖是下一个点。
参数代码:
1 |
|
这道题的麻烦点是如何定义状态转移的阶段,因为没有明显的阶段。
可以考虑的办法是:将点按高度排序,这样从高度低的点开始,往高的点做状态转移。
所以:
dp[x][y] = max(dp[x'][y'])+1
dp[x'][y']
为上下左右相邻并且高度更低的点1 |
|
此题更容易想到的写法还是记忆化搜索:对每一个点作为开始点进行一次 DFS,同时在进行递归调用的时候,如果当前点处理过,则返回上次的结果。
参考代码如下:
1 |
/** |
P1115 最大子段和 是最经典的一类动态规划问题。思路如下:
dp[i] = v[i]
dp[i] = dp[i-1]+v[i]
因为 dp[i] 在转移方程上只与 dp[i-1]相关,所以它最终结构上被可以被化简成类似贪心的策略,即:
1 |
|
P4017 最大食物链计数最佳的做法是做记忆化的搜索。
记录下出度为 0 的结点,从这些结点开始去寻找,把各种可能的路径加总。同时在 DFS 的时候,记录下搜索的结果。
参考代码如下:
1 |
/** |
P2871 Charm Bracelet S 是最最标准的 01 背包问题。可以作为基础练习。
参考代码:
1 |
|
经典的 01 背包问题:
状态转移方程:dp[j] = max(dp[j], dp[j-w[i]]+t[i])
。
需要注意答案最大超过了 int,需要用 long long。
1 |
|
P1002 过河卒此题是标准的记忆化搜索。有两个陷阱:
相关代码:
1 |
/** |
P1064 金明的预算方案 是一道 01 背包的变型题。题目增加了附件的概念,初看起来没法下手,但是题目增加了一个限制条件:附件最多只有 2 个。
所以,我们可以将 01 背包的“选或不选”两种情况扩充成以下 5 种情况:
然后就可以用 01 背包来实现该动态规划了。我们把每种物品的费用当作背包的体积,把每种物品的价格*权重
当作价值。
转移方程是:dp[i]=max(dp[i], 5 种物品选择情况)
,每种选择情况下,dp[i]=max(dp[i], dp[i-该选择下的花费]+该选择下的收益)
。
另外,需要注意,输入数据的编号可能不按顺序提供,有以下这种情况:
1 |
100 3 |
以下是参考程序:
1 |
|
P1077 摆花 一题是 NOIP2012 普及组的第三题。
dp[i][j]
表示前 i 种花,摆在前 j 个位置上的种数。状态转移方程:
1 |
dp[i][j] = dp[i-1][j] 不放第 i 种花 |
这道题的难点:没有想到 dp[0][0]=1
。因为后面推导的时候,dp[i-1][j-k]
中 j==k
的时候,也是一种可能的情况,要统计进来。
参考代码:
1 |
|
P1164 小A点菜一题阶段比较明显。每一道菜点不点是一个明显阶段。所以:
dp[i][j]
表示前 i 道菜,用 j 的价格,能够点的方案数对于每道菜,有点或不点两种方案,所以:
dp[i][j] = dp[i-1][j]+dp[i-1][j-a[i]]
由于 i 阶段只与 i-1 阶段相关,所以可以把阶段压缩掉,只留一维。最后压缩后的方案是:
dp[j]
表示用 j 的价格可以点到的点的种数dp[0] = 1
,因为这样才可以把后面的结果递推出来dp[j] = dp[j] + dp[j-a[i]]
因为和 01 背包类似的原因,压缩后需要倒着用 for 循环,否则每道菜就用了不止一次了。
参考代码:
1 |
|
P2392 考前临时抱佛脚 此题可以用动态规划,也可以用搜索,因为每科只有最多 20 个题目,所以搜索空间最大是 2^20 等于约 100 万。
以下是搜索的代码:
1 |
/** |
用动态规划解题时,此题可以把每次复习看作一次 01 背包的选择。每道题的价值和成本相同。背包的目标是尽可能接近 sum/2,因为sum 最大值为 20*60 = 1200
,所以背包大小最大是 600。
1 |
|
2025-01-05 10:28:04
贪心算法讲起来容易,就是问题求解的每一步,都用一个局部最佳的策略,如果能够证明局部最佳的策略最终能够保证全局最佳,则可以用贪心算法。
在实际 CSPJ 比赛中,我们不用严谨的求解和证明,只需要尝试做一些反例,如果反例中找不到问题,就可以先用贪心求解。毕竟比赛中时间的权重因素比较高。
在教学中,我们先通过简单的题目让学生理解贪心的概念。之后就可以逐步增加难度,让学生意识到,写出贪心可能容易,但是想到贪心这种解法在比赛中并不那么显而易见。
贪心通常伴随着排序,所以对 STL 的 sort
以及 priority_queue
的熟练应用也是快速解决贪心题目的必备基础,在学习相关题目的时候,可以重点加强巩固相关知识点。
sort 函数内部使用快速排序实现,时间复杂度为 O(N*log(N))
。对于数据规模为 10 万左右的题目,出题人有可能是希望你用这个时间复杂度来解题的,所以可以留意一下是否需要排序。
对于普通类型,STL 自带了 greater<T>
和less<T>
两个比较器,以下是相关代码:
int v[100]; |
sort 函数通常和自定义的结构体排序搭配使用,以下是相关代码:
struct Person { |
推荐的教学题目如下:
题目名 | 说明 |
---|---|
P2240 部分背包问题 | 较简单的一道贪心题 |
P1223 排队接水 | 贪心进阶 |
P1803 凌乱的yyy | 贪心进阶 |
P5019 铺设道路 | NOIP2018 提高组真题 |
P2240 部分背包问题 是较简单的一道贪心题。唯一的陷阱是,学过动态规划的同学可能误以为这个是背包问题。但是在教学中,贪心算法的学习比动态规划更早,所以不会有这个误解。
此题的解题思路是:将金币按单位重量的价值排序,如果能放则放;放不了,则分割放部分。
我们定义了一个结构体,结构体中的 double p
用于保存单位重量的价值。在排序的时候,按 p 的大小来由大到小排序。
参考代码如下:
|
P1223 排队接水 此题的难度是需要推导出贪心的策略。具体推导过程如下:
由以上推导,我们只需要将打水时间按从小到大排序,然后加总时间即可。参考代码如下:
|
此题有两种贪心的思路,分别是:
此贪心的方法如下:
左端点排序(小的在前),左端点相同的,按右端点排序(小的在前)
比较当前区间和下一区间,如果下一区间与当前区间没有相交,则由于我们是按左端点排序的,后面的都不会相交,直接选择当前区间;否则这两个区间显然必须抛弃一个,由于我们是按左端点排序的,后面的区间左端点都是大于它们的,因此这两个的左端点已经没有意义了,为了留出更多的空间,留下右端点靠左的那一个即可。
参考代码如下:
/** |
此贪心的方法如下:
这种贪心的思路是:对于每一个结束时间,如果能排(开始时间在上一个结束时间之后),就尽量安排。如果不能排,则尝试下一个结束时间。
参考代码如下:
/** |
P5019 铺设道路是 NOIP2018 提高组真题。之所以作为提高组题目,是因为很难想到这种贪心策略,不过一旦想清楚,写起来是很简单的。
贪心策略是:
参考代码:
|
以上。
2025-01-01 10:41:17
2024 年从财务视角,业务整体有不小的进步。
23 年虽然业务增长不错,但是整体有将将近千万的亏损,而 24 年整体的赢利是上千万的。所以业务整体健康度更高。当然,因为我们严卡利润率,我们的营收规模在 2024 年基本上没有什么增长,还是在 2 个亿左右。希望 2025 年有所增长。
海外业务在收缩为一人之后,也有不小的起色。我们在韩国还是找到了一条基于 coupang 全拖管的立足之地,可以基于这个基本盘开始做增长。虽然小,但是不至于每个月担心巨大的亏损,所以能睡得着觉。
分产品来说,2024 年我们没有交付什么成功的新产品。虽然我们在年初上线了英语闪卡机,下半年上线了斑马拼音机 G2,但是这两款产品都没能上规模。不管是达人还是直播间,这两款产品都运营得比较艰难。
年底还有一个大的变化,就是我开始负责斑马童书。
童书是一个市场比玩教具小,同时竞争更加激烈的品类。但是对我来说,能够学习一个新的品类的玩法,也是一种成长,所以我还是很愿意投入精力在里面,看看能不能深耕出一些结果。
24 年一共读了 10 本书,以下是读书笔记:
写作方面,整理了以下文章:
今年还写了一篇涉及农夫山泉的文章《替农夫山泉说句话》,整个过程对我的帮助也很大,让我理解了情绪的力量。虽然当时争议很大,但事后看来,我的观点是对的,这也让我很开心。
今年开始系统性将 CSPJ 培训作为自己的爱好,我打算把这作为自己退休后的生活内容。因为目标在 20 年之后,所以我也开始慢慢总结自己在信息学竞赛上的经验,共分享了以下几篇文章:
除了爱好外,今年还做了一些事情来悦己:
今年也买了一些软件:
今年理财在贯彻自己年初目标上执行得还可以。
年初定下来的定投目标,执行比较顺利。513500 算是一个很不错的 QDII 标的,唯一的缺点就是综合管理费是 0.91%
年初还想在合适的时候赎回指数增强产品,这个也在年底做了。之前持有了三年的金锝和九坤的 500 指数增强,发现不同的产品增强的成绩差很多,能差 10% 以上。
赎回了元盛 CTA。元盛给我的理解是:它能够在经济上行和下行的时候,都能捕捉到套利机会。但是元盛近两年的收益都是负的,我无法理解为什么这两年都没有机会。和管理团队的沟通机会也很好,所以赎回了。
今年整体港股和 A 股都有不错的收益。A 股整体有 19.05% 的收益。
港股里面:
今年在理财上也有更多的思考和成长。比如:
2024-12-22 22:37:53
其实我以前一直不理解雷军。
原因一是我在猿辅导工作,我们做的产品都是追求创新和高品质。因为成本不低,所以我们的产品定价不那么便宜。像我们公司的学练机、月子中心、咖啡、月龄盒,以及我负责的斑马玩教具,说实话定价在行业都是比较高的。
原因二是我比较欣赏的人,不管是公司内部的同事,还是公司外部的一些人,都对 “性价比” 这个词表现出不喜欢。这种不喜欢主要是站在商业角度,这种模式做起来太辛苦,太容易失败。
原因三是我自己曾负责过一款基于微信传播的英语学习产品。在这个产品失败前,我们尝试过极致的低价,但是最后并没有带来同等回报的增长,所以我知道,低价并不好做。
最近读了根据雷军口述整理出来的《小米创业思考》,终于有那么一点点理解雷军要做什么了。
以下是一些感悟。
雷军的 “极致性价比” 的想法来自 Costco,他在采访中说,一个在中国国内卖几千块钱的新秀丽的行李箱,在 Costco 只需要几百块钱。同时,雷军是一个有比较多社会责任感的企业家,他希望在互联网时代,大家可以用厚道的价格买到极致体验的东西,于是,小米成了他这个理想的实践地。
企业的存在,首先是因为有社会价值,即用户需求。首先因为用户需要某种服务,才会有相应的企业存在。在用户需求的基础下,企业才会有自己的经营使命和战略,战略应该围绕着自己的社会价值,去更好地满足自己的社会价值,这样的企业才能活得更久。
小米运用 “极致性价比” 逻辑,选择了一个极度差异化的经营模式,这种模式下:
所以,小米其实是选了一条几乎没有人,也几乎没有人走成功的路。
所以,了解完小米的逻辑之后,我理解了雷军。其实常见的经营模式雷军都知道,也都理解,但是雷军就是想走一条不一样的路。同时他也认为这条路虽然难,但是对于开创者的回报巨大。
小米这种模式,需要同时做到三点:产品好、价格低,以及要有合理的利润(也就是股东回报),雷军称之为 “不可能三角”。那他是如何完成的呢?
产品好。雷军要求团队只做高端和创新的产品,即便是做充电宝,也是将原本用在笔记本电脑上的铝外壳做到了充电宝中。除了产品好外,小米在打造新品时,首先考量的第一要素是,产品是否具备“明天属性”。“明天属性”是指代表先进趋势的体验,而且这种体验是用户一旦用过就不想放手的。比如用户一旦用了智能手机,就再也不想用非智能手机了。
价格低。雷军相信厚道的定价会带来规模效应,所以,他的很多产品是贴着成本价来定的。首款小米手机,成本 2000 块,他就定价 1999。这充分诠释了他对于价格的理解。
合理利润。这么低的价格还能有利润吗?只有向制造环节要规模效应和生产效率,同时向流通环节要效率。
在合理利润这个点上,雷军做了很多事情。比如在制造环节:
在向流程环节要效率这个点上,雷军遇到了很大的挑战,没有线下的渠道愿意与他合作。于是在初期,他只能和自己的售后合作伙伴来合作开店,最终把线下渠道的成本压到了 10% 左右。而传统的渠道,成本是 20% 左右。
但是,即使到了现在,小米在合理利润这个点上,也没有完全通过市场检验。在手机端,小米因为有大量应用市场广告和 App 预装等服务性收费,才使得他有足够的利润。但是在硬件端,不是每款硬件都可以靠服务收费的,比如大部分小米生态链产品就不太需要服务,小米还需要在未来回答这些问题。
小米切入造车行业,刚开始下属的提案有很多创新。雷军觉得不好,他觉得大公司做新业务的三个大坑:认知错位、惯性思维、偶像包袱。总觉得自己牛逼,做新业务要干件大事,但是自己在新领域很可能就是一个小学生,有很多该交的学费都还没交。
所以,雷军要求团队 “先确保做一款好车,一款能够与当下同级所有产品比拼的好车,在确保这个目标的基础上,再考虑颠覆的部分。”
当目标变成 “一款好车” 时,颠覆不颠覆就不那么重要了,什么东西好拿过来借鉴就好了。于是,小米的第一款车显得很熟悉,很多保时捷上的设计被借鉴来了,大家也被一款好的设计所吸引。
虽然入局晚了几年,但小米汽车还是获得了一个梦幻开局。
雷军在书中提到了消费电子行业的规律:当 15-20 年后行业进入成熟期,全球前 5 的品牌必将手握 80% 以上的份额。也就是说,只有最终进入全球行业前 5,做到年出货 1000 万台以上才有意义。
雷军在进入这个行业的最初,就想好了 20 年后的终局。这种终局思维才让他能够做长期主义的事情,包括投入三电等基础能力的研发,包括为造车留够足够的资金,也包括他自己的 All in 行为。
芒格说:如果你知道自己可能死在哪里,就永远不要去那个地方。雷军在书中提了很多小米犯的错误,这些错误让我记忆犹新。以下是一些记录:
小米早期连 SIM 卡的卡针都要用 10 倍于同行的材质和工艺,这事后来被雷军叫停了。雷军认为,所有的产品体验成本,应该用在用户价值上,如果用户用不到,就是自嗨。SIM 卡的卡针大部分用户只会用一次,这个卡针上就没必要用 10 倍于同行的成本。
在消费品行业,一些产品包装也会有同样的问题。如果消费者收到的产品过度包装,消费者就会认为 “羊毛出在羊身上”,这反倒是一种浪费。
雷军认为,自己在红米品牌上犯了错,以及之前用了很多 X 米的生态链品牌都是不对的,这些品牌模糊了小米品牌。所以,后来红米改名成为了 Redmi。
小米品牌,最终只用在了非常核心的产品上,包括:手机、电视、路由、音箱、笔记本电脑,以及后来做的汽车。
以上。
2024-12-15 16:54:30
在学习完数据结构队列(queue)后,就可以让学生学习宽度优先搜索了。
宽度优先搜索(BFS)的形式相对固定,但是写起来代码偏长,学生在学习的时候,老是容易忘掉一些环节,所以需要加强练习。
我整理了一个 BFS 的模版,每次教学前让孩子复述这个环节,通过这种方式来强化模版的记忆,帮助学生掌握这个算法。
模版如下:
void bfs() { |
在教学宽度优先搜索的初期,其实并不需要将入队的数据整合成结构体。这样反而会让代码变得更复杂。可以直接将需要入队的数据成组地 push 和 pop,这样就实现了简易的类似结构体的效果。
推荐的教学题目如下:
题目名 | 说明 |
---|---|
B3625 迷宫寻路 | 新手入门,没有陷阱,学习方向数组写法 |
P1443 马的遍历 | 需要求步数,需要写 8 个方向 |
P1135 奇怪的电梯 | BFS 不仅仅可以是在地图上,也可以是另外的搜索形式 |
P1162 填涂颜色 | 学习标记技巧:将地图往外扩一圈 0 ,减少标记难度 |
P1825 Corn Maze S | 变种的地图,可以传送 |
P1451 求细胞数量 | 多次的 BFS 标记 |
推荐更多练习的题目如下,可作为基础训练之用:
题目名 | 说明 |
---|---|
P1746 离开中山路 | 比较标准的练习,无坑 |
P1506 拯救oibh总部 | 强化P1162 填涂颜色 中学到的标记技巧 |
P1331 海战 | 多次 BFS 标记的同时,如何判断标记物是矩行 |
以下题目难度更高一些,可以作为强化训练之用:
题目名 | 说明 |
---|---|
P1141 01迷宫 | 数据量很大,需要提前保存查询结果 |
P2802 回家 | 状态变为走过时的血量有没有变高 |
P8604 危险系数 | [蓝桥杯 2013 国 C]题目,用 BFS 暴力尝试 |
Takahashi is Slime 2 | 变种的 BFS,需要用优先队列 |
以下是详细的例题代码说明。
B3625 迷宫寻路 是一道非常基础的宽度优先搜索,只需要输出 YES 或者 NO,对输出的要求也较小,适合拿来入门教学。
在本例题中,我们也要开始教会学生定义 movex、movey 数组,后续在迷宫一类的宽度搜索题目中,这种技巧非常见。movex、movey 的快速定义技巧是:movex 和 movey 的结构交替,每一组都是一个 1 和一个 0,同时变换 1 的正负号。记住这样的技巧就可以快速定义出这两个数组。代码如下:
int movex[]={-1,1,0,0}; |
本例还需要一个数组标记是否走过,我们使用 flag 数组。参考代码如下:
/** |
有了上面的代码,我们可以在题目上做变动,比如把输出的要求改为:
如果能到达,则输出到达终点的最短步数 ,引导学生思考,现有的代码要做怎样的改造,才能实现新的要求。
于是,我们讨论得出,需要将”步数”引入到代码中,于是,原来的代码增加了两处修改:
改动的代码如下:
/** |
当我们需要输出路径的时候,我们需要做两件事情:
1、把 BFS 经过的数据全部保存下来。这个时候我们就不能用队列了,只能用 vector,然后另外用一个变量 idx 来记录处理过的元素下标。于是,判断是否处理完的条件变成了如下的形式:
while (idx != q.size()) |
2、我们需要对每个元素中增加一个 parent
变量,记录它是来自哪一个下标。这样就可以把整个路径串起来。如下的形式:
struct Node { |
最终,整体的代码如下:
/** |
有了迷宫寻路的变种练习基础,我们就可以正式练习用 BFS 来求最近的步数一类的题目了。这其中比较适合的题目是: P1443 马的遍历。
《马的遍历》一题要求我们把所有位置的最近距离都求出来,我们可以用一个数组来保存结果。
同时,马可以跳 8 个方向,有了之前的建 movex, movey 的经验,我们知道,每组数是 1 与 2 的各种组合。于是可以快速写出来这两个方向数组。
具体写法是:
-2,-2,-1,-1,1,1,2,2
具体如下所示:
int movex[]={-2,-2,-1,-1,1,1,2,2}; |
完整的《马的遍历》的代码如下:
/** |
本题还有一个小的教学点,就是用 memset 来初始化值为 -1。可以顺便教学 memset 可以初使化的值,告诉学生不是每种值都可以用 memset 来初始化。
P1135 奇怪的电梯 一题的意义在于,用非地图的形式来教学 BFS,让学生知道 BFS 不仅仅可以是在地图上。
但从实现来说,此题的难度相对较小。此题的参考代码如下:
/** |
P1162 填涂颜色 可以用来学习地图标记的一个技巧:将地图往外扩一圈 0 ,减少标记难度。实际在写的时候,只需要从下标 1 开始读数据即可。
此题的参考代码如下,代码的最后用注释带了一个测试用例。
/** |
P1506 拯救oibh总部 强化上一题学到的技巧。
同时我们此题学习用 memset 将 char 数组统一设置成字符’0’:
memset(tu, '0', sizeof(tu)); |
参考代码:
/** |
P1825 Corn Maze S 增加了“地图传送”这种新的玩法,使得 BFS 代码写起来会更加复杂一点。
像这种更复杂的 BFS,我们就可以引入结构体,来让代码更整洁一点。结构体定义如下:
struct Node { |
因为在 BFS 的过程中,我们还需要记录步数,所以我们用 STL 的 pair 来存储队列元素。借此题,我们完成了 pair 的教学。
pair 的关键用法如下:
// 定义 |
完整的代码如下:
/** |
P1451 求细胞数量 是一道非常基础的 BFS 题目。此题需要多次调用 BFS,参考代码如下:
/** |
P1331 海战 一题的标记矩形的形式比较难想到,我个人用的是另外一个判断方法:看看所填充的坐标最小和最大值计算出来的矩形面积与标记的数量是否刚好匹配。
参考代码如下:
/** |
P1141 01迷宫 这道题的难度在于,我们需要 BFS 之后,把结果全部保存下来,之后每次查询的时候把答案直接输出就可以了。
参考代码:
/** |
P1746 离开中山路参考代码如下:
/** |
P2802 回家一题的解题技巧是:将 flag 数组用于保存走上去时的最大血量。如果走上去最大血量可以更高,也是可以再次走的。
另外,当只剩 1 格血时,下一步不管走到哪儿都是死,所以就不用扩展了。
参考代码如下:
/** |