用Python通过SMTP发送邮件
1 | # -*- coding: utf-8 -*- |
1 | # -*- coding: utf-8 -*- |
1 | # -*- coding: utf-8 -*- |
在LeetCode上提交的C代码并不需要include标准库头文件,判题系统会自动包含,并且在二叉树的题目会额外包含TreeNode结构。我希望有一个简洁的main.cpp可以直接提交到LeetCode,里面包含一些最常用的函数,调试输出的代码放在bits/stdc.h中(故意和标准库头文件同名),通过宏定义只在引入本地自定义的bits/stdc++.h时开启cout输出,这样可以直接把main.cpp的全部内容直接提交到网站条件,会自动屏蔽掉所有调试输出代码而不会报错。
main.cpp
1 | #include "bits/stdc++.h" |
一定要在结尾undef掉,不然会导致LeetCode误判
bits/stdc++.h
1 | // #include <bits/stdc++.h> |
今天在B站看到一个视频《斐波那契数列,全网最优解》,UP主给出了求解斐波那契数列通项公式的推导思路。
因为我早年也对这种数列有过研究,而且记得一个更简单的解法,所以记录一下。
斐波那契数列是这样一种数列:
a(1) = a(2) = 1
a(n) = a(n-1) + a(n-2), n>=2
上面是通过递推公式的形式给出的定义,我们注意到递推公式是前两项的线性组合。而线性变换可以通过矩阵表示,我们不妨转换思路来求向量
(an,an+1)
的通项公式
我们写出根据a(n-1), a(n)推得a(n), a(n+1)的递推公式
{an=anan+1=an−1+an
写成矩阵的形式
(anan+1)=(an−1an)[0111]
我们可以看到这就类似等比数列的递推公式,只不过公比q是个矩阵,等比数列通项公式是
an=a1∗qn−1
类比得到,上面递推公式的通项公式
(anan+1)=(a1a2)[0111]n−1=(11)[0111]n−1
BTW: 其实这里矩阵和数还是有点区别的,要利用矩阵乘法有结合律(本来是先做向量和矩阵乘法的,通项公式是先做了后面的矩阵乘法最后再让向量左乘矩阵),而且是方阵才能求幂,而这里都是满足的。
对于斐波那契数列的变形也特别容易推导,无论是改变首项还是改变递推关系,包括把两项和变成前n项的线性组合,只要还是线性的,就可以这么推导。
之前用在SQLAlchemy的ORM模型的类名(驼峰风格)和数据库表名(下划线风格)的转换。
Python类名驼峰风格这个不用解释,数据库表名使用下划线风格主要是因为一些数据库系统如果使用了带大写字母的表名,那么在select、insert、update、delete语句中都要用特殊分割符包住表名才能使用,很麻烦。
1 | # 驼峰转下划线 |
基于SQLAlchemy的Upserter,当时是基于SQLAlchemy写的,不过最后似乎没怎么用到SQLAlchemy的特性,只是取了一下数据库的类型。
1 | # -*- coding: utf-8 -*- |
std::priority_queue 是C++标准库提供的优先队列(最大堆)实现,位于头文件
默认情况下要求元素有“小于”运算,取堆顶,返回最大值。
可以通过模板参数调整排序方式让其返回最小值,或者为自定义类型定义排序方式。
1 | template <class T, class Container = vector<T>, class Compare = less<typename Container::value_type> > |
模板参数:
T是数据类型
Container是维护最大(小)堆使用的容器类型
Compare是一个function object的类型,定义了排序方式
function object是一种对象,这个对象的类重载了括号运算符,也就是 operator() ,所以这个对象可以使用 obj(…),看上去就像在调用一个function一样。
1 | #include <queue> |
1 | vector<Student> students = {...}; |
使用lambda表达式的好处是让“比较方法的描述”接近sort的调用,无论从编写还是阅读都是更好的。
使用lambda表达式的坏处是,不方便复用比较方法。
实际上priority_queue有一个构造函数,可以传递一个比较对象,如果不传递就会用模板参数定义默认的比较对象。
1 | explicit priority_queue (const Compare& comp = Compare(), Container&& ctnr = Container()); |
我们可以通过构造函数参数传递一个lambda表达式定义比较方式,我们期望的定义优先队列的方式是
1 | priority_queue<Node> PQ([](const Node& a, const Node& b) { |
但是很遗憾,我们并不能这样定义,这会导致编译错误,原因是我们在模板参数仅传递了数据类型T,而没有传递Compare,因此Compare使用了默认的less
因此我们不得不传递Compare为我们定义的lambda表达式的类型,这里可以使用 decltype
关键字,这个关键字直到C++11才被引入。
1 | // 通过lambda表达式定义序 |
看上去和通过定义比较器定义优先队列似乎差不多,实际上lambda表达式的魅力在于可以访问当前上下文中的其他变量。
例如:假设我们有一个 vector<Student>
存储着学生信息,我们想定义一个存储学号的优先队列priority_queue
1 | vector<Student> students = {...}; |
如果用定义比较器类的方式则需要通过构造函数传递id2stu的引用,然后绑定给成员变量。
1 | vector<Student> students = {...}; |
本质是一样的,但是写法有些累赘。
问题:在字符串s中查找字符串p首次出现的位置。
正常情况下对s和p进行匹配的最坏时间复杂度是O(len(s)*len(p)),我们用i,j分别从s,p的头部进行匹配,每次匹配失败我们回退j到0,i+=1,进行下一轮匹配。
1 | int find(const string& s, const string& p) { |
KMP的思想就是预处理p得到next数组,保证i不回退,next就是预先算出i不回退的情况下j应该回退到哪,这样算法复杂度就降到了O(len(s)+len(p)) 也就是 O(len(s))。
有时候模式串是固定的,需要重复在不同的串中查找模式串,所以next数组也可以预先算好一直复用。
1 | // 计算next数组,即j不匹配时的回退位置 |
有了next数组后的匹配就想前面说的,只要根据next进行回退就可以了,没有过多技巧。
那么主要讲一下next数组的生成思路,根据next的定义,其实next[i]表示的是p[i]前面最长能有多少字符和p的开头匹配
例如:我们生成 "aabaaab"的next数组,考察next[4]和next[5],p[4]的前面最长有“a”和p的开头匹配,所以next[4]=1,
p[5]的前面最长有“aa”和p的开头匹配,所以next[5]=2。
1 | 0123456 |
总有next[0] = next[1] = 0,我们只要从下标2开始计算next。
对于next[i],我们可以采用数学归纳法的思维,我们找到i-1回退的位置,取j = next[i-1],如果p[i-1]==p[j],那么显然next[i] = next[i-1] + 1,如果p[i-1]!=p[j]呢,next[i]=0吗?并不是
我们还是以 aabaaab 为例,考察next[6],首先我们算出了next[0…5]=[0, 0, 1, 0, 1, 2],而p[5] !=p[2] (‘a’ != ‘b’)
1 | 0123456 |
这里就有个技巧了,对于p[i-1]和p[j]不匹配时,我们想知道让j回退多少,我们可以利用next数组的含义,尝试让j回退到next[j],再看看p[i-1]和p[j]是否相等,我们在生成next的时候就用到了规模更小的next,还是数学归纳法的思维,j=next[5]=2, 因为p[5]!=p[2] ,令j=next[j]=next[2]=1,而p[5]==p[1],所以next[6] = next[1] + 1 = 2,大致的理解思路就是这样,严格的证明见:前缀函数与 KMP 算法 - OI Wiki (oi-wiki.org)
实现比较简单直接看代码,说几点:
(i%MAX_SIZE+MAX_SIZE)%MAX_SIZE
是为了支持负数下标,如果不需要负下标可以直接i%MAX_SIZE
i&(MAX_SIZE-1)
来代替取模,而且同样支持负数下标,真是又快又好 _总的来说这两个数据结构在比赛中基本用不到。
1 | template<typename T, int MAX_SIZE> |
树状数组又称二叉索引树(Binary Indexed Tree),又以其发明者命名为Fenwick树。
是一种支持以O(logn)时间计算区间和同时以O(logn)时间修改元素值的数据结构。
它的功能可以被线段树替代,而且线段树提供了更多功能,树状数组的优势是实现简单。
树状数组提供两种操作:
1)对单点赋值。
2)查询区间和。
(事实上也可以扩展出区间修改和单点查询,我们暂不考虑)
假设我们有一个数组
1 | arr = {3, 8, 3, 3, 5, 6, 8, 7}; |
通常情况比如我们求区间[2,7)的和需要遍历区间上的元素,O(区间长度),如何减少运算次数呢,最简单的思路是我们预处理前缀和sum[],令
1 | sum[i]= arr[0]+arr[1]+...+arr[i-1] |
当我们需要算[2,7)的区间和,其实就算[0,7)前缀和减去[0,2)的前缀和,即sum[7]-sum[2],这样就可以用O(1)的时间算出任意区间的和,如果数组元素不会发生动态变化这样是可以的,但如果需要交替修改数组元素和查询区间和,这样处理会导致前缀和维护的成本很高,原本的arr[i]=x,我们不得不修改所有k>i的sum[k]来维护前缀和,这样修改数组元素的时间就从原本的O(1)变成了O(n)。
有没有方法可以在修改单点值的便利性和查询区间和的便利性上做个折中呢,肯定是有的,我们可以预处理一些子段和而不是所有前缀和,让修改单点值和查询区间和都只需要访问O(logn)的元素,树状数组和线段树都是类似这个思想。
这是在讲线段树时的图,如果我们只考虑前缀和,即从0开始的区间和,这中间很多子段和的存储是不必要的,我们来看[0, 7)的和,如果我们有了下图维护的子段和信息,[0, 7)的和最快可以通过 [0, 4)的和 + [4, 6)的和 + [6,7)的和 = 17 + 11 + 8 计算得到。
我们看图时是很容易想到的,那么这个[0, 4)、[4, 6)、[6,7)划分是怎么得出来的呢,我们可以把前缀和[0, 7)的右端点7转换成2进制,即111,如果仅保留最左侧的1其他位置0,得到100,就是十进制的4,保留最左侧的两个1,得到110,就是十进制的6,保留最最侧的三个1,得到111,就是十进制的7。4,6,7 正好和我们的划分是一样的。7并不是特殊的,可以选择其他数字也都有这个规律。那么给定一个i,我们就可以通过不断把最右侧1变成0,记录这个过程中所有的数,就可以得到需要用到的子段和的划分。具体操作方式我们可以通过位运算。
1 | // 得到n二进制最右侧的1表示的数 |
还有一些等价的写法
1 | n & ~(n-1) 或 n ^ (n & (n-1)) |
划分子段和的方式有了,刚才提到如果我们只计算前缀和,线段树维护的这些子段很多是多余的,再结合我们的划分方式,其实只要把子段和存储在子段的右端点即可。需要用到的子段右端点是不会重复的,因为任何一个右端点i 对应唯一子段就是 [k , i),其中k是i的二进制去掉最右的1。也就是下面这样,虚线上方圆圈内的值就是存储在右端点的子段和。
那么我们就可以仅用一个数组来存储上面的这棵树了。
1 | bitree[] = {0, 3, 11, 3, 17, 5, 11, 8, 43}; |
实际上这里 len(bitree) = len(arr) + 1,不过bitree[0] 代表空的子段和,总是0,想省下多出来的1个单位空间也是可以的,不过没有必要。
所以求任意的区间和,我们先转成前缀和相减,再划分为子段和去bitree[]里取值就可以了
例如:
1 | [2, 7)的和 |
用代码实现就是
1 | // 查询前缀和 |
剩下的问题是如何修改元素值呢?我们看上面的树状图,当要修改元素4的时候,会影响到[4,5)、[4,6),[0,8) 三个区间,即一直要沿着父节点修改到根,那么就是我们用bitree的下标表示就是5-6-8,如何得到这一串数呢,是否也和二进制存在某种关系呢,直接说答案,从要修改的元素编号+1开始,每次令i+=lowbit(i) 得到下个序号(正好是把查询里的减法变成加法),直到下标超出bitree的长度,事实上最后一次的下标总是bitree的根,也就是最后一个的元素。
1 | // 修改元素i的值 |
这里实现的是add方法,如果我们想设置元素i为新值,可以
1 | add(i, -query(i,i+1)); // 减去旧值 |
和线段树比一下是不是简单很多
1 | template <typename T, int N=(1<<17)> |