Acwing基础课 算法模板

算法模板

文章目录

基础算法

快速排序

  1. 定义变量
  2. 找前面不小于 x 的数, 后面不大于 x 的数, 交换
  3. 递归处理 ,j 为边界
void quick_sort(int q[], int l, int r)
{
	if (l >= r) return;
	
	int x = q[l + r >> 1], i = l - 1, j = r + 1;
	while(i < j)
	{
		do i++; while(q[i] < x);
		do j--; while(q[j] > x);
		
		if(i < j) swap(q[i], q[j]);
	}
	
	quick_sort(q, l, j);
	quick_sort(q, j + 1, r);
}

归并排序

  1. 递归处理,以 mid 为边界
  2. 双指针排序两个数组 放在 临时数组 tmp 里
  3. 处理没排完的数 从tmp重新放回 q[l, r]
void merge_sort(int q[], int l, int r)
{
    if (l >= r) return;

    int mid = l + r >> 1;
    merge_sort(q, l, mid);
    merge_sort(q, mid + 1, r);

    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
        else tmp[k ++ ] = q[j ++ ];

    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];

    for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}

整数二分

  1. 当前 mid 值 在要求的点 的右侧 r = mid, l = mid + 1
  2. 当前 mid 值 在要求的点 的左侧 l = mid (+ 1), r = mid - 1
bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

浮点数二分

bool check(double x) {/* ... */} // 检查x是否满足某种性质

double bsearch_3(double l, double r)
{
    const double eps = 1e-6;   // eps 表示精度,取决于题目对精度的要求
    while (r - l > eps)
    {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

高精度加法

  1. 反向存储 即从个位开始存
  2. C[i] = (A[i] + B[i] + t) % 10
  3. 最后判断一次进位
// C = A + B, A >= 0, B >= 0
vector<int> add(vector<int> a, vector<int> b)
{
    vector<int> c;
    int t = 0;
    for(int i = 0; i < a.size() || i < b.size(); i++)
    {
        if(i < a.size()) t += a[i];
        if(i < b.size()) t += b[i];
        c.push_back(t % 10);
        t /= 10;
    }
    if(t) c.push_back(t);
    return c;
}

高精度减法

  1. 反向存储 即从个位开始存
  2. C[i] = ((A[i] - B[i] - t) + 10 )% 10
  3. 最后判断前导零
// C = A - B, 满足A >= B, A >= 0, B >= 0
vector<int> sub(vector<int> &A, vector<int> &B)
{
    vector<int> C;
    for (int i = 0, t = 0; i < A.size(); i ++ )
    {
        t = A[i] - t;
        if (i < B.size()) t -= B[i];
        C.push_back((t + 10) % 10);
        if (t < 0) t = 1;
        else t = 0;
    }

    while (C.size() > 1 && C.back() == 0) C.pop_back();
    return C;
}

高精度乘低精度

  1. 反向存储 即从个位开始存
  2. C[i] = (A[i] * b + t) % 10
  3. 最后判断前导零
// C = A * b, A >= 0, b >= 0
vector<int> mul(vector<int> a, LL b)
{
    vector<int> c;
    LL t = 0;
    for(int i = 0; i < a.size(); i++)
    {
        t += a[i] * b;
        c.push_back(t % 10);
        t /= 10;
    }
    while(t) c.push_back(t %10), t/= 10;
    while (C.size() > 1 && C.back() == 0) C.pop_back();
    return c;
}

高精度除以低精度

  1. 反向存储 即从个位开始存
  2. 从高位开始计算 C[i] = (r * 10 + A[i])/ b
  3. 翻转 判断前导零
// A / b = C ... r, A >= 0, b > 0
vector<int> div(vector<int> &A, int b, int &r)
{
    vector<int> C;
    r = 0;
    for (int i = A.size() - 1; i >= 0; i -- )
    {
        r = r * 10 + A[i];
        C.push_back(r / b);
        r %= b;
    }
    reverse(C.begin(), C.end());
    while (C.size() > 1 && C.back() == 0) C.pop_back();
    return C;
}

一维前缀和

S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]

二维前缀和

S[i, j] = 第i行j列格子左上部分所有元素的和
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]

一维差分

给区间[l, r]中的每个数加上c:B[l] += c, B[r + 1] -= c

二维差分

给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c

位运算

求n的第k位数字: n >> k & 1
返回n的最后一位1:lowbit(n) = n & -n

双指针

for (int i = 0, j = 0; i < n; i ++ )
{
    while (j < i && check(i, j)) j ++ ;

    // 具体问题的逻辑
}
常见问题分类:
    (1) 对于一个序列,用两个指针维护一段区间
    (2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作

离散化

  1. 存储 添加,查询,所有涉及的坐标
  2. 排序 去重
  3. find(x) 二分 找到下标
  4. 处理 添加, 查询
vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end());   // 去掉重复元素

// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于x的位置
{
    int l = 0, r = alls.size() - 1;
    while (l < r)
    {
        int mid = l + r >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return r + 1; // 映射到1, 2, ...n
}

区间合并

// 将所有存在交集的区间合并
void merge(vector<PII> &segs)
{
    vector<PII> res;

    sort(segs.begin(), segs.end());

    int st = -2e9, ed = -2e9;
    for (auto seg : segs)
        if (ed < seg.first)
        {
            if (st != -2e9) res.push_back({st, ed});
            st = seg.first, ed = seg.second;
        }
        else ed = max(ed, seg.second);

    if (st != -2e9) res.push_back({st, ed});

    segs = res;
}

数据结构

单链表

// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;

// 初始化
void init()
{
    head = -1;
    idx = 0;
}

// 在链表头插入一个数a
void insert(int a)
{
    e[idx] = a, ne[idx] = head, head = idx ++ ;
}

// 将头结点删除,需要保证头结点存在
void remove()
{
    head = ne[head];
}

双链表

// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;

// 初始化
void init()
{
    //0是左端点,1是右端点
    r[0] = 1, l[1] = 0;
    idx = 2;
}

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
    e[idx] = x;
    l[idx] = a, r[idx] = r[a];
    l[r[a]] = idx, r[a] = idx ++ ;
}

