算法学习心得

最近学(复习?)了很多省选算法,先把心得写在这里,将来如果忘了拿来复习吧

一、树链剖分

树链剖分是处理一类在树的一条链上修改、查询最大/最小值/权值和的算法。效率nlog^2n,大概数据在3w到5w左右比较正常吧

树链剖分不支持导致树的形态发生改变的操作,比如插入/删除一条边

对于某一修改x到y路径上的东西操作,正常的模拟做法是先提出x和y的lca,然后一步一步往上走处理x到lca和y到lca的路径

树链剖分的思想只是在这个基础上用线段树优化一步一步往上走的过程

1、无论是暴力还是正解,都要先把无根树转成有根树

所以第一遍dfs1:无根树转成有根树,先求出这些东西:

fa[i][j]:i节点往上走2^j步能走到的点,这一步主要是求lca用的

depth[i]:i节点的深度,还是lca用

son[i]:第i个节点下面的儿子数

2、接下来就要考虑怎样用线段树模拟一步一步往上走的过程

线段树里存的是点权,如果题目给的是边权的话就用这条边指向的儿子节点的点权表示

为什么线段树会快呢?因为如果往上走的时候如果上面的一条链是线段树的一段连续的区间,那么可以直接logn提出我们需要的东西

但是不一定每次往上走的时候走的路径都是线段树的一段连续的区间,所以一开始点权加入线段树的顺序就很重要了

那么怎样才能尽可能保证走的是连续的区间呢

引入轻边与重边:对于一个节点x,它下面可能连出很多边,每条边指向它的一个儿子

假设x的所有儿子中son[y]最大,那么连接(x,y)的边是重边,y是x的重儿子。其他x下面连出的边都是轻边,x的其他儿子也就是轻儿子

找到一个点之后,就直接先把重儿子加入线段树,之后再依次处理其他轻儿子

分析一下这样做的作用:找出儿子中son[y]最大的,相当于子树大小最大的,那么操作经过这些点的概率最大

因此在线段树中使x和y放在一起,那么x和y被一起访问的概率比其他x和x的儿子z被一起访问的概率大

有一个比较不太严格的结论:从x到lca的路径大致要在线段树中找logn次,每次logn,所以平均每次操作log^2n

我的写法可能有点锉

首先还是要一个dfs2:算出每个点加入线段树的顺序

place[i]:i在线段树中的位置

pplace[i]:place[i]的反操作,保存线段树中位置是i的点

belong[i]:线段树中找到i往上走能走到的最上面的一个点。换句话说,在线段树中belong[i]和i之间的边是连续的一段,但是belong[i]的父亲节点和这一段是不连续的

然后一个buildtree建树,叶节点的权就是v[pplace[k]]

对于询问(from,to),在往上走的时候就可以这样写:

while (belong[from]!=belong[to])

{

   l=place[belong[from]];:线段数中能走到的最上面的点,belong[from]和from在一个连续的区间

   r=place[from];:注意加入线段树的顺序是从上往下的,因此belong[from]在from之前加入,lr千万别搞混

   search_in_tree(1,l,r);:各种线段树的查询修改操作不用讲了吧

   from=fa[belong[from]][0];:from到belong[from]都做完了,因此from直接跳到belong[from]的父亲节点

}

  l=place[to];

  r=place[from];

  search_in_tree(1,l,r);:别忘了最后from和to在线段树中连续了之后再搞一次

