算法X —— 基础篇

文章目录

数据结构

栈,队列

  • s t a c k < T > stack<T> stack<T>
  • q u e u e < T > queue<T> queue<T>

滑动窗口

无重复字符的最长子串

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        unordered_set<char> us;
        int left = 0, right = 0;
        int out = 0;
        while (right < s.size()) {
            us.emplace(s[right++]);
            out = max(out, right - left);
            while (!us.empty() && us.count(s[right]))
                us.erase(s[left++]);
        }
        return out;
    }
};

滑动窗口中位数

class Solution {
public:
    // 方法1 滑动窗
    vector<double> medianSlidingWindow_1(vector<int>& nums, int k) {
        int N = nums.size();
        vector<double> med;
        multiset<int> win(nums.begin(), nums.begin() + k);
        auto mid = next(win.begin(), k / 2);
        for (int i = k; i < N + 1; i++) { // N + 1 是为了最后一个
            double medVal = double(*mid) + double(*next(mid, k % 2 - 1));
            med.push_back(medVal / 2);
            if (i == N) continue;
            win.insert(nums[i]);
            if (nums[i] < *mid) mid--;
            if (nums[i - k] <= *mid) mid++;
            win.erase(win.lower_bound(nums[i - k]));
        }
        return med;
    }
    // 方法2 堆
    vector<double> medianSlidingWindow(vector<int>& nums, int k) {
        int N = nums.size();
        vector<double> med;
        unordered_map<int, int> hash_tab;
        priority_queue<int> smaller; // max heap
        priority_queue<int, vector<int>, greater<int>> bigger; // min heap
        int idx = 0;
        while (idx < k) smaller.push(nums[idx++]);
        for (int j = 0; j < k / 2; j++) {
            bigger.push(smaller.top());
            smaller.pop();
        }
        while (true) {
            med.push_back(k & 1 ? smaller.top() :
                0.5 * (double(smaller.top()) + double(bigger.top())));
            if (idx == nums.size()) break;
            int out = nums[idx - k];
            int ins = nums[idx++];
            int balance = 0; // is balanced
            balance += out <= smaller.top() ? -1 : 1;
            hash_tab[out]++;
            if (!smaller.empty() && ins <= smaller.top()) {
                balance++;
                smaller.push(ins);
            } else {
                balance--;
                bigger.push(ins);
            }
            // re-balanced heaps
            if (balance < 0) {
                smaller.push(bigger.top());
                bigger.pop();
                balance++;
            }
            if (balance > 0) {
                bigger.push(smaller.top());
                smaller.pop();
                balance--;
            }
            // remove invalid nodes
            while (hash_tab[smaller.top()]) {
                hash_tab[smaller.top()]--;
                smaller.pop();
            }
            while (!bigger.empty() && hash_tab[bigger.top()]) {
                hash_tab[bigger.top()]--;
                bigger.pop();
            }
        }
        return med;
    }
};

并查集

  • 处理集合
class uf { // 并查集 (不可删除节点)
private:
	int con;
	vector<int> sz;
	vector<int> pa;
public:
    uf (int n) {
        for (int i = 0; i < n; ++i) {
            sz.push_back(1);
            pa.push_back(i);
        }
        this->con = n;
    }
    void merge(int x, int y) { // union
        // 按秩合并
        int rx = find(x);
        int ry = find(y);
        if (rx == ry) return;
        if (sz[rx] > sz[ry]) {
            pa[ry] = rx;
            sz[rx] += sz[ry];
        } else {
            pa[rx] = ry;
            sz[ry] += sz[rx];
        }
        con--;
    }
    int find(int x) {
        // 路径压缩
        while (pa[x] != x) {
            pa[x] = pa[pa[x]];
            x = pa[x];
        }
        return x;
    }
    bool connect(int x, int y) {
        return find(x) == find(y);
    }
    int count() {
        return con;
    }
};

STL 容器

  • Set,Map,Priority_queue
  • 慢,但是好用,能过很多题

倍增思想

  • 顾名思义,备注就是步长成倍增加
  • 一般都是 成 2倍
  • 任何数都可以用 2的幂次方序列的和 来表示

区间最值查询 RMQ

  • 数列区间的最大值:输入一串数字,给你 M 个询问,每次询问就给你两个数字 X,Y,要求你说出 X 到 Y 这段区间内的最大数
  • Range Minimum/Maximum Query
  • 线段树算法 预处理和查询 都是 O(logn)
  • 思想:使用区间dp出每个点起 2 k 2^k 2k长度的极值 大区间的极值 使用 小区间的极值构成!
  • 方法1 Sparse Table (ST) 预处理 O(nlgn) 查询 O(1)
  • 定义 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示 从 i i i 为起点, 2 j 2^j 2j 为长度的 区间 的极值
#include<iostream>
#include<cmath>
#define max(a, b) (a > b ? a : b)

const int space = 1e5 + 7;
int dp[space][34] = { {} };
//dp[i][j]表示从i开始长度为2^(j-1)中的答案
// 当然不包括第i + 2^(j-1)的数据;
int lenth[34] = {};
int log_2[33] = {};
int N, M;

void ST(int N) {
    for (int i = 1; i <= N; i++) scanf("%d", &dp[i][0]);
    for (int idx = 1; lenth[idx] <= N; idx++) {
        for (int now = 1; now + lenth[idx] <= N + 1; now++) {
            // now ~ now + 2^(idx-1) ~ now + 2^(idx-1) + 2^(idx-1)
            dp[now][idx] = max(dp[now][idx - 1], 
                dp[now + lenth[idx - 1]][idx - 1]);
        } 
    }
}

int RMQ(int left, int right) {
    int k = right - left + 1;
    k = log(k) / log(2);//计算log2(k),从left+lenth[k]要小于等于right,否则会有不确定的数据
    while (left + lenth[k] < right - lenth[k] + 1) k++;//注了这段代码也可以AC,写成这样更容易理解且更保险;
    printf("%d\n", max(dp[left][k], dp[right - lenth[k] + 1][k]));
}

int main(void)
{
    lenth[0] = 1;
    for (int i = 1; i < 31; i++) lenth[i] = lenth[i - 1] << 1;
    scanf("%d %d", &N, &M);
    // preprocessing
    ST(N);
    while (M--) {
        int left, right;
        scanf("%d %d", &left, &right);
        // query
        RMQ(left, right);
    }
    return 0;
}

二叉树的最近公共祖先 LCA

  • 除了后序遍历,(步长始终为1)
  • 也可以采用 倍增思想 TODO
  • 参考链接

树状数组 + 线段树

RMQ的树状数组的实现

在这里插入图片描述

#include <bits/stdc++.h>
using namespace std;

const int space = 1e6 + 7;
int A[space] = {}, B[space] = {}; // A树状数组 + B原数组
int N = 0, M = 0;

int lowest_bit(int n) {return n & (-n);}
void update(int pos, int data, int arr_len) {
    for (int i = pos; i <= arr_len; i += lowest_bit(i)) { // 父节点
        A[i] = max(A[i], data); // 往父节点上更新 即往大区间更新
    }
}

int query(int left, int right) {
    // 兄弟节点 从右往左 表示从右往左的多个相邻的子区间
    int max_ans = INT_MIN, i = right;
    while (i >= left) { // 兄弟节点往左递归
        max_ans = max(max_ans, B[i]);
        if (i - lowest_bit(i) + 1 < left) i--; // 都是单个元素 就按原数组计算最大值
        else { // 仍可进行 往左兄弟节点 的递归
            max_ans = max(max_ans, A[i]);
            i -= lowest_bit(i);
        }
    }
    return max_ans;
}

// 下面3个函数此题中不需要 用于元素更新(加减)
void add(int pos, int k) { // 用k更新pos处的元素
    while (pos <= N) {
        B[pos] += k;
        pos += lowest_bit(pos); // 往父节点更新
    }    
}
int get(int pos) { // 获取数组pos处的前缀和(包括pos)
    int res = 0;
    while (pos > 0) {
        res += B[pos];
        pos -= lowest_bit(pos); // 往左兄弟接节点更新
    }
    return res;
}
int sum(int p1, int p2) {
    return get(p2) - get(p1 - 1); 
}

// Main Loop
int main() {
    scanf("%d %d", &N, &M);
    for (int i = 1; i <= N; i++) {
        scanf("%d", &B[i]);
        update(i, B[i], N);
    }
    while (M--) {
        int x, y;
        scanf("%d %d", &x, &y);
        printf("%d\n", query(x, y));
    }
    return 0;
}

RMQ的线段树的实现

在这里插入图片描述

#include <bits/stdc++.h>
using namespace std;

const int space = 1e5 + 7;
long long N, M;
int nums[space] = {0};

