算法竞赛常用的数据结构

数据结构

单链表

const int N = 100010;

int head;//头结点下标
int e[N];//e[i]表示结点i的值
int ne[N];//结点i的下一个指针
int idx;//存储当前到了哪个结点

//初始化
void init()
{
    head = -1;
    idx = 0;
}
//将x插到头结点上
void add_to_head(int x)
{
    e[idx] = x;
    ne[idx] = head;
    head = idx ++;
}
//将x插到下标为k的后面
void add(int k, int x)
{
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx ++;
}
//删除头结点
void remove_head()
{
    head = ne[head];
}
//删除下标为k后的元素
void remove(int k)
{
    ne[k] = ne[ne[k]];
}

双链表

//e[]表示值,r[]表示右结点,l[]表示左结点,idx表示用到了哪个结点
const int N = 100010;
int e[N], r[N], l[N], idx;
//初始化
void init()
{
    //0是左端点,1是右端点
    r[0] = 1;
    l[1] = 0;
    idx = 2;//0和1用了两个位置
}
//在k右边插入x
void add(int k, int x)//k左边插入为 add(l[k], x);
{
    e[idx] = x;
    l[idx] = k;
    r[idx] = r[k];
    l[r[k]] = idx;
    r[k] = idx ++;
}
//删除k
void remove(int k)
{
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}

const int N = 100010;
//tt表示栈顶
int stk[N], tt = 0;
//入栈
stk[++ tt];
//出栈
tt --;
//栈顶值
stk[tt];
//判断栈是否为空
if(tt > 0)  not empty;
else empty;

队列

const int N = 100010;
//tt代表队尾,hh代表队首
int q[N], tt = -1, hh = 0;
//x入队
q[++ tt] = x;
//出队
hh ++;
//队头值
q[hh];
//队尾值
q[tt];
//判断是否为空
if(hh <= tt) not empty;
else empty;

单调栈

//找出每个数左边离它最近的比它小/大的数
const int N = 100010;
int stk[N], tt = 0;
for(int i = 0; i < n; i ++)
{
    while(tt && stk[tt] >= x)//栈不为空且栈顶大于等于x               大/小 改符号<
        t --				//就出栈
        if(tt) cout << stk[tt];
    
    stk[ ++ tt] = x;
}

单调队列

思路:

最小值和最大值分开来做,两个for循环完全类似,都做以下四步:

1.解决队首已经出窗口的问题;
2.解决队尾与当前元素a[i]不满足单调性的问题;
3.将当前元素下标加入队尾;
4.如果满足条件则输出结果;

需要注意的细节:

上面四个步骤中一定要先3后4,因为有可能输出的正是新加入的那个元素;
1.队列中存的是原数组的下标,取值时要再套一层,a[q[]];
2. 算最大值前注意将hh和tt重置;
3.此题用cout会超时,只能用printf;
4.hh从0开始,数组下标也要从0开始。

#include<iostream>
using namespace std;
const int N = 1000010;
int a[N], q[N];
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    int n, k, hh = 0, tt = -1;
    cin >> n >> k;
    for(int i = 0; i < n; i ++)
    {
        cin >> a[i];
        if(hh <= tt && i - k + 1 > q[hh]) hh ++;//若队首出窗口 hh+1
        while(hh <= tt && a[i] <= a[q[tt]]) 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 ++)
    {
        cin >> a[i];
        if(hh <= tt && i - k + 1 > q[hh]) hh ++;
        while(hh <= tt && a[i] >= a[q[tt]]) tt --;
        q[++ tt] = i;
        if(i >= k - 1) cout << a[q[hh]] << " ";
    }
    return 0;
}

KMP算法匹配字符串

//s[]为长串,p[]为匹配串,n为p[]长度,m为p[]长度
char s[M], p[N];
int n, m;
int ne[N];
int main()
{
    cin >> n >> p + 1 >> m >> s + 1;
    //next数组
    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;
    }
    //kmp匹配
    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)
        {
            //匹配成功
            //具体逻辑
            j = ne[j];
        }
    }
    return 0;
}

Trie树(字典树)

高效的存储和查找字符串集合的数据结构

