算法笔记

算法\数据结构学习笔记

1.主席树

朴素做法,对于每一个版本都新建一个线段树,显然炸空间

我们考虑修改之后只有修改的点到根节点被修改,我们可以新建这些节点,并与未修改的节点连接

主席树模板:动态第k大

前置知识:权值线段树查找第k大

对于1~i建一个主席树,再通过其前缀性质相减,最后查询即可

1.离散化

STL大法好,sort,unique,lowerbound一套带走

2.如何储存主席树

const int maxn = 2e5+5;
struct node
{
	int l,r,sum;
}hjt[maxn*40];
int cnt,root[maxn];

由于我们需要存历史版本,所以不用初始建树

插入函数

void insert(int l,int r,int pre,int &p,int x)
{
	hjt[++cnt]=hjt[pre];
    p=cnt;
    hjt[now].sum++;
    if(l==r)
        return;
    int mid=(l+r)>>1;
    if(x<=mid)
        insert(l,mid,hjt[pre].l,hjt[p].l,x);
    else 
        insert(mid+1,r,hjt[pre].r,hjt[p].r,x);
}

询问第k大

int query(int l,int r,int lp,int rp,int k)
{
   	if(l==r)
        return l;
	int tmp = hjt[hjt[rp].l].sum-hjt[hjt[lp].l].sum;
    if(k<=tmp)
        return query(l,mid,hjt[lp].l,hjt[rp].l,k);
    else 
        return query(mid+1,r,hjt[lp].r,hjt[rp].r,k-tmp);
}

2.可持久化数组

模板题:LGP3919

奇技淫巧:STL rope

//rope
#include<ext/rope>

rope是个块状链表,支持O(1)复制

正解:主席树

1.用原数组建立一个普通线段树

2.根据题意建立版本

3.点分治

例题:POJ 1741

对于给定的一颗树,求两点距离不超过k的点对数量

首先简化问题,路径经过一个点的两点距离不超过k的点对数量

那么,就以该点作为根节点,预处理出所有点到根节点的距离,设为dis**t[i]

然后对其进行排序,再双指针扫一遍求出符合要求的点对数目

但很显然,当两点的LCA不是所选的根节点时,不符合题意,我们需要容斥

对于根节点的每一个子树,我们再把其所有dist进行排序,再求出加和大于k的即可。

解决了这个之后,原问题就很好解决了,只需要把刚才那个根节点删除,并继续重复上述操作即可

为了使得这个操作次数尽量少,我们每次选取的节点应为树的重心

所以,整个算法的流程就是:

1.找树的重心

2.以该点为根结点,处理出所有dist

3.排序,双指针计算和大于k的

4.容斥,找子树中大于k的,计算答案

5.重复1

代码实现:

1.找树的重心

int sz[maxn],tsz,maxp[maxn],rt;//子树大小,总大小,最大儿子,根
void findroot(int x,int f)
{
	sz[x]=1;
	maxp[x]=0;
	for(int i=head[x];i;i=nxt[i])
	{
		int y=to[i];
		if(vis[y]||y==f)
		continue;
		findroot(y,x);
		sz[x]+=sz[y];
		maxp[x]=max(maxp[x],sz[y]);
	}
	maxp[x]=max(maxp[x],tsz-sz[x]);
	if(maxp[x]<maxp[rt])
	rt=x;
}

2.处理dist

int s[maxn],cnt=0;
void dist(int x,int f,int d)
{
	s[++cnt]=d;
	for(int i=head[x];i;i=nxt[i])
	{
		int y=to[i];
		if(y==f||vis[y])
		continue;
		dist(y,x,d+val[i]);
	}
}

3.计算