struct Node {
    int start, end, maxV; // [start, end], maxV
    // 其实按照相同的递归方式的话 start和end是不必存储的 
    // 在递归入口参数处加上就行
} tr[4 * space]; // 线段树长度 长于 原数组长度 因为原数组只存在于 其叶节点

void build(int pos, int start, int end) {
    if (start == end) {
        tr[pos] = {start, end, nums[start]};
        return;
    }
    int mid = start + (end - start) / 2;
    int left = 2 * pos + 1;
    int right = 2 * pos + 2;
    build(left, start, mid);
    build(right, mid + 1, end);
    tr[pos] = {start, end, max(tr[left].maxV, tr[right].maxV)};
    // cout << start << " " << end << " " << tr[pos].maxV << endl;
}

int query(int pos, int start, int end) {
    if (tr[pos].end < start || tr[pos].start > end) return 0;
    if (start <= tr[pos].start && tr[pos].end <= end) return tr[pos].maxV;
    // int mid = tr[pos].start + (tr[pos].end - tr[pos].start) / 2; // 不需要 因为子节点存储了二分的区间
    int maxV = INT_MIN;
    maxV = query(pos * 2 + 1, start, end); // 左
    return max(maxV, query(pos * 2 + 2, start, end)); // 右
}

// 下面这个更新函数在本题中不需要
void update(int cur, int idx, int val) { // 从cur开始 旨在将idx更新为val
    if (tr[cur].start == tr[cur].end) {
        nums[idx] = tr[cur].maxV = val;
        return;
    }
    int left_node  = 2 * cur + 1;
    int right_node = 2 * cur + 2;
    int mid = tr[cur].start + (tr[cur].end - tr[cur].start) / 2;
    if (tr[cur].start <= idx && idx <= mid)
        update(left_node, idx, val);
    else update(right_node, idx, val);
    tr[cur].maxV = max(tr[left_node].maxV, tr[right_node].maxV);
}
// Main Loop
int main() {
    // // 下面两句是为了 加速输入输出流
    // std::ios::sync_with_stdio(false); // 解除 stdio 和 cout / cin 的绑定 做了之后要注意不要同时混用cout和printf之类
    // std::cin.tie(0); // 解除 cin 和 cout 的绑定 否则每次执行<<都要flush IO负担 0表示NULL
    
    // cin >> N >> M; // TLE
    // for (int i = 1; i <= N; i++) cin >> nums[i];
    scanf("%d %d", &N, &M);
    for (int i = 1; i <= N; i++) scanf("%d", &nums[i]);
    build(1, 1, N);
    int start, end;
    while (M--) {
        // cin >> start >> end; // TLE
        // cout << query(1, start, end) << endl;
        scanf("%d %d", &start, &end);
        printf("%d\n", query(1, start, end));
    }
    return 0;
}

三大平衡树

Treap

  • 核心是 利用随机数的二叉排序树的各种操作复杂度平均为O(lgn)
#include <cstdio>
#include <cstring>
#include <ctime>
#include <iostream>
#include <algorithm>
#include <cstdlib>
#include <cmath>
#include <utility>
#include <vector>
#include <queue>
#include <map>
#include <set>
#define max(x,y) ((x)>(y)?(x):(y))
#define min(x,y) ((x)>(y)?(y):(x))
#define INF 0x3f3f3f3f
#define MAXN 100005

using namespace std;

int cnt=1,rt=0; //节点编号从1开始

struct Tree
{
    int key, size, pri, son[2]; //保证父亲的pri大于儿子的pri
    void set(int x, int y, int z)
    {
        key=x;
        pri=y;
        size=z;
        son[0]=son[1]=0;
    }
}T[MAXN];

void rotate(int p, int &x)
{
    int y=T[x].son[!p];
    T[x].size=T[x].size-T[y].size+T[T[y].son[p]].size;
    T[x].son[!p]=T[y].son[p];
    T[y].size=T[y].size-T[T[y].son[p]].size+T[x].size;
    T[y].son[p]=x;
    x=y;
}

void ins(int key, int &x)
{
    if(x == 0)
        T[x = cnt++].set(key, rand(), 1);
    else
    {
        T[x].size++;
        int p=key < T[x].key;
        ins(key, T[x].son[!p]);
        if(T[x].pri < T[T[x].son[!p]].pri)
            rotate(p, x);
    }
}

void del(int key, int &x) //删除值为key的节点
{
    if(T[x].key == key)
    {
        if(T[x].son[0] && T[x].son[1])
        {
            int p=T[T[x].son[0]].pri > T[T[x].son[1]].pri;
            rotate(p, x);
            del(key, T[x].son[p]);
        }
        else
        {
            if(!T[x].son[0])
                x=T[x].son[1];
            else
                x=T[x].son[0];
        }
    }
    else
    {
        T[x].size--;
        int p=T[x].key > key;
        del(key, T[x].son[!p]);
    }
}

int find(int p, int &x) //找出第p小的节点的编号
{
    if(p == T[T[x].son[0]].size+1)
        return x;
    if(p > T[T[x].son[0]].size+1)
        find(p-T[T[x].son[0]].size-1, T[x].son[1]);
    else
        find(p, T[x].son[0]);
}

int find_NoLarger(int key, int &x) //找出值小于等于key的节点个数
{
    if(x == 0)
        return 0;
    if(T[x].key <= key)
        return T[T[x].son[0]].size+1+find_NoLarger(key, T[x].son[1]);
    else
        return find_NoLarger(key, T[x].son[0]);    
}

Splay-Tree 伸展树

  • 双旋的过程就是一个建立相对平衡的二叉树的一个过程
  • 模板代码:支持相同值,支持区间删除,支持懒惰标记
int cnt, rt;
int Add[MAXN];

struct Tree{
    int key, num, size, fa, son[2];
}T[MAXN];

inline void PushUp(int x)
{
    T[x].size=T[T[x].son[0]].size+T[T[x].son[1]].size+T[x].num;
}

inline void PushDown(int x)
{
    if(Add[x])
    {
        if(T[x].son[0])
        {
            T[T[x].son[0]].key+=Add[x];
            Add[T[x].son[0]]+=Add[x];
        }
        if(T[x].son[1])
        {
            T[T[x].son[1]].key+=Add[x];
            Add[T[x].son[1]]+=Add[x];
        }
        Add[x]=0;
    }
}

inline int Newnode(int key, int fa) //新建一个节点并返回
{
    ++cnt;
    T[cnt].key=key;
    T[cnt].num=T[cnt].size=1;
    T[cnt].fa=fa;
    T[cnt].son[0]=T[cnt].son[1]=0;
    return cnt;
}

inline void Rotate(int x, int p) //0左旋 1右旋
{
    int y=T[x].fa;
    PushDown(y);
    PushDown(x);
    T[y].son[!p]=T[x].son[p];
    T[T[x].son[p]].fa=y;
    T[x].fa=T[y].fa;
    if(T[x].fa)
        T[T[x].fa].son[T[T[x].fa].son[1] == y]=x;
    T[x].son[p]=y;
    T[y].fa=x;
    PushUp(y);
    PushUp(x);
}

void Splay(int x, int To) //将x节点移动到To的子节点中
{
    while(T[x].fa != To)
    {
        if(T[T[x].fa].fa == To)
            Rotate(x, T[T[x].fa].son[0] == x);
        else
        {
            int y=T[x].fa, z=T[y].fa;
            int p=(T[z].son[0] == y);
            if(T[y].son[p] == x)
                Rotate(x, !p), Rotate(x, p); //之字旋
            else
                Rotate(y, p), Rotate(x, p); //一字旋
        }
    }
    if(To == 0) rt=x;
}

int GetPth(int p, int To) //返回第p小的节点 并移动到To的子节点中
{
    if(!rt || p > T[rt].size) return 0;
    int x=rt;
    while(x)
    {
        PushDown(x);
        if(p >= T[T[x].son[0]].size+1 && p <= T[T[x].son[0]].size+T[x].num)
            break;
        if(p > T[T[x].son[0]].size+T[x].num)
        {
            p-=T[T[x].son[0]].size+T[x].num;
            x=T[x].son[1];
        }
        else
            x=T[x].son[0];
    }
    Splay(x, 0);
    return x;
}

int Find(int key) //返回值为key的节点 若无返回0 若有将其转移到根处
{
    if(!rt) return 0;
    int x=rt;
    while(x)
    {
        PushDown(x);
        if(T[x].key == key) break;
        x=T[x].son[key > T[x].key];
    }
    if(x) Splay(x, 0);
    return x;
}

int Prev() //返回根节点的前驱 非重点
{
    if(!rt || !T[rt].son[0]) return 0;
    int x=T[rt].son[0];
    while(T[x].son[1])
    {
        PushDown(x);
        x=T[x].son[1];
    }
    Splay(x, 0);
    return x;
}

