想要了解矩阵快速幂,就不得不提到矩阵的概念。矩阵就像一个二维数组,存储了一组数据,如:
用 表示矩阵第 行第 列的数据,如 。
矩阵的加法和实数加法类似,要求运算的两个矩阵大小相等,将对应位置相加即可。
与加法相同,对应位置相减即可。
矩阵乘法并非对应位置相乘。
若矩阵 有意义,那么 的列数要求与 的行数相等。
若 的列数与 的行数等于 :
即结果第 行第 列的值为 的第 行与 的第 列的对应项相乘后求和。
一个很直观的理解是,把矩阵 放在结果的左侧, 矩阵 放在结果的下方,将行与列延长,对应相乘后结果写在交点处。
注意矩阵乘法满足结合律,但是不一定满足交换律, 成立不一定代表 成立。
To be continued.
快速幂可以 求解 ,对矩阵同样适用。
代码:
1 | int ans=1; |
可以使用重载运算符的办法直接进行矩阵快速幂,方法和普通快速幂一样,但是要注意乘法顺序。ans
的初始值要特别注意。如果想让它充当1,则需要设置成单位矩阵 。
使用矩阵快速幂,可以用 的时间复杂度完成 次递推关系,如斐波那契数列。矩阵可以通过递推关系推算出来。
矩阵由转移关系得来。以斐波那契数列为例:
其余类似。
已知
求 和 。
1 |
|
We all know computers, and we basically know how to use it, but we have no idea about how does it work.
If I can present well, and you can understand well, We can watch a video!
Inside the computer, there are loads of parts, and the most important one is the CPU(Central Processing Unit).
The CPU contains basic modules, which contains logic gates, which contains transistors.
What is a transistor?
Transistor is the most basic part of a computer. It is just an electronic switch, but without it, your computer cannot compute anything.
The transistor has three pins, collector, base, and emitter, also, C, B, E for short. You must be familiar with this because we’ve met it before.
Transistor processes information and the information is called BITS, which can be set to either 0 or 1.
Combining transistors, you will get logic gates, which can get some input and create some output. For example, an AND gate produces an output of 1 if all its inputs are one, and output of 0 otherwise.
Combining logic gates, you will get compute modules, which can do some basic calculating, such as .
Once you can add small numbers, you can add multiple times to add big numbers. And once you can add, you can also multiply, once you can multiply, you can basically do anything.
A video downloaded from YouTube, which is the reference for this article.
Download Here
数组,结构体,二叉树
有时候我们会遇到一些大规模的区间查找和区间修改问题,比如让你维护一个 长度的数列,要求操作有区间求和、区间加(区间每个数加上一个值),让你在一秒内完成 次操作。
暴力是肯定不行的,数据范围太大,操作太多,会超时。
所以我们就有一种专门解决大范围区间修改查询的数据结构:线段树。
线段树本质上是把整个数列拆分了,用一个一个区间来表示。
比如有 个节点,根节点代表整个数列的区间 ,根节点的两个子节点代表根区间二分成两部分,也就是 和 。
而子节点也是一棵线段树。
一直往下延伸,叶子结点就代表着单一位置 。
下图是一个 的示例:
每一个节点都存储着它所代表的区间的信息,比如这个区间的和。
如查找 ,则执行以下步骤:
设若查找区间 ,首先从根节点开始:
-当前区间 不在 内部,经过判断 横跨左右子节点,分别在左右子节点查找查找: 和 。
-于是在 内查找 ,经过两次下放到右子树最终返回。
-接着在 内查找 , 横跨左右子树,继续查找: 和 。
-进入 ,在区间内部,直接返回。由 进入 后返回,查找完毕。
我们会发现,修改时如果像查找一样做,那么有一些细小的叶子修改就改不到,如刚才举例的 ,如果只修改 区间,那么单独查询 和 的时候就会出错。要是每一个修改都下放到叶子节点,那这个算法就和暴力一样了。所以我们需要一些巧妙地解决办法。
我们在树的节点上加一个标记:。
所谓 ,就是要懒,就是要在事情迫不得已要做的时候把它做了,所以我们每次那些细小的叶子修改就不做他,直接记录在 标记上,等到要查询的时候,再从标记上下放到子结点上,查询到哪里就下放到哪里。
修改后的结点,需要同时把所有祖先结点全部更新。
暴力算法的复杂度是 的,而线段树的复杂度是 的,因为每次树深入一层,区间长度都会减半。
1 |
|
代码揍这么简单:
1 | int fibo(int n){ |
要是再加个快速幂就更棒了(够快了,懒得写)
]]>我们举个例子吧:
有一个毒瘤水管工,他会造水管,有一天他造了一个水管网络,就像一个图。其中有一个点只有出边,就是起点,还有一个点只有入边,是终点。
点之间有一些管子,这些管子都有单位时间内的容量,现在毒瘤水管工想知道,他的管子在单位时间内在起点终点之间最多能流多少水。
増广就是在残余网络中寻找从源点到汇点的可行路径(増广路),并将该路径上的所有边的容量减去路径中的最小容量,形成新的残余网络,人话就是找一条能走的路,然后把路走掉。
例如:
如果当前有这样一个残余网络,那么就是一条増广路,最小容量是4,进行増广过后就形成了这样一张图:
如何寻找増广路?直接dfs即可。
有时候,程序増广的时候会出现爆炸性错误,例如还是那个图:
有两条増广路,万一程序选错了怎么办?
这时就要请出反向边了。
每次増广的时候,在残余网络上逆着増广路径建容量与増广路径最小容量相等的反向边,比如刚才那张图,就顺着建容量为4的边。相当于把原来的那条路抵消掉了,如果増广时走过了反向边,就相当于把原来的増广撤回去了。
这就给了程序一个反悔的机会。
Dinic的优化就是用bfs建立了由s开始的一个分层图,每次寻找増广路时必须让边上的层数严格递增,就可以确保每一步都离汇点近了一些这样就不会陷入毒瘤数据卡成的死循环,比如这样的著名毒瘤数据:
在这个数据中,如果用朴素算法,就会让中间容量为1的边上下抖动抽搐,等到他抽了999次的时候才把上面和下面的999减没。如果用Dinic,两次直接求出999+999。
存图方式(邻接表,邻接矩阵),并查集。
不会的快进入链接学习吧!
生成树,就是从一个图中选中条边,使得这些边构成一棵树,并包含图中的所有节点。
最小生成树,就是找到一种生成树,使得这个生成树的边权和最小。
这种方法有点类似Dijstra,就是每次从所有过的点遍历能达到的边,从其中选择一条最小的,加入生成树。
以此类推,最后就生成出来了这样一个图:
这就是prim算法 ,代码我不会写在后面。
这个算法本质上就是把所有边按照边权排序,然后直接爆炸按顺序判断要不要加进生成树里。
kruskal算法使用了一种极速闪电致命又自杀的东西:并查集。
他有多快呢?
好了我们在建一个图模拟一下吧
先给边排序。最小的是,把他拿出来,判断一下。怎么判断呢?首先访问一下并查集看一看,这个边连接的两个点在不在同一个集合内,不在的话就把这条边加入生成树,然后把两个点合并。否则忽略这一条变,继续。这一条边符合要求,加进并查集里,现在是一个集合,剩下都是独立的。
现在最小的有和,我们都判断一下,都可以。
然后就是和,依然都是可以的。
这样,一颗活灵活现的生成树就出现了。
好了!
kruskal的代码又短又易于理解,甚至可以直接用数组存边,所以他非常好写,推荐。
1 |
|
1 |
|
哈哈,简单到爆,没有。
并查集是一种快到爆炸的集合算法,可以进行两项基本操作:合并两个集合(并)、查询两个参数是否在一个集合内(查)。这也是它名字的由来。
他有多快呢?
有多可怕:
所以n是的时候复杂度还只有5。
有多大,我把他拷进来的时候整个电脑卡死了。我不得不强制重启,然后重新写一遍这段。他有19729位。想通了吧?
这么高端的算法,是怎么实现的呢?
其实
它的本质就是一个数组,和一个函数
存的实际上就是几棵树。
就是的父亲。
做的操作就是递归顺着找所在的树的根。
代码:
1 | int getf(int x){ |
那这个算法就很低端了
那还讲个鬼啊
所以
我们首先随机造出一些操作:
1 | 10 |
其中,merge代表合并,check代表查询。
如果按照刚才所的算法,那么在第一次查询之前,就会出来这样的森林:
到最后,就形成了这样一个繁杂的森林,要找到一个点的根,就需要走很长一段路。这就拉长了时间。
为了缩减时间,超级优化就出现了:路径压缩。
路径压缩其实也很简单:在查找根节点的同时,把自己也链接到根节点上,使得树的深度不超过2。
代码:
1 | int getf(int x){ |
发现没有,和之前的代码相比,只改了一个地方,就让时间大大压缩。
这时我们在模拟一下。
第一次合并:
第二次合并时,首先寻找2,5两个节点的根节点,2的根就是2,5的根是1,于是直接把2链接到1上。
第三次,第四次合并把3链接到了1上,然后把4顺着3也链接到了1上,第五次连接了6和7。
第七次第八次链接成了一长串,然后经过路径压缩都链接到6上了。
最后一次,把9和1链接起来了,这时深度又超过了2,一下还压缩不下去,不过没关系,查询的时候就会把它压缩的。
比如查询7和4的时候就会分别寻找7和4的根节点,一路递归找上去的时候就直接把路径压缩好了,除了8还链接在6上,其他全部链接到1上了。
多么有趣啊!
自己想去吧,核心代码和思路都给出来了。
有一个巨大的坑,就是要预设成,不然会爆炸。
加油!克服恐惧的最好办法就是面对恐忄快去写吧!
必备:存图方式(邻接表,邻接矩阵),dfs序。
维护:线段树、树状数组、BST。
不会的快进入链接学习吧!
树链剖分,简单来说就是把树分割成链,然后维护每一条链。一般的维护算法有线段树,树状数组和BST。复杂度为。
对于每一个节点,它的子节点中子树的节点数最大的为重儿子,连接到重儿子的边称为重边。例如:
加粗节点为重儿子。节点的重量都一样,所以你想上哪个成为重儿子那个就是重儿子。除重儿子和重边外的节点和边均为轻儿子或轻边。根不是重儿子也不是轻儿子 ,有脑子的人都会想一想,它根本就不是什么儿子! 。
以轻儿子或者根为起点的,由重边连接的一条连续的链称为重链。特别地,若一个叶子结点是轻儿子,那么便有一条以该叶子结点为起点的长度为1的重链。上图中,是一条重链,一个节点也是一条重链。
树链剖分的预处理本质上就是2个dfs。
一共完成四项任务。
代码:
1 | void dfs_1(int u,int f){ |
完成三项任务。
代码:
1 | void dfs_2(int u,int tp){ |
这里以洛谷的P3384为例
要求维护四种操作:
要处理两点间的路径时:
首先找到两个节点中较深的那个点,然后将对该店所在链进行处理,并将该点移动至所在链顶端节点的父节点。因为是按照轻重边为优先级做的dfs,所以一条链上的编号一定是连续的。
循环执行直到两个点在一条链上,这时再处理两个点之间的区间即可。
要处理一个点的子树时:
因为是dfs序,所以子树的dfs序一定是连续的区间,直接处理该区间即可。
因为都是处理区间,所以用线段树维护。
因为题目中还有一个毒瘤%p,所以代码显得很繁杂。
1 |
|
完结撒花~~
]]>作文
作文大全等
阅读题
现代文阅读技能训练
篇
文学常识
待购买
文言文
世说新语
论语
待购买
备注
待购买
待购买
由 月到 月,喜马拉雅《人类群星闪耀时》录制共 篇,完成率 。朗读至第三章:亨德尔的复活。
日记由 坚持记录至 ,其中 日至 日停顿。
文言文学习未坚持,只完成了两三次。
对《人类群星闪耀时》的阅读进行到了第五章,前四章内容分别为:巴尔沃亚发现大西洋,君士坦丁堡被攻陷,亨德尔被中风击倒后重回音乐生涯,以及拿破仑的失败。
]]>