int calc(int x,int v)
{
	cnt=0;
	dist(x,0,v);
	sort(s+1,s+1+cnt);
	int l=1,r=cnt,sum=0;
	while(l<r)
	{
		while(s[l]+s[r]>k&&l<r)
		r--;
		while(s[l]+s[r[<=k&&l<r)
		{
		sum+=r-l;
		l++;
		}
	}c++
	return sum;
}

4.分治

void divide(int x)
{
	ans+=calc(x,0);
	vis[x]=1;
	for(int i=head[x];i;i=nxt[i])
	{
		int y=to[i];
		if(vis[y])
		continue;
		ans-=calc(y,val[i]);
		tsz=sz[y];
		rt=0;
		findroot(y,0);
		divide(rt);		
	}
}

4.动态点分治~

例题:震波

两种操作:

1.询问对于一个点所以距离不超过k的点的权值和

2.修改某个点的权值

如果只有单次操作1,那么随便搞一搞就能行

那么多次呢?这里就需要用到动态点分治了

我们先对原树做一个映射,建一棵点分树,方法就是按点分治时候重心的选取来建。

根据重心的性质,点分树的树高不会超过\(logn\),这是一个非常好的性质,意味着我们可以暴力的从某个节点一直跳到根节点。

点分树还有个十分重要的性质:

​ 对于任意两个节点\(x,y\),可以确定它们在点分树上的\(LCA\)一定在原树中\(x\)\(y\)的路径上

​ 证明:

​ 设\(z=LCA(x,y)\)(在点分树上),那么也就是说,将\(z\)删除后,\(x,y\)会被分到两个不同的联 通块中,也就是说\(z\)\(x\)\(y\)的路径上。

那么,怎么统计答案呢?

​ 考虑对每个点建立一个数据结构,对于这道题,我们可以对每个节点建动态开点线段树,下标表示距离该点的距离,权值表示点权和。对于每个点,该线段树记录该点点分树子树上的所有点的信息。

​ 然后考虑如何统计答案,先找到询问的点,每次往上跳到父亲节点,再统计。但这样我们发现会算重来的那个子树,所以我们再用线段树维护一下每个子树对其父亲节点的信息。

​ 设询问节点为\(x\),父亲节点为\(fa\),在统计完\(x\)的信息后,我们需要统计\(fa\)的剩余子树的信息,之后再往上跳,才能统计到所有点。我们发现,所想让\(fa\)剩余子树中的点到\(x\)的距离为\(k\),那么其到\(fa\)的距离就是\(k-dis(x,fa))\),因为fa一定在原树中他们的路径上,之后再把\(x\)中算重的那部分减去即可。

​ 所以,总结一下动态点分治的流程:

​ 1.建立点分树

​ 2.对每个节点维护两个数据结构,一个维护子树自身的信息,一个维护子树到父亲节点的信息,用于容斥

​ 3.对于修改或查询,暴力向上跳即可

5.莫队~

​ 莫队NOIP之前学了一点,感觉还是挺有用的。感觉考试不太可能出大数据结构题,即使出了也不敢写正解,写了正解也调不出来。所以莫队、分块、珂朵莉树这种乱搞算法还是挺有用的,好写还能骗分。

​ 莫队算法是一种通过离线处理询问、暴力求解的算法,可以把莫涛大神写到作文里充当素材

6.Splay

​ 平衡树是一种很常见的数据结构,它基于二叉搜索树,但由于插入顺序等原因,二叉搜索树的形态可能变成链,或是近于链,这就会导致查找的时间复杂度退化到\(O(n)\),为了避免这种情况,我们需要让这棵树平衡起来

关于树旋转

​ 右旋拎左右挂左

​ 左旋拎右左挂右

Splay

​ 引入模板题

​ 要求插入\删除、查询排名、查询排名为x的数、求x的前驱、求x的后继

1.需要的变量

​ sz:表示当前节点个数

​ rt:表示当前根节点的编号

​ f[x]:表示编号x节点的父亲的编号

​ key[x]:表示编号x节点对应的数值

​ size[x]:表示以编号x的节点的子树的大小

​ recy[x]:表示编号x节点的数值的重复次数

​ son[x][0\1]:表示编号x节点的左/右儿子编号

2.操作

1.查询父子关系

bool ident(int x,int f){return ch[f][1]==x;}

2.建立父子关系

void connect(int x,int f,int s){
    fa[x]=f;
    ch[f][s]=x;
}

3.旋转(上旋)

void rotate(int x){
    int f=fa[x],ff=fa[f],k=ident(x,f);
    connect(ch[x][k^1],f,k);
    connect(x,ff,ident(f,ff));
    connect(f,x,k^1);
    update(f),update(x);
}

4.SPLAY

void splay(int x,int top){//把x转到top儿子
	if(!top) root=x;
    while(fa[x]!=top){
        int f=fa[x],ff=fa[f];
        if(ff=!top) ident(f,ff)^ident(x,f)?rotate(x):rotate(f);
        rotate(x);
    }
}

5.查询、删除

void ins(int v,int &now=root,int fa=0){
    if(!now) newnode(now,fa,v),splay(now,0);
    else if(v<val[now]) ins(v,ch[now][0],now);
    else if(l>val[now]) ins(v,ch[now][1],now);
    else cnt[now]++,splay(now,0);
}
void del(int v,int now=root){
    if(v==val[now])delnode(now);
    else if(v<val[now]) del(v,ch[now][0]);
    else del(v,ch[now][0]);
}
void newnode(int &now,int fa,int v){
    val[now=++cnt]=v;
    fa[now]=fa;
    size[now]=cnt[now]=1;
}
void delnode(int x){
    splay(x,0);
    if(cnt[x]>1) cnt[x]--;
    else if(ch[x][1]){
        int p=ch[x][1];
        while(ch[p][0]) p=ch[p][0];
        splay(p,x);
        connect(ch[x][0],p,0);
        root=p;
        fa[p]=0;
        update(p);
    }
    else root=ch[x][0],fa[root]=0;
}

7.DFS序

DFS序就是DFS时的顺序,也就是进栈出栈的顺序,利用DFS序,我们可以对树进行一些操作

1.单点修改,子树查询

​ 可以观察到,子树的范围就是子树根节点往后size大小的一段区间,所以用线段树就可以做到

2.区间修改,单点查询

​ 对于一棵树,我们要建边,求从根节点到某一节点经过的边数

​ 很显然,对于新建的边,我们可以转成儿子的点权,然后就是区间修改,单点查询。

​ 还有一种做法,括号序列,例如下DFS序(进出栈均标出)

​ 1 2 2 3 4 4 5 5 4 1

​ 我们对于每一次进栈+1,出栈-1,即:

​ 1 1 -1 1 1 -1 1 -1 -1 -1

​ 我们对这个求前缀和,就是经过的边数

​ 括号序列还有很多其他的用法,之后再说

3.区间修改,区间查询(树状数组做法)

​ 对于区间修改、区间查询,一般都会写线段树,但是那玩意虽然拉过来个学数学竞赛的都会,可相比于树状数组来说又臭又长。所以我们考虑如何使用树状数组进行区间修改、区间查询。

​ 先考虑我们如何进行区间修改、单点查询,我们很自然能想到查分,所以直接在查分数组上建一个树状数组即可

​ 现在我们还需要区间查询,我们设原数组为a,查分数组为b

​ 那么则有\(\Sigma_{k=1}^ib_j=a_j\)

​ 也就是说:\(\Sigma_{k=1}^ia_i=i*b_1+(i-1)*b_2....+b_i\)

​ 很显然,我们需要把b的下标与系数相匹配,设\(sum_i=\Sigma_{j=1}^ia_j\)

​ 那么,\(sum_i=i*(b_1+b_2+...b_i)-\Sigma_{j=1}^i((j-1)*b_j)\)

​ 前面维护一下\(b\)的前缀和,之后乘\(i\),后面维护一下\((i-1)*b_i\)的前缀和,然后相减即可

8.替罪羊树

9.FHQ Treap

Treap=heap+tree

优:简单

劣:常数大

维护平衡的方式:

分裂\合并

节点信息:左右子树编号,值,索引,子树大小

基础操作
struct FHQ{
    int l,r,val,key,size;
    int newnode(int v){
        val[++cnt]=v;
        key[cnt]=rand();
        size[cnt]=1;
        return cnt;
    }
    //按值分裂,把树拆成两个数,一个树的值全部小于等于某值,另一颗全部大于给定的值
	//按大小分裂,一棵树的大小全部小于给定的大小,另一棵全部大于等于给定的大小
    void update(int now){
        size[now]=size[l[now]]+size[r[now]]+1;
    }
    void split(int now,int v,int &x,int &y){
        if(!now) x=y=0;
        else{
            if(val[now]<=v){
                x=now;
                split(r[now],v,r[now],y);
            }
            else{
                y=now;
                split(l[now],v,x,l[now]);
            }
        }
        update(now);
    }
    //合并
    int merge(int x,int y){//保证x的值小于等于y上的
        if(!x||!y) return x+y;
        if(key[x]>key[y]){
            r[x]=merge(r[x],y);
            update(x);
            return x;
        }
        else{
            l[y]=merge(x,l[y]);
            update(y);
            return y;
        }
    }
    //插入 按val分裂,再合并
    void ins(int v){
       	split(root,val,x,y);
        root=merge(merge(x,newnode(val)),y);
    }
    void del(int v){
        split(root,v,x,z);
        split(x,v-1,x,y);
        y=merge(l[y],r[y]);
        root=merge(merge(x,y),z);
    }
    void getrank(int v){
        split(root,val-1,x,y);
        printf("%d",size[x]+1);
        root=merge(x,y);
    }
    void getnum(int rank){
        int now=root;
        while(now){
            if(size[l[now]]+1==rank)break;
            else if(size[l[now]]>=rank)
                now=l[now];
            else{
                rank-=size[l[now]]+1;
                now=r[now];
            }
        }
        printf("%d\n",val[now]);
    }
}
void pre(int v){
    split(root,v-1,x,y);
    int now=x;
    while(r[now])
        now=r[now];
    printf("%d\n",v[now]);
    root=merge(x,y);
}
区间操作(文艺平衡树)

1.把操作区间split拆出来

2.打个lazytag

3.合并回去

void pushdown(int now){
    swap(l[now],r[now]);
    reverse[l[now]]^=1;
    reverse[r[now]]^=1;
    reverse[now]=0;
}
void split(int now,int siz,int &x,int &y){
    if(!now) x=y=0;
    else{
        if(reserse[now]) pushdown(now)
        if(size[l[now]]<siz){
            x=now;
            split(r[now],siz-size[l[now]]-1,r[now],y); 
        }
        else{
            y=now;
            split(l[now],siz,x,l[now]);
        }
        update(now);
    }
}
int merge(int x,int y){
    if(!x||!y) return x+y;
    if(key[x]<key[y]){
        if(reverse[x]) pushdown(x);
        r[x]=merge(r[x],y);
        update(x);
        return x;
    }
    else{
        if(reverse[y]) pushdown(y);
        l[y]=merge(x,l[y]);
        update(y);
        return y;
    }
}
void reverse(int l,int r){
    int x,y,z;
    split(root,l-1,x,y);
    split(y,r-l+1,y,z);
    reverse[y]^=1;
    root=merge(merge(x,y),z);
}
void output(int now){
    if(!now)return;
	if(reverse) pushdown(now);
    output(l[now]);
	printf("%d\n",val[now]);
    output(r[now]);
}

10.可并堆

左偏树

简单且实用

int lson[],rson[],val[],dis[]//向右走的最远距离
int merge(int x,int y){
   if(!x||!y) return x+y;
    //小根堆
    if(val[x]>val[y]) swap(x,y);
    rson[x]=merge(rson[x],y);
    if(dis[rson[x]]>dis[lson[x]]) swap(rson[x],lson[x]);
    dis[x]=dis[rson[x]]+1;
	return x;
}

11.博弈论~

1.nim游戏

​ 描述:两个人轮流在n个石堆里取数,每次 只能在一个石堆里取少于m个石头,不能不取,最后一个取完的胜

​ nim游戏是经典的ICG游戏,对于ICG游戏我们有如下定义

​ 1.两名选手

​ 2.轮流行动,每次只能在有限的合法操作内选择一个

​ 3.对于任何一种可能的局面,合法操作集合只取决于局面本身

​ 4.如果轮到某名选手,且这个局面的合法移动集合为空,则这名选手负

我们将局面分为两类:

​ 1.P-position:在当前局面下,先手必败

​ 2.N-position:在当前局面下,先手必胜

有如下转换:

​ 1.当前操作集合为空的局面是P-position

​ 2.所有可以直接移动到P-position的局面是N-position

​ 3.所有移动都只能到N-position的局面是P-position

算法实现

步骤1:将所有终结位置标记为必败点(P点); 步骤2: 将所有一步操作能进入必败点(P点)的位置标记为必胜点(N点) 步骤3:如果从某个点开始的所有一步操作都只能进入必胜点(N点) ,则将该点标记为必败点(P点) ; 步骤4: 如果在步骤3未能找到新的必败(P点),则算法终止;否则,返回到步骤2。

2.SG函数

引入经典问题

给定一个有向无环图和一个起始顶点上的一枚棋子,Alice和Bob交替的将这枚棋子沿有向边进行移动,无法移动者判负。问是否有必胜策略。

实际上,这个游戏可认为是所有ICG游戏的抽象模型,也就是说,任何一个ICG游戏都可以通过把每个局面看成一个顶点,对每个局面和它的子局面连一条有向边来抽象成这个模型,下面就在这个图的定点上定义SG函数

对于任意

​ x=x1+x2+.....+xn

​ g(x)=g(x1) xor g(x2) xor ... xor g(xn)

SG值相同的局面,可以认为局面本质相同

我们有以下转移方程

\[SG(x) = mex(SG(y)|y是x的后继) \]

对于出度为零的点,其SG值为0,因为它的后继集合为空集。对于一个SG(x)=0的顶点x,它的所有后继y都满足sg(y)$\not=$0

当且仅当SG(x)=0,x局面是P-position

12.凸包

凸包是一个计算几何中的概念。

在一个实数向量空间V中,对于给定集合X,所有包含X的凸集的交集S被称为X的凸包

算法:Graham扫描法

时间复杂度\(O(nlogn)\)

步骤

​ 1.找到纵坐标最小的点,将其作为原点

​ 2.对所有点进行极角排序,如果极角相同就选择距离原点较近的点排在前面。

​ 3.对于每个新节点,看其与栈顶元素组成的向量和栈前两个点组成的向量的叉积大小关系,弹出不合法元素,最后入栈

代码
#include<bits/stdc++.h>
using namespace std;
int n;
double ans;
struct point{
	double x,y;
}p[10005],s[10005];
double check(point a1,point a2,point b1,point b2){
	return (a2.x-a1.x)*(b2.y-b1.y)-(b2.x-b1.x)*(a2.y-a1.y);
}
double d(point a,point b){
	return sqrt((b.y-a.y)*(b.y-a.y)+(b.x-a.x)*(b.x-a.x));
}
bool cmp(point p1,point p2){
	double tmp=check(p[1],p1,p[1],p2);
	if(tmp>0) return 1;
	if(tmp==0&&d(p[0],p1)<d(p[0],p2))
		return 1;
	return 0;
}
int main(){
	scanf("%d",&n);
	double mid;
	for(int i=1;i<=n;i++){
		scanf("%lf%lf",&p[i].x,&p[i].y);
		if(i!=1&&p[i].y<p[1].y){
			mid=p[1].y;p[1].y=p[i].y;p[i].y=mid;mid=p[1].x;p[1].x=p[i].x;p[i].x=mid;
		}
	}
	sort(p+2,p+1+n,cmp);
	s[1]=p[1];
	int cnt=1;
	for(int i=2;i<=n;i++){
		while(cnt>1&&check(s[cnt-1],s[cnt],s[cnt],p[i])<=0)
			cnt--;
		cnt++;
		s[cnt]=p[i];
	}
	s[cnt+1]=p[1];
	for(int i=1;i<=cnt;i++){
		ans+=d(s[i],s[i+1]);
	}
	
	printf("%.2lf\n",ans);
	return 0;
}

13.线性筛 ^

​ 线性筛素数的原理不必多说,考虑一个合数可以被唯一分解成一个最小质数和某数的乘积,所以当i%prime=0的时候直接break掉即可

​ 对于欧拉函数,其定义为小于某变量的数中与其互质的个数,则可推断出:

\[\phi(x)=x\prod(1-\frac{1}{p_i})(p_i为x的质因子) \]

14.±1RMQ

​ 序列差值绝对值为1(约束RMQ)

实现方式:分块

分块的思路确定了。那么每个块的长度我们该如何确定呢?

是否可以将每块的长度len定为img?如果这样就没有利用好+1,-1这个条件了。

在这里,我们设img。则块数img

注意到相邻的两个数只有+1,-1两个条件。根据乘法原理,最多只有img种本质不同的情况。

那么,我们可以先预处理出所有本质不同的情况的所有答案,即每一种本质不同情况都做一次RMQ。易知,这样做的时间复杂度不超过常数级别。

做完RMQ预处理后,就可以根据每一块+1/-1的性质套进上面的情况中,加上偏移量offset即可。

记第i块的最小值为A'[i],然后根据序列A'做一次块间的RMQ即可。

则预处理的时间复杂度为img。每次查询的时间复杂度为img

15.莫比乌斯反演

莫反可以说是数论里最有意思的内容之一了

首先什么是反演?

已知我们知道如下函数:

\[F(n)=\sum_{d|n}f(d) \]

我们需要找到\(f与F\)的关系,这就是反演

我们定义莫比乌斯函数\(\mu(x)\)

\[\sum_{d|n}\mu(d)=[n=1] \]

16.字符串哈希

哈希:把输入映射到一个值域较小的范围

字符串哈希可以将一个字符串映射成整数,用整数的比较代替字符串的比较,从而优化比较时间复杂度

方法:把字符串视为一个b进制数

\[f(s)=\sum_{i=1}^ls[i]*b^{l-i}(mod M) \]

b和M的选择

1.b和M互质,使得其值域相对分散

访问子串哈希值:维护前缀和,再乘减获得子串哈希值

应用:

1.字符串匹配:求出模式串哈希值后,求出文本串每个长度为模式串长度的子串,比较i

2.最长回文子串:正反处理出字符串哈希,枚举对称中心

17.KMP~

主要思想:利用匹配失败后的信息,快速再次匹配

第一步:获得模式串内部信息

对于模式串的每个前缀,我们需要找出一个字符串,既是pre-i的真后缀,也是前缀,并且长度最长,记为next数组

第二步:通过模式串内部信息减少比较次数

第三步:如果匹配失败,移动

如何求next数组?

模式串与自身匹配

next[1]=0;
for(int i=2,j=0;i<=n;i++){
    while(j>0&&a[i]!=a[j+1]) j=nxt[j];
    if(a[i]==a[j+1]) j++;
    nxt[i]=j;
}

18.LCT

模板题:

1.查询一个链的异或和

2.连接x到y

3.删除x到y的边

4.把x的权值变成y

LCT使用类似储存有向图和无向图的方式来区分实边虚边:单向边(儿->父)为虚边,双向边为实边

虚边是正常Splay的储存方式,虚边是一个根节点的fa指针指向一个节点,而这个节点的儿子节点没有该节点

性质:

1.所有节点存在且仅存在于一个Splay里

2.每个Splay的节点深度按中序遍历顺序递增

const int maxn=1e5+5;
struct node{
    int fa,ch[2],val,res//异或结果
    bool tag;
}s[maxn];
#define ls(x) (s[x].ch[0])
#define rs(x) (s[x].ch[1])
#define fa(x) (s[x].fa)
#define ident(x,f) (rs(f)==x)
#define connect(x,f,s) s[fa(x)=f].ch[s]=x
#define update(x) s[x].res=s[ls(x)].res^s[rs[x]].res^s[x].val
#define ntroot(x) (ls(fa(x))==x||rs(fa(x))==x) //判断是否为Splay的根
#define reverse(x) swap(ls(x),rs(x)),s[x].tag^=1
void pushdown(int x){
    if(s[x].tag){
        if(ls(x))reverse(ls(x));
        if(rs(x))reverse(rs(x));
    }
    s[x].tag=0;
}
access

从x到原树根节点打通

void access(int x){
	for(int y=0;x=fa[y=x]){
		splaying(x);
		rs(x)=y;
		update(x);
	}
}
makeroot

把原树的根为x

void mkroot(int x){
    access(x);
    splaying(x);
    reverse(x);
}

步骤:

1.access(x)

2.伸展x

3.翻转整颗Splay

findroot
int findroot(int x){
    access(x);
    splaying(x);
    while(ls(x)){
        pushdown(x);
        x=ls(x);
    }
    splaying(x);
    return x;
}

步骤:

1.makeroot(x);

2.如果findroot(y)=x,证明数据不合法,不连了

3.把x的父亲置为y,连成一条虚边

cut断边

1.makeroot(x)

2.判断数据是否合法

3.双向断边

4.更新信息

void link(int x,int y){
    mkroot(x);
    if(findroot(y)==x) return;
    fa(x)=y;
}
void cut(int x,int y){
    mkroot(x);
    if(findroot(y)!=x||fa(y)!=x||ls(y)) return;
    fa(y)=rs(x)=0;
    update(x);
}
split

把x到y的路径拆成splay上

步骤:

1.makeroot(x)

2.access(y)

3.splaying(y)

void split(int x,int y){
    mkroot(x);
    access(y);
    splaying(y);
}

虚边对splay的修改

void rotate(int x){
    int f=fa(x),ff=fa(f),k=ident(x,f);
    connect(s[x].ch[k^1],f,k);
    fa(x)=ff;
    if(ntroot(f))s[ff].ch[ident(f,ff)]=x;
    connect(f,x,k^1);
    update(f),update(x);
}
void splaying(int x){
    pushdown(x);
    while(ntroot(x)){
    if(ntroot(f))    
    ident(f,ff)^ident(x,f)?rotate(x):rotate(f);
    rotate(x);
	}
}
总代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
struct node{
	int fa,ch[2],val,res;
	bool tag;
}s[maxn];
#define ls(x) (s[x].ch[0])
#define rs(x) (s[x].ch[1])
#define fa(x) (s[x].fa)
#define ident(x,f) (rs(f)==x)
#define connect(x,f,k) s[fa(x)=f].ch[k]=x
#define update(x) s[x].res=s[ls(x)].res^s[rs(x)].res^s[x].val
#define ntroot(x) (ls(fa(x))==x||rs(fa(x))==x)
#define reverse(x) swap(ls(x),rs(x)),s[x].tag^=1
void pushdw(int x){
	if(s[x].tag){
		if(ls(x)) reverse(ls(x));
		if(rs(x)) reverse(rs(x));
	}
	s[x].tag=0;
}
void pushall(int x){
	if(ntroot(x)) pushall(fa(x));
	pushdw(x);
}
void rotate(int x){
	int f=fa(x),ff=fa(f),k=ident(x,f);
	connect(s[x].ch[k^1],f,k);
	fa(x)=ff;
	if(ntroot(f)) s[ff].ch[ident(f,ff)]=x;
	connect(f,x,k^1);
	update(f),update(x);
}
void splaying(int x){
	pushall(x);
	while(ntroot(x)){
		int f=fa(x),ff=fa(f);
		if(ntroot(f)) ident(f,ff)^ident(x,f)?rotate(x):rotate(f);
		rotate(x);
 	}
}
void access(int x){
	for(int y=0;x;x=fa(y=x)){
		splaying(x);
		rs(x)=y;
		update(x);
	}
}
void mkroot(int x){
	access(x);
	splaying(x);
	reverse(x);
}
int findroot(int x){
	access(x);
	splaying(x);
	while(ls(x)){
		pushdw(x);
		x=ls(x);
	}
	splaying(x);
	return x;
}
void link(int x,int y){
	mkroot(x);
	if(findroot(y)==x) return;
	fa(x)=y;
}
void cut(int x,int y){
	mkroot(x);
	if(findroot(y)!=x||fa(y)!=x||ls(y)) return;
	fa(y)=rs(x)=0;
	update(x);
}
void split(int x,int y){
	mkroot(x);
	access(y);
	splaying(y);
}
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&s[i].val);
	while(m--){
		int opt,x,y;
		scanf("%d%d%d",&opt,&x,&y);
		switch(opt){
			case 0:
				split(x,y);
				printf("%d\n",s[y].res);
				break;
			case 1:
				link(x,y);
				break;
			case 2:
				cut(x,y);
				break;
			case 3:
				splaying(x);
				s[x].val=y;
				update(x);
				break;
		}
	}
	return 0;
}
拓展:关于LCT求最小生成树