int Succ() //返回根结点的后继 非重点
{
    if(!rt || !T[rt].son[1]) return 0;
    int x=T[rt].son[1];
    while(T[x].son[0])
    {
        PushDown(x);
        x=T[x].son[0];
    }
    Splay(x, 0);
    return x;
}

void Insert(int key) //插入key值
{
    if(!rt)
        rt=Newnode(key, 0);
    else
    {
        int x=rt, y=0;
        while(x)
        {
            PushDown(x);
            y=x;
            if(T[x].key == key)
            {
                T[x].num++;
                T[x].size++;
                break;
            }
            T[x].size++;
            x=T[x].son[key > T[x].key];
        }
        if(!x)
            x=T[y].son[key > T[y].key]=Newnode(key, y);
        Splay(x, 0);
    }
}

void Delete(int key) //删除值为key的节点1个
{
    int x=Find(key);
    if(!x) return;
    if(T[x].num>1)
    {
        T[x].num--;
        PushUp(x);
        return;
    }
    int y=T[x].son[0];
    while(T[y].son[1])
        y=T[y].son[1];
    int z=T[x].son[1];
    while(T[z].son[0])
        z=T[z].son[0];
    if(!y && !z)
    {
        rt=0;
        return;
    }
    if(!y)
    {
        Splay(z, 0);
        T[z].son[0]=0;
        PushUp(z);
        return;
    }
    if(!z)
    {
        Splay(y, 0);
        T[y].son[1]=0;
        PushUp(y);
        return;
    }
    Splay(y, 0);
    Splay(z, y);
    T[z].son[0]=0;
    PushUp(z);
    PushUp(y);
}

int GetRank(int key) //获得值<=key的节点个数
{
    if(!Find(key))
    {
        Insert(key);
        int tmp=T[T[rt].son[0]].size;
        Delete(key);
        return tmp;
    }
    else
        return T[T[rt].son[0]].size+T[rt].num;
}

void Delete(int l, int r) //删除值在[l, r]中的所有节点 l!=r
{
    if(!Find(l)) Insert(l);
    int p=Prev();
    if(!Find(r)) Insert(r);
    int q=Succ();
    if(!p && !q)
    {
        rt=0;
        return;
    }
    if(!p)
    {
        T[rt].son[0]=0;
        PushUp(rt);
        return;
    }
    if(!q)
    {
        Splay(p, 0);
        T[rt].son[1]=0;
        PushUp(rt);
        return;
    }
    Splay(p, q);
    T[p].son[1]=0;
    PushUp(p);
    PushUp(q);
}

Size Balance Tree

  • SBT能够保证树的高度在lgn,这样对于插入,删除操作都能够准确保证时间复杂度在O(lgn)
  • 详见 陈启峰SBT
int cnt, rt;

struct Tree
{
    int key, size, son[2];
}T[MAXN];

inline void PushUp(int x)
{
    T[x].size=T[T[x].son[0]].size+T[T[x].son[1]].size+1;
}

inline int Newnode(int key)
{
    ++cnt;
    T[cnt].key=key;
    T[cnt].size=1;
    T[cnt].son[0]=T[cnt].son[1]=0;
    return cnt;
}

void Rotate(int p, int &x)
{
    int y=T[x].son[!p];
    T[x].son[!p]=T[y].son[p];
    T[y].son[p]=x;
    PushUp(x);
    PushUp(y);
    x=y;
}

void Maintain(int &x, int p) //维护SBT的!p子树
{
    if(T[T[T[x].son[p]].son[p]].size > T[T[x].son[!p]].size)
        Rotate(!p, x);
    else if(T[T[T[x].son[p]].son[!p]].size > T[T[x].son[!p]].size)
        Rotate(p, T[x].son[p]), Rotate(!p, x);
    else return;
    Maintain(T[x].son[0], 0);
    Maintain(T[x].son[1], 1);
    Maintain(x, 0);
    Maintain(x, 1);
}

inline int Prev() //返回比根值小的最大值 若无返回0
{
    int x=T[rt].son[0];
    if(!x) return 0;
    while(T[x].son[1])
        x=T[x].son[1];
    return x;
}

inline int Succ() //返回比根值大的最小值 若无返回0
{
    int x=T[rt].son[1];
    if(!x) return 0;
    while(T[x].son[0])
        x=T[x].son[0];
    return x;
}

void Insert(int key, int &x)
{
    if(!x) x=Newnode(key);
    else
    {
        T[x].size++;
        Insert(key, T[x].son[key > T[x].key]);
        Maintain(x, key > T[x].key);
    }
}

bool Delete(int key, int &x) //删除值为key的节点 key可以不存在
{
    if(!x) return 0;
    if(T[x].key == key)
    {
        if(!T[x].son[0])
        {
            x=T[x].son[1];
            return 1;
        }
        if(!T[x].son[1])
        {
            x=T[x].son[0];
            return 1;
        }
        int y=Prev();
        T[x].size--;
        return Delete(T[x].key, T[x].son[0]);
    }
    else
        if(Delete(key, T[x].son[key > T[x].key]))
        {
            T[x].size--;
            return 1;
        }
}

int GetPth(int p, int &x) //返回第p小的节点
{
    if(!x) return 0;
    if(p == T[T[x].son[0]].size+1)
        return x;
    if(p > T[T[x].son[0]].size+1)
        return GetPth(p-T[T[x].son[0]].size-1, T[x].son[1]);
    else
        return GetPth(p, T[x].son[0]);
}

int GetRank(int key, int &x) //找出值<=key的节点个数
{
    if(!x) return 0;
    if(T[x].key <= key)
        return T[T[x].son[0]].size+1+GetRank(key, T[x].son[1]);
    else
        return GetRank(key, T[x].son[0]);
}

树链剖分,树分治

  • 国家集训队论文 <<分治算法在树的路径问题中的应用>>
  • PDF链接

在这里插入图片描述

  • 树分治和树上启发式合并的复杂度正确性的保证。
  • LCA 最近公共祖先

搜索

深度优先

  • 汉诺塔

宽度优先

  • 迷宫

迭代+DFS

双向搜索

二分 查找

三分 查找

  • 几何里面用得到
  • 类似二分,不过是采用 三分 的方式
  • 二分解决的是 单调有序的 数组查找 问题 (一维有序问题)
  • 三分引申到二维(二次函数),得是凸函数
    在这里插入图片描述
const double EPS = 1e-10;
double calc(double x) {
    // f(x) = -(x-3)^2 + 2;
    return -(x-3.0)*(x-3.0) + 2;
}
double ternarySearch(double low, double high) {
    double mid, midmid;
    while (low + EPS < high)
    {
        mid = (low + high) / 2;
        midmid = (mid + high) / 2;
        double mid_value = calc(mid);
        double midmid_value = calc(midmid);
        if (mid_value > midmid_value)
            high = midmid;
        else
            low = mid;
    }
    return low;
}

A* , A星算法

  • 启发式搜索
  • 路径规划,计算最低通过成本
  • f ( n ) = g ( n ) + h ( n ) f(n)=g(n)+h(n) f(n)=g(n)+h(n)
  • 从起始搜索点到当前点的代价 + 当前结点到目标结点的估值
  • 一种具有f(n)=g(n)+h(n)策略的启发式算法能成为A*算法的充分条件是:
    • 搜索树上存在着从起始点到终了点的最优路径。
    • 问题域是有限的。
    • 所有结点的子结点的搜索代价值>0。
    • h ( n ) = < h ∗ ( n ) h(n)=<h*(n) h(n)=<h(n) h ∗ ( n ) h*(n) h(n)为实际问题的代价值)。

字符串

  • 字符串问题:子串、子序列、前后缀、字典序、回文串
  • 前缀(preffix(str)):真前缀不包括字符串本身
  • 后缀(suffix(str)):真后缀不包括字符串本身

匹配问题

  • 单模式串/多模式串 匹配
  • 暴力法 最坏是 O(N^2)
  • 哈希法 / KMP 线性复杂度

字典树:在上下文中搜索多个模态

实现Trie 类形式

class Trie {
private:
    bool isEnd;
    Trie* next[26];
public:
    /** Initialize your data structure here. */
    Trie() {
        isEnd = false;
        memset(next, 0, sizeof(next));
    }
    
    /** Inserts a word into the trie. */
    void insert(string word) {
        Trie* cur = this;
        for (auto& c : word) {
            if (!cur->next[c-'a']) 
                cur->next[c-'a'] = new Trie();
            cur = cur->next[c-'a'];
        }
        cur->isEnd = true;
    }
    