以[ZJOI2008]树的统计为例,以下为核心代码:

  1 inline void dfs1(int x,int dep)
  2 {
  3     if (mrk[x])return;
  4     mrk[x]=1;depth[x]=dep;son[x]=1;
  5     for(int i=1;i<=15;i++)
  6       fa[x][i]=fa[fa[x][i-1]][i-1];
  7     for (int i=head[x];i;i=e[i].next)
  8     if (!mrk[e[i].to])
  9     {
 10         fa[e[i].to][0]=x;
 11         dfs1(e[i].to,dep+1);
 12         son[x]+=son[e[i].to];
 13     }
 14 }
 15 inline void dfs2(int x,int chain)
 16 {
 17     int k=0,mx=0;
 18     place[x]=++tt;belong[x]=chain;
 19     pplace[tt]=x;
 20     for (int i=head[x];i;i=e[i].next)
 21       if (fa[x][0]!=e[i].to)
 22       {
 23         if (son[e[i].to]>mx)
 24         {
 25             mx=son[e[i].to];
 26             k=e[i].to;
 27         }
 28       }
 29     if(!k)return;
 30     dfs2(k,chain);
 31     for(int i=head[x];i;i=e[i].next)
 32       if (e[i].to!=k&&e[i].to!=fa[x][0])
 33         dfs2(e[i].to,e[i].to);
 34 }
 35 inline void update(int k)
 36 {
 37     tree[k].mx=max(tree[k<<1].mx,tree[k<<1|1].mx);
 38     tree[k].tot=tree[k<<1].tot+tree[k<<1|1].tot;
 39 }
 40 inline void buildtree(int now,int l,int r)
 41 {
 42     tree[now].l=l;tree[now].r=r;
 43     if (l==r)
 44     {
 45         tree[now].mx=v[pplace[l]];
 46         tree[now].tot=v[pplace[l]];
 47         return;
 48     }
 49     int mid=(l+r)>>1;
 50     buildtree(now<<1,l,mid);
 51     buildtree(now<<1|1,mid+1,r);
 52     update(now);
 53 }
 54 inline int LCA(int a,int b)
 55 {
 56     if (depth[a]<depth[b])swap(a,b);
 57     int res=depth[a]-depth[b];
 58     for (int i=0;i<=15;i++)
 59       if (res & (1<<i))a=fa[a][i];
 60     for (int i=15;i>=0;i--)
 61       if (fa[a][i]!=fa[b][i])
 62       {
 63         a=fa[a][i];
 64         b=fa[b][i];
 65       }
 66     if(a==b)return a;
 67     return fa[a][0];
 68 }
 69 inline int ask_in_tree(int now,int x,int y)
 70 {
 71     int l=tree[now].l,r=tree[now].r;
 72     if (l==x&&y==r)return tree[now].mx;
 73     int mid=(l+r)>>1;
 74     if (y<=mid)return ask_in_tree(now<<1,x,y);
 75     else if (x>mid)return ask_in_tree(now<<1|1,x,y);
 76     return max(ask_in_tree(now<<1,x,mid),ask_in_tree(now<<1|1,mid+1,y));
 77 }
 78 inline int sum_in_tree(int now,int x,int y)
 79 {
 80     int l=tree[now].l,r=tree[now].r;
 81     if (l==x&&y==r)return tree[now].tot;
 82     int mid=(l+r)>>1;
 83     if (y<=mid)return sum_in_tree(now<<1,x,y);
 84     else if (x>mid)return sum_in_tree(now<<1|1,x,y);
 85     return sum_in_tree(now<<1,x,mid)+sum_in_tree(now<<1|1,mid+1,y);
 86 }
 87 inline int ask(int from,int to)
 88 {
 89     int l,r,mx=-inf;
 90     while (belong[from]!=belong[to])
 91     {
 92         l=place[belong[from]];
 93         r=place[from];
 94         mx=max(mx,ask_in_tree(1,l,r));
 95         from=fa[belong[from]][0];
 96          
 97     }
 98     l=place[to];
 99     r=place[from];
100     mx=max(mx,ask_in_tree(1,l,r));
101      
102     return mx;
103 }
104 inline int sum(int from,int to)
105 {
106     int l,r;
107     int s=0;
108     while (belong[from]!=belong[to])
109     {
110         l=place[belong[from]];
111         r=place[from];
112         s+=sum_in_tree(1,l,r);
113         from=fa[belong[from]][0];
114     }
115     l=place[to];
116     r=place[from];
117     s+=sum_in_tree(1,l,r);
118     return s;
119 }
120 inline void change(int now,int x,int dat)
121 {
122     int l=tree[now].l,r=tree[now].r;
123     if (l==r)
124     {
125         tree[now].mx=tree[now].tot=dat;
126         return;
127     }
128     int mid=(l+r)>>1;
129     if (x<=mid)change(now<<1,x,dat);
130     else change(now<<1|1,x,dat);
131     update(now);
132 }
count

二、莫队算法

首先,orz hzwer

莫队算法是离线处理不带修改的区间询问的算法,效率nsqrt(n)

其实说到底也就是先对询问按一种顺序排个序,然后直接模拟处理的算法。但是因为是按照一定顺序做询问,所以有办法证明算法复杂度是nlogn