最小生成树一般都是克鲁斯卡尔算法,易于理解而又方便实现。

而LCT支持cut与link操作,同样也能实现最小生成树

考虑这样一个问题:

对于这样一个图,其边权有两个关键字,求出1~n路径上关键字最大值的和最小值

这里,我们设两个关键字为a、b,我们只需要以a为关键字排序,然后对b做最小生成树即可。

由于这里b关键字不满足贪心,不能用克鲁斯卡尔,所有我们需要用LCT来求最小生成树。

对于LCT求最小生成树,我们很容易得到一种方法:

​ 对于每条边,如果它连接了两个不联通的联通块,那么直接link

​ 如果连了之后会形成环,那么把环上边权最大的断掉即可

具体实现

​ 首先记录当前答案:ans

​ 对于每一条边,如果这条边它连接了两个不联通的联通块,我们直接连接,但是因为我们LCT维护的是点的信息,这会比较麻烦处理。LCT经常会有换根操作,不方便把边权转到点权。

​ 所以比较暴力的想法就是,把当前的边当作一个点,那么其编号为\(n+id\)

​ 然后这个点的点权为其边权,这个时候连边。假设其沟通了\(x,y\),即:

w[id+n]=z;
link(x,id+n),link(id+n,y);

具体代码实现:

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5010;
int ch[maxn][2],fa[maxn],val[maxn],id[maxn];
bool tag[maxn];
int mx[maxn];
int ans,n,m;
#define lson(x) ch[x][0]
#define rson(x) ch[x][1]
#define ntroot(x) (ch[fa[x]][1]==x||ch[fa[x]][0]==x)
#define ident(x,f) (x==ch[f][1])
#define connect(x,f,s) ch[fa[x]=f][s]=x;
#define reverse(x) swap(lson(x),rson(x)),tag[x]^=1
void update(int x){
	id[x]=x,mx[x]=val[x];
	if(mx[lson(x)]>mx[x]) mx[x]=mx[lson(x)],id[x]=id[lson(x)];
	if(mx[rson(x)]>mx[x]) mx[x]=mx[rson(x)],id[x]=id[rson(x)];
}
void pushdown(int x){
	if(tag[x]){
		if(lson(x))
		reverse(lson(x));
		if(rson(x))
		reverse(rson(x));
	}
	tag[x]=0;
}
void pushall(int x){
	if(ntroot(x)) pushall(fa[x]);
	pushdown(x);
}
void rotate(int x){
	int f=fa[x],ff=fa[f],k=ident(x,f);
	connect(ch[x][k^1],f,k);
	fa[x]=ff;
	if(ntroot(f)) ch[ff][ident(f,ff)]=x;
	connect(f,x,k^1);
	update(f),update(x);
}
void splaying(int x){
	pushall(x);
	while(ntroot(x)){
		int f=fa[x],ff=fa[f];
		if(ntroot(f)) ident(x,f)^ident(f,ff)?rotate(x):rotate(f);
		rotate(x);
	}
}
void access(int x){
	for(int y=0;x;x=fa[y=x]){
		splaying(x);
		rson(x)=y;
		update(x);
	}
}
void mkroot(int x){
	access(x);
	splaying(x);
	reverse(x);	
}
int findroot(int x){
	access(x);
	splaying(x);
	while(lson(x)){
		pushdown(x);
		x=lson(x);
	}
	splaying(x);
	return x;
}
void link(int x,int y){
	mkroot(x);
	if(findroot(y)==x) return;
	fa[x]=y;
}
void cut(int x,int y){
	mkroot(x);
	if(findroot(y)!=x||lson(y)||fa[y]!=x) return;
	rson(x)=fa[y]=0;
}
void split(int x,int y){
	mkroot(x);
	access(y);
	splaying(y);
}
int Ident;
bool check(int x,int y){
	mkroot(x);
	return findroot(y)!=x;
}
int main(){
	scanf("%d%d",&n,&m);
	Ident=n;
	int x,y,now,z;
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&x,&y,&z);
		val[++Ident]=z;
		if(check(x,y))		
			link(x,Ident),link(Ident,y),ans+=z;
		else{
			split(x,y);
			now=id[y];
			if(mx[now]<=z) continue;
			ans+=(z-mx[now]),splaying(id[y]);
			fa[lson(now)]=fa[rson(now)]=0;
			link(x,Ident),link(Ident,y);
		}
	}
	printf("%d\n",ans);
	return 0;
}