// 删除节点a
void remove(int a)
{
    l[r[a]] = l[a];
    r[l[a]] = r[a];
}

// tt表示栈顶
int stk[N], tt = 0;

// 向栈顶插入一个数
stk[ ++ tt] = x;

// 从栈顶弹出一个数
tt -- ;

// 栈顶的值
stk[tt];

// 判断栈是否为空,如果 tt > 0,则表示不为空
if (tt > 0)
{

}

队列

普通队列
// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;

// 向队尾插入一个数
q[ ++ tt] = x;

// 从队头弹出一个数
hh ++ ;

// 队头的值
q[hh];

// 判断队列是否为空,如果 hh <= tt,则表示不为空
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];

// 判断队列是否为空,如果hh != tt,则表示不为空
if (hh != tt)
{

}

单调栈

常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ;
    stk[ ++ tt] = i;
}

单调队列

常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
    while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头是否滑出窗口
    while (hh <= tt && check(q[tt], i)) tt -- ;
    q[ ++ tt] = i;
}

KMP

// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; 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 <= n; i ++ )
{
    while (j && s[i] != p[j + 1]) j = ne[j];
    if (s[i] == p[j + 1]) j ++ ;
    if (j == m)
    {
        j = ne[j];
        // 匹配成功后的逻辑
    }
}

Trie树

在这里插入图片描述

1.高效存储

2.查找字符串是否出现过 以及 出现的次数

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

// 插入一个字符串
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.询问两个元素是否在一个集合当中

基本原理: 每个集合用一颗树来表示,树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x] 表示 x 的父节点

问题1:如何判断树根:if (p[x] == x)

问题2:如何求 x 的集合编号:while (p[x] != x) x = p[x]; // O(n)

问题3:如何合并两个集合:x、y分别是两个集合的根节点,p[x] = y;

路径压缩优化:查到祖先之后就把px存成祖先

时间复杂度近乎 O(1)

朴素并查集
    int p[N]; //存储每个点的祖宗节点

    // 返回x的祖宗节点 + 路径压缩
    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 的并查集

837.连通块中点的数量

    int p[N], size[N];
    //p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量

    // 返回x的祖宗节点
    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所在的两个集合:
    if(find(a) == find(b)) continue; // 防止重复加入
    
    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)的偏移量

// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;

// 交换两个点,及其映射关系
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 <= 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)
    {
        heap_swap(u, t);
        down(t);
    }
}

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

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

一般哈希

拉链法
	int h[N], e[N], ne[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;
    }
开放寻址法
    int h[N];

    // 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
    int find(int x)
    {
        int t = (x % N + N) % N;
        while (h[t] != null && h[t] != x)
        {
            t ++ ;
            if (t == N) t = 0;
        }
        return t;
    }

字符串哈希

核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
小技巧:取模的数用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];
}

C++ STL

vector, 变长数组,倍增的思想
    size()  返回元素个数
    empty()  返回是否为空
    clear()  清空
    front()/back()
    push_back()/pop_back()
    begin()/end()
    []
    支持比较运算,按字典序

pair<int, int>
    first, 第一个元素
    second, 第二个元素
    支持比较运算,以first为第一关键字,以second为第二关键字(字典序)

string,字符串
    size()/length()  返回字符串长度
    empty()
    clear()
    substr(起始下标,(子串长度))  返回子串
    c_str()  返回字符串所在字符数组的起始地址

queue, 队列
    size()
    empty()
    push()  向队尾插入一个元素
    front()  返回队头元素
    back()  返回队尾元素
    pop()  弹出队头元素

priority_queue, 优先队列,默认是大根堆
    size()
    empty()
    push()  插入一个元素
    top()  返回堆顶元素
    pop()  弹出堆顶元素
    定义成小根堆的方式:priority_queue<int, vector<int>, greater<int>> heap;

stack, 栈
    size()
    empty()
    push()  向栈顶插入一个元素
    top()  返回栈顶元素
    pop()  弹出栈顶元素

deque, 双端队列
    size()
    empty()
    clear()
    front()/back()
    push_back()/pop_back()
    push_front()/pop_front()
    begin()/end()
    []

set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
    size()
    empty()
    clear()
    begin()/end()
    ++, -- 返回前驱和后继,时间复杂度 O(logn)

    set/multiset
        insert()  插入一个数
        find()  查找一个数
        count()  返回某一个数的个数
        erase()
            (1) 输入是一个数x,删除所有x   O(k + logn)
            (2) 输入一个迭代器,删除这个迭代器
        lower_bound()/upper_bound()
            lower_bound(x)  返回大于等于x的最小的数的迭代器
            upper_bound(x)  返回大于x的最小的数的迭代器
    map/multimap
        insert()  插入的数是一个pair
        erase()  输入的参数是pair或者迭代器
        find()
        []  注意multimap不支持此操作。 时间复杂度是 O(logn)
        lower_bound()/upper_bound()

unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
    和上面类似,增删改查的时间复杂度是 O(1)
    不支持 lower_bound()/upper_bound(), 迭代器的++,--

bitset, 圧位
    bitset<10000> s;
    ~, &, |, ^
    >>, <<
    ==, !=
    []

    count()  返回有多少个1

    any()  判断是否至少有一个1
    none()  判断是否全为0

    set()  把所有位置成1
    set(k, v)  将第k位变成v
    reset()  把所有位变成0
    flip()  等价于~
    flip(k) 把第k位取反

双指针

for (int i = 0, j = 0; i < n; i ++ )
{
    while (j < i && check(i, j)) j ++ ;

    // 具体问题的逻辑
}
常见问题分类:
    (1) 对于一个序列,用两个指针维护一段区间
    (2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作

离散化

vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end());   // 去掉重复元素

// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于x的位置
{
    int l = 0, r = alls.size() - 1;
    while (l < r)
    {
        int mid = l + r >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return r + 1; // 映射到1, 2, ...n
}

区间合并

// 将所有存在交集的区间合并
void merge(vector<PII> &segs)
{
    vector<PII> res;

    sort(segs.begin(), segs.end());

    int st = -2e9, ed = -2e9;
    for (auto seg : segs)
        if (ed < seg.first)
        {
            if (st != -2e9) res.push_back({st, ed});
            st = seg.first, ed = seg.second;
        }
        else ed = max(ed, seg.second);

    if (st != -2e9) res.push_back({st, ed});

    segs = res;
}

数学知识

试除法判定质数

bool is_prime(int x)
{
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
            return false;
    return true;
}

试除法分解质因数

void divide(int x)
{
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
        {
            int s = 0;
            while (x % i == 0) x /= i, s ++ ;
            cout << i << ' ' << s << endl;
        }
    if (x > 1) cout << x << ' ' << 1 << endl;
    cout << endl;
}

朴素筛法求素数

int primes[N], cnt;     // primes[]存储所有素数
bool st[N];         // st[x]存储x是否被筛掉

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (st[i]) continue;
        primes[cnt ++ ] = i;
        for (int j = i + i; j <= n; j += i)
            st[j] = true;
    }
}

线性筛法求素数

int primes[N], cnt;     // primes[]存储所有素数
bool st[N];         // st[x]存储x是否被筛掉

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}

试除法求所有约数

vector<int> get_divisors(int x)
{
    vector<int> res;
    for (int i = 1; i <= x / i; i ++ )
        if (x % i == 0)
        {
            res.push_back(i);
            if (i != x / i) res.push_back(x / i);
        }
    sort(res.begin(), res.end());
    return res;
}

约数个数和约数之和

如果 N = p1^c1 * p2^c2 * ... *pk^ck
约数个数: (c1 + 1) * (c2 + 1) * ... * (ck + 1)
约数之和: (p1^0 + p1^1 + ... + p1^c1) * ... * (pk^0 + pk^1 + ... + pk^ck)

欧几里得算法

int gcd(int a, int b)
{
    return b ? gcd(b, a % b) : a;
}

求欧拉函数

int phi(int x)
{
    int res = x;
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
        {
            res = res / i * (i - 1);
            while (x % i == 0) x /= i;
        }
    if (x > 1) res = res / x * (x - 1);

    return res;
}

筛法求欧拉函数

int primes[N], cnt;     // primes[]存储所有素数
int euler[N];           // 存储每个数的欧拉函数
bool st[N];         // st[x]存储x是否被筛掉


void get_eulers(int n)
{
    euler[1] = 1;
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i])
        {
            primes[cnt ++ ] = i;
            euler[i] = i - 1;
        }
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
            int t = primes[j] * i;
            st[t] = true;
            if (i % primes[j] == 0)
            {
                euler[t] = euler[i] * primes[j];
                break;
            }
            euler[t] = euler[i] * (primes[j] - 1);
        }
    }
}

快速幂

求 m^k mod p,时间复杂度 O(logk)。

int qmi(int m, int k, int p)
{
    int res = 1 % p, t = m;
    while (k)
    {
        if (k&1) res = res * t % p;
        t = t * t % p;
        k >>= 1;
    }
    return res;
}

扩展欧几里得算法

// 求x, y,使得ax + by = gcd(a, b)
int exgcd(int a, int b, int &x, int &y)
{
    if (!b)
    {
        x = 1; y = 0;
        return a;
    }
    int d = exgcd(b, a % b, y, x);
    y -= (a/b) * x;
    return d;
}

高斯消元

// a[N][N]是增广矩阵
int gauss()
{
    int c, r;
    for (c = 0, r = 0; c < n; c ++ )
    {
        int t = r;
        for (int i = r; i < n; i ++ )   // 找到绝对值最大的行
            if (fabs(a[i][c]) > fabs(a[t][c]))
                t = i;

        if (fabs(a[t][c]) < eps) continue;

        for (int i = c; i <= n; i ++ ) swap(a[t][i], a[r][i]);      // 将绝对值最大的行换到最顶端
        for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c];      // 将当前行的首位变成1
        for (int i = r + 1; i < n; i ++ )       // 用当前行将下面所有的列消成0
            if (fabs(a[i][c]) > eps)
                for (int j = n; j >= c; j -- )
                    a[i][j] -= a[r][j] * a[i][c];

        r ++ ;
    }

    if (r < n)
    {
        for (int i = r; i < n; i ++ )
            if (fabs(a[i][n]) > eps)
                return 2; // 无解
        return 1; // 有无穷多组解
    }

    for (int i = n - 1; i >= 0; i -- )
        for (int j = i + 1; j < n; j ++ )
            a[i][n] -= a[i][j] * a[j][n];

    return 0; // 有唯一解
}

递推法求组合数

// c[a][b] 表示从a个苹果中选b个的方案数
for (int i = 0; i < N; i ++ )
    for (int j = 0; j <= i; j ++ )
        if (!j) c[i][j] = 1;
        else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;

通过预处理逆元的方式求组合数

