【算法竞赛备赛】数据结构

文章目录

  • 前言
    • cin, cout加速代码句
  • 链表
  • 单链表
    • 双链表
    • 单调栈
  • 队列
    • 普通队列
    • 循环队列
    • 单调队列
  • KMP
  • Trie
  • 并查集
    • 朴素并查集
    • 维护集合大小的并查集
  • 堆(Heap)
    • 堆排序
    • 带映射堆
    • 模拟堆
  • Hash表(散列表)
    • 冲突处理方式
      • 开放寻址法
      • 拉链法
    • 字符串哈希
  • 参考资料

前言

记录算法竞赛中经过考察的数据结构,其中包括树与图的存储,高级数据结构并查集,Hash表的实现,KMP,栈与队列,以及STL中各容器的使用,需要重点掌握

cin, cout加速代码句

cin.tie(0);
ios::sync_with_stdio(false);

链表

单链表

// head表示链表头
// e表示当前结点下的值
// ne表示当前结点的下一结点
// idx相当于指针
int head, e[N], ne[N], 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++;
}

// 下标是k的点的后一个点移除
void remove(int k) {
    ne[k] = ne[ne[k]];
}

AcWing826.单链表

双链表

int e[N], l[N], r[N], idx;

// 将0定义为头结点,1定义为尾结点
void init() {
    r[0] = 1, l[1] = 0, idx = 2;
}

// 在第k个点的右侧插入x
void add(int k, int x) {
    e[idx] = x;
    r[idx] = r[k];
    l[idx] = k;
    l[r[k]] = idx;
    r[k] = idx++;
}

// 移除第k个位置上的点
void remove(int k) {
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}

// 在最左边插入一个结点
void insertL(int x)
{
    e[idx] = x;
    l[idx]=0;
    r[idx]=r[0];
    r[0]=idx;
    l[r[idx]]=idx;
    idx++;
}

// 在最右边插入一个结点
void insertR(int x)
{
    e[idx] = x;
    r[idx]=1;
    l[idx]=l[1];
    r[l[idx]]=idx;
    l[1]=idx++;
}

AcWing827.双链表

数组模拟栈

int stk[N], tt;

// 插入元素
stk[++tt] = x;

// 弹出元素
tt--;

// 取栈顶值
stk[tt];

// 判断栈是否为空
if (tt > 0) {
    
}

单调栈

从当前位置的左边找到一个离它最近且比它小的数

维护一个栈,来实现上述问题

引申:每当倒序寻找一个数时,可以考虑用栈来简化问题,减少时间复杂度

// 时间复杂度o(n),栈里的每个元素只进栈出栈一次,2n次操作
int stk[N], tt;

while (n -- ) {
        int x;	scanf("%d", &x);
        while(tt && stk[tt] >= x) tt--;
    	...
        stk[++tt] = x;
    }

队列

普通队列

// hh队头,tt队尾
int q[N], hh, tt = -1;

// 入队
q[++tt] = x;

// 出队
hh++;

// 队头的值
q[hh];

// 队列是否为空
if (hh <= tt) {
    
}

循环队列

// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;

// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;

// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;

// 队头的值
q[hh];

// 判断队列是否为空
if (hh != tt) {
    
}

单调队列

维护一个单调队列,队头元素即为最小值

// 维护一个单调递增的队列
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;
    ...
}

AcWing154.滑动窗口

KMP

重点为对next数组的理解(当前学的时候就没掌握,终于在y总的带领弄懂了^_^

next[i] = j;

表示的是在子串中,p[1, j] = p[i - j + 1, i]

即当前位置往前数的j个,与该串开头的j个相等(相当于一个回退的过程)

作用:KMP中next数组的出现,使得子串在与主串比较时,不必从头开始比较。在子串往后移动的过程中,(假设成功匹配)必然会出现与原有位置重合的部分,而next数组就是为了记录这一重合部分的长度,避免了 o ( n 2 ) o(n^2) o(n2)的时间复杂度

// s[]是长文本,p[]是模式串,m是s的长度,n是p的长度
// 求next数组
for (int i = 2, j = 0; i <= n; i++) {
    // next[1] = 0,所以下标从2开始
    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];
    }
}