    /** Returns if the word is in the trie. */
    bool search(string word) {
        Trie* cur = this;
        for (auto& c : word) {
            cur = cur->next[c-'a'];
            if (!cur) return false;
        }
        return cur->isEnd;
    }
    
    /** Returns if there is any word in the trie that starts with the given prefix. */
    bool startsWith(string prefix) {
        Trie* cur = this;
        for (auto& c : prefix) {
            cur = cur->next[c-'a'];
            if (!cur) return false;
        }
        return true;
    }
};

单词搜索II

  • 二维字符矩阵,搜索字典中的所有单词
class Solution {
public:
    // Trie 树
    struct TrieNode {
        TrieNode *child[26];
        string str; // 方便取字符串
        TrieNode() : str{""} {
            for (auto& c : child) c = NULL;
        }
    };
    struct Trie {
        TrieNode *root;
        Trie() : root(new TrieNode()) {}
        void insert(string s) {
            TrieNode *p = root;
            for (auto& a : s) {
                int i = a - 'a';
                if (!p->child[i]) p->child[i] = new TrieNode();
                p = p->child[i];
            }
            p->str = s;
        }
    };
    // 前缀树 + backtrack
    vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
        vector<string> res;
        if (words.empty() || board.empty() || board[0].empty()) return res;
        vector<vector<bool>> vis(board.size(), vector<bool>(board[0].size(), false));
        Trie* T = new Trie();
        for (auto& w : words) T->insert(w);
        for (int i = 0; i < board.size(); i++) {
            for (int j = 0; j < board[0].size(); j++) {
                if (T->root->child[board[i][j] - 'a'])
                    search(board, T->root->child[board[i][j] - 'a'], i, j, vis, res);
            }
        }
        return res;
    }
    void search(vector<vector<char>>& board, TrieNode* p, int i, int j, 
            vector<vector<bool>>& vis, vector<string>& res) {
        // clear保证不重复
        if (!p->str.empty()) {res.push_back(p->str); p->str.clear();} 
        int dirs[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
        vis[i][j] = true;
        for (auto& d : dirs) {
            int y = i + d[0], x = j + d[1];
            if (y < 0 || y >= board.size() || x < 0 || x >= board[0].size()) continue;
            if (vis[y][x] || !p->child[board[y][x] - 'a']) continue;
            search(board, p->child[board[y][x] - 'a'], y, x, vis, res); 
        }
        vis[i][j] = false;
    }
};

数组形式

int son[N][26],cnt[N],idx;  
//cnt数组记录以当前节点为结尾的字符串的数目,idx为节点编号
int n;
void insert(string& s){
    int n = s.length();
    int p = 0;
    for(int i = 0; i<n; i++){
        int u = s[i]-'a';
        if(!son[p][u]) son[p][u] = ++idx;
        p = son[p][u];
    }
    cnt[p] ++;
}