首先预处理出所有阶乘取模的余数fact[N],以及所有阶乘取模的逆元infact[N]
如果取模的数是质数,可以用费马小定理求逆元
int qmi(int a, int k, int p)    // 快速幂模板
{
    int res = 1;
    while (k)
    {
        if (k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}

// 预处理阶乘的余数和阶乘逆元的余数
fact[0] = infact[0] = 1;
for (int i = 1; i < N; i ++ )
{
    fact[i] = (LL)fact[i - 1] * i % mod;
    infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
}

Lucas定理

若p是质数,则对于任意整数 1 <= m <= n,有:
    C(n, m) = C(n % p, m % p) * C(n / p, m / p) (mod p)

int qmi(int a, int k, int p)  // 快速幂模板
{
    int res = 1 % p;
    while (k)
    {
        if (k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}

int C(int a, int b, int p)  // 通过定理求组合数C(a, b)
{
    if (a < b) return 0;

    LL x = 1, y = 1;  // x是分子,y是分母
    for (int i = a, j = 1; j <= b; i --, j ++ )
    {
        x = (LL)x * i % p;
        y = (LL) y * j % p;
    }

    return x * (LL)qmi(y, p - 2, p) % p;
}

int lucas(LL a, LL b, int p)
{
    if (a < p && b < p) return C(a, b, p);
    return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}

分解质因数法求组合数

当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用:
    1. 筛法求出范围内的所有质数
    2. 通过 C(a, b) = a! / b! / (a - b)! 这个公式求出每个质因子的次数。 n! 中p的次数是 n / p + n / p^2 + n / p^3 + ...
    3. 用高精度乘法将所有质因子相乘

int primes[N], cnt;     // 存储所有质数
int sum[N];     // 存储每个质数的次数
bool st[N];     // 存储每个数是否已被筛掉


void get_primes(int n)      // 线性筛法求素数
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}


int get(int n, int p)       // 求n!中的次数
{
    int res = 0;
    while (n)
    {
        res += n / p;
        n /= p;
    }
    return res;
}


vector<int> mul(vector<int> a, int b)       // 高精度乘低精度模板
{
    vector<int> c;
    int t = 0;
    for (int i = 0; i < a.size(); i ++ )
    {
        t += a[i] * b;
        c.push_back(t % 10);
        t /= 10;
    }

    while (t)
    {
        c.push_back(t % 10);
        t /= 10;
    }

    return c;
}

get_primes(a);  // 预处理范围内的所有质数

for (int i = 0; i < cnt; i ++ )     // 求每个质因数的次数
{
    int p = primes[i];
    sum[i] = get(a, p) - get(b, p) - get(a - b, p);
}

vector<int> res;
res.push_back(1);

for (int i = 0; i < cnt; i ++ )     // 用高精度乘法将所有质因子相乘
    for (int j = 0; j < sum[i]; j ++ )
        res = mul(res, primes[i]);

卡特兰数

给定n个0和n个1,它们按照某种顺序排成长度为2n的序列,满足任意前缀中0的个数都不少于1的个数的序列的数量为: Cat(n) = C(2n, n) / (n + 1)

NIM游戏

给定N堆物品,第i堆物品有Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。

我们把这种游戏称为NIM博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。
所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。
NIM博弈不存在平局,只有先手必胜和先手必败两种情况。

定理: NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ … ^ An != 0

公平组合游戏ICG

若一个游戏满足:

  1. 由两名玩家交替行动;
  2. 在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
  3. 不能行动的玩家判负;

则称该游戏为一个公平组合游戏。
NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。

有向图游戏

给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。
任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。

Mex运算

设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即:
mex(S) = min{x}, x属于自然数,且x不属于S

SG函数

在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1, y2, …, yk,定义SG(x)为x的后继节点y1, y2, …, yk 的SG函数值构成的集合再执行mex(S)运算的结果,即:
SG(x) = mex({SG(y1), SG(y2), …, SG(yk)})
特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。

有向图游戏的和

设G1, G2, …, Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步。G被称为有向图游戏G1, G2, …, Gm的和。
有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:
SG(G) = SG(G1) ^ SG(G2) ^ … ^ SG(Gm)

定理
有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。
有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。


搜索与图论

树与图的存储

树是一种特殊的图,与图的存储方式相同。
对于无向图中的边ab,存储两条有向边a->b, b->a。
因此我们可以只考虑有向图的存储。

(1) 邻接矩阵:g[a][b] 存储边a->b

(2) 邻接表:

// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx;

// 添加一条边a->b
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

// 初始化
idx = 0;
memset(h, -1, sizeof h);

树与图的遍历

时间复杂度 O(n+m),n 表示点数, m 表示边数

深度优先遍历
int dfs(int u)
{
    st[u] = true; // st[u] 表示点u已经被遍历过

    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j]) dfs(j);
    }
}
宽度优先遍历
queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);

while (q.size())
{
    int t = q.front();
    q.pop();

    for (int i = h[t]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true; // 表示点j已经被遍历过
            q.push(j);
        }
    }
}

拓扑排序

时间复杂度O(n + m),n 表示点数,m 表示边数

bool topsort()
{
    int hh = 0, tt = -1;

    // d[i] 存储点i的入度
    for (int i = 1; i <= n; i ++ )
        if (!d[i])
            q[ ++ tt] = i;

    while (hh <= tt)
    {
        int t = q[hh ++ ];

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (-- d[j] == 0)
                q[ ++ tt] = j;
        }
    }

    // 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。
    return tt == n - 1;
}

朴素dijkstra(稠密图)

时间复杂度O(n² + m),n 表示点数,m 表示边数

int g[N][N];  // 存储每条边
int dist[N];  // 存储1号点到每个点的最短距离
bool st[N];   // 存储每个点的最短路是否已经确定

// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    for (int i = 0; i < n - 1; i ++ )
    {
        int t = -1;     // 在还未确定最短路的点中,寻找距离最小的点
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        // 用t更新其他点的距离
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);

        st[t] = true;
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

堆优化dijkstra(稀疏图)

时间复杂度O(mlogn),n 表示点数,m 表示边数

typedef pair<int, int> PII;

int n;      // 点的数量
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储所有点到1号点的距离
bool st[N];     // 存储每个点的最短距离是否已确定

// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});      // first存储距离,second存储节点编号

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int ver = t.second, distance = t.first;

        if (st[ver]) continue;
        st[ver] = true;

        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({dist[j], j});
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

Bellman-Ford(有边数限制)

时间复杂度O(nm),n 表示点数,m 表示边数

int n, m;       // n表示点数,m表示边数
int dist[N];        // dist[x]存储1到x的最短路距离
int backup[N];  // 备份数组

struct Edge   // 边,a表示出点,b表示入点,w表示边的权重
{
    int a, b, w;
}edges[M];

// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
    for (int i = 0; i < n; i ++ )
    {
    	memcpy(backup, dist, sizeof dist);
        for (int j = 0; j < m; j ++ )
        {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            dist[b] = min(dist[b], backup[a] + w);
        }
    }

    if (dist[n] > 0x3f3f3f3f / 2) return -1;
    return dist[n];
}

spfa(队列优化的Bellman-Ford)

