最近是一脸懵逼的集训时间
好吧,其实学的也蛮多的:搜索(bfs,dfs),
图论(包括了边表,邻接表,邻接矩阵,传递闭包,三角形迭代,floyed,Bellman-Ford,spfa,Dijkstra,Prim,Kurskal),
基本数论(gcd,lcm,费马小定理,线性筛法,欧拉函数,同余方程),
树状数组,
差分,差分约束,
线段树,
KMP算法,LCA,
dp(对拍,背包问题),AC自动机,Manachar算法,左偏树等等。(有背景色的内容在本文有简述)
学的多,忘得也多,所以不得不写篇blog纪念一下。
(好吧其实是咱老师要咱写)
/*--------------------------------------第一章:立志要种出一片森林的小孩-------------------------------------------*/
学完线段树之后,看到一大长串的代码就头大,知道大概五六天前,为了学LCA不得不重新拾起这一被本人抛弃的数据结构(To be honnest 本人在哪之前连模板题都懒得敲)。没想到一敲便上了瘾。
从云里雾里到有那么一丁点的感觉,还是走得有那么一点艰辛的,也花了很大的力气搞懂了几个傻不啦叽的问题:
1.线段树是什么:说简单点,他就是一颗二叉树,其root节点的左儿子的下标可看成root*2,右儿子则为root*2+1,对应的,root节点的父节点就可以表示为root/2(向下整取)。
2.线段树存的到底是什么:直到在lougu上敲模板题的时候才开始思考这个问题:线段树存的是一个区间的数值之和,尽管那个区间内可能只有一个元素。
3.线段树可以干什么:在我的印象中,线段树就是一种对区间问题有奇效的数据结构。
4.lazy标记到底是干什么的:lazy标记就像当于一个区间加减的印记,可以帮助我们减少递归的次数并且在必须的时候顺便把数值传递到下一个区间。
本人对线段树的理解目前就这么浅尝辄止,有待进一步的开发与升级。
线段树代码镇帖:
种树:
void build(int root,int l,int r)
{
int mid=l+r>>1;
if (l==r) {
tree[root].zhi=read();return;}
build(root<<1,l,mid);
build((root<<1)+1,mid+1,r);
tree[root].zhi=tree[root<<1].zhi+tree[root<<1|1].zhi;
}
区间修改:
void growl(int root,int l,int r)
{
if(k2r)
return;
if(k1<=l&&k2>=r)
{tree[root].add++;tree[root].lazy++;return;}
int mid=(l+r)/2;
int delta=tree[root].lazy;
tree[root*2].lazy+=delta;
tree[root*2].add+=delta;
tree[root*2+1].add+=delta;
tree[root*2+1].lazy+=delta;
tree[root].lazy=0;
growl(root*2,l,mid);
growl(root*2+1,mid+1,r);
tree[root].add=max(tree[root*2].add,tree[root*2+1].add);
}
区间查询:
int find(int root,int l,int r)
{
if(k2r)
return -1;
if(k1<=l&&k2>=r)
return tree[root].add;
int mid=(l+r)/2;
int delta=tree[root].lazy;
tree[root*2].lazy+=delta;
tree[root*2].add+=delta;
tree[root*2+1].add+=delta;
tree[root*2+1].lazy+=delta;
tree[root].lazy=0;
return max(find(root*2,l,mid),find(root*2+1,mid+1,r));
}
为什么这么写会补上的,本人现在要去备考了
首先,线段树的父节点与子节点的关系在上文已经阐述,由于它的唯一确定性,所以我们可以用一维数组来存储他。
所以三个核心代码的结构都有异曲同工之妙,其中if(k2r)表示,当要操作的区间的右(左)端点已经超出了函数的右(左)区间的时候,自然要将函数结束即return,if(k1<=l&&k2>=r)则表示当我们要操作的区间已经在函数的左右区间之间了,那么就可以通过lazy标记与区间修改大大的减少时间,体现出线段树的优秀,
int mid=(l+r)/2;
int delta=tree[root].lazy;
tree[root*2].lazy+=delta;
tree[root*2].add+=delta;
tree[root*2+1].add+=delta;
tree[root*2+1].lazy+=delta;
tree[root].lazy=0;
咳咳,敲黑板,这段代码可谓是核心中的核心,它帮助我们在做一些其它操作的时候传递了lazy标记,是一节省时间的利器,至于为什么这么写,看到代码之后应该可以秒推,至于最后一句,则是根据不同的需求改的,并没有什么万能的东西,所以需要随机应变的应对。
/*-------------------------------------------第二章:数论是什么,可以吃吗??---------------------------------------*/
作为一个没有数学功底的可怜的小朋友,突然接触到了数论
费马小定理:(@百度百科)
https://baike.baidu.com/item/%E8%B4%B9%E9%A9%AC%E5%B0%8F%E5%AE%9A%E7%90%86/4776158?fr=aladdin
欧拉函数:(@百度百科)
https://baike.baidu.com/item/%E6%AC%A7%E6%8B%89%E5%87%BD%E6%95%B0
同余方程:(@百度百科)
https://baike.baidu.com/item/%E5%90%8C%E4%BD%99%E6%96%B9%E7%A8%8B
但其实说白了,就是围绕着同余方程;
那么什么是同余方程呢:他的定义式是这样的(p|(a-b)),那么a=b(mod p)(请将‘=’自动虚伪成同余符号)。
这个同余方程,本人一开始的理解是错的,所以痛苦了好一阵子。
数论的学习也告诉了我,理论的学习与应用还真不是一回事儿(太难应用了!!!)。
gcd代码(安慰一下受伤的数论)
int gcd(int a,int b)
{
return (b==0)?a:gcd(b,a%b);
}
/*---------------------------------------------第三章:累死人的图论-----------------------------------------------------*/
图论相对于什么数论就简单许多了,所以本人对他的理解也自然而然的比数论好那么一点。
在我的理解里,图论就是借助图来解决问题,就像数学里的数形结合。
图又分有向图与无向图两种,存储方式也有边表,邻接表与邻接矩阵三种。
从存储方式讲起:
说实话,我更喜欢邻接矩阵,应为他比较具象,构造简单,好理解与应用,但世上总没有十全十美的东西,邻接矩阵也有他的优点与缺点,同样,邻接表也是。
邻接矩阵最大的优点已经阐述过了,他是一种以二维数组为载体,下标为点,数值为权的存储方式,但他有一个致命的缺点导致他在于邻接表的竞争中彻底落了下风:需要二维数组和三重循环暴搜的他,时空复杂度相比邻接表简直是一个天上一个地下。
下面是邻接表(本人语文能力不行,拍图),相对于邻接矩阵,他的优点是以链表与结构体的形式作为载体,所占空间相对小,时间复杂度相对低,且遍历与更改都非常容易,而他的缺点就显得微不足道了:仅仅是比较抽象,构造需要一个简单的函数而已,其中e数组表示变,linkk数组表示便所对应的下标
构造函数
insert(int begin,int value,int end){ e[++len].value=value; e[len].next=linkk[begin]; linkk[begin]=len; e[len].doult=end;}
遍历循环
for(int i=linkk[k];i;i=e[i].next)
最后是边表,他也是以结构体为载体,但他只存储起点,终点与权值,构造与遍历都究极简单,但他有一个不得不说的秘密:我不知道他除了无向图判负环之外还有什么卵用。
关于有向图与无向图的区别一句话搞定:无向图就是双向的有向图
图论中最常见的问题就是求最小生成树是本文开头那一串怎么也记不住名字的算法(Prim,Kurskal);
但最简单的当属Kurskal,它运用了并查集的思想,以sort函数为工具,以一个类似贪心的方法,求最最小生成树,十分巧妙。
找最短路(floyed,Bellman-Ford,spfa,Dijkstra)也是一类常见的问题
这四种算法中,首推spfa究极简单(相对其他三个来说,一个会爆时间,一个代码超长,一个是他的退化版)。
他的主要思想为少跑重复的路,运用队列维护算法,每次只用被松弛的边去松弛其他边,避免了无用的重复计算,大大的减少了时间。
/*--------------------------------第四章:暴力出奇迹,对拍保平安-----------------------------------------------------*/
dp是什么,动态规划呗,这个一直被我视为与递推没有什么区别的东西总能给我惊喜。
poi
dp主要有三个部分组成:阶段,状态,转移方程。它还有一个非常重要的条件,那就是无后效性(一个状态不会对下一个状态产生影响)
1.阶段:说白了就是把一个问题按一定条件分成N段,每一段便是一个阶段。
2.状态:就是一些不可控的客观因素。
3.转移方程:dp的核心就是这个,它可以帮助我们有一个已知状态得到我们想要的状态。
dp问题中最经典的本人认为导弹拦截问题(noip1999)
我就是打死也要拦截这一枚导弹,你想咋的?通过这一个信念,推出答案
dp怎么能离开背包问题 (@某不知名大佬)(https://blog.csdn.net/pi9nc/article/details/8142876(据说作者是咱老师的学生))
01背包,多重背包,二维背包,依赖背包…………,各种背包铺天盖地;
这里以最简单的01背包为展开
先拍码:
for(int i=1;i<=n;i++)
for(int j=V;j>=1;j--)
if(i-d[j]>0)
f[j]=max(f[j],f[i-d[i]]+v[i]);
其中V表示背包的容量,v[i]表示i物品的价值,d[i]表示i物品的代价,f[j]表示剩余j容量时价钱的最大值;
明确了每一个变量的意义之后,状态转移方程便比较容易看懂了,那么为什么j要倒着来呢?原来,在使用一维数组的时候,要保证再推这一个状态的时候前面的状态没有被改变,所以只好倒着来了。
至于其他的背包,我认为都可以有01背包直接或间接的推导出来,例如加一层状态或改变循环顺序等等,所以在此不做描述。
/*---------------------------------第五章:看毛片算法??---------------------------------------------------*/
(@matrix67) http://www.singlex.net/195.html
(这个究极详细,推荐一读)
好吧其实就是KMP算法.
这个算法是一个大佬同学讲的,开始时听的一脸懵逼,敲完模板题之后恍然大悟(实践出真知系列)。
这个算法提出了一个新的定义:失配函数,这是一个究极巧妙地东西,堪比罗巴切夫斯基定理。
他的大意是这样的:给定字符串例如 ababababababab
我们需要维护一个存储着失配函数的数组,而失配函数就是失配的哪一个字符相同的最长前缀(语文学的不好,望原谅)。
然后在查找子串的时候就有奇效了:每次我们的模板串与子串失配,就只要跳回到他的失配函数继续匹配就好了,应为反正一样,就不要在比一次了,这样就可以省下大量的时间,用来干其他的事
(然而作者写的第一个KMP算法超时了
poi
。
/*----------------------------------------第六章:虚伪的数据结构------------------------------------------------------*/
看什么看,说的就是你,树状数组!!
树状数组,听名字好高大上,实际上也非常方便:
就是这么一张经典的图片,把树状数组介绍的十分清晰
树状数组主要是利用了位运算的思想,主要的部分有这三个函数:
int lowbit(int k)
{return k&-k}//取出最右边的0(2进制);
void add(int k,int delta)
{
while(k<=n)
{
c[k]+=delta;
k+=lowbit(k);
}
}//使某一个值加/减;
int getsum(int k)
{
int t=0;
while(k>0)
{
t+=c[k];
k-=lowbit(k);
}
return t;
}//求出1~k的区间和;
这三个函数看似非常简单,实则非常简单,相信不用本人解释,各位大牛就已经自己对着图解出来了;
如果还没有理解的话,这里有张lowbit表:
简而言之:这是一种优秀的数据结构!!
/*--------------------------------------------第七章:差分真好玩------------------------------------------------------*/
差分是什么:我认为,这是一种以优化区间修改为目的的算法,当我们需要进行区间修改的时候只需要在差分数组上进行两个点的修改就行了,是不是灰常的方便。
这个比较尴尬,其他都是会讲不会用,这个是会用不会讲
但还是把几个概念列出以下
差分数组:将原数组的元素与它前一个元素相减得到的数组。
差分数组的前缀和:可以理解为前缀和中的最后一个元素的下标,在原数组中对应下标的变化值。
介于作者已经没有什么词语可以形容她了,就草草的结束吧 其实是要吃饭了poi)
/*-----------------------------------------------第八章:并没有听得太懂的东西---------------------------------------*/
下面列出黑名单:LCA算法,
Manacha
r算法,差分约束,AC自动机,左偏树…………(作者在此表示深刻的反省)
说真的,这几种算法都只听懂了理论,连模板题都不会敲,怪我,能力太低,导致了对代码无能为力。
先贴几个链接
LCA Manachar 差分约束 AC自动机 左偏树
虽然没怎么听懂,但还是写下一些我的看法
(请掠过LCA与Manachar)
差分约束:其实这个不是特别的难,只不过在讲课的时候咱老师并没有讲它的定义,所以本人对他的本质并不是很清晰,也就无从谈起对他的理解了
AC自动机:trie树上跑一个 看毛片KMP。
左偏树:以合并两棵树为核心操作(添加元素:看作一个只有一个节点的树与母树结合。删除元素:添加元素的逆向)的一种数据结构,但问题是:我不会建树!我不会建树!!我不会建树!!!我不会建树!!!!我不会建树!!!!!所以就…………
/*----------------------------------------------番外:并查集-----------------------------------------------------------*/
并查集n个星期前讲的知识,到现在才会了一点。
与树状数组相似,并查集也有三段核心代码(都比较好理解,所以不予解释)
int getfather(int x)
{
return (father[x]==x)? x: father[x]=getfather(father[x]);
}//找到父节点
void relative(int x,int y)
{
x=getfather(x);
y=getfather(y);
if(x==y) return;
father[y]=x;
}//合并
bool check(int x,int y)
{return getfather(x)==getfather(y)}//判断是否在一个集合
看上去更简单对不对, 其实它真的很简单
它的核心思想我认为是我管你爸爸是谁,我只要知道你和他有关系就行了的赖皮思想;
而它的应用却不止这么简单相见luoguP1196银河狗雄传;
这里是正解(耗费了我n天的大好时光)
#include
using namespace std;
#define C getchar()
#define maxn 500010
#define rep(i,j,p) for(int i=j;i<=p;i++)
#define ri(b) b=read()
inline int read()
{
int x=0;
char ch;
bool flag=true;
for(;ch>'9'||ch<'0';ch=C)
if(ch=='-')
flag=false;
for(;ch>='0'&&ch<='9';ch=C)
x=(x<<3)+(x<<1)+(ch^48);
return flag?x:-x;
}
int father[maxn]={};
int countl[maxn]={};
int before[maxn]={};
int t;
char a;
int si,sj;
int fa,fb;
/*--------------------------------------------定义区-----------------------------------------------------------------*/
void cb()
{
rep(i,1,30000)
{father[i]=i;
before[i]=1;}
}
int getfather(int x)
{
if (father[x]==x)return x;
int y=getfather(father[x]);
countl[x]+=countl[father[x]];
return father[x]=y;
}
void Un(int x,int y)
{
countl[x]+=before[y];
father[x]=y;
before[y]+=before[x];
before[x]=0;
}
/*--------------------------------------------预备区------------------------------------------------------------------*/
void work()
{
ri(t);
rep(i,1,t)
{
a=C;
ri(si),ri(sj);
fa=getfather(si);
fb=getfather(sj);
if(a=='M')
{
Un(fa,fb);
}
else
{
if(fa!=fb)
printf("%d\n",-1);
else
printf("%d\n",abs(countl[si]-countl[sj])-1);
}
}
}
/*--------------------------------------------工作区-------------------------------------------------------------------*/
int main()
{
cb();
work();
return 0;
}
看上去和并查集没有半毛钱关系是不是?但他就是并查集;
也是并查集一个比较巧妙 其实一点也不巧妙的运用。
但说实话,并查集也非常的优秀( •̀ ω •́ )y,也就导致了有关与它的题目非常灵活,令人称奇
/*---------------------------------------------------完结线-----------------------------------------------------------*/
温馨提示:本文有几个小彩蛋poi。
||本文的错误欢迎在评论区指出还有,有哪位dalao知道怎么解决文字之间的空行||