注意,要将数组开成n+m大小

19.笛卡尔树

每个点拥有一对键值(u,v)

对于u是一颗二分查找树

对于v是一个二叉堆

对于这样一个序列

9 3 7 1 8 12 10 20 15 18 5

我们以val为小根堆键值,id为查找树键值则可以建出如图的树

(懒得画图,感受一下吧)

那么,如何\(O(n)\)建树呢

我们考虑新加入一个节点,这个节点应该是第一个比他大的值的父亲节点(堆的性质),是第一个小于它的儿子节点,用单调栈即可维护出,具体实现如下

for(int i=1;i<=n;i++){
    while(top>0&&val[i]<val[sta[top]]) top--;
    if(top) rson[sta[top]]=i;
    lson[i]=sta[top+1];
    sta[++top]=i;
    sta[top+1]=0;
}

我们考虑这样一个问题:

对于一堆积木,它的高度如下

3 4 2 4 3 3 4

我们想找到最大的矩形

考虑原先的做法:单调栈

维护出每个点右边和左边第一个比他矮的位置即可。

那么怎么用笛卡尔树实现呢?

我们对这个序列以高度为小根堆键值,下标为搜索树键值建立一棵笛卡尔树

我们发现,对于一个节点的子树,都是比他矮的

且左子树中都是编号编号比他小的,右子树中都是编号比他大的。

