二、数据结构
(一).链表与邻接表
1.单链表:可实现图和树(AcWing 826.单链表)
简介:在算法竞赛中链表多为静态链表,即利用数组实现;而不是申请空间(动态链表)
(1)算法思想:
想象一个由head指针依次指向多个结点,形成一个链条。每个结点具有e[](结点的数值),ne[](指向的下一个结点的序号)。
(2)代码实现思路:
首先设置e[] ne[]两个数组,设置idx表示当前点的序号。
初始化链表:将head头指针指向的结点序号设为-1,idx指向第一个结点,序号为0;
插入在头结点后面:先将该点的值赋值给e[idx],再将该点的next指针指向head的序号,即ne[idx]=head,再将head结点的序号设为该结点的序号,即ne[head]=idx,最后将idx指向下一个要插入的结点
插入在下标为k的结点后面:先将该点的值赋值给e[idx],再将该点的next指针指向k结点的next指针,即ne[idx]=ne[k],再将k结点的next指针指向该结点,即ne[k]=idx,最后将idx指向下一个要插入的结点(ps:若想在某一结点前面插入只能从头遍历)
删除下标为k的后面的结点:将k结点的next指针指向k结点的下一个结点的下一个结点,即ne[k]=ne[ne[k]]
因序号idx从0开始,因此第k个结点后面的结点,即第k-1个结点后面的结点。
(3)代码实现:
#include <iostream>
using namespace std;
const int N=1e6+10;
int e[N],ne[N],idx,head;
//初始化
void init()
{
head=-1;
idx=0;
}
//插入在头节点后面
void add_to_head(int x)
{
e[idx]=x;
ne[idx]=head;
head=idx;
idx++;
}
//插入在第K个节点后面
void add(int k,int x)
{
e[idx]=x;
ne[idx]=ne[k];
ne[k]=idx;
idx++;
}
//删除第K个结点后面的结点
void remove(int k)
{
ne[k]=ne[ne[k]];
}
int main() {
int m;
cin>>m;
init();
while(m--)
{
char s;
cin>>s;
if(s=='D')
{
int k;
cin>>k;
if(k==0) head = ne[head];
else remove(k-1);
}
else if(s=='H')
{
int x;
cin>>x;
add_to_head(x);
}
else
{
int k,x;
cin>>k>>x;
add(k-1,x);
}
}
for(int i=head;i!=-1;i=ne[i]) cout<<e[i]<<' ';
return 0;
}
2.双链表(AcWing 827.双链表)
(1)算法思想:
在单链表的基础上,将ne[]换成左右两个指针数组l[],r[],依次指向结点的左节点和右节点
(2)代码实现思路:
整体思路与单链表一致
首先设置e[] l[] r[]两个数组,设置idx表示当前点的序号。
初始化链表:将头节点设为序号0,尾结点设为序号1,使得头节点的r[0]=1,尾结点的l[1]=0;
插入在下标为k的结点后面:先将该点的值赋值给e[idx],再将该点的r指针指向k结点的r指针,即r[idx]=r[k],将该点的l指针指向k结点,即l[idx]=k,再将k结点的右节点的l指针指向该结点,即l[r[k]]=idx,最后将将k结点的r指针指向该结点,即r[k]=idx。
删除下标为k的后面的结点:将k结点的左节点的r指针指向k结点的右节点,即r[l[k]]=r[k],将k结点的右节点的l指针指向k结点的左节点,即l[r[k]]=l[k]
注意:由于idx从2开始,因此第k个插入的点后面相当于k+1。
(3)代码实现:
#include <iostream>
#include <string>
using namespace std;
const int N=1000010;
int r[N],l[N],e[N],idx,head;
int n;
void init()
{
r[0]=1;
l[1]=0;
idx=2;
}
void add(int k,int x)
{
e[idx]=x;
r[idx]=r[k];
l[idx]=k;
l[r[k]]=idx;
r[k]=idx;
idx++;
}
void remove(int k)
{
l[r[k]]=l[k];
r[l[k]]=r[k];
}
int main()
{
cin>>n;
init();
while(n--)
{
string op;
cin>>op;
if(op=="L")
{
int x;
cin>>x;
add(0,x);
}
else if(op=="R")
{
int x;
cin>>x;
add(l[1],x);
}
else if(op=="D")
{
int k;
cin>>k;
remove(k+1);
}
else if(op=="IL")
{
int k,x;
cin>>k>>x;
add(l[k+1],x);
}
else
{
int k,x;
cin>>k>>x;
add(k+1,x);
}
}
for(int i=r[0];i!=1;i=r[i]) cout<<e[i]<<" ";
}
(二).栈与队列
1.栈(AcWing 828.模拟栈)
(1)算法思想:
先进后出
(2)代码实现:
#include <iostream>
#include <string>
using namespace std;
const int N=1000010;
int st[N],tt;
int main()
{
int n;
cin>>n;
while(n--)
{
string op;
cin>>op;
if(op=="push")
{
int x;
cin>>x;
st[++tt]=x;
}
else if(op=="pop")
{
tt--;
}
else if(op=="query")
{
cout<<st[tt]<<endl;
}
else
{
if(tt>0) puts("No");
else puts("Yes");
}
}
return 0;
}
2.队列(AcWing 829.模拟队列)
(1)算法思想:
先进先出
(2)代码实现:
#include <iostream>
#include <string>
using namespace std;
const int N=1000010;
int queue[N],hh,tt=-1;
int main()
{
int n;
cin>>n;
while(n--)
{
string op;
cin>>op;
if(op=="push")
{
int x;
cin>>x;
queue[++tt]=x;
}
else if(op=="pop")
{
hh++;
}
else if(op=="query")
{
cout<<queue[hh]<<endl;
}
else
{
if(hh<=tt) puts("No");
else puts("Yes");
}
}
return 0;
}
3.单调栈(AcWing 830.单调栈)
(1)算法思想:
由于输出的是每一个位置左侧离他最近的比他小的数min,且是在当前位置从后往前遍历,符合先进后出的规则,可设置一个栈,从栈顶开始寻找比这个位置小的数,找到结束循环,没找到就弹出栈顶元素。同时弹出的元素不会是之后的元素的min,因为如果假设是之后元素的min,但是弹出的元素大于当前元素,因此如果弹出的元素是后面的元素的min,那么当前元素一定更靠近后面的元素,所以应该是这个元素最靠近后面的元素,因此符合规则,最后将当前元素压入栈中。
(2)代码实现思路:
当栈顶元素大于当前元素同时栈不空,就弹栈,即tt--,否则结束循环。若最后tt不为0,则输出,若为0,则输出1,最后将当前元素入栈。
(3)代码实现:
#include <iostream>
using namespace std;
const int N = 1e6+10;
int stk[N],tt=0;
int main() {
int n;
scanf("%d",&n);
while(n--)
{
int x;
scanf("%d",&x);
while(tt&&stk[tt]>=x) tt--;
if(tt) printf("%d " ,stk[tt]);
else printf("-1");
stk[++tt]=x;
}
return 0;
}
3.单调队列(AcWing 154. 滑动窗口)
(1)算法思想:
以输出最小值为例,当窗口内的元素个数达到k时,输出该窗口的最小值。考虑暴力算法,每滑动一次窗口,都比较k次,寻找到最小值,注意到如果左侧的数字大于右侧的数据,则可以在下一次滑动前删除左侧的数据,因此减少比较的次数。因此可以设置一个滑动窗口队列(队头在左,队尾在右),比较队尾和当前即将进入滑动窗口的数字大小,如果队尾数字大,则删除队尾。因此该队列中的数字全都小于待插入的数字,并且队头是最小的数字,再将该元素插入到队尾,最后输出队头即可。
(2)代码实现思路:
先确保滑动窗口队列的队头仍在窗口内,否则队头右移,接着比较待插入元素和队尾的关系,如果队尾大,则删队尾,再插入待插元素,当已插入的序号个数大于窗口值再进行输出,输出最小值,即队头。
(3)代码实现:
#include <iostream>
using namespace std;
const int N= 1e6+10;
int a[N],q[N];
int main() {
int n,k;
cin>>n>>k;
for(int i=0;i<n;i++) cin>>a[i];
int hh=0,tt=-1;
for(int i=0;i<n;i++)
{
if(hh<=tt&&i-k+1>q[hh]) hh++;//判断队头是否已经滑出窗口,即当前序号与队头序号是否在同一窗口内,若不在,则将队头向右移动
while(hh<=tt&&a[q[tt]]>=a[i]) tt--;
q[++tt]=i;
if(i>=k-1) cout<<a[q[hh]]<<" ";
}
cout<<endl;
hh=0,tt=-1;
for(int i=0;i<n;i++)
{
if(hh<=tt&&i-k+1>q[hh]) hh++;
while(hh<=tt&& a[q[tt]]<=a[i]) tt--;
q[++tt]=i;
if(i>=k-1) cout<<a[q[hh]]<<" ";
}
return 0;
}
(三).KMP算法
简介:常用于字符串匹配,利用字符串的前缀和后缀相同优化算法时间复杂度。
1.AcWing 831.KMP字符串匹配
(1)算法思想:
令字符串第一个字符的序号为1,首先对需要查找的字符串p设置一个next数组:记录每一个i位置最长的公共前后缀的长度(即p[1~j]=p[i-j+1~i]);再根据next数组寻找p在总字符串s出现的位置,即比较i指针和j指针的字符相同,若相同,j指针前移,否则退回到next[j]位置,再比较j+1与i位置是否相等,若仍不相等,则继续退回next[j],当j指针移动至p数组的末尾时说明查找完成,返回此时p在s的位置。(注意i,j指针的同步性)
(2)代码实现思路:
字符串的起始序号设置为1,创建next数组:对于p字符串而言,如果i指针与j指针下一个字符不相等,则将j指针移动到ne[j],如果相等,则将j向右移动一个位置。再将i指针的Next数组设置为当前的j。总而言之,就是寻找每一个i位置的ne[j],使得p[1~j]=p[i-j+1~i],这样在后续匹配中,当j指针与模式串指针i匹配失败时,不用依次往后移动,而是直接移动j-ne[j]个位置。
匹配:类似于上述的操作,只是将i指针在s字符串中移动,如果i指针与j指针下一个字符不相等,则将j指针移动到ne[j],如果相等,则将j向右移动一个位置。如果j移动到p数组的末端,即j=n,则输出当前p在s的位置,即i-n,同时利用next数组快速进行下一次寻找,即输出之后将j设置为ne[j]。
(3)代码实现:
#include <iostream>
using namespace std;
const int N =1e6+10;
char s[N],p[N];
int n,m;
int ne[N];
int main() {
cin>>n>>p+1>>m>>s+1;//将字符串p,s每一个字符向后移动一个位置
for(int i=2,j=0;i<=n;i++)
{
while(j&&p[i]!=p[j+1]) j=ne[j];
if(p[i]==p[j+1]) j++;
ne[i]=j;
}
for(int i=1,j=0;i<=m;i++)
{
while(j&&s[i]!=p[j+1]) j=ne[j];
if(s[i]==p[j+1]) j++;
if(j==n)
{
printf("%d ",i-n);
j=ne[j];
}
}
return 0;
}
(四).Trie(字典树)
1.Acwing 835.Trie字符串统计
(1)算法思想:
利用树,将边看成字母,结点看成点的序号。插入:设置一个son[p][u]数组,其中p代表结点序号,u代表该节点指向的下一个结点的边的字符(用26个数字对应26个字母),每插入一个字符,检查该字符指向的边是否已存在(即它的前缀字母已经被记录过),若不存在则创建一个边存储该字符,再更新序号,将一个单词录入完成后,p应该为最后一个字母指示的结点,因此可通过cnt[p]数组记录该单词的记录次数。
(2)代码实现思路:
插入:首先设置p结点为0,即根节点,将待插入的单词每一个字母转化为数字,从头开始遍历,若该字母已经插入过,则将p结点更新为当前序号,若没有插入过,则创建一个结点,并更新当前序号,即son[p][u]=++idx,当一个单词读入完成后,将该单词的个数加1,即将cnt[p]++
查询:首先设置p结点为0,即根节点,将待插入的单词每一个字母转化为数字,从头开始遍历,若该字母已经插入过,则将p结点更新为当前序号,若没有插入过,则返回0,当单词读入完成,返回cnt[p]即为该单词的存入次数。
(3)代码实现:
#include <iostream>
using namespace std;
const int N=1e5+10;
char str[N];
int son[N][26],cnt[N];
int n,idx;
void insert(char str[])
{
int p=0,u;
for(int i=0;str[i];i++)
{
u=str[i]-'a';
if(!son[p][u]) son[p][u]=++idx;
p=son[p][u];
}
cnt[p]++;
}
int query(char str[])
{
int p=0,u;
for(int i=0;str[i];i++)
{
u=str[i]-'a';
if(!son[p][u]) return 0;
p=son[p][u];
}
return cnt[p];
}
int main() {
cin>>n;
while(n--)
{
char o;
cin>>o>>str;
if(o=='I') insert(str);
else cout<<query(str)<<endl;
}
return 0;
}
2.Acwing 143. 最大异或对
(1)算法思想:
建立trie树:读入每一个数字,将该数字转化为二进制,从根节点开始记录每个数的最高位,若未被记录过就插入一个新的点。
求某一个数的最大异或值:从高位开始寻找与该数字该位相反的结点,若找到则将res加上1右移该位置的位数,若找不到则只能走到相同的点,最后返回res。
求最大异或值,枚举所有数,执行上面的函数,最后返回最大的res。
(2)代码实现思路:
插入(建树):设置p=0,表示从根节点开始枚举,由于本题最大数字为2^31,因此循环枚举某数的二进制最高位可以从30开始,到0结束,(i>=0可以改写为~i),然后用u记录该数在第i位的数字(见二进制表示第k位数字),如果树中没有此结点,就新建一条边指向新结点,再更新p结点为刚刚的结点。
查询某数的最大异或值:设置p=0,res=0,从根节点开始枚举,与上面的写法差不多,后用u记录该数在第i位的数字,如果存在与u相反的结点(即s[p][u]不为0),那么就将res加上1左移u,同时更新p为该相反的结点。否则不存在与u相反的结点的话,只更新p结点到下一个结点即可。最后返回res。
(3)代码实现:
#include <iostream>
#include <algorithm>
using namespace std;
const int N=100010,M=3000000;
int son[M][2],idx=0,a[N];
int n,res;
void insert(int x)
{
int p=0;
for(int i=30;~i;i--)
{
int u=x>>i&1;
if(!son[p][u]) son[p][u]=++idx;
p=son[p][u];
}
}
int query(int x)
{
int res=0,p=0;
for(int i=30;~i;i--)
{
int u=x>>i&1;
if(son[p][!u])
{
res+=1<<i;
p=son[p][!u];
}
else
p=son[p][u];
}
return res;
}
int main()
{
cin>>n;
for(int i=0;i<n;i++)
{
cin>>a[i];
insert(a[i]);
}
int res=0;
for(int i=0;i<n;i++) res=max(res,query(a[i]));
cout<<res;
return 0;
}