时间复杂度 平均情况下O(m),最坏情况下 O(nm),n 表示点数,m 表示边数

int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储每个点到1号点的最短距离
bool st[N];     // 存储每个点是否在队列中

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])     // 如果队列中已存在j,则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

spfa判断负环

时间复杂度O(nm),n 表示点数,m 表示边数

int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N], cnt[N];        // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
bool st[N];     // 存储每个点是否在队列中

// 如果存在负环,则返回true,否则返回false。
bool spfa()
{
    // 不需要初始化dist数组
    // 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。

    queue<int> q;
    for (int i = 1; i <= n; i ++ )
    {
        q.push(i);
        st[i] = true;
    }

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;       // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

floyd

时间复杂度O(n³),n 表示点数

初始化:
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;

// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

朴素Prim

时间复杂度O(n² + m),n 表示点数,m 表示边数

int n;      // n表示点数
int g[N][N];        // 邻接矩阵,存储所有边
int dist[N];        // 存储其他点到当前最小生成树的距离
bool st[N];     // 存储每个点是否已经在生成树中


// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim()
{
    memset(dist, 0x3f, sizeof dist);

    int res = 0;
    for (int i = 0; i < n; i ++ )
    {
        int t = -1;
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        if (i && dist[t] == INF) return INF;

        if (i) res += dist[t];
        st[t] = true;

        for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
    }

    return res;
}

Kruskal算法

时间复杂度O(mlogm),n 表示点数,m 表示边数

int n, m;       // n是点数,m是边数
int p[N];       // 并查集的父节点数组

struct Edge     // 存储边
{
    int a, b, w;

    bool operator< (const Edge &W)const
    {
        return w < W.w;
    }
}edges[M];

int find(int x)     // 并查集核心操作
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int kruskal()
{
    sort(edges, edges + m);

    for (int i = 1; i <= n; i ++ ) p[i] = i;    // 初始化并查集

    int res = 0, cnt = 0;
    for (int i = 0; i < m; i ++ )
    {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;

        a = find(a), b = find(b);
        if (a != b)     // 如果两个连通块不连通,则将这两个连通块合并
        {
            p[a] = b;
            res += w;
            cnt ++ ;
        }
    }

    if (cnt < n - 1) return INF;
    return res;
}

染色法判别二分图

时间复杂度O(n + m),n 表示点数,m 表示边数

int n;      // n表示点数
int h[N], e[M], ne[M], idx;     // 邻接表存储图
int color[N];       // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色

// 参数:u表示当前节点,c表示当前点的颜色
bool dfs(int u, int c)
{
    color[u] = c;
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (color[j] == -1)
        {
            if (!dfs(j, !c)) return false;
        }
        else if (color[j] == c) return false;
    }

    return true;
}

bool check()
{
    memset(color, -1, sizeof color);
    bool flag = true;
    for (int i = 1; i <= n; i ++ )
        if (color[i] == -1)
            if (!dfs(i, 0))
            {
                flag = false;
                break;
            }
    return flag;
}

匈牙利算法

时间复杂度O(nm),n 表示点数,m 表示边数

int n1, n2;     // n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N], e[M], ne[M], idx;     // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N];       // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个
bool st[N];     // 表示第二个集合中的每个点是否已经被遍历过

bool find(int x)
{
    for (int i = h[x]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true;
            if (match[j] == 0 || find(match[j]))
            {
                match[j] = x;
                return true;
            }
        }
    }

    return false;
}

// 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点
int res = 0;
for (int i = 1; i <= n1; i ++ )
{
    memset(st, false, sizeof st);
    if (find(i)) res ++ ;
}

最近公共祖先(LCA)

在这里插入图片描述

时间复杂度O(nm),n 表示点数,m 表示边数

int n1, n2;     // n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N], e[M], ne[M], idx;     // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N];       // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个
bool st[N];     // 表示第二个集合中的每个点是否已经被遍历过

bool find(int x)
{
    for (int i = h[x]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true;
            if (match[j] == 0 || find(match[j]))
            {
                match[j] = x;
                return true;
            }
        }
    }

    return false;
}

// 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点
int res = 0;
for (int i = 1; i <= n1; i ++ )
{
    memset(st, false, sizeof st);
    if (find(i)) res ++ ;
}

动态规划

在这里插入图片描述

背包问题

01背包:每个物品只有一个

完全背包:每个物品有无限个 (从小到大)

多重背包:每个物品有n个

分组背包:有n组物品,每组只能选一个

// 通常情况下
for 物品
	for 体积
		for 决策

//  情况        初始化         遍历顺序        状态转移
// 求数量 	    f[0] = 1                  f[i] += f[i - v]
// 求最大         全0          m ~ v     f[i - v1][j - v2]
// 求最小  f[0] = 0,其他正无穷  m ~ 0      f[max(i - v1, 0)][max(j - v1, 0)]
// 体积恰好 f[0] = 0,其他负无穷

// 二维费用 + 体积至少为n, m 求最小费用
for(int i = n; i >= 0; i--)
	for(int j = m; j >= 0; j--)
    	f[i][j] = min(f[i][j], f[max(i - v1, 0)][max(j - v2, 0)] + w);
01背包
// 二维
for(int i = 1; i <= n; i++)
	for(int j = 0;j <= m; j++)
    {
    	f[i][j] = f[i - 1][j];   // 左边一定可以选
    	if(j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);      
    }
    
// 一维
// 在二维的基础上,将第一维使用滚动数组代替
// f[j - v[i]] 表示的是 f[i - 1][]上一层还未更新的值
// 如果从小到大更新 f[j - v[i]] 会在 f[j] 之前更新
// 变成当前层的值,所以采用从大到小更新
for(int i = 1; i <= n; i++)
	for(int j = m; j >= v[i]; j--)
		f[j] = max(f[j], f[j - v[i]] + w[i]);
完全背包
// 二维
// 和01背包不一样的是 完全背包是从第i层的值中更新
for(int i = 1; i <= n; i++)
	for(int j = 0;j <= m; j++)
    {
    	f[i][j] = f[i - 1][j];   // 左边一定可以选
    	if(j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);      
    }

