0%

2024年5月22日,茅台发布“贵州茅台2023年度股东大会会议资料”,里面包含了“2024 年度财务预算”。

预算表示,总收入预计增长15%,营业成本增长15%,费用增长13%。

24年年报刚出时,全部以15%预估全年利润,参见 茅台2023年年报

此处仅需调整费用的增长率,带来的变化就是859变为861,基本可认为误差范围内,无需修改。

2023年核心数据

营业总收入1477亿,同比增长19%。

归母净利747亿,同比增长19%。

分红388亿,利润的51.9%,折合每股分红30.876元。

细分

23年总营收1506亿,构成

  1. 营业收入1477亿
    1. 白酒,1472亿
      1. 茅台酒,1266
      2. 系列酒,206
    2. 酒店、冰淇淋等,5亿
  2. 财务公司利息收入29亿
    1. 股份占比51%

渠道细分
1472亿 = 直销672亿 + 批发800亿

地区细分
1472亿 = 国内1429亿 + 国外44亿

2024年利润预测

科目 2023年 2024年预测 备注
营业总收入 1506 1732 目标增长15%
营业收入 1477
利息收入 29
营业成本 119 137 按增长15%估计
税金及附加 222 255 按增长15%估计
销售、管理费用 145 167 按增长15%估计
财务费用 -18 -18 不变
营业利润 1037 1191
所得税 261 300
净利润 775 891
归母净利 747 859

估值

860亿净利润,30倍市盈率,合理估值860 * 30 = 25800亿。

三年后估值:

24年归母净利增长15%到860,25年接近1000亿,26年继续按15%增长到1150亿。按30市盈率,估值大概1150 * 30=34500亿。

理想买点,三年后估值打对折,17250亿。

理想买点,当年50倍市盈率,860 * 50 = 43000亿。

核心数据

(全文无特殊说明,金额均指人民币,单位亿元)

营业收入,6090亿,去年5546,同比增长10%

非国际归母净利,1577亿,去年1156,同比增长36%

收入细分

腾讯的收入主要由增值服务、网络广告、金融科技及企业服务三大板块组成,三块业务收入贡献占99%,具体如下

板块 金额 占比 增长
增值服务 2984 49% 4%
广告 1015 17% 23%
金融科技及企业服务 2038 33% 15%

增值服务

增值服务同比增长4%,至2984亿。

分类 金额 占比 增长 备注
游戏 1799 60% 5%
- 国内 1267 2%
- 国际 532 14% 剔除汇率,增长8%
社交网络 1185 40% 1%
- 会员
- 直播
总计 2984 100% 4%

广告

网络广告同比增长23%,至1015亿。

主要由视频号、微信搜一搜和广告平台升级带动。

分行业,汽车行业减少,其他行业增加;消费品、互联网、大健康显著增加。

金融科技及企业服务

金融科技及企业服务同比增长15%,至2038亿。

金融科技增长两位数,主要由支付活动增加和理财服务收入增加所带动。

企业服务增长两位数,主要由视频号带货服务费和云服务所带动。

其他

成本

成本3159,总体持平;其中交易成本和渠道及分销成本增加,带宽与服务器成本及内容成本下降。

交易成本: todo

渠道及分销成本: 如推广游戏,像元梦之星的各种广告、直播推广等。

带宽与服务器成本及内容成本: 如腾讯视频、腾讯音乐等,缩减投入,降低版权购买数量,同时这些业务又是服务器、带宽的消耗大户,自然带来成本下降。

毛利

毛利2931,增长23%,毛利率由43%提升至48%。

毛利的变化代表收入结构的变化。由毛利率低的(音乐直播、游戏直播)向毛利率高的(视频号广告、平台服务费)转移。

板块 金额 毛利率
增值服务 1619 54%
广告 513 51%
金融科技及企业服务 806 40%
其他 -790 -15%
总计 2931 48%

重要事项

2024年计划回购1000亿港元

於二零二四年,我們建議派發截至二零二三年十二月三十一日 止年度的股息每股3.40港元(約等於 3 320億港元),增長42%,並計劃將我們的股份 回購規模至少翻倍,從二零二三年的490億港元增加至二零二四年的超1,000億港元。

在股价低于实际价值时回购,能够增加投资人的权益。

2024年派息322亿港元

2023年每股3.4港元,2022年为2.4港元。总股本94.62亿。

预计2023年流程分红现金3.4*94.62=322亿港元

财务报表重排

若干項目已自經營盈利以上重新分類至經營盈利以下,且前期的比較數字已相應重列。詳見綜合財務報表附註1