也就是说,在这段连续的区间里,根节点是最矮的,只需要用其权值乘子树大小就是答案了

(用分治的思想带入一下)

20.STL总结

STL大法好,适用于各种暴力的代码简化

关于迭代器

定义

vector<int>::iterator it;//for vector
vector

可伸缩的数组,可用来保存有向图

可以实现伪动态开点树状数组

queue

queue一般手写实现

priority_queue是优先队列,可以当作堆

set

其内部结构是一棵红黑树,可以支持很多平衡树的操作

s.begin()//最小元素的迭代器
s.end()//最大元素的迭代器
s.insert()//插入一个元素
s.find()//返回寻找元素的迭代器,若无,返回end
s.lower/upper_bound//返回>=x/>x元素中最小的一个
s.erase()//删除某一元素
s.count()//返回某一元素的个数

21.矩阵加速递推

矩阵乘法:矩阵和矩阵的乘法叫做矩阵乘法。

仅当第一个矩阵的列数和第二个矩阵的行数相等时才能定义

矩阵乘法不满足交换律

如何运算:略

引入模板题:

​ 设斐波那契数列为\(F(x)\),求\(F(n)\%(1e9+7),n\in1e9\)

22.动态规划专题

动态规划是一种思想,也是OI的重点,这里总结一下动态规划的心得,并记录一些不错的题