// 一维
// 完全背包更新的是第i层的值 所以从小到大
for(int i = 1; i <= n; i++)
	for(int j = v[i];j <= m; j++)
    	f[j] = max(f[j], f[j - v[i]] + w[i]);      
多重背包
// 一维  1000
int cnt = 0;  // 分成多个组的数量
for(int i = 1; i <= n; i++) 
{
	int a, b, s;
    cin >> a >> b >> s;
    
    // 打包成不同组
    int k = 1;
    while(k <= s)
    {
    	cnt ++;
        v[cnt] = a * k;
        w[cnt] = b * k;
        s -= k;
        k *= 2;
    }
    if(s > 0)  // 剩余的加进去最后一组
    {
    	cnt ++;
        v[cnt] = a * s;
        w[cnt] = b * s;
    }
}
    
n = cnt;   // 更新 n为组的数量

//转换为 01背包 
for(int i = 1; i <= n; i++)
        for(int j = m; j >= v[i]; j--)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
            
// 不提前存的写法     
		for(int k = 1; k <= s; k *= 2)
		{
			for(int j = m; j >= k * v; j--)
				f[j] = max(f[j], f[j - k * v] + k * w);
			s -= k;
		}
		if(s) 
			for(int j = m; j >= s * v; j--)
				f[j] = max(f[j], f[j - s * v] + s * w);

单调队列优化

for(int j = 0; j < v; j ++)  // 余数
{
	memcpy(g, f, sizeof f);  // 上一层的值
	int hh = 0, tt = -1;
	for(int k = j; k <= m; k += v)
	{
		if(hh <= tt && q[hh] < k - s * v) hh ++;  // 滑出区间
		if(hh <= tt) f[k] = max(f[k], g[q[tt]] + (k - q[tt]) / v * w);  // 更新当前层的值
		while(hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt--; 
		q[++tt] = k;
	}
}
分组背包
for(int i = 1; i <= n; i++)
	for(int j = m; j >= 0; j--)
    	for(int k = 0; k < s[i]; k++)
        	if(v[i][k] <= j)
            	f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
混合背包
for(int i = 1; i <= n; i++)
{
	int v, w, s;
	cin >> v >> w >> s;
	if(!s) for(int j = v; j <= m; j++) f[j] = max(f[j], f[j - v] + w);
	else
	{
		if(s == -1) s = 1;    // 01背包是特殊的多重背包
		for(int k = 1; k <= s; k *= 2)
		{
			for(int j = m; j >= k * v; j--)
				f[j] = max(f[j], f[j - k * v] + k * w);
			s -= k;
		}
		if(s) 
			for(int j = m; j >= s * v; j--)
				f[j] = max(f[j], f[j - s * v] + s * w);
	}
}
有依赖的背包
#include <bits/stdc++.h>
using namespace std;

const int N = 110;

int v[N], w[N], f[N][N];
int h[N], e[N], ne[N], idx;
int n, m;

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u)
{
	// 点u必须选
    for(int i = m; i >= v[u]; i--) f[u][i] = w[u];
    
    for(int i = h[u]; ~i; i = ne[i])  // 循环物品组
    {
        int son = e[i];
        dfs(son);
        
        for(int j = m; j >= v[u]; j--) // 体积
            for(int k = 0; k <= j - v[u]; k ++) // 决策:分配给子树的空间
                f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
    }
}
int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    int root;
    for(int i = 1; i <= n; i++)
    {
        int p;
        cin >> v[i] >> w[i] >> p;
        if(p == -1) root = i;
        else add(p, i);
    }
    
    dfs(root);
    cout << f[root][m];
}
求具体方案
// 只能用二维
for(int i = n; i >= 1; i--)  // 倒着求
	for(int j = m; j >= 0; j++)
	{
		// ... 背包模型
		f[i][j] = f[i + 1][j];   // 由 i + 1 层推导
		if(j >= v[i]) f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);
	}

int j = m;
for(int i = 1; i <= n; i++)
	// 如果前面是由后面推导过来的,就是最佳方案
	if(j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i])
		cout << i << " ", j -= v[i];

// 如果是从前面推到过去的
    int j = m;
    for(int i = n; i; i--)
        for(int k = 0; k <= j; k++)
            if(f[i][j] == f[i - 1][j - k] + w[i][k])
            {
                p[i] = k;
                j -= k;
                break;
            }
求最优方案个数
memset(f, -0x3f, sizeof f);
f[0] = 0;
g[0] = 1;
for(int i =1 ; i <= n; i++)
{
    int v, w;
    cin >> v >> w;
    for(int j = m; j >= v; j--)
    {
        int cnt = 0;
        int maxv = max(f[j], f[j - v] + w);
        if(maxv == f[j]) cnt += g[j];
        if(maxv == f[j - v] + w) cnt += g[j - v];
        g[j] = cnt % mod;
        f[j] = maxv;
    }
}

int res = 0;
for(int i =0 ; i <= m; i++)
	res = max(res, f[i]);
int cnt = 0;
for(int i = 0; i <= m; i++)
    if(res == f[i])
        cnt += g[i], cnt %= mod;

线性DP

数字三角形模型

从左上角 到 右下角

for(int i = 1; i <= n; i++)
	for(int j = 1; j <= m; j++)
		f[i][j] = max(f[i - 1][j], f[i][j - 1]) + W[i][j];

两条路

for(int k = 2; k <= n + m; k ++)
	for(int i1 = 1; i1 <= n; i1++)
		for(int i2 = 1; i2 <= n; i2++)
		{
			int j1 = k - i1, j2 = k - i2;
			if(j1 >= 1 && j1 <= m && j2 >= 1 && j2 <= m)
			{
				int t = w[i1][j1];
				if(i1 != i2) t += w[i2][j2];
				f[k][i1][i2] = max(f[k - 1][i1][i2], max(f[k - 1][i1 - 1][i2], max(f[k - 1][i1][i2 - 1], f[k - 1][i1 - 1][i2 - 1]))) + t;
			}
		}