int query(string &s){
    int p = 0;
    int n = s.length();
    for(int i = 0; i<n; i++){
        int u = s[i] - 'a';
        if(!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];
}

KMP

前缀表/函数 理解

前缀函数 π \pi π (prefix function):

  • π [ i ] \pi[i] π[i] 代表子串 s [ 0 … i ] s[0 \ldots i] s[0i] 与其后缀相等的最长真前缀(proper prefix, 即 不包含本身的前缀) 。
  • π [ i ] = max ⁡ k = 0 … i { k : s [ 0 … k − 1 ] = s [ i − ( k − 1 ) … i ] } \pi[i]=\max _{k=0 \ldots i}\{k: s[0 \ldots k-1]=s[i-(k-1) \ldots i]\} π[i]=k=0imax{k:s[0k1]=s[i(k1)i]}
  • “abaaab" − [ 0 , 1 , 0 , 1 , 2 , 2 , 3 ] -[0,1,0,1,2,2,3] [0,1,0,1,2,2,3]

在这里插入图片描述
在这里插入图片描述

void prefix_table(string& pattern, vector<int>& prefix) {
    int N = pattern.size();
    prefix.resize(N, 0);
    prefix[0] = 0;
    int len = 0, idx = 1; // 分别表示最长前后缀 和 当前处理的位置
    while (idx < N) {
        if (pattern[idx] == pattern[len])
            prefix[idx++] = ++len;
        else {
            if (len > 0) len = prefix[len - 1]; // 回退找最适合的位置
            else prefix[idx++] = 0; // 找到头了 0
        }
    }
    prefix.insert(prefix.begin(), -1); // 移下位 方便算法搜索
}

int kmp_search(string& text, string& pattern, vector<int>& prefix) {
    int N = pattern.size(), M = text.size();
    if (N == 0) return 0; // 空串
    if (M == 0) return -1; // 空模板
    prefix_table(pattern, prefix);
    int i = 0, j = 0; 
    while (i < M) {
        if (j == N - 1 && text[i] == pattern[j]) { // 找到一个
            cout << "The Patten is at: " << i - j << endl;
            j = prefix[j];  
            // return i - j;
        } 
        if (text[i] == pattern[j]) { // 匹配中
            i++; j++;
        } else { // 不匹配需要移动对齐patten
            j = prefix[j];
            if (j == -1) {
                i++; j++;
            }
        }
    }
    return -1;
}

DP的写法

class Solution {
public:
    int strStr(string haystack, string needle) {
        if (needle.empty()) return 0;
        vector<vector<int>> dp(needle.size(), vector<int>(256, 0));
        KMPCreator(needle, dp);
        int stat = 0;
        for (int i = 0; i < haystack.size(); ++i) {
            stat = dp[stat][haystack[i]];
            if (stat == needle.size()) return i - stat + 1;
        }
        return -1;
    }
private:
    void KMPCreator(string& needle, vector<vector<int>>& dp) {
        int shadow = 0;
        dp[0][needle[0]] = 1;
        for (int i = 1; i < needle.size(); ++i) {
            for (int c = 0; c < 256; c++)
                dp[i][c] = dp[shadow][c];
            dp[i][needle[i]] = i + 1;
            shadow = dp[shadow][needle[i]];
        }
    }
};

ExKMP

  • ExKMP,国外叫 “Z Algorithm"
  • z [ i ] z[i] z[i] s t r str str 和 从 i i i 开始的 s t r str str 的后缀的 最大公共前缀 长度
  • i = = 0 i == 0 i==0 处的值无意义,不考虑!
    在这里插入图片描述
  • Z函数的计算方法,朴素O(N^2)
vector<int> z_function_trivial(string s) {
  int n = (int)s.length();
  vector<int> z(n);
  for (int i = 1; i < n; ++i)
    while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++z[i];
  return z;
}
  • 改进后 O(N)
  • 最优匹配段:在所有已探测到的匹配段中,我们将保持结尾最靠右的那一个 [ l : r ] [l : r] [l:r]
  • 下标 r r r可被认为是字符串 s s s已被算法扫描的边界;任何超过该点的字符都是未知的
vector<int> z_function(string s) {
  // 最右匹配段 [l : r] 
  int n = (int)s.length();
  vector<int> z(n);
  for (int i = 1, l = 0, r = 0; i < n; ++i) {
    if (i <= r) z[i] = min(r - i + 1, z[i - l]);
    while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++z[i];
    if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
  }
  return z;
}

应用

  • 查找子串:在text中查找pattern,① 构建新的字符串 s = text + 分隔符 + patten PS:分隔符不在 text和pattern中!
    在这里插入图片描述
  • 本质不同子串查找两个子串不同,当且仅当有这两个子串长度不一样 或者长度一样且有任意一位不一样
    在这里插入图片描述
  • 字符串压缩:寻找一个最短的字符串 ,使得 t 可以被 s 的一份或多份拷贝的拼接表示,实现了对 s 的压缩!(解法的有效性 证明 同 kmp一样!)
    在这里插入图片描述

自动机

  • 确定有限状态自动机,一种数学模型
  • 包括:字符集、状态集(起始状态+接收状态)、转移函数
  • 状态 + 字符接受(动作)
  • 包括:字典树 + KMP自动机 + AC自动机 + 回文自动机 + 序列自动机

AC自动机:AC 自动机其实就是 Trie 上的自动机

  • Aho-Corasick automaton
  • 可以认为是 Trie + KMP,两个步骤:
    • 基础的 Trie 结构:将所有的模式串构成一棵 Trie
    • KMP 的思想:对 Trie 树上所有的结点构造失配指针
  • 多模式字符串 匹配算法
  • KMP算法的树形扩展
  • 一般KMP只能处理一个patten, 而结合了Trie树就可以 多个patten 查找
  • 节点存储信息 {child[26], fail指针, vector<int> exist }
  • fail指针 i->fail = j 失配指针
    • KMP 只对一个模式串做匹配,而 AC 自动机要对多个模式串做匹配。有可能 fail 指针指向的结点对应着另一个模式串,两者前缀不同
    • 表示 word[j]word[i] 的最长后缀状态
    • 在一个分支上走不下去了之后,我们不必头铁,可以换个方向继续往下走,而fail数组的作用就是这样,当发生失配时,我们可以通过预处理出的fail数组告诉我们其他分支的信息 (即满足当前字符串最长后缀的节点),然后继续向后遍历即可
    • 利用fail指针来补充trie树的信息,使其成为一个图,然后在匹配过程中,我们无需考虑fail指针,直接令 j = t r [ j ] [ t ] j = tr[j][t] j=tr[j][t],这里其实就包括了 t + ′ a ′ t+'a' t+a这个字符如果不存在时的情况,如果不存在,其实就等价于 j = t r [ f a i l [ j ] ] [ t ] j = tr[fail[j]][t] j=tr[fail[j]][t]
  • exist列表 表示当前节点为止记录上的单词长度 序列
    在这里插入图片描述
  • 代码 模板
  • 记录 所有匹配上的 字符串 的总数
#pragma warning(disable:4996)
#include <iostream>
#include <cstring>
#include <string>
#include <cmath>
#include <cstdio>
#include <stdio.h>
#include <cstdlib>
#include <algorithm>
#include <vector>
#include <set>
#include <map>
#include <iomanip>
#define rep(i,a,b) for(int i = a; i <= b ; i ++ )
#define pre(i,a,b) for(int i = b ; i >= a ; i --)
#define ll long long
#define inf 0x3f3f3f3f
#define ull unsigned long long
#define ios ios::sync_with_stdio(false),cin.tie(0)
using namespace std;
typedef pair<int, int> PII;
const int N = 10010, S = 55, M = 1000010;

int n;
int tr[N * S][26], cnt[N * S], idx; // 节点编号 长度
char str[M]; // 字符串
int q[N * S], ne[N * S]; // BFS队列 fail指针

void insert() {
	int p = 0;
	for (int i = 0; str[i]; i++) {
		int t = str[i] - 'a';
		if (!tr[p][t]) tr[p][t] = ++idx;
		p = tr[p][t];
	}
	cnt[p]++;
}

void build() {
	int hh = 0, tt = -1;
	for (int i = 0; i < 26; i++)
		if (tr[0][i])
			q[++tt] = tr[0][i];
	while (hh <= tt) {
		int t = q[hh++];
		for (int i = 0; i < 26; i++) {
			int p = tr[t][i];
			if (!p) tr[t][i] = tr[ne[t]][i];
			else {
				ne[p] = tr[ne[t]][i];
				q[++tt] = p;
			}
		}
	}
}
int main() {
	int T;
	scanf("%d", &T);
	while (T--) {
		memset(tr, 0, sizeof tr);
		memset(cnt, 0, sizeof cnt);
		memset(ne, 0, sizeof ne);
		idx = 0;
		scanf("%d", &n);
		for (int i = 0; i < n; i++) {
			scanf("%s", str);
			insert();
		}
		 打印树结构
		//for (int i = 0; i <= 5; i++) {
		//	for (int j = 0; j < 26; j++) {
		//		if (tr[i][j]) {
		//			cout << "Node " << i << " have "
		//				<< char(j + 'a') << " is #" << tr[i][j] << endl;
		//		}
		//	}
		//}
		//cout << endl;
		//build();
		 打印树结构 其实我还不知道为啥要改变树的结构!
		//for (int i = 0; i <= 5; i++) {
		//	for (int j = 0; j < 26; j++) {
		//		if (tr[i][j]) {
		//			cout << "Node " << i << " have "
		//				<< char(j + 'a') << " is #" << tr[i][j] << endl;
		//		}
		//	}
		//}
		scanf("%s", str);
		int res = 0;
		for (int i = 0, j = 0; str[i]; i++) {
			int t = str[i] - 'a';
			j = tr[j][t];
			int p = j;
			while (p) {
				res += cnt[p];
				//cnt[p] = 0; // 只统计一次
				p = ne[p]; // 不漏解
			}
		}
		printf("%d\n", res);
	}
	return 0;
}

后缀数组:Suffix Array

  • 定义:由两个数组组成:
    • s a [ i ] s a[i] sa[i] 表示将所有后缀排序后第 i i i 小的后缀的编号
    • r k [ i ] r k[i] rk[i] 表示后缀 i i i 的排名
  • 性质: s a [ r k [ i ] ] = r k [ s a [ i ] ] = i s a[r k[i]]=r k[s a[i]]=i sa[rk[i]]=rk[sa[i]]=i

在这里插入图片描述

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;
const int N = 1000010;

char s[N];
int n, w, sa[N], rk[N << 1], oldrk[N << 1];
// 为了防止访问 rk[i+w] 导致数组越界,开两倍数组。
// 当然也可以在访问前判断是否越界,但直接开两倍数组方便一些。

int main() {
  int i, p;

  scanf("%s", s + 1);
  n = strlen(s + 1);
  for (i = 1; i <= n; ++i) sa[i] = i, rk[i] = s[i];

  for (w = 1; w < n; w <<= 1) {
    sort(sa + 1, sa + n + 1, [](int x, int y) {
      return rk[x] == rk[y] ? rk[x + w] < rk[y + w] : rk[x] < rk[y];
    });  // 这里用到了 lambda
    memcpy(oldrk, rk, sizeof(rk));
    // 由于计算 rk 的时候原来的 rk 会被覆盖,要先复制一份
    for (p = 0, i = 1; i <= n; ++i) {
      if (oldrk[sa[i]] == oldrk[sa[i - 1]] &&
          oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]) {
        rk[sa[i]] = p;
      } else {
        rk[sa[i]] = ++p;
      }  // 若两个子串相同,它们对应的 rk 也需要相同,所以要去重
    }
  }

  for (i = 1; i <= n; ++i) printf("%d ", sa[i]);

  return 0;
}
  • 寻找最小的循环移动位置,将字符串S复制一份变成SS就转化成了后缀排序问题
  • 在字符串中找子串:构造T的后缀数组,如果S在T中出现过,那么必定是一些后缀的前缀,二分查找!
  • 从字符串首尾取字符最小化字典序

后缀自动机 Suffix Automaton

  • 处理字符串后缀
  • 集训队论文
  • 陈立杰ppt

广义后缀自动机

后缀树

回文树

Manacher

  • 求字符串的 最长回文子串
  • 把一般意义上的 n方 复杂度算法,提升为 O(n)复杂度

动态规划 O(n^2)

vector<int> d1(n), d2(n); 
// 分别表示以位置  为中心的长度为奇数和长度为偶数的回文串个数
for (int i = 0; i < n; i++) {
  d1[i] = 1;
  while (0 <= i - d1[i] && i + d1[i] < n && s[i - d1[i]] == s[i + d1[i]]) {
    d1[i]++;
  }

  d2[i] = 0;
  while (0 <= i - d2[i] - 1 && i + d2[i] < n &&
         s[i - d2[i] - 1] == s[i + d2[i]]) {
    d2[i]++;
  }
}

马拉车算法 线性

char s_n[5000]; // &1#2^ 处理后的字符串
int p[5000]; // p[i]的i位置的回文串的半径
string longestPalindrome(string s) {
    string res = "";
    manacher(s, res);
    return res;
}
// 修改原字符串到 字符串长度变为奇数
int init(string& str) {
    int len = str.size();
    s_n[0] = '$';
    s_n[1] = '#';
    int j = 2;
    for (int i = 0; i < len; i++) {
        s_n[j++] = str[i];
        s_n[j++] = '#';
    }
    s_n[j++] = '^';
    s_n[j] = '\0'; // 终止符加上 
    return j;
}
int manacher(string& str, string& res) {
    int len = init(str);
    int max_len = -1;
    // mx:以id为中心的最长回文的右边界 
    // mx = id + p[id]
    // p[i]: 以i为中心的最长回文半径(包括中心字符)
    int id = 0, mx = 0; 
    int final_id = 0;
    res = "";
    // 求解位置和长度
    for (int i = 1; i < len; i++) {
        if (i < mx) p[i] = min(p[2 * id - 1], mx - i);
        else p[i] = 1;
        while (s_n[i - p[i]] == s_n[i + p[i]]) p[i]++;
        if (mx < i + p[i]) {
            id = i;
            mx = i + p[i];
        }
        if (p[i] - 1 > max_len) {
            max_len = p[i] - 1; // 最长回文串的长度
            final_id = i; // 最长回文串的位置(修改后的字符串)
        }
    }
    // 获取回文串
    int left = max(0, final_id - max_len);
    int right = min(len, final_id + max_len);
    for (int i = left; i <= right; i++) {
        if (s_n[i] != '#' && s_n[i] != '^' && s_n[i] != '$')
            res += s_n[i];
    }
    return max_len;
}