截至二零二三年十二月三十一日止年度,本集團綜合收益表中若干項目已被重新分類。先前在 「其他收益╱(虧損)淨額」中的若干項目被重新分類至「投資收益╱(虧損)淨額及其他」,包括 (i)以權益法入賬的投資的減值撥備;(ii)商譽及業務合併產生的其他無形資產的減值撥備;(iii) 處置及視同處置投資公司的收益淨額;(iv)投資產生的公允價值變動及股息收入;(v)捐款及其 他。「投資收益╱(虧損)淨額及其他」以及「利息收入」呈列於「經營盈利」以下。管理層認為,該 等修訂後的綜合收益表呈列能更好地反映本集團日常運營的業績及投資活動相關的收入及收益 ╱虧損的財務影響,將有助於綜合財務報表使用者更好地了解本集團的財務表現。二零二二年 的比較數字已重列以與本年呈列一致。

年报P34表格。
简略如下

移动 移动
……
毛利
利息收入
……
其他收益(中的部分)
经营利润
投资收益
利息收入
……

重排后,利息收入和投资收益,不再算作经营盈利,更多象征着腾讯不再将其认定为主业。

估值

2023年归母净利1577,不考虑疫情恢复,不考虑收入结构,不考虑毛利率、净利率比收入增长率高,归母净利按收入增速10%计算,三年后2026年归母净利可达1577 * 1.1^3 = 2100亿

无风险收益率按3%-4%,合理市盈率25-30倍,对应合理市值为 2100 * (25 + 30) / 2 = 57750亿

目前20240-03-24,市值27327亿港元,25255亿人民币,不到3年后合理市值的一半,在理想买点以下。

2024年卖点,2023年利润的50倍,1577 * 50 = 78850,对应股价900港元;哪怕是40倍,市值也要6万亿以上,对应股价720+港元。

美梦做起来吧。

2023最后一天,给博客加个分类,也给自己挖个坑,后面写点其他东西。

引言

在计算机中,程序和数据并没有一个明显的分界线。数据可以被当作程序执行,程序也可以作为数据使用。

前者,很多语言提供了eval功能,将一段数据作为代码执行,比如JavaScript

1
eval("alert(1)")

这里我们主要说一下后者。

假设有一门语言,它可以定义函数,调用函数,但没有提供基础的数字类型,我们要如何使用函数来实现数字这一特性。

为了实现这个特性,我们要对这个特性有个初步的了解。

也就是,什么是数字?

在日常生活中,我们可以说,我有3个苹果、他打了我2下、口袋里空空如也1块钱也没有等等。

我们把数字作为计数的一个东西(循环定义?),换句话说是某一个东西(上述苹果)或某个动作(打人)的重复。

3个苹果就是有一个苹果,又有一个苹果,还有一个苹果;

打人2下,就是打一下,再打一下;

0就是空空如也,口袋没钱或者没执行打人这个动作。

如果一种语言可以定义函数、调用函数,那我们就可以通过定义一个函数,然后再调用这个函数表示数字,调用这个函数n次就代表数字n。

这种程序表示数字的方式叫邱奇编码,数字叫邱奇数。以lambda演算的发明者阿隆佐·邱奇命名。

定义数字

邱奇数就是函数被调用的次数。

为了使函数可以被调用多次,我们让这个函数调用的返回值能够被重新应用于这个函数。

换句话说,这个函数的参数和返回值的类型是相同的。

比如定义函数f,函数参数为a

那1就是f(a),2就是f(f(a)),以此类推。

对于0,就是函数f,一次也没被调用。

没被调用怎么表示?

我们需要注意的是,数字是函数被调用的次数,在数字被使用前,函数是不能被调用的,否则调用完了数据也就看不到了。

有点像上发条的玩具,定义一个数字就是给玩具上好适当的发条,使用这个数字时,就是打开开关,玩这个玩具看被上了多少发条。

也就是说1,实际上是——接受一个函数和此函数的参数,并将这个参数应用到这个函数上。是一个动作,也是一个函数。

1
2
3
4
5
one = \f a -> f a

--two = \f a -> f (f a)
--two = \f a -> f . f a
two = \f -> f . f

另外一种语言,比如js写就是

1
2
3
4
5
6
7
var one = function(f, a) {
return f(a)
}

var two = function(f, a) {
return f(f(a))
}

既然如此,0就是

1
zero = \f a -> a

传进的函数f,被忽略

简单计算

数字是可以被计算的,如两个数字相加

先从简单的开始,加一个固定的数,如加1

按照之前的定义,加1,就是在原来的基础上,多执行一次相同的动作

比如:

1
addOne n f a = f (n f a)

为什么这样写?为什么会有三个参数,这分别代表什么意思?

在分析之前,我们先考虑下类型,邱奇数的类型、作为依据的被调用函数的类型以及计算函数参数的类型等

首先看被调用函数的类型

之前提到

为了使函数可以被调用多次,我们让这个函数调用的返回值能够被重新应用于这个函数。

换句话说,这个函数的参数和返回值的类型是相同的。