首先,把n个元素分为sqrt(n)块,每块有sqrt(n)个元素。然后我们以询问的左端点所在的块的编号为第一关键字,以右端点为第二关键字排序。

一开始我们用两个指针l=1,r=0来表示当前我们已经记录的区间。然后按照排完的顺序依次处理询问。

bzoj3781为例:

题意是给定一个序列a,每次对于询问[l,r],输出Σc[i]^2,其中c[i]表示在区间[l,r]中i出现的次数

那么先考虑模拟的做法:

当前我们做到了[x,y],现在已知在区间[x,y]中的c[i],以及ans=Σc[i]^2。那么如何可以从c[i]以及x,y,ans继续转移呢?

我们现在要求[x,y+1]的ans了。那么[x,y+1]的答案比[x,y]多的就是a[y+1]对整个区间的贡献。只要加上a[y+1]带来的贡献就可以了。

只需要y++   -->   ans-=c[y]^2   -->   c[y]++   -->  ans+=c[y]^2

就可以做到从区间[x,y]转移到[x,y+1]了。

那么如果我们现在要求[x,y-1]的ans,只需在原来的ans中扣掉a[y]带来的贡献。

只需要ans-=c[y]^2   -->   c[y--]   -->   ans+=c[y]^2   -->   y--

就可以从[x,y]转移到[x,y-1]

那么[x+1,y]和[x-1,y]就是同理了

这样我们就可以从[x,y]转移到任意的[x',y']了

但是为什么按着这样的顺序模拟就不会T呢?

在此引用黄巨大的话:

考虑第i个询问和第i+1个询问之间的关系:

一、i与i+1在同一块内,r单调递增,所以r是O(n)的。由于有n^0.5块,所以这一部分时间复杂度是n^1.5。
二、i与i+1跨越一块,r最多变化n,由于有n^0.5块,所以这一部分时间复杂度是n^1.5
三、i与i+1在同一块内时变化不超过n^0.5,跨越一块也不会超过2*n^0.5,不妨看作是n^0.5。由于有n个数,所以时间复杂度是n^1.5
于是就变成了O(n^1.5)了

以下给出我写的bzoj3781的核心代码

 1 int n,m,l=1,r=0,k,sqrtn,ans;
 2 int rep[50010];
 3 struct query{
 4     int l,r;
 5     int from,rnk,ans;
 6 }q[50010];
 7 int s[50010],a[50010];
 8 bool operator <(const query &a,const query &b)
 9 {return a.from<b.from||a.from==b.from&&a.r<b.r;}