回文分割

  • 最近流行

字符串分割成由回文子串构成的序列

class Solution {
public:
    vector<vector<string>> partition(string s) {
        res.clear();
        int N = s.size();
        dp.resize(N, vector<int>(N, false));
        init(s);
        dfs(0, s);
        return res;
    }
private:
    vector<vector<int>> dp;
    vector<vector<string>> res;
    vector<string> path;
    void init(string& str) {
        int N = str.size();
        for (int j = 0; j < N; j++) {
            for (int i = 0; i <= j; i++) {
                if (i == j) dp[i][j] = true;
                else if (str[i] == str[j] && (j - i < 2 || dp[i + 1][j - 1]))
                    dp[i][j] = true;
            }
        }
    }
    void dfs(int pos, string& str) {
        int N = str.size();
        if (pos == N) {
            res.push_back(path);
            return;
        }
        for (int i = pos; i < N; i++) {
            if (dp[pos][i]) {
                path.push_back(str.substr(pos, i - pos + 1));
                dfs(i + 1, str);
                path.pop_back();
            }
        }
    }
    // bool checkPalindrome(string& str, int left, int right) {
    //     if (left > right) return false;
    //     while (left < right) {
    //         if (str[left++] != str[right--]) 
    //             return false;
    //     }
    //     return true;
    // }
};

字符串分割成回文串序列的最少分割次数

class Solution {
public:
    int minCut(string s) {
        int N = s.size();
        init(s);
        dp2.resize(N + 1, INT_MAX); dp2[0] = 0;
        for (int right = 1; right <= N; right++) {
            for (int left = 1; left <= right; left++) {
                if (dp1[left - 1][right - 1])
                    dp2[right] = min(dp2[right], dp2[left - 1] + 1);
            }
        }
        return dp2[N] - 1;
    }
private:
    vector<vector<bool>> dp1; // 前一个DP判断区间是不是回文
    vector<int> dp2; // 后一个DP计算回文串个数
    void init(string& str) {
        int N = str.size();
        dp1.resize(N, vector<bool>(N, false));
        for (int right = 0; right < N; right++) {
            for (int left = 0; left <= right; left++) {
                if (left == right) dp1[left][right] = true;
                else if (str[left] == str[right] && 
                        (right - left < 2 || dp1[left + 1][right - 1])) {
                    dp1[left][right] = true;
                }
            }
        }
    }
};

序列自动机

最小表示法

  • Finding the smallest cyclic shift
  • 解决 字符串 最小表示问题 的 方法
  • 循环同构:
    • S 和 T 循环同构 ==> S [ i : n ] + S [ 1 : i − 1 ] = T S[i : n] + S[1:i-1] = T S[i:n]+S[1:i1]=T
  • S的最小表示:和 S 循环同构 的所有 字符串中 字典序最小 的字符串!
int k = 0, i = 0, j = 1;
while (k < n && i < n && j < n) {
  if (sec[(i + k) % n] == sec[(j + k) % n]) {
    k++;
  } else {
    sec[(i + k) % n] > sec[(j + k) % n] ? i = i + k + 1 : j = j + k + 1; // 第i + k 处不满足字符相等 就前k个字符都得跳过!
    if (i == j) i++; // 否则随意 加位置
    k = 0;
  }
}
i = min(i, j); // i 和 j 较小值为答案!

Lyndon 分解

  • Lyndon串:若字符串S满足其本身的字典序严格小于其任何一个真后缀,则称S是 Lyndon 串
  • 等价于 S 本身是所有 S 的循环同构串中字典序唯一最小的一个串
  • Lyndon 分解:串 s s s 的 Lyndon 分解记为
    • s = w 1 w 2 ⋯ w k s=w_{1} w_{2} \cdots w_{k} s=w1w2wk, 其中所有 w i w_{i} wi 为简单串
    • 并且他们 的字典序按照非严格单减排序,即 w 1 ≥ w 2 ≥ ⋯ ≥ w k w_{1} \geq w_{2} \geq \cdots \geq w_{k} w1w2wk 。这样的分解存在且唯一!

Duval 算法

  • 如果一个字符串 t t t 能够分解为 t = w w ⋯ w ˉ t=w w \cdots \bar{w} t=wwwˉ 的形式
  • 其中 w w w 是一 个 Lyndon 串,而 w ˉ \bar{w} wˉ w w w 的前缀 ( w ˉ (\bar{w} (wˉ 可能是空串 ) , ), ), 那么称 t t t 是近似简单串 ( pre-simple ) 或者 近似 Lyndon 串
  • Lyndon 串也是 近似 Lyndon 串
  • Duval 算法运用了贪心的思想。算法过程中我们把串 s s s 分成三个部分 s = s 1 s 2 s 3 , s=s_{1} s_{2} s_{3}, s=s1s2s3, 其中 s 1 s_{1} s1 是一 个 Lyndon 串,它的 Lyndon 分解已经记录 ; s 2 s_{2} s2 是一个近似 Lyndon 串 ; s 3 s_{3} s3 是未处理的部分。
  • 整体描述一下,该算法每一次尝试将 s 3 s_{3} s3 的首字符添加到 s 2 s_{2} s2 的末尾。如果 s 2 s_{2} s2 不再是近似 Lyndon 串,那么我们就可以将 s 2 s_{2} s2 截出一部分前缀 ( 即 Lyndon 分解 ) ) ) 接在 s 1 s_{1} s1 末尾。
// duval_algorithm
vector<string> duval(string const& s) {
  int n = s.size(), i = 0;
  vector<string> factorization;
  while (i < n) {
    int j = i + 1, k = i;
    while (j < n && s[k] <= s[j]) {
      if (s[k] < s[j])
        k = i;
      else
        k++;
      j++;
    }
    while (i <= k) {
      factorization.push_back(s.substr(i, j - k));
      i += j - k;
    }
  }
  return factorization;
}
  • 可用于求解 最小表示法 问题!
  • 在分解的过程中记录每一次的近似 Lyndon 串的开头即可
// smallest_cyclic_string
string min_cyclic_string(string s) {
  s += s;
  int n = s.size();
  int i = 0, ans = 0;
  while (i < n / 2) {
    ans = i;
    int j = i + 1, k = i;
    while (j < n && s[k] <= s[j]) {
      if (s[k] < s[j])
        k = i;
      else
        k++;
      j++;
    }
    while (i <= k) i += j - k;
  }
  return s.substr(ans, n / 2);
}

图论

图的存储

常见的

  • 直接存边:由于直接存边的遍历效率低下,一般不用于遍历图
struct Edge {
  int u, v; // 起点 终点
  int w; // 权重
};
vector<Edge> e;
  • 邻接矩阵:邻接矩阵只适用于没有重边(或重边可以忽略)的情况
vector<vector<bool> > adj;
  • 邻接表:支持动态增加元素的数据结构构成的数组
vector<vector<int> > adj;
unordered_map<int, pair<int, int>> adj;

链式前向星:用链表实现的邻接表

  • 以储存边的方式来存储图
  • 读入每条边的信息,将边存放在数组中,把数组中的边按照起点顺序排序(可以使用基数排序,如下面例程),前向星就构造完了
  • 通常用在点的数目太多,或两点之间有多条弧的时候
  • 一般在别的数据结构不能使用的时候才考虑用前向星。除了不能直接用起点终点定位以外,前向星几乎是完美的!

在这里插入图片描述

#include <iostream>
#include <vector>
using namespace std;
int n, m;
vector<bool> vis;
vector<int> head, nxt, to;
void add(int u, int v) {
	// 表示与第i条边同起点的下一条边的存储位置
	nxt.push_back(head[u]); // 边 - 边
	// 表示以i为起点的第一条边存储的位置
	head[u] = to.size(); // 点 - 边
	// 表示第i条边的终点
	to.push_back(v); // 边 - 点
}
bool find_edge(int u, int v) {
	for (int i = head[u]; ~i; i = nxt[i]) {  // ~i 表示 i != -1
		if (to[i] == v) {
			return true;
		}
	}
	return false;
}
void dfs(int u) {
	if (vis[u]) return;
	vis[u] = true;
	for (int i = head[u]; ~i; i = nxt[i]) dfs(to[i]);
}
int main() {
	cin >> n >> m;
	vis.resize(n + 1, false);
	head.resize(n + 1, -1);
	for (int i = 1; i <= m; ++i) {
		int u, v;
		cin >> u >> v;
		add(u, v);
	}
	return 0;
}
  • 存各种图都很适合,但不能快速查询一条边是否存在,也不能方便地对一个点的出边进行排序。
  • 优点是边是带编号的,有时会非常有用,而且如果 cnt 的初始值为奇数,存双向边时 i ^ 1 即是 i 的反边(常用于 网络流 )