这里包含以下信息:

  1. 这个被调用函数是一个函数(显而易见)
  2. 这个函数可以接受一个参数
  3. 这个函数的参数类型与返回值类型相同,返回值可以被重新应用于此函数

也就是我们可以给此函数的类型起个别名,用以下表示

1
2
-- 接受一个类型a,返回一个类型a,别名Func
type Func a = a -> a

再看数字的类型

之前也有提到

1,实际上是——接受一个函数和此函数的参数,并将这个参数应用到这个函数上。是一个动作,也是一个函数。

同样,这包含以下信息:

  1. 这个邱奇数是一个函数
  2. 这个函数接受两个参数,一个是之前提到的被调用Func a,一个是可被应用到函数的参数,也就是a
  3. 考虑这个函数的返回值类型,由于函数体只是简单的将参数应用到被调用函数上,然后返回,所以其类型也就是Func a的返回值类型,同样是a

于是,我们就有了邱奇数的类型别名

1
2
-- 接受一个类型Func a和类型a,返回一个类型a,别名ChurchNum
type ChurchNum a = Func a -> a -> a

对于加一函数addOne

很明显,它接受一个类型ChurchNum a,返回另一个比它大1的另一个ChurchNum a

也就是

1
addOne :: ChurchNum a -> ChurchNum a

如果返回值类型不用邱奇数的类型别名,则为

1
addOne :: ChurchNum a -> (a -> a) -> a -> a

那addOne的实现

1
2
addOne :: ChurchNum a -> (a -> a) -> a -> a
addOne n f a = f (n f a)

可以理解为,addOne接受一个ChurchNum a类型的邱奇数,实参为n,返回另一个邱奇数,这个返回的邱奇数(类型为函数),接受两个参数,实参分别为f和a

到这里,两个邱奇数相加的函数add,也就很容易出来了

1
2
3
4
-- 类型,接受两个邱奇数,返回另一个邱奇数
add :: ChurchNum a -> ChurchNum a -> ChurchNum a
-- 实现,先将参数a应用于f n1次,再将结果应用f n2次
add n1 n2 f a = n2 f (n1 f a)

检验

我们一直在说邱奇数是一组动作,这组动作代表一个参数被应用于一个函数多少次

还举例说定义数字像是给玩具上发条,使用数字是让玩具动起来

那怎样才能让玩具动起来

我们可以用实际的例子来说明

比如我们可以说动作是——给一个字符串头上加一个字符'a',动作的参数就是被加的字符串,初始参数这里我们用空字符串

1
2
let f = ('a':)
let a = ""

做几个简单的计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 0,执行这个函数,看看结果,也就是让玩具动起来
Prelude> zero f a
-- 返回空字符串,也就是加字符a这个动作没有被执行
""
-- 通过0加一定义1
Prelude> one = addOne zero
-- 通过1加一定义2
Prelude> two = addOne one
-- 通过1 + 2定义3
Prelude> three = add one two
-- 查看3的结果
Prelude> three f a
-- 字符a被加到字符串头上3次
"aaa"

虽然我们之前假设系统中没有数字类型,但为了方便我们的验证,用其做一下检查是没问题的

我们可以说动作是——给一个系统数字加一,动作的初始参数是0

运行

1
2
3
4
5
6
7
let f = (+1)
let a = 0

one = addOne zero
two = addOne one
three = add one two
three f a

得到

1
2
3
4
5
6
7
8
Prelude> let f = (+1)
Prelude> let a = 0
Prelude>
Prelude> one = addOne zero
Prelude> two = addOne one
Prelude> three = add one two
Prelude> three f a
3

概述