Trie

维护一个根结点为0的树,来处理字符串集合(精确到每一个字符),提供字符串的插入查询操作

/**
* 0既是根结点,又是空结点
* son[][]存储树中每个结点的子结点
* cnt[]存储以某个结点结尾的单词的数量
* idx为每个结点编号
*/
int son[N][26], cnt[N], idx;

void insert(char *str) {
    int p = 0;
    
    for (int i = 0; str[i]; i++) {
        // 将小写字母映射为0-25的整数
        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];
}

并查集

  • 并:将两个集合合并
  • 查:询问两个元素是否在同一个集合当中

基本原理:每个集合用一棵树表示,树根就代表集合的编号。对于操作,遍历查找该点的父结点,判断父结点是否相同即可。对于,相当于两棵树的合并。

朴素并查集

//存储每个点的祖宗节点
int p[N]; 

// 返回x的祖宗节点(核心)
int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]); // 路径压缩
    return p[x];
}

// 初始化,让每个结点独立成根
for (int i = 1; i <= n; i ++ ) p[i] = i;

// 将a并到b
p[find(a)] = find(b);

维护集合大小的并查集

//存储每个点的祖宗节点
int p[N], size[N]; 

// 返回x的祖宗节点(核心)
int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

// 初始化,让每个结点独立成根
for (int i = 1; i <= n; i ++ ) {
    p[i] = i;
    size[i] = 1;
}

// 将a并到b
if (find(a) == find(b)) continue; // 如果a与b已经连通,则不进行操作
size[find(b)] += size[find(a)];
p[find(a)] = find(b);

// 返回x结点所在集合结点的个数
size[find(x)];

堆(Heap)

通常是一个可以被看做一棵完全二叉树(当树的深度为h时,前h-1层的结点数都达到最大个数,第h层的结点均集中在最左边)的数组对象

将根结点最大的堆叫做最大堆大根堆,根结点最小的堆叫做最小堆小根堆

手写堆支持以下操作:

  1. 插入一个数
  2. 求集合当中的最小值
  3. 删除最小值
  4. 删除任意一个元素
  5. 修改任意一个元素
// 下标从1开始,2x为左结点,2x+1为右结点
// size指向堆的最后一个元素
heap[++size] = x; up(size);
heap[1];
heap[1] = heap[size]; size--; down(1);
heap[k] = heap[size]; size--; down(k); up(k);
heap[k] = x; down(k); up(k);

堆排序

// 建堆
int h[N], size;
for (int i = n / 2; i; i--) down(i);


void down(int u) {
    int t = u;
    if (2 * u <= size && h[2*u] < h[t]) t = 2 * u;
    if (2 * u + 1 <= size && h[2*u+1] < h[t]) t = 2 * u + 1;
    
    if (t != u) {
        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;
    }   
}

AcWing838.堆排序

带映射堆

  • ph[]存储的是第x个插入的元素是……(对应到堆的下标)
  • hp[]存储的是堆中的某个数是第几个插入的
int h[N], ph[N], hp[N], size;

for (int i = n / 2;i ; i--) down(i);

void heap_swap(int a, int b) {
    // 因这里的a, b是堆中的数,所以先用hp[a]转换成第几个插入的数
    swap(ph[hp[a]], ph[hp[b]]);
    swap(hp[a], hp[b]);
    swap(h[a], h[b]);
}

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 (t != u) {
        heap_swap(t, u);
        down(t);
    }
}

void up(int u) {
    while (u / 2 && h[u / 2] > h[u]) {
        heap_swap(u/2, u);
        u /= 2;
    }
}

模拟堆

#include 
#include 
using namespace std;

const int N = 1e5 + 10;
int h[N], ph[N], hp[N], sz;


void heap_swap(int a, int b) {
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a], hp[b]);
    swap(h[a], h[b]);
}