单源最短路

  • Dijkstra,spfa

多源最短路

  • Floy

最小生成树

  • Kruskal,Prim

拓扑排序

  • 课程表ii 入度
class Solution {
public:
    bool topSort(vector<vector<int>>& G, vector<int>& O, vector<int>& D) {
        int N = D.size(), idx = 0;
        queue<int> Q;
        for (int i = 0; i < N; i++) 
            if (D[i] == 0) Q.push(i);
        while (!Q.empty()) {
            int X = Q.front(); Q.pop();
            O[idx++] = X;
            for (int i = 0; i < G[X].size(); i++) {
                int x = G[X][i]; D[x]--;
                if (D[x] == 0) Q.push(x);
            }
            G[X].clear();
        }
        if (idx == N) return true;
        return false;
    }
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        vector<int> inD(numCourses), res(numCourses);
        vector<vector<int>> G(numCourses);
        for (int i = 0; i < prerequisites.size(); i++) {
            G[prerequisites[i][1]].push_back(prerequisites[i][0]);
            inD[prerequisites[i][0]]++;
        }
        if (topSort(G, res, inD)) return res;
        return {};
    }
};
  • 矩阵中的递增路径 出度
class Solution {
public:
    int longestIncreasingPath(vector<vector<int>>& matrix) {
        if (matrix.empty() || matrix[0].empty()) return 0;
        H = matrix.size(); W = matrix[0].size();
        vector<vector<int>> outDgree(H, vector<int>(W, 0));
        for (int i = 0; i < H; i++) {
            for (int j = 0; j < W; j++) {
                for (int k = 0; k < 4; k++) {
                    int i1 = i + dirs[k][0], j1 = j + dirs[k][1];
                    if (i1 < 0 || i1 >= H || j1 < 0 || j1 >= W) continue;
                    if (matrix[i1][j1] <= matrix[i][j]) continue;
                    ++outDgree[i][j]; // 出度
                }
            }
        }
        queue<pair<int, int>> Q;
        for (int i = 0; i < H; i++) {
            for (int j = 0; j < W; j++) {
                if (outDgree[i][j] == 0) 
                    Q.push(make_pair(i, j)); // 从出度为0为边界条件 反向更新所有可达点
            }
        }
        int ans = 0;
        while (!Q.empty()) {
            ++ans;
            int sz = Q.size();
            for (int i = 0; i < sz; i++) {
                auto X = Q.front(); Q.pop();
                int y = X.first, x = X.second;
                for (int k = 0; k < 4; k++) {
                    int x1 = x + dirs[k][1], y1 = y + dirs[k][0];
                    if (x1 < 0 || x1 >= W || y1 < 0 || y1 >= H) continue;
                    if (matrix[y][x] <= matrix[y1][x1]) continue;
                    --outDgree[y1][x1]; // (y1, x1) -> (y, x)
                    if (outDgree[y1][x1] == 0) Q.push(make_pair(y1, x1));
                }
            }
        }
        return ans;
    }
private:
    int dirs[4][2] = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}};
    int H, W;
};

最大流,最小费用流

  • Dinic,zkw

匈牙利算法

  • KM(网络流TLE后,用这个)

欧拉回路

Tarjan

  • 强连通,边点双联通,割点,桥

2-SAT

  • 有很多个集合,每个集合里面有若干元素,现给出一些取元素的规则,要你判断是否可行,可行则给出一个可行方案。如果所有集合中,元素个数最多的集合有k个,那么我们就说这是一个k-sat问题,同理,2-sat问题就是k=2时的情况
  • k > 2 NP完全问题
  • 而k == 2,存在多项式解

动态规划

背包问题(背包九讲)

  • 01背包,分组背包,完全背包,树上背包

记忆化搜索

  • dfs + bfs

数位dp

  • 小于N有多少怎样怎样的数字,N很大

状态压缩dp

  • 通常用于各种集合问题
  • 位运算 + DP

树形dp

概率dp

  • 不知道大家学过随机过程没
  • 概率 DP 用于解决概率问题与期望问题

dp优化

  • 斜率优化,单调队列优化,四边形不等式优化

数学

组合数学

  • 离散 / 具体 数学,数论、排列组合、概率期望、多项式 等!
  • 求C(n,m),求斯特林数,卡特兰数,各种混着dp,容斥,鸽笼

基本操作

位运算

  • 高效地进行某些运算,代替其它低效的方式。
  • 表示集合。(常用于 状压 DP )
  • 题目本来就要求进行位运算
  • 一些替换有用的操作 OI -wiki

快速幂

  • ( a b ) % m (a ^ b) \% m (ab)%m
long long binpow(long long a, long long b, long long m) {
  a %= m;
  long long res = 1;
  while (b > 0) {
    if (b & 1) res = res * a % m;
    a = a * a % m;
    b >>= 1;
  }
  return res;
}

进制转换

  • 二进制 + 八进制 + 十六进制

高精度计算 / bignum计算

  • Arbitrary-Precision Arithmetic
  • 常用 字符串 表示 字符串的 表示!
入门:加减乘除
#include <cstdio>
#include <cstring>

static const int LEN = 1004;

int a[LEN], b[LEN], c[LEN], d[LEN];

void clear(int a[]) {
  for (int i = 0; i < LEN; ++i) a[i] = 0;
}

void read(int a[]) {
  static char s[LEN + 1];
  scanf("%s", s);
  clear(a);

  int len = strlen(s);
  for (int i = 0; i < len; ++i) a[len - i - 1] = s[i] - '0';
}

void print(int a[]) {
  int i;
  for (i = LEN - 1; i >= 1; --i)
    if (a[i] != 0) break;
  for (; i >= 0; --i) putchar(a[i] + '0');
  putchar('\n');
}

void add(int a[], int b[], int c[]) {
  clear(c);
  for (int i = 0; i < LEN - 1; ++i) {
    c[i] += a[i] + b[i];
    if (c[i] >= 10) {
      c[i + 1] += 1;
      c[i] -= 10;
    }
  }
}

void sub(int a[], int b[], int c[]) {
  clear(c);
  for (int i = 0; i < LEN - 1; ++i) {
    c[i] += a[i] - b[i];
    if (c[i] < 0) {
      c[i + 1] -= 1;
      c[i] += 10;
    }
  }
}

void mul(int a[], int b[], int c[]) {
  clear(c);
  for (int i = 0; i < LEN - 1; ++i) {
    for (int j = 0; j <= i; ++j) c[i] += a[j] * b[i - j];

    if (c[i] >= 10) {
      c[i + 1] += c[i] / 10;
      c[i] %= 10;
    }
  }
}

inline bool greater_eq(int a[], int b[], int last_dg, int len) {
  if (a[last_dg + len] != 0) return true;
  for (int i = len - 1; i >= 0; --i) {
    if (a[last_dg + i] > b[i]) return true;
    if (a[last_dg + i] < b[i]) return false;
  }
  return true;
}

void div(int a[], int b[], int c[], int d[]) {
  clear(c);
  clear(d);

  int la, lb;
  for (la = LEN - 1; la > 0; --la)
    if (a[la - 1] != 0) break;
  for (lb = LEN - 1; lb > 0; --lb)
    if (b[lb - 1] != 0) break;
  if (lb == 0) {
    puts("> <");
    return;
  }

  for (int i = 0; i < la; ++i) d[i] = a[i];
  for (int i = la - lb; i >= 0; --i) {
    while (greater_eq(d, b, i, lb)) {
      for (int j = 0; j < lb; ++j) {
        d[i + j] -= b[j];
        if (d[i + j] < 0) {
          d[i + j + 1] -= 1;
          d[i + j] += 10;
        }
      }
      c[i] += 1;
    }
  }
}

int main() {
  read(a);

  char op[4];
  scanf("%s", op);

  read(b);

  switch (op[0]) {
    case '+':
      add(a, b, c);
      print(c);
      break;
    case '-':
      sub(a, b, c);
      print(c);
      break;
    case '*':
      mul(a, b, c);
      print(c);
      break;
    case '/':
      div(a, b, c, d);
      print(c);
      print(d);
      break;
    default:
      puts("> <");
  }

  return 0;
}
压位 高精度
  • 不用字符串里面 每个位 进行 分解,用于计算
  • 可以两位 两位 的计算,比如 按照 100进制进行 分解
  • 以加法为例