1.线性DP

1.Making the Grade

易得b中的数字一定是a中的,考虑到\(n\)的范围特别小,我们想一些暴力一点的算法。

第一维肯定是到了第几位,我们还需要知道b数列最后一个是什么,才能让我们满足单调这一条件。所以第二维就是\(b\)数列最后一位是什么

2.Mobile Service

设置状态,肯定需要任务完成数量,及三个人的位置情况。我们考虑到肯定会有一个人在上次任务要求的位置上,所以我们就只设置两维就可以了

23.树套树

顾名思义,树上套树

模板1:二逼平衡树

区间排名、区间排名为k的值、修改某一数值、查询区间前驱、查询区间后继

引入:树套树

这里我们要使用线段树套平衡树

对于线段树的每个节点,建一棵平衡树,来维护线段树所代表的区间的信息

著名的算法学家LLQ告诉我,哪个平衡树最熟写哪个

所以直接线段树套Splay

明天写完

暂鸽

24.数位DP

例题:

求出范围内二进制中0的数目不小于1的数目的数字的数量。

显然,我们可以设置状态:
\(dp[i][j][k]\)表示还有\(i\)位没有枚举到,且已有\(j\)个1,\(k\)个0的数字的数量

我们使用记忆化搜索来解决

int dfs(int len,int sum1,int sum0,int zero,int limit){//长度、1数量、0数量、是否全是前导0、是否和范围的前len位重合
	if(!len){
		if(zero||sum0>=sum1)return 1;
		return 0;
	}
	if(!zero&&!limit&&dp[len][sum1][sum0]!=-1) return dp[len][sum1][sum0];
	int sum=0;
	for(int i=0;i<=(limit?num[len]:1);i++){
		if(zero&&!i) sum+=dfs(len-1,0,0,zero,limit&&(i==num[len]));
		else{
			if(i) sum+=dfs(len-1,sum1+1,sum0,0,limit);
			else sum+=dfs(len-1,sum1,sum0+1,0,limit&&(num[len]==i));
		}
	}
	if(!zero&&!limit) dp[len][sum1][sum0]=sum;
	return sum;
}