10 inline void solve(int x)
11 {
12     while (l<q[x].l){ans-=rep[a[l]]*rep[a[l]];rep[a[l]]--;ans+=rep[a[l]]*rep[a[l]];l++;}
13     while (l>q[x].l){l--;ans-=rep[a[l]]*rep[a[l]];rep[a[l]]++;ans+=rep[a[l]]*rep[a[l]];}
14     while (r<q[x].r){r++;ans-=rep[a[r]]*rep[a[r]];rep[a[r]]++;ans+=rep[a[r]]*rep[a[r]];}
15     while (r>q[x].r){ans-=rep[a[r]]*rep[a[r]];rep[a[r]]--;ans+=rep[a[r]]*rep[a[r]];r--;}
16     q[x].ans=ans;
17 }
18 int main()
19 {
20     n=read();m=read();k=read();
21     sqrtn=sqrt(n);
22     for (int i=1;i<=n;i++)a[i]=read();
23     for (int i=1;i<=m;i++)
24     {
25         q[i].l=read();q[i].r=read();
26         q[i].from=(q[i].l-1)/sqrtn+1;
27         q[i].rnk=i;
28     }
29     sort(q+1,q+m+1);
30     for (int i=1;i<=m;i++)
31       solve(i);
32     for (int i=1;i<=m;i++)
33     s[q[i].rnk]=q[i].ans;
34     for (int i=1;i<=m;i++)
35       printf("%d
",s[i]);
36 }
bzoj3781

 三、2-sat算法

2-sat是处理关于二元组中两个元素之间有特殊关系(比如不能同时取,不能同时不取,等等)的一种算法

首先推荐一篇讲2-sat问题挺详细的文章 

这篇真的很好

黄巨大看了也说好

没有什么一串又一串的公式的,全都是讲解

不过copy别人的劳动成果就不太好了吧……

只发超链接了:http://blog.csdn.net/jarjingx/article/details/8521690

 总之……按着我自己的理解再写一遍

首先,假设存在这样的关系:如果要取A必须取B,那么A到B连边。

但是注意到“取”的状态是不能直接转移的,反而“不取”的状态可以转移。

换句话说,如果当前这个点取了,那么接下来所有还能取的点不能唯一确定状态,反而那些不能取的就能确定了。

所以把图反建,那么一条B到A的边就表示取A必须取B,即不取B就不能取A。

先tarjan缩点,然后拓扑排序。按照上面所说,B到A的边就表示不取B就不能取A,所以就可以转移“不取”的状态了

为什么要拓扑排序?这个问题我还是靠直观感觉:边x->y可以看成一种相对于y的限制,那么显然入度越小限制越少,入度为0就没有限制了。当然没限制的先做啊

 四、后缀数组

明天省选一试……在退役之前把我会的都写在这里吧……也许以后很少再接触OI了……为什么莫名的伤感

后缀数组(SA),是一类处理字符串问题的算法。nlogn的时间效率,常数也还不算太大,它的应用还是很广的。

顾名思义,它就是比较一个字符串的所有后缀的字典序大小的算法。

有两种算法:一种是基于倍增思想的做法,另一种就是传说中的DC3。这里介绍倍增法

假设我们有字符串S,要把S的所有后缀根据字典序排序,最简单的比较方法当然是把n个后缀直接快排。但是有一个很严重的问题是,我们需要做nlogn次比较,但是和数字的比较不同,字符串的比较不是O(1)的,而是O(n)的。这样总复杂度是n^2logn,使我们无法接受的。

那么怎样才能更快算出字典序呢

很显然SA这样基于比较大小的算法时间复杂度一般不会少于nlogn。分析一下刚才的快排,就能发现瓶颈在于比较后缀的大小的时候太慢。

而倍增法就是很好的利用了字符串后缀的一些性质,能在O(1)的时间内完成比较。

我们用suffix[i]表示从i开始的S的这个后缀,即S[i]~S[n]

这时候开始我们就要用到倍增思想了

我们每次将后缀的前2^k个字符先提出来排序,先不管怎么做,只要2^k>n,那么我们就能得到要求的排名了

定义sa数组,sa[k][i]=x,表示当前我们排序后缀的前2^k的字符,当前排名第i小的后缀是suufix[x]。相对应的,定义逆数组rnk,rnk[k][i]=x,表示当前第suffix[i]的排名是x。那么只要知道sa就能算出rnk,只要知道rnk就能算出sa

显然sa[1]和rnk[1]是很好预处理的。需要桶排一下

ps:这里桶排也是有特殊意义的,桶排保存i字母出现的次数的前缀和v[i],然后每次出现一个s[i],就让rnk[v[s[i]]--]=i。这样正确性显然,而且保证了越前面的后缀的rnk越大

考虑当前我们做完了后缀的前2^(k-1)个字符的排序,现在要根据sa[k-1][...]和rnk[k-1][...]算出sa[k]和rnk[k]。

比如一个串abacbba,当前我们排完了后缀前2^1也就是前2个字母的顺序,现在算前2^2也就是前4个的顺序。

排序应当是这样的

    a ab ac ba ba bb cb

sa 7 1  3  6   2   5   4

rnk2 4 3  6   5   4   1

显然如果存在rnk[k][i]>rnk[k][j],那么随着k的增大,rnk[k][i]也会一直大于rnk[k][j]。一旦符号定了,就不可能再改了。这个很好yy

现在考虑比较rnk相同的suffix[2]和suffix[6]

注意到所有长度为2的后缀都已经比较过了

那么实际上我们比较s[2~5]和s[6~7]

即是比较s[2~3]+s[4~5]和s[6~7]+“空集”

也就是说长度为2^k的后缀,即是两个长度为2^(k-1)的后缀拼起来。而长度为2^(k-1)的后缀我们又都已经算出来了,那么比较大小就是O(1)的啦!

——by zhber,转载请注明来源
原文地址:https://www.cnblogs.com/zhber/p/4131712.html