驼峰和下划线命名风格互转
之前用在SQLAlchemy的ORM模型的类名(驼峰风格)和数据库表名(下划线风格)的转换。
Python类名驼峰风格这个不用解释,数据库表名使用下划线风格主要是因为一些数据库系统如果使用了带大写字母的表名,那么在select、insert、update、delete语句中都要用特殊分割符包住表名才能使用,很麻烦。
1 | # 驼峰转下划线 |
之前用在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)> |
C++ STL 有提供priority_queue,但是它没有接口修改已经加入队列的元素的优先级。有时候我们希望修改队列中元素的优先级,可以通过priority_queue + 最后更新的时间戳,例如用一个map或unordered_map存储每个key最后更新的时间戳,然后将重复的key和priority的pair加入到priority_queue中,在取得队头元素时根据时间戳判断是否丢弃,这可以解决大多数问题,但如果遇到插入和更新特别多的情况,为了避免priority_queue迅速膨胀,我们不得不自己实现最大堆。
实际上通过之前讲过的Treap稍加改造就可以得到一个可以动态更新key优先级的Heap
1 | #include <iostream> |
线段树是一棵二叉树,每个节点维护一个区间和区间上的值,是一种用来维护 区间信息 的数据结构。
我们先从线段树能提供的操作上来理解。想象一个数组,每个下标上可以存一个值。所谓区间就是一段连续的数组下标。
我们可以进行的操作有:
1)区间赋值:设置一个区间内所有下标对应的值
2)单点赋值:设置单点的值
3)区间查询:查询一个区间内的最大值 / 最小值 / 值的和
4)单点查询:查询一个点的值
如果用数组来实现上述操作,所有区间操作的时间复杂度都是O(n),n是区间长度,单点操作的复杂度都是O(1),而线段树可以把上述全部操作的时间复杂度同时变成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)的元素,树状数组和线段树都是类似这个思想,其实树状数组的设计更符合这个思想,但它只能用于求和,树状数组能做的线段树都能做,我们之后有空再讲树状数组,对于线段树我们先在这个数组上生成一个子段和数组,让
1 | b = { arr[0]+arr[1], arr[2]+arr[3], arr[4]+arr[5], arr[6]+arr[7]} = {11, 6, 11, 15}; |
这样求区间[2, 7)的和的时候,可以取b[1] + b[2] + arr[6],只需要算三个数的和就行了。
更近一步,我们可以在数组b上再合并建立 c = { b[0]+ b[1], b[2]+b[3], …}, d, e, …,直至把整个数组合并成一个元素,如下图
这样之后,对于这个数组上的任意区间的和都可以只取O(logn)的元素的和来得到。
为了方便,我们通过二叉树来维护这些数据,我们把最上面的整个数组的和43作为二叉树的根,同时记住这个节点对应的左右端点即0和8,我们把这个节点的数据表示为(0, 8, 43),然后把它的左孩子就是如图的和为17的节点,数据为(0, 4, 17),同理右孩子(4, 8, 26),以此类推,这样就可以建立一颗完全二叉树(我这里有意将数组的大小选为2的幂,如果不是2的幂我们也可以扩充到2的幂来建树)。
对于修改单点值的操作,我们要从树根一直修改到叶子,正好修改了树的深度个节点,复杂度也是O(logn)。
因为是完全二叉树,我们可以通过数组来实现,用data[0]
作为根,data[k]
的孩子是data[2*k+1]
和data[2*k+2]
1 | #include <iostream> |
输出
1 | DEBUG:[2,4) sm[k]=6 |
可以看到,和预期的一样,只通过3个节点的值求和算出了区间[2,7)的和为25。
对于最大值、最小值的线段树,我们只需要把更新节点值的代码改掉。
对于区间更新,例如把[a,b)的值全部设置为x,或者让[a,b)的值全部增加x,我们不能对区间内的点逐一更新,否则复杂度会变成O(nlogn)了,区间更新时,我们要引入懒惰标志,我们还是从根开始拆分区间,如果当前节点的区间被完全覆盖,我们就更新当前节点值并不向下继续更新,并把待更新的值记在懒惰标志内,直到查询需要用到节点值时再逐级更新并把懒惰标志一层层推下去。
我们可以看到上述线段树实现方式使用的数据空间是数组最大下标的2倍,有时候区间的范围很大,但区间的更新和查询操作的数量并不多,我们可以先统计所有的区间端点,比如有n个端点,然后排序,在所有区间端点和 0,1,…,n-1 之间做一一映射,然后就可以把线段树的空间降到 2*n,和区间个数相关而和区间范围无关。
我们也可以不用数组表示的完全二叉树来实现线段树,而用一棵记录左右孩子指针的动态申请空间的普通二叉树,根据更新的区间来动态生成区间节点,这一般被称作动态开点。
下面是动态开点线段树的完整实现,包含了懒惰标志的使用,默认区间范围(-IINF, IINF),支持负下标,支持使用下标读写单点数据,支持使用下标修改区间数据,支持区间求和、最大值、最小值查询。
区间表示前闭后开[b, e)
支持单点修改 tree[i] = x; tree[i] += x;
支持区间修改 tree[{b,e}] = x; tree[{b,e}] += x;
支持取单点值 tree[i];
支持取区间sum/min/max tree.sum(b, e);
1 | typedef long long ll; |
类比C++ STL容器,记录Python容器初始化的一些技巧和陷阱
C++写法
1 | vector<set<int>> a(n); |
正确的Python写法
1 | a = [set() for i in range(n)] |
一个错误的Python写法
1 | a = [set()] * n |
如果是常数值,这样写没问题,但是set()是对象,这会导致a[0] a[1] … a[n-1] 存储的都是指向同一个set对象的引用
C++写法
1 | map<int, set<int>> b; |
有用的但不舒服Python写法
1 | b = {} |
有用且舒服的Python写法
1 | from collections import defaultdict |
如果我们想定义一个二重嵌套的defaultdict,类似map<int, map<int>>
,不能简单地使用 defaultdict(defaultdict(int))
,因为defaultdict需要传递的参数是一个类型T,可以通过T()构造出默认的对象,而defaultdict(int)
是对象而不是对象的类型,其实关键是通过T()能得到构造出默认对象就可以,所以我们可以定义一个方法返回defaultdict(int)
1 | def defaultdict_int_creator(): |
这样就可以定义一个二重嵌套的defaultdict,更简单地,我们可以把函数改成lambda表达式
1 | # 二重嵌套 defaultdict |
质数(又称素数):只能被1和自身整除的大于1的正整数。
质数是初等数论的重要研究对象,有关质数的常用算法有如下一些:
线性筛法:O(n)时间筛出n以内的所有质数
质数判定:判断给定数n是否是质数。大质数判定有Miller Rabin测试法
合数分解:找到给定合数n的一个非平凡因子。大数分解有Pollard Rho分解法
BTW:一些和整除、因子、质数相关的题目,先做质因数分解会后豁然开朗,int范围内的数至多有1300多个因子,先分解再枚举所有因子,甚至对所有因子双层循环的时间都是可以接受的。
下面给出一个包含线性筛、Miller Rabin测试、Pollard Rho分解、质因数分解的质数工具类
1 | #include <iostream> |