25.AC自动机

AC自动化机是一种有限状态自动机,常用于多模式字符串匹配。其原理类似于Trie+KMP

建立一个AC自动机

1.对于所有模式串建Trie树

2.对Trie树上的所有结点建立失配指针

Trie的建立

有手就行

构造失配指针

失配指针是在失配时用来跳转的指针。

失配指针需要的是相同后缀

考虑字典树中当前的节点u,u的父亲位p,p通过字符c指向u,即tr[p,c]=u

假设小于u的所有节点的fail指针都已求得

1.如果tr[fail[p],c]存在,则让u的fail指针指向tr[fail[p],c],相当于在p和fail[p]后面加一个字符c,分别对应u和fail[u]

2.如果tr[fail[p],c]不存在,那么我们继续找到tr[fail[fai[p]],c],重复1的判断过程,一直到fail指针跳到根节点

3.如果真的没有,就让fail指针指向根节点

26.对拍

不多说,直接上模板

#include<bits/stdc++.h>
using namespace std;
int rand(int l,int r){
	return l+rand()%(r-l+1);
}
int hp[3001];
void gen(int n){
	FILE *file = fopen("tt.in","w");
	fprintf(file,"%d\n",n);
	for(int i=1;i<=n;i++){
		fprintf(file,"%d ",rand(1,n));
	}
	fprintf(file,"\n%d\n",n);
	memset(hp,0,sizeof(hp));
	fclose(file);
}
int main(){
	while(1){
		gen(1000);
		system("./std < tt.in > tt.ans");
		system("./1 < tt.in > tt.out");
		if(system("diff tt.out tt.ans")){
			printf("WA");
			break;
		}
		printf("AC\n");
	}
	return 0;
}