const int N = 100010;
int son[N][26];//存储树中的每一个节点的子节点
int cnt[N];//存储以每个节点结尾的单词数量
int idx; 
char str[N];
//插入
void insert(char *str)
{
    int p = 0;//根节点
    for(int i = 0; str[i]; i ++)
    {
        int 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;
    for(int i = 0; str[i]; i ++)
    {
        int u = str[i] - 'a';
        if(!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];
}

并查集

并查集:
1.将两个集合合并
2.询问两个元素是否在一个集合当中
时间复杂度近乎O(1)
基本原理:每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点储存它的父节点,p[x]表示x的父节点。
问题1:如何判断树根:if(p[x] == x)
问题2:如何求x的集合编号:while(p[x] != x) x = p[x];(常用优化方法:路径压缩)
问题3:如何合并两个集合:px是x的集合编号,py是y的集合编号。p[x] = y;

  • 朴素并查集

    int p[N];//存储每个节点的祖宗节点
    //返回祖宗节点 + 路径压缩
    int find(int x)
    {
        if(p[x] != x)//不是祖宗节点
        p[x] = find(p[x]);
        return p[x];
    }
    //初始化 假设编号为1~n
    for(int i = 1; i <= n; i ++)
        p[i] = i;
    //将a,b集合合并
    p[find(a)] = find(b);
    
  • 维护Size的并查集

    int p[N],Size[N];//Size[]只有祖宗节点才有意义,表示此祖宗节点所在集合有多少节点
    //返回祖宗节点 + 压缩路径
    int find(int x)
    {
        if(p[x] != x)
        p[x] = find(p[x]);
        return p[x];
    }
    //初始化,编号为1~n
    for(int i = 1; i <= n; i ++)
    {
        p[i] = i;
        Size[i] = 1;
    }
    //将a,b集合合并   将a树插到b根上
    Size[find(b)] += Size[find(a)];//顺序不能变,先加后合并
    p[find(a)] = find(b);
    
  • 维护

    维护到祖宗节点距离的并查集:
    
      int p[N], d[N];
      //p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
    
      // 返回x的祖宗节点
      int find(int x)
      {
          if (p[x] != x)
          {
              int u = find(p[x]);
              d[x] += d[p[x]];
              p[x] = u;
          }
          return p[x];
      }
    
      // 初始化,假定节点编号是1~n
      for (int i = 1; i <= n; i ++ )
      {
          p[i] = i;
          d[i] = 0;
      }
    
      // 合并a和b所在的两个集合:
      p[find(a)] = find(b);
      d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量
    
    
    

const int N = 100010;
int h[N], Size;
void down(int u)
{
    int t = u;
    if(u * 2 <= Size && h[u * 2] < h[t]) t = u * 2;//左儿子存在且左儿子小于父亲,t= 左儿子
    if(u * 2 + 1 <= Size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;//右儿子存在且右儿子小于父亲,t= 右儿子
    if(u != t)//如果根节点不是最小
    {
        swap(h[u], h[t]);//交换
        down(t);//递归处理
    }
}
void up(int u)
{
    while(u / 2 && h[u / 2] < h[u])//有父节点且父节点大于子节点
    {
        swap(h[u / 2], h[u]);
        u /= 2;
    }
}
//初始化预处理堆O(n)
for(int i = n / 2; i >= 0; i ++)
    down(i);
//堆排序
#include<iostream>
using namespace std;
const int N = 100010;
int h[N], Size;
void down(int u)
{
    int t = u;
    if(u * 2 <= Size && h[u * 2] < h[t]) t = u * 2;
    if(u * 2 + 1 <= Size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if(u != t)
    {
        swap(h[u],h[t]);
        down(t);
    }
}
int main()
{
    int n, m;
    cin >> n >> m;
    Size = n;
    for(int i = 1; i <= n; i ++)
        cin >> h[i];
    for(int i = n / 2; i ; i --)
    down(i);
    while(m --)
    {
        cout << h[1] << " ";
        h[1] = h[Size];
        Size --;
        down(1);
    }
    return 0;
}

哈希表

在算法竞赛中,我们常采用0x3f3f3f3f来作为无穷大。0x3f3f3f3f主要有如下好处:
0x3f3f3f3f的十进制为1061109567,和INT_MAX一个数量级,即109数量级,而一般场合下的数据都是小于109的。
0x3f3f3f3f * 2 = 2122219134,无穷大相加依然不会溢出。
可以使用memset(array, 0x3f, sizeof(array))来为数组设初值为0x3f3f3f3f,因为这个数的每个字节都是0x3f。

//**拉链法**
const int N = 100010;
int e[N], ne[N], h[N], idx;
//哈希表中插入一个数
void insert(int x)
{
    int k = (x % N + N) % N;
    e[idx] = x;
    ne[idx] = h[k];
    h[k] = idx ++;
}
//哈希表中查找一个数
bool find(int x)
{
    int k = (x % N + N) % N;
    for(int i = h[k]; i != -1; i = ne[i]);
    	if(e[i] == x)
            return true;
    return false;
}
#include<cstring>
memset(h, -1, sizeof h);//初始哈希表全赋值-1


//**开放寻址法
const int N = 200010, NULL = 0x3f3f3f3f;//N开2~3倍
int h[N];
int find(int x)
{
    int k = (x % N + N) % N;
    while(h[k] != NULL && h[k] != x)//如果没坑且值不是找的,往后走
    {
        k ++;
        if(k == N)//如果全走完没坑回到开头
            k = 0;
    }
    return k;
}
memset(h, 0x3f, sizeof h);

字符串哈希(懵的批爆)

核心思想:将字符串看成P进制数,P的经验值是13113331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果

typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64

// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
    h[i] = h[i - 1] * P + str[i];
    p[i] = p[i - 1] * P;
}

// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}
作用:查找两端之间内的字符串是否相等
#include<iostream>
using namespace std;
typedef unsigned long long ULL;
const int N = 100010, P = 131;
ULL h[N], p[N];
char str[N];
ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
    int n, m;
    cin >> n >> m >> str + 1 ;
    p[0] = 1;
    for(int i = 1; i <= n; i ++)
    {
        p[i] = p[i - 1] * P;
        h[i] = h[i - 1] * P + str[i];
    }
    while(m --)
    {
        int l1, r1, l2, r2;
        cin >> l1 >> r1 >> l2 >> r2;
        if(get(l1, r1) == get(l2, r2))
        cout << "Yes" << endl;
        else
        cout << "No" << endl;
    }
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值