基础数据结构

前言

感觉没什么好讲的,虽然天天待在组里,但却感觉发生了太多太多,从来没有感觉我自己怎么的菜过,简单的东西不想搞,难的东西搞不动,我已经因为网络流欲哭无泪了,也许我就是不能理解它吧,这几天心境很不好,也许会伴随这三年了,带来的负面影响,栈队列链表链接表这些简单的东西,本来是想一天搞完的,结果搞了5,6天。还感觉自己没序号,更有强大的爆踩我的czf,现在已经在写搜索了(我好想写搜索啊),超越我进度100年,嘲讽我这样学的很扎实,没办法,菜就是一种天生属性,改不掉,我是什么人,我按照我自己的脚步,别人我无法模仿,更无法超越。

维护的思想

  1. 离线和在线
  2. 根据已有推出未知

性质

后进先出

常见数据结构灵活运用

双栈同步

两个栈同步插入,同步删除,一个栈保存数据,另外一个栈处理数据,动态维护前k大数据(如维护栈中最小值)

推广:双平衡树,维护一个序列的单调性的同时,维护最值(其孪生兄弟离散化+树状数组,可以维护一个序列的单调性的同时,同时维护最值,但能够维护更多附加信息,更加灵活)

双栈对顶

将序列的前k个数字按角标顺序存储到一个栈中,其余的数字按角标顺序存储到另外一个栈中,可以动态维护第k个元素。

推广:双堆对顶,动态维护第k大元素

全局变量记录全局变化

如果一个容器发生了相同的变化,不妨用一个变量记录这个变化,而不是一个一个去修改。

进出栈序列

n个元素出栈序列

法一:搜索,枚举是否出栈和入栈

法二:二维递推

(f[i][j])表示有i个元素未入栈,j个元素在栈中的出栈方案数,有(f[i][j]=f[i-1][j+1]+f[i][j-1]),边界(f[0][0]=1),答案(f[n][0])

法三:一维递推

(f[i])为i个元素的出栈序列,根据一个确定的元素的出栈的时间,还剩下未出栈的元素有j个,我们有(f[i]=sum_{j=1}^if[j-1] imes f[i-j]),边界(f[0]=1)

法四:catalan数

容易知道答案为(cat_n=frac{C_{2n}^n}{n+1}),其中(cat_n)表示n个0和n个1的全排列,并且保证每个位置前缀0的个数大于等于1的方案数,而栈有一个出栈操作和入栈操作,都有n次,把入栈看做0,出栈看做1,就存在一个操作序列,又要满足任何位置入栈的次数总要比出栈多,而这样一个序列唯一对应一种出栈序列,故catalan数即所求。


简单证明catalan数

证明:利用补集和一一对应

对于长度为2n的如上序列,首先如果恒不满足条件,必然可以找到一个最靠前位置(2p+1)使1的个数正好比0多一个,此时,(2p+1)的前缀0的个数有(p),1的个数有(p+1),(2p+2)的后缀,有0的个数(n-p),有1的个数(n-p-1),此时若从位置p+1开始后缀取反的话,那么总共会有0(n-1),1(n+1),显然每种方案对应一种唯一的n-1个0,n+1个1的排列的方案。

对于n-1个0,n+1个1的方案,显然可以正好找到一个最靠前位置(2p+1)满足前缀0的个数为(p),1的个数为(p+1),后面有0(n-1-p),有1(n-p),此时如果把后缀(2p+2sim 2n)取反,得到一个有(n)个0,(n)个一序列,而存在一个位置满足1的个数比0多,于是n-1个0和n+1个1的排列可以catalan序列的不合法方案。

于是易知,两者一一对应,于是我们有

[C_{2n}^n-C_{2n}^{n+1}=frac{2n!}{n!n!}-frac{2n!}{(n+1)!(n-1)!}=frac{2n! imes (n+1)}{(n+1)!n!}-frac{2n! imes n}{(n+1)!n!}= ]

[frac{2n!}{n!(n+1)!}=frac{2n!}{n!n! imes (n+1)}=frac{C_{2n}^n}{n+1}=cat_n ]


表达式计算

前缀表达式

递归处理即可

后缀表达式

栈处理,遇到一个符号,取栈中两个元素计算变为一个元素

中缀表达式

中缀表达式转后缀表达式,做法:

建立一个字符数组存储表达式序列,一个存储符号的栈

  1. 遇到数字加入表达式序列
  2. 遇到符号,将之与栈顶的符号比较优先级,平级或者栈顶符号级别高的话,则弹出栈中符号,加入表达式序列,重复操作,否则加入栈中。
  3. 遇到左括号加入符号栈,遇到右括号弹出栈中到表达式序列元素一直到遇到左括号,然后弹出左括号。