27.DP优化

说实话,我认为OI里上限最高的就是DP和贪心了。其他的算法要么具有极强的特征性,要么套路十分明显。

DP和贪心能做到灵活多变。很多题都需要先贪心找性质再去做。

1.DP时间复杂度的分析

DP高时间效率的关键是它减少了冗余,即重复计算的次数。DP实在不断将问题规模增大的同时,记录已求解子问题的解,再通过转移到达当前问题。

下面给出动态规划时间复杂度的决定因素

**时间复杂度=状态总数*每个状态转移的状态数*每次状态转移的时间 **

2.DP优化思路

一:减少状态总数

1.改进状态表示

2.选择适当的规划方向

二.减少每个状态转移的状态数

1.四边形不等式和决策单调性

2.决策量的优化

3.合理组织状态

4.细化转移状态

三:减少转移的时间

1.减少决策时间

2.减少计算时间

总结:

推导DP方程时,我们需要关注三个要点:状态、决策、转移

状态

状态的选择需要从多角度考虑,思维难度极高,选择的好坏直接影响解题

决策和转移

优化决策和转移通常都十分套路化.

下面来看一下对于决策的优化


一:矩阵优化

利用矩乘的结合律进行加速转移,具体见矩阵加速递推


二:数据结构优化DP

【前言】

在转移时,我们常常需要找都某个范围内的最优值,选出最佳决策。

如求区间最值时使用线段树,动态区间操作时使用平衡树等。

【例题】

暂无


三:决策单调性优化

【前言】

形如\(dp[i]=min(dp[j]+w[i,j])(j\in[L_i,R_i])\)的DP方程被称为\(1D/1D\)动态规划。其中\(L_i,R_i\)单调递增,\(w(j,i)\)决定着优化策略选择

针对决策点特有的性质,可以大大降低寻找最优决策点的时间

其本质是优化决策


1.【定义】
【决策单调性】

\(j_0[i]\)表示\(dp[i]\)的最优转移决策点,那么决策单调性可描述为\(\forall i\le j,j_0[i]\le j_0[j]\),也就是说随着i的增大,最优决策点非严格递增

【四边形不等式】

\(w(x,y)\)为定义在整数集合的一个二元函数,若\(\forall a\le b\le c\le d,w(a,c)+w(b,d)\le w(a,d)+w(b,c)\),那么函数\(w\)满足四边形不等式

为什么叫做四边形不等式?在\(w(x,y)\)函数的二维矩阵中挖一块四边形,左上角加右下角小于等于左下角加右上角


2.【定理】

1.四边形不等式的另一种形式

\[w(a,b)+w(a+1,b+1)\le w(a+1,b)+w(a,b+1) \]

即左上角加右下角小于等于左下角加右上角

2.\(1D/1D\)动态规划具有决策单调性当且仅当函数\(w\)满足四边形不等式时成立


3.【证明决策单调性】

以该DP方程为例

\[dp[i]=min(dp[j]+(S[i]-S[j]-1-L)^2) \]

很明显,这是个\(1D/1D\)的方程,其中\(w(i,j)=(S[i]-S[j]-1-L)^2\)

证明略。

最后可以得出\(w(i,j)\)满足四边形不等式,即该方程满足决策单调性。

通常,我们使用打表来证明\(w\)函数的递变规律

4.单调数据结构优化

【介绍】

形如\(dp[i]=max(dp[j]+function(i)+function(j))\)的方程都可以尝试使用单调数据结构来优化

其本质是通过将不可能成为最佳决策的点舍弃来进行优化的。

由于过于简单,不加赘述

5.斜率优化

【前言】

对于这样的方程

\[dp[i]=min(a[i]*b[j]+c[j]+d[i]) \]

我们发现因为\(a[i]*b[j]\)的关系,我们不能用单调队列来优化,所以可以尝试斜率优化

1.【理解方式】

以该方程为例:

\[dp[i]=min(dp[j]+(S[i]-S[j]-L)^2) \]

为了方便描述,将min去掉,即

\[dp[i]=S[i]^2-2S[i]L+dp[j]+(S[j]+L)^2-2S[i]S[j] \]

1.【数形结合】

我们首先把同类型的划分到一起,即:

\[dp[i]=(−2S[i]S[j])+(dp[j]+(S[j]+L)^2)+(S[i]^2−2S[i]L) \]

暂鸽,太麻烦了

28.矩阵树定理

​ 省选之前最后一篇算法笔记了吧。

​ 我们先定义两个矩阵,第一个是度数矩阵,第二个是连通矩阵,度数矩阵减去连通矩阵记为我们要的矩阵,记为T,去掉任意一行或一列,得到T',生成树的个数就是这个矩阵T'的行列式。

​ 行列式高斯消元搞一搞就可以了。

posted @ 2021-04-09 19:32  岚默笙  阅读(69)  评论(1编辑  收藏  举报