void down(int u) {
    int t = u;
    if (u*2 <= sz && h[u*2] < h[t]) t = 2*u;
    if (u*2+1 <= sz && h[u*2+1] < h[t]) t = 2*u+1;
    
    if (t != u) {
        heap_swap(t, u);
        down(t);
    }
}

void up(int u) {
    while (u/2 && h[u/2] > h[u]) {
        heap_swap(u/2, u);
        u /= 2;
    }
}

int main() {
    int n, m = 0;
    scanf("%d", &n);
    
    while (n -- ) {
        char op[10];
        int k, x;
        scanf("%s", op);
        
        if (!strcmp(op, "I")) {
            scanf("%d", &x);
            sz++;
            m++;
            h[sz] = x;
            ph[m] = sz;
            hp[sz] = m;
            up(sz);
        } else if (!strcmp(op, "PM")) {
            printf("%d\n", h[1]);
        } else if (!strcmp(op, "DM")) {
            heap_swap(1, sz);
            sz--;
            down(1);
        } else if (!strcmp(op, "D")) {
            scanf("%d", &k);
            k = ph[k];
            heap_swap(k, sz);
            sz--;
            down(k), up(k);
        } else if (!strcmp(op, "C")) {
            scanf("%d%d", &k, &x);
            k = ph[k];
            h[k] = x;
            down(k), up(k);
        }
    }
    
    return 0;
}

Hash表(散列表)

散列表,又叫哈希表(Hash Table),是能够通过给定的关键字的值直接访问到具体对应的值的一个数据结构。也就是说,把关键字映射到一个表中的位置来直接访问记录,以加快访问速度。

典型的以空间换时间的存储结构

// 取质作为哈希函数
for (int i = 100000;;i++) {
    bool flag = true;
    for (int j = 2; j*j < i; j++) {
        if (i % j == 0) {
            flag = false;
            break;
        }
    }
    if (flag) {
        cout << i << endl;
        break;
    }
}

冲突处理方式

由于Hash表是将大规模的数映射到一个相对较小的数组空间上,必然会出现两个值的键相同的情况,这被称为哈希冲突,也叫哈希碰撞

开放寻址法

若冲突,则直接往后一位

const int N = 200003, null = 0x3f3f3f3f;
int h[N];

memset(h, 0x3f, sizeof h);

// 能找到,则返回这个数的下标;找不到,则返回该数应该插入的位置
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;
}

拉链法

单链表的形式来处理冲突

int h[N], e[N], ne[N], idx;

memset(h, -1, sizeof h); // #include 

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;
} 

字符串哈希

重点掌握,判等字符串的一把利剑!!!

  1. 将字符串看成P进制的数
  2. 转换为十进制 mod Q

1)经验值 : P = 131 o r 13331 P=131or13331 P=131or13331, Q = 2 64 Q=2^{64} Q=264

2)计算任一前缀的字符串哈希:

h ( i ) = h ( i − 1 ) ∗ P + s t r ( i ) h(i) = h(i - 1) * P + str(i) h(i)=h(i1)P+str(i)

3)计算出任一子串(L->R)的哈希值:

h [ R ] − h [ L − 1 ] ∗ P R − L + 1 h[R] - h[L-1]*P^{R-L+1} h[R]h[L1]PRL+1(令两者同次)

注意,子串哈希值的大小只和它的内容有关,和在原字符串中的位置无关

typedef unsigned long long ull; // (2^64)
const int P = 131 or 13331;

// 存储字符串
char str[N];
// h[]存储每一前缀和的哈希值,p[]存储经验值的各次方
ull h[N], p[N];

// 初始化经验值和前缀和的哈希值
p[0] = 1;
for (int i = 1; i <= n; i ++ ) {
    p[i] = p[i - 1] * P;
    h[i] = h[i - 1] * P + str[i];
}

// 计算l-r这一子区间的哈希值,从而判断两个子串是否相同
ull get(int l, int r) {
    return h[r] - h[l - 1] * p[r - l + 1];
}

参考资料

AcWing算法基础课

你可能感兴趣的:(算法竞赛,数据结构,算法)