原理是优先级高的符号一定要后处理,于是需要等数输完,而优先级低的符号一定要前面的算完才能算。

最大子矩阵

问题

有n个宽度为1的长方形,第i个长方形高度为(h_i),下端对齐x轴,然后按编号顺序紧靠在一起,询问其中最大的子矩阵。

首先子矩阵必然高度为一个长方形的高度,否则再提升一个高度,结果会更加优秀,我们有能力枚举子矩阵的高度,也就是枚举矩形,现在问题是确定这个矩形向左最远能够延伸距离,和向右延伸距离,暴力扫描就是(O(n^2)),于是考虑维护,考虑单调队列,维护矩形单调递增的高度,顺带记录每个矩形向左延伸的最远的矩形编号,如果一个新的矩形进入单调队列,发现其高度小于队尾的矩形高度,于是队尾最多只可以延伸到新的矩形,此时可以算出子矩阵以队尾的矩形高度的最大值,而队尾的矩形向左延伸的矩形又可以赋值给新的矩形,最后弹出队尾,以此类推,直至不能操作为止,把新的矩形加入队尾,其实是利用了单调队列的一个性质,就是单调递增对的队列两个元素之间的所有元素都要比这两个元素大,因此可以(O(n))维护。

队列

单调队列

用途

解决单调性问题,维护单调性

性质

单调递增的队列中两个元素之间的元素必然都比这两个元素大。

链表

双向链表

使用范围

离线维护第k个数据,一次读完所有数据,倒序枚举

参考代码:

template<class free>
struct list{
	struct iter{
		iter*pre,*next;free v;
	}*head,*tail,*lt;
	il void initialize(){
		head=new iter(),tail=new iter();
		head->next=tail,tail->pre=head;
	}
	il void recycle(){
		while(head!=tail)
			head=head->next,
				delete head->pre;
		delete tail;
	}
	il void insert(iter *p,free v){
		lt=new iter{p->pre,p,v};
		p->pre=p->pre->next=lt;
	}
	il void erase(iter *p){
		p->next->pre=p->pre;
		p->pre->next=p->next;
		delete p;
	}
};

应用

邻值查找

给定一个长度为n的数列({a_i})(数列中数字互不相同),对于每个i(in[2,n]),询问(min_{j=1}^{i-1}{|a_i-a_j|})(如果值相同,选择较小的(a_j))。

解:

法一:set

注意到问题为单调性问题,答案即前i个数排序后,i的左右相邻两个数进行计算,取min即可,于是我们可以利用set在线维护这个单调性,然后在线插入后查找这个数所在位置相邻的两个数即可,时间复杂度(O(nlog(n)))

参考代码:

#include <iostream>
#include <cstdio>
#include <set>
#define il inline
#define ri register
#define intmax 0x7fffffff
using namespace std;
struct pi{
	int z,p;
	il bool operator<(const pi&x)const{
		return z<x.z;
	}
};
set<pi>S;
set<pi>::iterator l,m,r;
il void read(int&);
int main(){int n;read(n);
	for(int i(1),j,p,z;i<=n;++i){z=intmax;
		read(j),m=S.insert((pi){j,i}).first;
		if((l=m)!=S.begin())z=j-(--l)->z,p=l->p;
		if(++(r=m)!=S.end())if(z>r->z-j)z=r->z-j,p=r->p;
		if(i>1)printf("%d %d
",z,p);
	}
	return 0;
}
il void read(int &x){
	x^=x;ri char c;while(c=getchar(),c==' '||c=='
'||c=='
');
	ri bool check(false);if(c=='-')check|=true,c=getchar();
	while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
	if(check)x=-x;
}

法二:邻接表

这是一个询问维护问题,利用维护的思想,已知推未知,而且问题也没有强制在线,不妨离线,一次性读完所有数据,把({a_i})排序得到数列({b_i}),然后依次插入链表,顺带记录该元素在({a_i})中的位置,使链表有序,顺带维护({c_i})(a_i)在链表中的位置。

倒叙枚举(a_i),对于每个(i),于是我们可以快速查询到其在链表中的位置,因为此时链表是有序的,我们只要查找该元素在链表中的位置的前驱后继就可以得到答案,然后再把该数从链表中删除即可,时间复杂度(O(nlog(n)))

参考代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
#define il inline
#define ri register
#define Size 100500
#define intmax 0x7fffffff
using namespace std;
template<class free>
struct list{
	struct iter{
		iter *pre,*next;free v;
	}*head,*tail,*lt;
	il void initialize(){
		head=new iter(),tail=new iter();
		head->next=tail,tail->pre=head;
	}
	il void recycle(){
		while(head!=tail)
			head=head->next,
				delete head->pre;
		delete tail;
	}
	il iter* insert(iter *p,free v){
		lt=new iter{p->pre,p,v};
		p->pre->next=lt,p->pre=lt;
		return lt;
	}
	il void remove(iter *p){
		p->pre->next=p->next;
		p->next->pre=p->pre;
		delete p;
	}
};
struct data{
	int d,p;
	il bool operator<(const data&x)const{
		return d<x.d;
	}
}d[Size];
list<data>L;
list<data>::iter* z[Size];
il void read(int&),print(int);
il bool comp(const data&,const data&);
int main(){
	int n;read(n);
	for(int i(1);i<=n;++i)
		read(d[i].d),d[i].p=i;
	sort(d+1,d+n+1);
	L.initialize();
	for(int i(1);i<=n;++i)
		z[d[i].p]=L.insert(L.tail,d[i]);
	sort(d+1,d+n+1,comp),print(n),L.recycle();
	return 0;
}
il void print(int i){
	if(i<2)return;int j(intmax),k;
	if(z[i]->pre!=L.head)
		j=d[i].d-z[i]->pre->v.d,k=z[i]->pre->v.p;
	if(z[i]->next!=L.tail)
		if(z[i]->next->v.d-d[i].d<j)
			j=z[i]->next->v.d-d[i].d,k=z[i]->next->v.p;
	L.remove(z[i]),print(i-1),printf("%d %d
",j,k);
}
il bool comp(const data&a,const data&b){
	return a.p<b.p;
}
il void read(int &x){
	x^=x;ri char c;while(c=getchar(),c==' '||c=='
'||c=='
');
	ri bool check(false);if(c=='-')check|=true,c=getchar();
	while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
	if(check)x=-x;
}

维护中位数

题面

解:

法一:双堆对顶

因为问题可以转化为在线维护第k大的数,注意到维护第k个元素的性质,马上想到数据结构对顶,于是想到双堆对顶。

因此可以维护大根堆D,小根堆X,答案在大根堆对顶,当维护这是第几个元素的性质时,当一个元素x插入的时候,如果x大于等于D堆顶,不影响性质,加入X,否则,加入D,弹出D堆顶,加入到X堆顶。

于是可以在(O(nlog(n)))解决问题。

参考代码:

#include <iostream>
#include <cstdio>
#include <queue>
#include <vector>
#include <functional>
#define il inline
#define ri register
using namespace std;
priority_queue<int,vector<int>,less<int> >D;
priority_queue<int,vector<int>,greater<int> >X;
il void read(int&);
int main(){
	int lsy;read(lsy);
	while(lsy--){
		int id,m,i,j;read(id),read(m);
		while(D.size())D.pop();while(X.size())X.pop();
		read(j),D.push(j),printf("%d %d
%d ",id,m+1>>1,j);
		for(i=2;i<=m;++i){
			read(j);if(j>=D.top())X.push(j);
			else X.push(D.top()),D.pop(),D.push(j);
			if(i&1)D.push(X.top()),X.pop(),printf("%d ",D.top());
			if(!(i%20))putchar('
');
		}putchar('
');
	}
	return 0;
}
il void read(int &x){
	x^=x;ri char c;while(c=getchar(),c==' '||c=='
'||c=='
');
	ri bool check(false);if(c=='-')check|=true,c=getchar();
	while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
	if(check)x=-x;
}

法二:链表

询问维护问题没有强制在线,考虑离线,于是一次性读完序列({a_i})中所有元素,从小到大排序得到({b_i}),将({b_i})从小到大加入链表,顺带链表中每个元素维护其在({b_i})中的位置记作({c_i}),另外还有维护({d_i})表示每个(a_i)在链表中的位置。

倒序枚举(a_i),此时我们可以很轻松求出中位数,然后根据i的奇偶性,确定中位数的指针是否前移,然后删掉(a_i)在链表中的对应的元素(根据(d_i)),根据(c_i)可以知道删掉的元素与中位数的大小关系,从而确定指针是否后移,时间复杂度(O(nlog(n)))

参考代码

#include <iostream>
#include <cstdio>
#include <algorithm>
#define il inline
#define ri register
#define Size 100500
#define intmax 0x7fffffff
using namespace std;
template<class free>
struct list{
	struct iter{
		iter *pre,*next;free v;
	}*head,*tail,*lt;
	il void initialize(){
		head=new iter(),tail=new iter();
		head->next=tail,tail->pre=head;
	}
	il void recycle(){
		while(head!=tail)
			head=head->next,
				delete head->pre;
		delete tail;
	}
	il iter* insert(iter *p,free v){
		lt=new iter{p->pre,p,v};
		p->pre->next=lt,p->pre=lt;
		return lt;
	}
	il void remove(iter *p){
		p->pre->next=p->next;
		p->next->pre=p->pre;
		delete p;
	}
};
struct data{
	int d,p;
	il bool operator<(const data&x)const{
		return d<x.d;
	}
}d[Size];
list<data>L;
list<data>::iter* z[Size];
il void read(int&),print(int);
il bool comp(const data&,const data&);
int main(){
	int n;read(n);
	for(int i(1);i<=n;++i)
		read(d[i].d),d[i].p=i;
	sort(d+1,d+n+1);
	L.initialize();
	for(int i(1);i<=n;++i)
		z[d[i].p]=L.insert(L.tail,d[i]);
	sort(d+1,d+n+1,comp),print(n),L.recycle();
	return 0;
}
il void print(int i){
	if(i<2)return;int j(intmax),k;
	if(z[i]->pre!=L.head)
		j=d[i].d-z[i]->pre->v.d,k=z[i]->pre->v.p;
	if(z[i]->next!=L.tail)
		if(z[i]->next->v.d-d[i].d<j)
			j=z[i]->next->v.d-d[i].d,k=z[i]->next->v.p;
	L.remove(z[i]),print(i-1),printf("%d %d
",j,k);
}
il bool comp(const data&a,const data&b){
	return a.p<b.p;
}
il void read(int &x){
	x^=x;ri char c;while(c=getchar(),c==' '||c=='
'||c=='
');
	ri bool check(false);if(c=='-')check|=true,c=getchar();
	while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
	if(check)x=-x;
}

邻接表

用途

  1. 图论建边
  2. hash

hash

一般hash

思路

  1. 构造hash函数(H(x))(灵活运用(+- imes div)).
  2. 选定模数
  3. 套板子

板子:(封装)

template<class free>
struct unordered_map{
	struct data{
		data*next;free v;
	}*head[gzy],*pt;
	il void insert(free x){
		ri int key(x%gzy);
		pt=new data,pt->v=x;
		pt->next=head[key];
		head[key]=pt;
	}
	il data* find(free x){
		for(pt=head[x%gzy];pt!=NULL;pt=pt->next)
			if(pt->v==x)return pt;return NULL;
	}
};

基本模型

  1. 环上hash(拆环成链,利用同余枚举链的起点,注意反向枚举)
  2. 常用hash函数(H({a_i})=sum_{i=1}^na_i+prod_{i=1}^na_i)

字符串hash

对于字符串({s_i})

方法

  1. 将字符串看成p进制数,(序列左边为最高位)模数定为(2^{64}),即用unsigned long long类型的变量,自然溢出
  2. 预处理(base[i]=p^i)
  3. 构造前缀hash值(hs[i]),表示前i个字符的hash值,有递推公式(hs[i]=hs[i-1] imes p+s[i])
  4. 查询([l,r])的hash值,可以(hs[r]-hs[l-1] imes base[r-l+1])

兔子与兔子

标志

  1. 离散化,数字范围,数据范围小
  2. 字符串问题

其他操作

  1. 前缀hash值,和后缀hash值查询的字符串,正好顺序倒过来了(运用于最长回文子串)

最长回文子串

原题链接

定义一个字符串的子串,从前往后读和从后往前读的字符串相同,为回文子串,求长度为n字符串({a_i})中最长的回文子串。

法一:二分+hash

显然我们需要找到一种暴力做法,枚举子串左端点是不太好的,跳出思维定势,枚举回文子串的中间点,然后一个一个向两边查找,显然太慢了,于是考虑二进制优化中的二分,设二分区间为([l,r]),二分的对象是回文子串除去枚举端点的向右最远延伸距离。

对于回文子串长度为奇数,设回文子串中间的字符为i(reserve(s)表示将字符串翻转过来,其实stl也有这个函数,一个意思)

(mid=l+r>>1),如果(s[i+mid]==reserve(s[i-mid])),则代表回文子串长度还可以更长,令(l=mid+1),否则令(r=mid-1)

注意我们的二分形式是对有3种比较关系的题目而言的,而此时只有2种比较关系,相等与不相等,所以最终l肯定会过大,于是答案应该为(r),因此(l)初始应该从1开始二分,因为(r)会越界。

对于偶数的回文子串,枚举两个字符(i,i+1)

(mid=l+r>>1),如果(s[i-midsim i]=reserve(s[i+1sim i+mid+1])),令(l=mid+1),否则令(r=mid-1),同上的原因,最后的答案取(r)

那么至于如何快速查询两段字符串是否相等,我们只要维护前缀hash值和后缀hash值即可,这样不但支持快速查询,而且正好其中一个字符串相对于另外一个字符串是reserve的,时间复杂度(nlog(n)).

参考代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#define il inline
#define ri register
#define ll long long
#define ull unsigned ll
#define Size 1000100
#define jzm 19260817
using namespace std;
char s[Size];int sl,gzy;
ull ls[Size],rs[Size],base[Size];
il ull askl(int,int),askr(int,int);
template<class free>il free Min(free,free);
template<class free>il free Max(free,free);
int main(){base[0]=1;
	for(int i(1);i<=1000000;++i)
		base[i]=base[i-1]*jzm;
	while(scanf("%s",s+1)){
		sl=strlen(s+1);int i,l,mid,r,ans(0);
		if(sl==3)if(s[1]=='E'&&s[2]=='N'&&s[3]=='D')break;
		for(i=1;i<=sl;++i)ls[i]=ls[i-1]*jzm+s[i];rs[sl+1]=0;
		for(i=sl;i;--i)rs[i]=rs[i+1]*jzm+s[i];
		for(i=1;i<=sl;++i){
			l=0,r=Min(i-1,sl-i);
			while(l<=r){
				mid=l+r>>1;
				if(askr(i,i+mid)==askl(i-mid,i))l=mid+1;
				else r=mid-1;
			}ans=Max(ans,r*2+1);
			if(s[i]!=s[i+1])continue;
			l=0,r=Min(i-1,sl-i-1);
			while(l<=r){
				mid=l+r>>1;
				if(askr(i+1,i+mid+1)==askl(i-mid,i))l=mid+1;
				else r=mid-1;
			}ans=Max(ans,r*2+2);
		}printf("Case %d: %d
",++gzy,ans);
	}
	return 0;
}
template<class free>
il free Max(free a,free b){
	return a>b?a:b;
}
template<class free>
il free Min(free a,free b){
	return a<b?a:b;
}
il ull askr(int l,int r){
	return rs[l]-rs[r+1]*base[r-l+1];
}
il ull askl(int l,int r){
	return ls[r]-ls[l-1]*base[r-l+1];
}

法二:manacher算法

首先存在一个字符串技巧,也可以说是数学技巧,询问((n)+(n+1)=2n+1)是什么类型的数,显然是奇数,而如果n是一个字符串的长度,那么(n+1)就正好是它的间隔数(包括边界),两者之和为奇数。

于是如果把一个字符串这样操作,将间隔全部放上一个原字符串没有的字符如(#),原来的所有子串无论奇数长度,还是偶数长度,全部变为奇数长度,于是我们的最长回文子串也就不存在奇数长度还是偶数长度,统一为奇数长度,省去了分来讨论。

从左至右扫描,对于第i个位置而言,记录一个(mx)表示前(i-1)个位置的回文子串向右最远延伸的距离,(id)为这个回文子串的对称中心所对应的位置,设(p_i)为以第i个位置为中心的最长回文子串的长度-1再除2,记(c_i)为以第i个位置为中心的最长回文子串,显然(p_i=len(c_i)/2),接下来开始分类讨论

* (i<mx),记i关于id的对称点为(j')

  1. (j-p[j']<mx),那么此时(c[j'])有一部分被(c[id])包含,将包含的这一部分关于(id)对称得到的子串恰好为回文子串,且恰好以i为对称中心,因此(p[i])长度至少,也就是(p[j']),假设还能够在这个基础上延伸,因为(i,j')关于id对称,而(c[id])又是回文子串,那么会导致(p[j'])值应该更大,但是这个已经求出来了,故矛盾。

  2. (j-p[j']>mx),同样的道理,(p[i])至少并且为(mx-i),如果还能够延伸那么会导致(p[id])增加,矛盾。

  • (igeq mx),容易知道(c[i])可以延伸的更长,但是会使mx增大。

于是容易知道在(i<mx)的时候,查询为(O(1)),而(igeq mx),为暴力寻找,但是寻找的时候增大了(mx),于是(mx)最终移动了n步,其他的查询均为1步,于是时间复杂度为(O(n))

参考代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#define il inline
#define ri register
#define Size 1000100
using namespace std;
char s[Size],t[Size<<1];
int sl,p[Size],tt,gzy,ans;
template<class free>il free Min(free,free);
template<class free>il free Max(free,free);
int main(){t[0]='@';
	while(scanf("%s",s+1)){sl=strlen(s+1),t[tt=1]='#',ans=1;
		if(sl==3)if(s[1]=='E'&&s[2]=='N'&&s[3]=='D')break;
		for(int i(1);i<=sl;++i)t[++tt]=s[i],t[++tt]='#';
		for(int i(1),id,mx(0);i<=tt;++i){
			if(mx>i)p[i]=Min(mx-i,p[2*id-i]);else p[i]=0;
			while(t[i+p[i]+1]==t[i-p[i]-1])++p[i];
			if(p[i]+i>mx)mx=p[i]+i,id=i;ans=Max(ans,p[i]);
		}printf("Case %d: %d
",++gzy,ans);
	}
	return 0;
}
template<class free>
il free Max(free a,free b){
	return a>b?a:b;
}
template<class free>
il free Min(free a,free b){
	return a<b?a:b;
}

字符串

基础知识

字典序

从前往后比较字符大小,全部相等按长度小者小。

字符串hash

  1. 前缀后缀hash值,查询子串hash值
  2. hash+二分比较子串大小和相等关系( ext{应用}egin{cases} ext{最长回文子串}\ ext{最长公共前(后)缀}end{cases})

最长回文子串

  1. 二分+hash
  2. manacher(egin{cases} ext{n+n+1=2n+1,统一子串奇偶性}\ ext{mx,id,对称}end{cases})

后缀数组

[egin{cases}SA[i] ext{表示按字典序排序的后缀}\Heigh[i] ext{表示SA[i]和SA[i+1]的最长公共前缀}end{cases} ]

做法:排序比较的方式改成用二分比较,二分比较查询两个子串用hash实现,时间复杂度(O(nlog(n)^2))

kmp

求长度为n字符串({a_i})是否为字符串({b_i})的子串的子串。

法一:字符串hash

法二:kmp

两个数组(egin{cases}next[i],a[1sim i] ext{的前缀和后缀的最大匹配长度}\f[i], ext{A前缀,B[1~i]后缀的最大匹配长度}end{cases})

三个性质:(egin{cases}1.next[i] ext{的结果只有next[i],next[next[i]],next[next[next[i]]]...}\2. ext{两个串1~i,1~j匹配的前提为1~i-1,1~j-1匹配}\3.f[i] ext{的可能的结果为next[f[i]],next[next[f[i]]]}end{cases})

应用:子串的循环节,最小覆盖子串

最小表示法

询问环状字符串({s[i]})的用链表示的最小字典序的字符串。

做法:拆环成链后后再补一截,以i,j表示字符串开头,比较到i+k,j+k相等,谁大令谁等于自己+k+1,答案取(min(i,j))

证明:

* kmp性质1

(j=next[i],j_0=next[j]),那么有

(a[1sim j]=a[i-j+1sim i],a[1sim j_0]=a[j-j_0+1sim j])

(a[j-j_0+1sim j]in a[1sim j]Rightarrow a[j-j_0+1sim j]=a[i-j_0+1sim i])

所以(a[1sim j_0]=a[i-j_0+1sim i]),因此(j_0)可以作为(next[i])的一个决策点

设存在(j')满足(j_0<j'<j)并且是(next[i])的一个决策点,那么有

(a[1sim j']=a[i-j'+1sim i]),而(a[i-j'+1sim i]in a[i-j+1sim i]),我们有

(a[i-j'+1sim i]=a[j-j'+1sim j]),所以(a[1sim j']=a[j-j'+1sim j]),因此(j')成为一个对于(next[j])而言比(j_0)更加优秀的决策,矛盾,故的证。

  • kmp性质3

(j=f[i],j_0=next[j]),那么有

(A[1sim j]=B[i-j+1sim i],A[1sim j_0]=A[j-j_0+1sim j])

又因为(A[j-j_0+1sim j]in A[1sim j]Rightarrow A[j-j_0+1sim j]=B[i-j_0+1sim i])

所以(A[1sim j_0]=B[i-j_0+1sim i]),因此(j_0)可以成为(f[i])的决策

假设存在(j_0<j'<j),满足(j')(f[i])的决策点,那么有

(A[1sim j']=B[i-j'+1sim i]),而(B[i-j'+1sim i]in B[i-j+1sim i]),知道

(B[i-j'+1sim i]=A[j-j'+1sim j]),于是(=A[1sim j']=A[j-j'+1sim j]),于是(j')可以成为(next[j])的更优决策,矛盾,故得证。

trie树

应用

  1. 字符串快速检索
  2. 查询某位上一个字符对应的字符串集合
  3. 异或问题

例题:前缀统计

二叉堆

基础知识

堆的性质

  1. 树上任意一个节点上的权值比以它为子树的所有节点都要小,根节点为最小值。
  2. 堆是一棵完全二叉树,完全二叉树可以用数组压位,即i的子节点为(i<<1,(i<<1)+1),父亲节点为(i>>1)

代码实现

封装版

template<class free>
struct heap{
	free a[Size];int n;
	il void push(free x){
		a[++n]=x;ri int p(n);
		while(p>1)
			if(a[p]<a[p>>1])
				swap(a[p],a[p>>1]),
					p>>=1;else break;
	}
	il void pop(){
		a[1]=a[n--];ri int p(1),s(2);
		while(s<=n){
			if(s<n&&a[s+1]<a[s])++s;
			if(a[s]<a[p])
				swap(a[s],a[p]),
					p=s,s=p<<1;
			else break;
		}
	}
};

双堆对顶

动态维护第k个元素

huffman树

问题

构造一个有n个叶子节点的k叉树(注意k叉树的意思是对于父亲的儿子的个数小于等于k),第i个叶子节点的权值为(a_i),离根的距离为(l_i),最小化(sum_{i=1}^na_il_i)

二叉huffman树

就是上诉问题的k=2,树上的贪心,采取合并性贪心,根据最优性贪心,也就是朴素思想,叶子节点权值小的放在下面,权值大的放在上面,我们知道权值最小的 两个 叶子节点一定要放在下面(为什么是两个看下面的性质),否则在最优解中最小或者次小叶节点向下替换可以更加优秀。

因此对于两个最优的结果必然被合并在了一起,自然考虑合并性贪心思想,把它们捆绑成一个叶子节点(或者看成一个节点带了两个儿子,其权值为两个儿子的和),权值为两个权值之和,于是不停地寻找最优叶子节点,就可以得到最终的答案,这个利用小根堆维护就可以做到(nlog(n))

k叉huffman树

不同点在于照搬二叉,会导致合并到最后不存在足够的节点可以合并成一个节点,也就不满足上面的贪心,于是我们可以加入几个0的权值的节点,让它正好可以合并成一个节点,问题是加几个呢?

注意到每次合并减少了(k-1)个节点,最好要剩下一个节点,根据同余的知识,也就是要(nequiv 1(mod k-1)),现在这个n不一定满足条件,于是假设加了x满足了条件,因此有(x+nequiv 1(mod k-1)),也就是(xequiv 1-n(mod k-1)),于是我们只要将(1-n)的剩余系映射的正整数的任意一个数就可以了(因为不可能加负数个节点)。

性质

  1. 叶子节点权值小的靠下,权值大的靠上
  2. 不存在一个节点的叶子节点和它兄弟节点的加起来小于等于k,特殊到二叉树上也就是不存在一个节点连一个叶子节点,否则把叶子节点上提可以更优
  3. 将huffman树的父亲节点的权值定义为儿子的权值之和,那么整棵树的权值之和除掉根节点就是问题中的所求

应用

合并果子

这道题目各位已经很熟悉,不在赘诉题意了,直接讲与huffman树的关系,容易知道每次合并类似与两个叶子节点合并成一个节点,权值为这两个叶子节点的和,答案累加这个和,在huffman树下,根据性质3,容易看出答案就是所有的节点的和除去根节点的和,这是一棵二叉huffman树。

荷马史诗

这里不想赘诉题意,主要讲的不是oi上的应用,题目只是一个引入,文字在计算机中都是要被编码的(显然编为二进制),而如果一个码是另外一个码的前缀,就难以判断文本的开头,而且容易出现错误,所以编码应该是各不相同,且不存在一个是另外一个的前缀,记每个编码的长度为(l_i),文字出现的次数为(a_i),的话,容易知道(sum a_il_i)就是文本的长度,而这个问题就类似与huffman树,编码的长度就是叶子节点的深度,而编码的要求又恰恰符合是huffman树的叶子节点,而且这还是一棵二叉huffman树,因为根节点带来的记过是一个常数,将之深度定为0即可。

参考代码:

#include <iostream>
#include <cstdio>
#include <queue>
#define il inline
#define ri register
#define ll long long
#define Size 100500
using namespace std;
template<class free>
struct heap{
	free a[Size];int n;
	il void push(free x){
		a[++n]=x;ri int p(n);
		while(p>1)
			if(a[p]<a[p>>1])
				swap(a[p],a[p>>1]),
					p>>=1;else break;
	}
	il void pop(){
		a[1]=a[n--];ri int p(1),s(2);
		while(s<=n){
			if(s<n&&a[s+1]<a[s])++s;
			if(a[s]<a[p])
				swap(a[s],a[p]),
					p=s,s=p<<1;
			else break;
		}
	}
};
struct dian{
 	ll v;int h;
	il bool operator<(const dian&x)const{
		return v==x.v?h<x.h:v<x.v;
	}
};
heap<dian>H;
il int mod(int,int);
template<class free>
il void read(free&);
int main(){
	int n,k;
	int i,j,h;ll ans(0),l;
	read(n),read(k);
	for(i=1;i<=n;++i)
		read(l),H.push({l,1});
	j=mod(1-n,k-1),n+=j;
	while(j--)H.push({0,1});
	j=(n-1)/(k-1);
	while(j--){l=h=0;
		for(i=1;i<=k;++i)
			l+=H.a[1].v,h=max
				(h,H.a[1].h),H.pop();
		ans+=l,H.push({l,h+1});
	}printf("%lld
%d",ans,H.a[1].h-1);
	return 0;
}
il int mod(int x,int p){
	return (x%p+p)%p;
}
template<class free>
il void read(free &x){
	x^=x;ri char c;while(c=getchar(),c==' '||c=='
'||c=='
');
	ri bool check(false);if(c=='-')check|=true,c=getchar();
	while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
	if(check)x=-x;
}

阶段性总结

[ ext{基础数据结构}egin{cases} ext{维护的思想}egin{cases} ext{离线和在线}\ ext{已知推未知}end{cases}\ ext{栈}egin{cases} ext{性质:先进后出}\ ext{灵活运用}egin{cases} ext{双栈对顶:动态维护第k个元素}\ ext{双栈同步:动态维护前k个元素}\ ext{全局变量记录全局变化}end{cases}\ ext{出栈序列方案数}egin{cases} ext{按出栈与入栈为决策枚举}\ ext{f[i][j]=f[i-1][j+1]+f[i][j-1]}\f[i]=sum_{j=1}^{i}f[j-1] imes f[i-j]\cat_n=frac{C_{2n}^n}{n+1}end{cases}\ ext{表达式计算}egin{cases} ext{前缀表达式:递归}\ ext{后缀表达式:栈}\ ext{中缀表达式:中缀转后缀,标准:}egin{cases} ext{优先级高的先运算}\ ext{优先级低的后运算}end{cases}end{cases}\ ext{最大子矩阵:单调栈}end{cases}\ ext{队列}egin{cases} ext{双端队列}\ ext{单调队列:解决单调性问题}end{cases}\ ext{链表}egin{cases} ext{用法:离线维护,倒序处理}\ ext{应用}egin{cases} ext{邻值查找(set)}\ ext{动态维护中位数(双堆对顶)}end{cases}end{cases}\ ext{邻接表:hash和图论}\hashegin{cases} ext{一般hash}egin{cases} ext{构造hash函数(环状hash}H({a_i}=sum_{i=1}^na_i+prod_{i=1}^na_i)\ ext{选定模数(质数)}end{cases}\ ext{字符串hash}egin{cases} ext{前缀}\ ext{后缀}end{cases}end{cases}\ ext{字符串}egin{cases} ext{字典序}\ ext{字符串hash(一维,多维)}egin{cases} ext{比较子串相等}\ ext{二分+hash}egin{cases} ext{比较子串字典序:后缀数组}\ ext{比较子串最长公共相等长度:最长回文子串}end{cases}end{cases}\ ext{最长回文子串}egin{cases} ext{二分+hash}\ ext{mancher}egin{cases}n+n+1=2n+1\mx,idend{cases}end{cases}\kmpegin{cases} ext{两个数组}egin{cases}next[i] ext{a串中以i结尾的前缀的前缀和后缀匹配的最长长度}\f[i] ext{ a串的前缀和b串的以i结尾的前缀的后缀最大匹配长度}end{cases}\ ext{三个性质}egin{cases} ext{1.next[i]的次优解为next[next[i]]}\ ext{2.1~i,1~j匹配前提为1~i-1,1~j-1匹配}\ ext{3.f[i]的次优解为next[f[i]]}end{cases}\ ext{两种应用}egin{cases} ext{循环节}\ ext{最小覆盖子串}end{cases}\exkmpend{cases}\ ext{最小表示法:谁大令谁等于谁+1}\ ext{trie树}egin{cases} ext{字符串快速检索}\ ext{查询一位上字符对应的字符串}\ ext{解决异或问题}end{cases}\ ext{ac自动机}\ ext{后缀数组}egin{cases} ext{倍增+基数排序求SA}\h[i]geq h[i-1]-1O(n) ext{求height[i]}\lcp(i,j)=min_{i<kleq j}{height[k]}end{cases}\ ext{巧妙方法}egin{cases} ext{字符串拼接,分隔符}\ ext{n+n+1=2n+1}end{cases}end{cases}\ ext{二叉堆}egin{cases} ext{性质}egin{cases} ext{父亲的权值大于子树中任意一个点的权值}\ ext{该树为一棵完全二叉树}end{cases}\ ext{双堆对顶:动态维护第k大}\ ext{动态维护前k小(和)}\ ext{huffman树}egin{cases} ext{性质}egin{cases} ext{权值大靠上,权值小靠下}\ ext{不存在一枝独秀}\ ext{所有点的权值和-根节点权值和为答案}end{cases}\ ext{应用}egin{cases} ext{合并果子}\ ext{荷马史诗}end{cases}end{cases}end{cases}end{cases} ]

尾声

是不是感觉这些东西学起来很没有意义,我也有这种感觉,但是我还是搞完了,反正总结了也会忘掉,浪费了时间,也不会因此提升思维,应该搞一搞还是有好处的吧。

原文地址:https://www.cnblogs.com/a1b3c7d9/p/11234094.html