动态规划(英语:Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

此为wiki百科中的定义,重点是分解

换句话说,就是:

  1. 将一个大问题,拆分成几个小问题

  2. 求小问题的解

  3. 推出大问题的解

以上还有个隐含的条件,即被分解的小问题要和大问题有联系,这样才能从小问题的解推出大问题的解

样例

背包问题

N件物品和一个容量是V的背包。每件物品只能使用一次。

i件物品的体积是v[i],价值是w[i]

求解在背包容量范围内,所能装物品的最大价值是多少

初步尝试

用最直观的想法来解决此问题,比较容易想到的是:

  1. 先装单位价值最高的
  2. 再继续装剩下的物品中,单位价值最高的

咦,看起来我把问题分解了

假设第k件物品是单位价值最高的,那只需要解决

N-1件物品(除了第k件)和一个容量为V-v[k]的背包。。。。

被分解的问题又可以以同样的方式进一步分解,分解的过程中N和V越来越小

若N=1或V<=0,也就有了唯一解

这样我们做到了之前所说的:第一步问题拆分和第二步求小问题的解

第三步是不是也可以做到了?

看起来很简单,把所有小问题的解串起来就可以了

我们找个简单的数算一下

1
2
3
4
N = 3
V = 5
v = [1,2,3]
w = [2,4,4]

很容易看出,第1、2个物品单位价值最高,都是2,剩余第3个为4/3

根据之前的思路

  1. 前两个单位价值一样,但第二个总价更高,所以我们先装第2个;

  2. 其次装第1个,其单位价值也是最高的;

  3. 此时,价值为2+4=6,已用容量为1+2=3,剩余容量2

  4. 再然后,发现剩余的那个已经装不进了

好像不对啊

如果装2和3的话,价值为4+4=8,总价值更大

问题出在什么地方?

我们装了单位价值较高的第1个,但是因为装了它,单位价值较低但总价较高的3装不进去了,如果不装它,反而恰好可以装进去

因小失大,为了1,丢了3

鼠目寸光,为了眼前的利益,放弃了后续可能的更高价值

以上,为贪心

再次尝试

那如何避免因鼠目寸光造成的因小失大呢?我确实也是根据说明先拆分问题再求解再推大问题的呀

因为之前只顾眼前,有问题,所以现在在选择时还需要进一步看一下做的本次选择对后续的影响

比如现在随便选择一个装进背包

若选的是第1个,则后续子问题就变成了

1
2
3
4
N = N - 1 = 2
V = V - 1 = 4
v = [x, 2, 3]
w = [x, 4, 4]

整个问题的解则为2 + 子问题的解

若选的是第2个,则后续子问题就变成了

1
2
3
4
N = N - 1 = 2
V = V - 2 = 3
v = [x, x, 3]
w = [x, x, 4]

注意,这里物品1也被拿掉了,相当于1没选,因为先拿2再拿1和先拿1再拿2是一样的,而后者会被选1的那种可能覆盖到,这里无需重复计算

整个问题的解则为4 + 子问题的解

若选的是第3个,则后续子问题就变成了

1
2
3
4
N = N - 1 = 2
V = V - 3 = 2
v = [x, x, x]
w = [x, x, x]

整个问题的解则为4 + 子问题的解

而这时,子问题中的物品已经没有了,结果肯定是0,无需继续拆了

由于不同的选择,产生的后续子问题也不一样,所以我们无法确定当前选择在后续看来是不是最优的

但我们可以确定,最优解在这三种选择中的一个(废话,所有可能的选择都被你暴力的一个个全列出来了-_-!)

且若每个子问题的解是最优的,那本次最优的选择为

1
max(2 + 子问题1的解, 4 + 子问题2的解, 4 + 子问题3的解)

没有其他可能了

到目前为止,我们前进了一小步

我们把这个大问题,拆成了3个小问题,并能确保能够从小问题的解得出大问题的解

虽然数量多了,但是问题简单了

同样

我们可以把3个小问题,用同样的方式,继续拆分

这样问题更多了,问题也更简单了

直到物品没了或背包满了,无需继续拆分为止

子问题求解后,上一级问题的解就显而易见了,一直到最上层

以上,我们解决了整个问题

这种方式叫。。。暴力,暴力的递归

优化

因为我们遍历了所有的可能

那,还有什么地方可以优化吗

我们继续

之前的数据可能不太好看,我们稍微调整一下,比如

1
2
3
4
N = 4
V = 5
v = [1,1,2,3]
w = [2,2,4,4]

我们添加了一个物品,新添加的物品和原来第一个一样

我们很容易看出:选择1和选择2,是一样的;选择1+2和选择3也是一样的

因为他们或他们的和 体积和价值完全一样

因选择他们中的一个而拆分出的子问题,有很大一部分是重叠的

但是在我们的计算中,子问题被计算了两次,重复了

为了避免重复计算

我们可以将子问题的解进行缓存,当第二次遇到同样的子问题时,直接返回已求出的解就可以了

回顾一下我们拆分子问题的过程

  1. 选择一个物品
  2. 剩余背包的体积和剩余物品组成了一个子问题

  1. 选择另一个物品(前一个物品不选,既选前一个又选当前这种情况不在此考虑,因为会被前一个物品选择后拆分出的子问题覆盖)
  2. 剩余背包的体积和其后物品组成了一个子问题(之所以是其后,原因同上)

其实第一种是第二种的特殊表现,可以归到第二种去

由此可见,子问题的唯一性由剩余背包的体积,和当前选择物品的后续物品组成,而后续物品的结尾位置是相同的,我们只需要知道起始位置就够了

体积的范围为0 - V,位置范围为0 - N

子问题的解是一个数值

所以我们可以在第一次遇到子问题时,将其解缓存到一个二维数组中,下次再遇到时,直接返回

以上,我们通过递归记忆化的方式,优化了此问题

优化后,子问题的范围最多有V * N

反向求解

那既然子问题的解是有限的

那我们可不可以先把所有子问题的解全求出来,然后在求出整个问题的解

当然可以,这种方式也就是所谓的递推

递推是递归的逆向

递归时,我们直接求解大问题,当大问题需要用到子问题的解时,再对子问题进行求解

递推时,我们通过已知小问题的解,推出更大问题的解,推导方向为最终的问题

那哪些问题的解是已知的?

我们之前递归时提到

直到物品没了或背包满了,无需继续拆分为止

无需拆分,也就是说当前问题的解已经的已知的了

显而易见,没物品可选和无空间可装,问题的解都是0

我们从这个最小的问题,一步步构造大问题

比这个小问题稍微大点的问题是什么?

物品多一个,或背包大一点

物品多一个,就多了一种可能,可能选了这个物品,总价值变的更高了,如果选择当前物品不能使总价值更高,那最高的总价值还是和之前的小问题一样,不会更差

若要选择多出来的这个物品,那意味着这个物品要占用一部分背包容量,即v[k],如果整个背包容量还装不下这个物品,那这个物品也就无法选择,若能装下,那选择这个物品的总价值为w[k] + (去掉这个物品所占用的空间、去掉这个物品所组成的小问题的最高价值)

若不选多出来的这个物品,那最高价值和少一个物品且同样背包容量组成的小问题的解一样

选或不选,两种情况取较大的那个,也就是这个变大了的问题的解

背包大一点,同样是多了一种可能,原来不能选择的物品,现在也许可以选了

计算方式同上

总结

总结一下

设i为物品范围,从0-N;j为背包容量,从0-V

f(i, j)表示为前i个物品,在背包容量为j的情况下的解

1
2
3
4
5
6
7
8
若i = 0 或 j = 0
则f(i, j) = 0

否则
f(i, j) = max(
w[i] + f(i - 1, j - v[i]), // 选择当前物品,若物品占用空间超过背包容量则为0
f(i - 1, j) // 不选当前物品
)

至于Wiki,动态规划是分解问题并求解的方法,但分解问题并求解的并不一定是动态规划

安装hexo

前置安装Nodejs和git

1
2
3
4
5
npm install hexo-cli -g
hexo init blog
cd blog
npm install
hexo server

初始化git仓库

1
2
3
git init
git add .
git commit -m "init"

添加Next主题

fork hexo-theme-next

next 主题官方地址 https://github.com/theme-next/hexo-theme-next

fork到自己账号下

添加为hexo的submodule

blog目录

1
git submodule add https://github.com/tongluyang/hexo-theme-next themes/next

修改hexo主题为next

hexo配置文件/_config.yml

1
theme: next

提交

1
2
git add .
git commit -m "add hexo theme next"

修改next配置

next配置文件themes/next/_config.yml

1
2
3
4
5
6
7
8
9
# Schemes
#scheme: Muse
#scheme: Mist
#scheme: Pisces
scheme: Gemini

avatar:
# Replace the default image and set the url here.
url: https://avatars2.githubusercontent.com/u/25279149?s=120&v=4 #/images/avatar.gif

提交

1
2
3
4
5
6
cd themes/next
git add .
git commit -m "scheme and avatar"
cd ../..
git add themes/next
git commit -m "update submodule of next"

修改基本信息

hexo配置文件/_config.yml

1
2
3
4
5
6
7
8
9
10
11
12
# Site
title: TongLuyang's Blog
subtitle: ''
description: ''
keywords:
author: TongLuyang
language: zh-CN
timezone: Asia/Shanghai

# URL
## If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/'
url: https://tongluyang.com

提交

1
2
git add .
git commit -m "update site info"

部署

如果要把源文件放在私有仓库,那么需要使用hexo-deployer-git

1
npm install hexo-deployer-git --save

修改hexo配置文件/_config.yml

1
2
3
4
5
deploy:
type: 'git'
repo: https://github.com/tongluyang/tongluyang.github.io.git
name: tongluyang
email: tongluyang@gmail.com

执行

1
hexo clean && hexo deploy

在《Real World Hashell》一书里,有一段通过foldr表示foldl的代码

1
2
3
4
myFoldl :: (a -> b -> a) -> a -> [b] -> a

myFoldl f z xs = foldr step id xs z
where step x g a = g (f a x)

刚看到这的时候,感觉特别难懂,尤其是之前没有函数式编程的经历,对柯里化了解不深的情况下,更加一头雾水

如果是顺序读这本书的话,我们应该已经知道了foldr的签名

1
2
Prelude> :type foldr
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b

困惑

这里最让人感到奇怪的有两点

  1. foldr的签名是接受三个参数,如下

    1. 一个函数,这个函数接受两个参数,类型a和类型b,返回一个类型b的值
    2. 一个类型b的初始值
    3. 一个可折叠类型a,这里我们简单当作一个列表

    但是,实际上我们传递了四个参数

  2. 上面说了,foldr的第一个参数是个函数,接受两个参数,但传递过去的step函数,有三个参数

上述两点,也可以总结成一点:参数个数不匹配

Currying

要理解这个问题,我们需要一些其他知识,也就是“部分函数应用和柯里化”(Currying)

这部分内容在原书后续有讲到

这两部分是相辅相成的,理解了部分函数应用和柯里化,会更容易弄明白foldr表示foldl;而弄清楚foldr表示foldl,则会对部分函数应用和柯里化有更深刻的理解

在说部分函数应用和柯里化之前,我们先看看一般命令式语言中的现象

比如有如下Java代码,两数相加

1
2
3
private int add(int a, int b) {
return a + b;
}

这个函数需要两个参数a和b,然后返回两数之和

我们可以通过add(1, 2)的方式调用这个函数,但是,add(1)则是不合法的,因为没有此种方法签名

与Java不同的是,在Haskell中,这是允许的

1
2
3
4
5
Prelude> add a b = a + b
Prelude> :type add
add :: Num a => a -> a -> a
Prelude> :type add 1
add 1 :: Num a => a -> a

在Haskell中,本质上,所有的函数都只接受一个参数

上面的add函数,看上去接受两个类型为a的参数,返回一个类型为a的值,实际上是先接受一个类型为a的参数,返回一个函数,这个函数再接受一个类型为a的参数,返回类型为a的值

就像我们有一个机床,可以将一堆铁疙瘩制作成各种机器,然后我们可以用制造出的机器,将原料加工成成品

现在我们要做衣服,做衣服需要布料,需要使用缝纫机,我们还没缝纫机,不过我们有机床,有铁疙瘩,可以造一个出来,在Haskell里可以表示为

1
2
3
4
5
type Iron = String
type Cloth = String
type Clothes = String

machineTool :: Iron -> Cloth -> Clothes

如果用类似Java语言解释这个签名的话,就是machineTool这个函数需要两个参数,铁和布,将这两个参数同时放进去,会出来一个衣服

在Haskell里,并不需要同时传两个参数,我们可以只传一个

1
sewMachine = machineTool "a piece of iron"

这是合法的,表示给机床一块铁,做出了一台缝纫机,缝纫机是个函数,可以继续接受参数,它现在接受一个Cloth类型的参数,返回Clothes类型的结果

1
2
*Main> :type sewMachine 
sewMachine :: Cloth -> Clothes

这个缝纫机可以接受一匹布,产生一件衣服 sewMachine "a piece of cloth"

解析

回到最初的右折叠表示左折叠问题

1
2
myFoldl f z xs = foldr step id xs z
where step x g a = g (f a x)

再看一下foldr的签名

1
2
Prelude> :type foldr
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b

foldr第一个参数是函数,其返回值类型为b,和foldr第二个参数,也就是foldr的初始值类型一样,也是b

foldr step id xs z这里,第二个参数,也就是初始值,是id

1
2
Prelude> :type id
id :: a -> a

我们看到id是一个函数,接受一个类型为a的参数,返回的类型同样也是a(实际上ididentity的缩写,本身的意思,id作用是返回参数本身)

既然初始值类型是函数,那foldr的第一个参数,返回的也是一个函数

我们看到step的定义,它可以接受三个参数,但是在foldr的应用过程中,只会应用两个(列表的从右往左下一个值和初始值或者上一次函数的计算结果),结合函数的部分应用和柯里化,我们知道step函数在应用两个参数后,返回的是另一个函数,这个函数接受一个参数(step函数参数的第三个)

也就是foldr的第一个参数,类型为(a -> b -> b),在实际执行过程中,a类型是列表内容的类型,第二个参数b类型,第一次是初始值,也就是id,实际类型是一个函数,同时,结果仍然是b,是一个函数,作为下一次计算(a -> b -> b)的第二参数

由于foldr的最终返回结果是一个函数,所以可以应用一个参数,这个参数在foldr step id xs z这里,也就是z,或者说可以把foldr step id xs z看作是(foldr step id xs) z,foldr全部折叠完,其最后结果是一个函数,然后用z作为函数的参数对这个最终函数进行调用

展开

参数个数的问题弄清楚后,我们可以把表达式进行展开,将实际参数带入到表达式的变量中

假设我们现在使用myFoldl对一个列表求和,列表是[1,2],求和的初始值是0

按照左折叠的规则,我们预期执行顺序应该是((0 + 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
myFoldl f z xs = foldr step id xs z
where step x g a = g (f a x)

-- 我们的调用为
myFoldl (+) 0 [1,2]
-- 参数带入变量
-- f == (+)
-- z == 0
-- xs == [1,2]

-- 也就是
foldr step id [1,2] 0

==> foldr (\x g a -> g (f a x)) id [1,2] 0
-- 参数再带入到变量,列表从右向左
-- x == 2
-- g == id
-- f == (+)

==> foldr (\x g a -> g (f a x)) (\a -> id ((+) a 2)) [1] 0
-- x == 1
-- g == (\a = id ((+) a 2))
-- f == (+)

==> foldr (\x g a -> g (f a x)) (\a -> (\a -> id ((+) a 2)) ((+) a 1)) [] 0

==> (\a -> (\a -> id ((+) a 2)) ((+) a 1)) 0
-- ↑ |........|
-- ↑ ↓
-- a == ((+) a 1) <----/

==> (\a -> (id ((+) ((+) a 1) 2))) 0
-- ↑
-- id返回参数本身

==> (\a -> ((+) ((+) a 1) 2)) 0
-- ↑
-- a == 0

==> ((+) ((+) 0 1) 2)
-- 改成中缀

==> ((+) 0 1) + 2
==> (0 + 1) + 2

经过上述将实际参数带入到变量,推导后,myFoldl的行为确实与预期一样,我们就这样通过foldr表示了foldl

简介

三向切分快排是针对特殊场景的一种优化,比较适合数组中重复元素比较多的时候。

与标准快速排序分为小于等于基准和大于基准两个部分不同,三向切分分为三个部分,也就是小于基准、等于基准和大于基准,也就是把标准快排的等于部分单独拆分出来,这样这部分相同的数据就不用再次比较了,所以,当重复元素比较多时,将会有比较大的性能提升。

实现

先放代码,后面解释

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
public void threeWayPartition(int[] nums) {
threeWayPartition(nums, 0, nums.length - 1);
}

private void threeWayPartition(int[] nums, int start, int end) {
if (start >= end) {
return;
}
int lt = start;
int eq = start;
int gt = end;
int base = nums[eq];
while (eq <= gt) {
if (nums[eq] > base) {
swap(nums, eq, gt);
gt--;
} else if (nums[eq] < base) {
swap(nums, lt, eq);
lt++;
eq++;
} else {
eq++;
}
}
threeWayPartition(nums, start, lt - 1);
threeWayPartition(nums, gt + 1, end);
}

private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}

代码的核心是while循环,主要涉及三个索引变量lt、eq、gt

三个变量可以将数据分为四个部分,除了结果的小于、等于、大于外,还有过程中的未比较部分

也就是

  • [start-lt) 代表小于基准的数据
  • [lt-eq) 代表等于基准的数据
  • [eq-gt] 代表未比较的数据
  • (gt-end] 代表大于基准的数据

其中,小于和等于在前半部分,大于在后半部分,为比较在中间,随着比较的进行,小于等于的向后扩展,大于的向前扩展,慢慢吃掉未比较的区域

初始时,所有的数据[start-end]都是未比较的,小于、等于、大于基数的数据都没有,所以lteq初始化为0,gt初始化为end

我们先将基准定义为开始第一个元素

我们遍历未比较的数据,也就是[eq-gt]

从第一个元素nums[eq]开始

由于基数就是第一个,自己和自己肯定相等,所以执行else逻辑,也就是和基数相等,相等时,等于基准的数据多了一个,eq索引后移

接下来第二个数与基准元素比较,如果这个数比基准元素大,我们是要把它放到数组的第三部分的,也就是(gt-end]区域,区域不包含qt,也意味着gt代表下一个大于基准的数将要被放入的位置,此时nums[eq]比基准大,所以两数交换位置,交换完成后,大于基准的数多了一个,所以gt前移,而eq位置的数据是从原来的qt来的,还是未比较的,所以eq的位置不动,还需要继续比较

如果num[eq]比基准元素小,我们要把它放入[start-lt)区域,同样,区域不包含ltlt是下一个比基准小的数的位置,所以交换eqlt位置的数据,将小于基准的数,也就是num[eq]放到lt那里,此时小于基数的数多了一个,lt前移;由于原始lt放的是等于基准的数据([lt-eq)),现在到eq这个位置来了,已经不需要再次比较了,相当于把“等于基准的数据”这个集合的第一个数挪到了最后的位置,数量不变,虽然数量不变,但是由于这个集合的位置是在小于基准那个集合的后面,小于基准的集合多了一个,所以eq也要同步后移

继续以相同的逻辑遍历

直到没有未比较的数据,也就是[eq-gt]这个集合为空,所以退出条件是gt > eq

经过一轮遍历后,数组被分为三个部分

也就是

  • [start-lt) 代表小于基准的数据
  • [lt-eq) 代表等于基准的数据
  • (gt-end] 代表大于基准的数据

[eq-gt]已经没有了

等于基准的数据已经不需要再排序了,小于和大于的都只是比基准大或小,内部并不一定有序,所以继续对两个区域用相同的逻辑进行排序

标准快排,只是基准那一个数不需要下次的排序,而不是等于基准的一批数,如果等于基准的数很多,就会造成浪费,而三向切分就是针对这种情况的一个优化。

上篇文章说到了拖延这个话题。

就拿这个博客举例,从十一月中旬搭建完成,本来就是要写东西的,结果后面一直到十二月底才开始写第一个字。

而这篇文章,看时间从19年12月31号开始写,写了一部分后暂停,后面过了个春节,才又开始继续写后面的部分,实在是太拖延了。

前一篇文章提到拖延的时候简单的列举了几条原因,现在来写一写更深层、更系统的原因,以及怎么才能告别拖延症。

拖延症的分类

当我们和其他人讨论一个东西的时候,我们首先需要确保说的是同一个东西,才能继续交流下去。否则就像是聊天不在同一个频道似的,你说你的他说他的,两人还都觉得对方怎么都说不通。

在拖延症这里,可以分为作为心理问题的拖延症和作为社会现象的拖延症。

作为心理问题的拖延症是严肃的,经常伴随着严重的心理问题,需要专业的心理医生或精神科大夫才能对此评判。

而作为社会现象的拖延症就比较轻松,更多的是一种调侃或自嘲。

这里说的只是后者。

下面再聊聊拖延症的诱因和如果克服拖延症,当然,只是理论,起码到目前为止,我还是有着比较严重的拖延症:)

拖延症的诱因

为什么我会拖延?

就是明明有事情做,非要拖着拖着,一直拖到不能再拖的时候,所以有人调侃deadline是第一生产力。

下面这些,也许是常见的造成拖延的原因

诱惑

现在社会诱惑太多、诱惑太近、诱惑太诱人,比如现在,我在电脑上敲这些字,连着网,非常容易的就可以刷刷新闻、刷刷知乎,可以有很多选择。而且我也想这么做。

除开电脑,还有手机,现在基本手机不离身,而手机上有各种推送,像朋友圈这些能刷的东西更多,还有游戏,游戏厂商有着几十几百号运营、策划,这些精英们设计的关卡,可以及时的给人们提供积极的、不确定的反馈,完全贴合人们的生理、心理需要。

这些都是诱惑,很多很近很诱人。

而你知道自己应该干的事情,比如写文章、写文档,往往过程单调,反馈不及时,需要长时间才能看到效果。

有心理学家把大脑分为热系统和冷系统,前者控制着人的本能的反应,自动触发、快速反应,后者控制着理智的部分,需要推理计算,消耗意志力。前者让人及时行乐,后者让人从更长远的眼光看待事情,而不是只看眼前。

刷新闻、刷知乎、刷朋友圈、刷剧打游戏这些是激活的热系统部分,你本能的就喜欢,而你知道自己应该做的事情,是冷系统在管的,你知道这些东西在长远的看来对你是有益的,但是需要意志力的介入才能行动。

压力

前面说到了热系统和冷系统,举得例子都是诱惑,是有吸引力的,刷新闻打游戏这些吸引着我们去做。而还有些东西是有排斥力的,比如说压力。

当一件事情很中性,比如发呆,可能平时我们不想做这件事情,但是如果还有一件有压力的事情让你选的话,没压力的就是有吸引力了。

就像是“趋利避害”,“两害取其轻”这样,当然这里的“害”都是从热系统的角度也就是当前来看。

完美主义

这一点我也很有体会,比如就本博客来说,之前一直拖着,就有个原因是觉得自己写的不好,想等自己写作练好了再发布出来,结果最终就是拖啊拖,越不写越写不好,越写不好越想拖到写好再写。

还有就是开源,本来自己遇到个事情,太麻烦,写个小程序给自已用,自己用完后想,哈哈我是个天才,有了这个东西方便多了,开源吧,看能不能有几个star!但是又想,这里有个坑,我自己用的时候知道有坑,不会让自己掉进去,但是别人不知道啊,等我先填完在开;啊啊啊,又发现了个问题,现在在我这里是这样,到别人那可能会那样,所以我得要灵活一下可配置才行,等我加个feature在发;等等等等,一直想着要写完美了在开源,结果是有事一耽搁,我自己的事情也解决了,这件事就忘了,开源的事也拖在那里了。

控制和反抗

这点主要出现在父母或上司有关的事情中。

比如你正想去写作业,然后,你的父母突然对你说,xxx,赶紧去写作业。这个时候往往你会比较抗拒,因为你本来就是想去的,结果父母叨叨,这个时候去就成了听父母的命令去而不是安自己意愿来。哪怕这个意愿就和父母的要求是一致的。为了反抗或者避免这样的误会(这个误会可能只有自己一个人知道),你可能会再拖一会,用来表达,这是我自己要去写作业的,而不是听你们的话才去的。

还有当你的上司指派给你一个任务时,如果你觉得这个任务很蠢,但又不能不做时,往往会拖一段时间,最好能到deadline,而不是及时完成。用这种方式来表达不满。

小结

有了诱因,就可以针对性的解决了,各个击破。
至于如何击破,留到下次再说吧,这篇已经拖了太久了,再不发出去又要废了。