//这里的 a,b,c 数组均为 p 进制下的数
//最终输出答案时需要将数字转为十进制
void add(int a[], int b[], int c[]) {
  clear(c);

  for (int i = 0; i < LEN - 1; ++i) {
    c[i] += a[i] + b[i];
    if (c[i] >= p) {  //在普通高精度运算下,p=10
      c[i + 1] += 1;
      c[i] -= p;
    }
  }
}
Karatsuba 乘法
  • 分治思想 用于 高精度 乘法!
int *karatsuba_polymul(int n, int *a, int *b) {
  if (n <= 32) {
    // 规模较小时直接计算,避免继续递归带来的效率损失
    int *r = new int[n * 2 + 1]();
    for (int i = 0; i <= n; ++i)
      for (int j = 0; j <= n; ++j) r[i + j] += a[i] * b[j];
    return r;
  }

  int m = n / 2 + 1;
  int *r = new int[m * 4 + 1]();
  int *z0, *z1, *z2;

  z0 = karatsuba_polymul(m - 1, a, b);
  z2 = karatsuba_polymul(n - m, a + m, b + m);

  // 计算 z1
  // 临时更改,计算完毕后恢复
  for (int i = 0; i + m <= n; ++i) a[i] += a[i + m];
  for (int i = 0; i + m <= n; ++i) b[i] += b[i + m];
  z1 = karatsuba_polymul(m - 1, a, b);
  for (int i = 0; i + m <= n; ++i) a[i] -= a[i + m];
  for (int i = 0; i + m <= n; ++i) b[i] -= b[i + m];
  for (int i = 0; i <= (m - 1) * 2; ++i) z1[i] -= z0[i];
  for (int i = 0; i <= (n - m) * 2; ++i) z1[i] -= z2[i];

  // 由 z0、z1、z2 组合获得结果
  for (int i = 0; i <= (m - 1) * 2; ++i) r[i] += z0[i];
  for (int i = 0; i <= (m - 1) * 2; ++i) r[i + m] += z1[i];
  for (int i = 0; i <= (n - m) * 2; ++i) r[i + m * 2] += z2[i];

  delete[] z0;
  delete[] z1;
  delete[] z2;
  return r;
}

void karatsuba_mul(int a[], int b[], int c[]) {
  int *r = karatsuba_polymul(LEN - 1, a, b);
  memcpy(c, r, sizeof(int) * LEN);
  for (int i = 0; i < LEN - 1; ++i)
    if (c[i] >= 10) {
      c[i + 1] += c[i] / 10;
      c[i] %= 10;
    }
  delete[] r;
}
  • 这种方法,会有整数溢出 问题!
高精度整数类 封装
#define MAXN 9999
// MAXN 是一位中最大的数字
#define MAXSIZE 10024
// MAXSIZE 是位数
#define DLEN 4
// DLEN 记录压几位
struct Big {
  int a[MAXSIZE], len;
  bool flag;  // 标记符号'-'
  Big() {
    len = 1;
    memset(a, 0, sizeof a);
    flag = 0;
  }
  Big(const int);
  Big(const char*);
  Big(const Big&);
  Big& operator=(const Big&);
  Big operator+(const Big&) const;
  Big operator-(const Big&) const;
  Big operator*(const Big&)const;
  Big operator/(const int&) const;
  // TODO: Big / Big;
  Big operator^(const int&) const;
  // TODO: Big ^ Big;

  // TODO: Big 位运算;

  int operator%(const int&) const;
  // TODO: Big ^ Big;
  bool operator<(const Big&) const;
  bool operator<(const int& t) const;
  inline void print() const;
};
Big::Big(const int b) {
  int c, d = b;
  len = 0;
  // memset(a,0,sizeof a);
  CLR(a);
  while (d > MAXN) {
    c = d - (d / (MAXN + 1) * (MAXN + 1));
    d = d / (MAXN + 1);
    a[len++] = c;
  }
  a[len++] = d;
}
Big::Big(const char* s) {
  int t, k, index, l;
  CLR(a);
  l = strlen(s);
  len = l / DLEN;
  if (l % DLEN) ++len;
  index = 0;
  for (int i = l - 1; i >= 0; i -= DLEN) {
    t = 0;
    k = i - DLEN + 1;
    if (k < 0) k = 0;
    g(j, k, i) t = t * 10 + s[j] - '0';
    a[index++] = t;
  }
}
Big::Big(const Big& T) : len(T.len) {
  CLR(a);
  f(i, 0, len) a[i] = T.a[i];
  // TODO:重载此处?
}
Big& Big::operator=(const Big& T) {
  CLR(a);
  len = T.len;
  f(i, 0, len) a[i] = T.a[i];
  return *this;
}
Big Big::operator+(const Big& T) const {
  Big t(*this);
  int big = len;
  if (T.len > len) big = T.len;
  f(i, 0, big) {
    t.a[i] += T.a[i];
    if (t.a[i] > MAXN) {
      ++t.a[i + 1];
      t.a[i] -= MAXN + 1;
    }
  }
  if (t.a[big])
    t.len = big + 1;
  else
    t.len = big;
  return t;
}
Big Big::operator-(const Big& T) const {
  int big;
  bool ctf;
  Big t1, t2;
  if (*this < T) {
    t1 = T;
    t2 = *this;
    ctf = 1;
  } else {
    t1 = *this;
    t2 = T;
    ctf = 0;
  }
  big = t1.len;
  int j = 0;
  f(i, 0, big) {
    if (t1.a[i] < t2.a[i]) {
      j = i + 1;
      while (t1.a[j] == 0) ++j;
      --t1.a[j--];
      // WTF?
      while (j > i) t1.a[j--] += MAXN;
      t1.a[i] += MAXN + 1 - t2.a[i];
    } else
      t1.a[i] -= t2.a[i];
  }
  t1.len = big;
  while (t1.len > 1 && t1.a[t1.len - 1] == 0) {
    --t1.len;
    --big;
  }
  if (ctf) t1.a[big - 1] = -t1.a[big - 1];
  return t1;
}
Big Big::operator*(const Big& T) const {
  Big res;
  int up;
  int te, tee;
  f(i, 0, len) {
    up = 0;
    f(j, 0, T.len) {
      te = a[i] * T.a[j] + res.a[i + j] + up;
      if (te > MAXN) {
        tee = te - te / (MAXN + 1) * (MAXN + 1);
        up = te / (MAXN + 1);
        res.a[i + j] = tee;
      } else {
        up = 0;
        res.a[i + j] = te;
      }
    }
    if (up) res.a[i + T.len] = up;
  }
  res.len = len + T.len;
  while (res.len > 1 && res.a[res.len - 1] == 0) --res.len;
  return res;
}
Big Big::operator/(const int& b) const {
  Big res;
  int down = 0;
  gd(i, len - 1, 0) {
    res.a[i] = (a[i] + down * (MAXN + 1) / b);
    down = a[i] + down * (MAXN + 1) - res.a[i] * b;
  }
  res.len = len;
  while (res.len > 1 && res.a[res.len - 1] == 0) --res.len;
  return res;
}
int Big::operator%(const int& b) const {
  int d = 0;
  gd(i, len - 1, 0) d = (d * (MAXN + 1) % b + a[i]) % b;
  return d;
}
Big Big::operator^(const int& n) const {
  Big t(n), res(1);
  int y = n;
  while (y) {
    if (y & 1) res = res * t;
    t = t * t;
    y >>= 1;
  }
  return res;
}
bool Big::operator<(const Big& T) const {
  int ln;
  if (len < T.len) return 233;
  if (len == T.len) {
    ln = len - 1;
    while (ln >= 0 && a[ln] == T.a[ln]) --ln;
    if (ln >= 0 && a[ln] < T.a[ln]) return 233;
    return 0;
  }
  return 0;
}
inline bool Big::operator<(const int& t) const {
  Big tee(t);
  return *this < tee;
}
inline void Big::print() const {
  printf("%d", a[len - 1]);
  gd(i, len - 2, 0) { printf("%04d", a[i]); }
}

inline void print(Big s) {
  // s不要是引用,要不然你怎么print(a * b);
  int len = s.len;
  printf("%d", s.a[len - 1]);
  gd(i, len - 2, 0) { printf("%04d", s.a[i]); }
}
char s[100024];

平衡三进制

  • 0 , 1 , 2 > − 1 , 0 , 1 > z , 0 , 1 0,1,2 > -1,0,1 > z,0,1 012>101>z01

博弈论

  • Nim游戏,SG函数(其余靠猜)

数论

  • 欧几里得算法(GCD),扩展欧几里得,欧拉筛,欧拉函数,莫比乌斯反演(百度贾志鹏线性筛),中国剩余定理

几何

  • 《挑战程序设计》最新版
  • 上海交大acm的《算法与实现》
  • 刘汝佳的《算法竞赛,入门经典》)(注意精度)

高数

  • 高斯消元
  • 数值积分
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值