最长上升子序列模型

AcWing 895. 最长上升子序列

// f[i]: 以第i个数为结尾的最大上升子序列
int res = 0;
for(int i = 1; i <= n; i++)
{
    f[i] = 1; 
    for(int j = 1; j < i; j++)
        if(a[j] < a[i])
            f[i] = max(f[i], f[j] + 1);
    res = max(res, f[i]);
}

以任意点为起点,只能 往左走 或 往右走

上升 或 下降 的 最长子序列

// 双向求解LIS
int res = 0;
for(int i = 1; i <= n; i++)
{
	f[i] = 1;
	for(int j = 1; j < i; j++)
    	if(a[i] > a[j])
        	f[i] = max(f[i], f[j] + 1);
   	res = max(res, f[i]);
}
for(int i = n; i; i--)
{
	f[i] = 1;
    for(int j = n; j > i; j--)
    	if(a[i] > a[j])
        	f[i] = max(f[i], f[j] + 1);
   	res = max(res, f[i]);
}

以任意点为起点,同时 往左走 和 往右走

先上升再下降 的 最长子序列

for(int i = 1; i <= n; i++)
{
	f[i] = 1;
	for(int j = 1; j < i; j++)
    	if(a[i] > a[j])
        	f[i] = max(f[i], f[j] + 1);
}

for(int i = n; i; i--)
{
	g[i] = 1;
    for(int j = n; j > i; j--)
    	if(a[i] > a[j])
        	g[i] = max(g[i], g[j] + 1);
}
    
int res = 0;
for(int i = 1; i <= n; i++)
	res = max(res, f[i] + g[i] - 1);

选择 最多数量的 不相交的线

给一边排序, 另一边必须为递增序列 才不会相交

转化为 求 另一边的最大上升子序列

在这里插入图片描述

求最少需要几个 上升(下降)子序列 把全部覆盖

贪心:从头遍历,每次从 已使用数组 找第一个 大于等于 该值 进行替换, 没有就开辟

int cnt = 0;
for(int i = 0; i < n; i++)
{
	int k = 0;
	while(k < cnt && a[i] > g[k]) k++;
	g[k] = a[i];
	if(k == cnt) cnt++;
}

求最少需要几个 上升 或 下降 子序列 把全部覆盖

暴搜:从头遍历,每次从选择放入 上升 或 下降 子序列中

void dfs(int u, int su, int sd)
{
	if(su + sd >= ans) return;
	if(u == n)
	{
		ans = su + sd;
		return;
	}
	
	int k = 0;
	while(k < su && a[u] < up[k]) k++;
	int t = up[k];
	up[k] = a[u];
	if(k == su) dfs(u + 1, su + 1, sd);
	else dfs(u + 1, su, sd);
	up[k] = t;
	
	k = 0;
	while(k < sd && a[u] > down[k]) k++;
	t = down[k];
	down[k] = a[u];
	if(k == sd) dfs(u + 1, su, sd + 1);
	else dfs(u + 1, su, sd);
	down[k] = t;
}

AcWing 897. 最长公共子序列

for(int i = 1; i <= n; i++)
	for(int j = 1; j <= m; j++)
		if(a[i] == b[j]) f[i][j] = f[i - 1][j - 1] + 1;
		else f[i][j] = max(f[i - 1][j], f[i][j - 1]);

AcWing 902. 最短编辑距离

在这里插入图片描述

int min_ed(char a[], char b[])
{
    int la = strlen(a + 1), lb = strlen(b + 1);
    
    for(int i = 0; i <= la; i++) f[i][0] = i;
    for(int i = 0; i <= lb; i++) f[0][i] = i;
    
    for(int i = 1; i <= la; i++)
        for(int j = 1; j <= lb; j++)
        {
            f[i][j] = min(f[i - 1][j], f[i][j - 1]) + 1;
            f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
        }
    
    return f[la][lb];
}

AcWing 272. 最长公共上升子序列

最长公共上升子序列

for(int i = 1; i <= n; i++)
{
	int maxv = 1;
    for(int j = 1; j <= n; j++)
    {
    	if(a[i] == b[j]) f[i][j] = maxv;
        else f[i][j] = f[i - 1][j];
        if(b[j] < a[i]) maxv = max(maxv, f[i - 1][j] + 1);
  	}
}

区间DP

for (int len = 1; len <= n; len++) {         // 区间长度 
    for (int i = 1; i + len - 1 <= n; i++) { // 枚举起点
        int j = i + len - 1;                 // 区间终点
        if (len == 1) {
            dp[i][j] = 初始值
            continue;
        }

        for (int k = i; k < j; k++) {        // 枚举分割点,构造状态转移方程
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w[i][j]);
        }
    }
}

状态压缩DP

Acwing.291 棋盘放长方形

Acwing.1064 国王

Acwing.292 二维

1. 预处理出合法状态 (不能相邻)
2. 寻找合法状态的转移关系 (上下两行关系:不能重叠)
3. 初始化 f[0][0]..[0] = 1, DP
for() 遍历 行 或 列i
	for() 遍历 限制j
		for() 遍历 合法状态a 
			for() 遍历 可转移状态b
				if(符合条件) f[i][j][a] += f[i - 1][j - c][b];

数位DP

询问 [x, y] 中满足某种性质的个数

在这里插入图片描述

// 位数
const int N = 15;

int f[N][N];

// 初始化 满足性质的个数
void init()
{
	// 初始化化第一位
    for(int i = 0; i <= 9; i++) f[1][i] = 1;
    
    for(int i = 2; i <= N; i++)
        for(int j = 0; j <= 9; j++)
            for(int k = j; k <= 9; k++)
                f[i][j] += f[i - 1][k];
}

int dp(int n)
{
	// 特判 0
    if(!n) return 1;
    
    vector<int> num;
    while(n) num.push_back(n % 10), n /= 10;
    
    // last 为上一位数留下的判断条件
    int res = 0;
    int last = 0;
    
   	// 从低位开始枚举
    for(int i = num.size() - 1; i >= 0; i--)
    {
        int x = num[i];
        
        // 左分支
        for(int j = last; j < x; j ++) res += f[i + 1][j];
        
        // 性质
        if(last > x) break;
        last = x;
        
        // 右分支特判
        if(!i) res ++;
    }
    
    return res;
}

贪心

区间问题

区间分组 / 最大区间厚度 问题
// 有若干个活动,第i个活动开始时间和结束时间是[a, b],同一个教室安排的活动之间不能交叠,求要安排所有活动,少需要几个教室?

// 有时间冲突的活动不能安排在同一间教室,与该问题的限制条件相同,即最小需要的教室个数即为该题答案。

// 我们可以把所有开始时间和结束时间排序
// 遇到开始时间就把需要的教室加1,遇到结束时间就把需要的教室减1
// 在一系列需要的教室个数变化的过程中,峰值就是多同时进行的活动数,也是我们至少需要的教室数。

for(int i = 0; i < n; i ++)
{
	int l, r;
	scanf("%d %d", &l, &r);
	b[idx ++] = l * 2;//标记左端点为偶数。
	b[idx ++] = r * 2 + 1;// 标记右端点为奇数。
}

sort(b, b + idx);

int res = 1, t = 0;
for(int i = 0; i < idx; i ++)
{
	if(b[i] % 2 == 0) t ++;
	else t --;
	res = max(res, t);
}

附录

ACM常用单词

abbreviation省略;缩写
adjacent sequence elements相邻的元素串
algebraic term代数项
alphabetical order字典序
alternately rise and fall交替上升和下降
approximate string matching 模糊匹配
arbitrary precision arithmetic 高精度计算
arithmetic mean 算数平均值
ascending order升序
aspect ratio固定长宽比
axis;axes轴
bandwidth reduction 带宽压缩
base 底边;幂的底数
calculate计算
calendrical calculations 日期
clique 最大团
clockwise order顺时针方向顺序
columns列
combinatorial problems 组合问题
comma逗号
composite numbers 合数
computational geometry 计算几何
concave 凹的
connected components 连通分支
constrained and unconstrained optimization 最值问题
convex hull 凸包
convex凸
coordinates坐标
cryptography 密码
cubes立方
data structures 基本数据结构
deformed变形的
denote表示;标志;预示;象征
determinants and permanents 行列式
diagonal对角
dial钟面,拨打
dictionaries 字典
difference 差
digit位数;数字
discrete fourier transform 离散傅里叶变换
distinct 不同的;独一无二的
divisor 因子,除数
divisor 因子;分母
drawing graphs nicely 图的描绘
drawing trees 树的描绘
edge and vertex connectivity 割边/割点
edge coloring 边染色
embed插入
equation方程式;等式
equivalent equation同解方程;等价方程
eulerian cycle欧拉循环
even偶数
executed 执行的;生效的
exponent 指数;幂
factorial 阶乘
factorial 阶乘; 因子的,阶乘的
factoring 因子分解
feedback edge/vertex set 最大无环子图
finite state machine minimization 有穷自动机简化
foggiest idea概念
follow by跟随,其后
fraction:分数;小部分
generating graphs 图的生成
generating partitions 划分生成
generating permutations 排列生成
generating subsets 子集生成
graph data structures 图
graph isomorphism 同构
graph partition 图的划分
graph problems-polynomial 图论-多项式算法
grid网格;方格;(地图上的)坐标方格
horizontal or vertical direction水平和垂直方向
improper fraction 假分数
in the range of 在…范围内
in the shape of a cross十字形
indentical相同的
independent set 独立集
inequality不等式
intersection detection 碰撞测试
intersection横断;横切;交叉
intersect相交
interval区间
kd-trees 线段树
knapsack problem 背包问题
like terms ,similar terms同类项
linear equation线性方程
linear programming 线性规划
literal coefficient字母系数
logarithm 对数
longest common substring 最长公共子串
loop环
maintaining line arrangements 平面分割
matching 匹配
matrix multiplication 矩阵乘法
matrix 矩阵
matrix矩阵
mean 平均值
medial-axis transformation 中轴变换
median and selection 中位数
minimal volume最小体积
minimum spanning tree 最小生成树
mixed number 带分数
motion planning 运动规划
motion多边形
nearest neighbor search 最近点对查询
negative负
network flow 网络流
numerator 分子
numerical coefficient 数字系数
numerical problems 数值问题
odd奇数
optimal最佳的
original equation原方程
origin原点
overbrim溢出
parity property奇偶性
planarity detection and embedding 平面性检测和嵌入
ploygon-shaped faces/ polygon多边形
point location 位置查询
polygon partitioning 多边形分割
positive正
present error呈现错误
prime 质数
priority queues 优先队列
proceed运行
process处理
proper fraction真分数
quadrant象限,四分之一圆
quotient 商
random number generation 随机数生成
range search 范围查询
range值域
rate of convergence 收敛速度
robustness 鲁棒性
root sign 根号
round()四舍五入(当取舍位为5时,若取舍位数前的小数为奇数则直接舍弃,若为偶数则向上取舍)
rounded to n decimal places 精确到小数点后n位
rows 行
scenario方案;(可能发生的)情况;
searching 查找
segment 段;分割
serial连续的
series系列
set and string problems 集合与串的问题
set cover 集合覆盖
set data structures 集合
set packing 集合配置
shape similarity 相似多边形
common superstring公共父串
shortest path 最短路径
simplifying polygons 多边形化简
solving linear equations 线性方程组
sorting 排序
specify 指定
stack overflow堆栈溢出
steiner tree steiner树
string matching 模式匹配
text compression 压缩
there are no special punctuation symbols or spacingrules没有特殊标点符号或间距的规则
times乘
topological sorting 拓扑排序
transitive closure and reduction 传递闭包
triangle inequality三角不等式
triangulation 三角剖分
two-dimensional array二维数组
union 并集
unique identifier唯一的标识符
variable变量
vertex coloring 点染色
vertex cover 点覆盖
vertex顶点
voronoi diagrams voronoi图
weighted average 加权平均值

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值