info

acwing 算法基础课笔记
课程地址:https://www.acwing.com/activity/content/11/
欢迎使用我的链接进行学生认证,认证后填写邀请码可以获得 AC 币,AC 币在购买课程时可以 1:1 抵扣。

AcWing【AC之星】教育优惠计划!https://www.acwing.com/user/security/school_verify/ac_stars/ 我的邀请码是:JGURH

tips

对于递归类型的算法一定要考虑终止条件,否则会死循环。
声明 or 定义局部变量时需要进行赋值,栈的初始值无法保证。
使用数组模拟链表时一定要注意 idx 初始化和调用 init() 函数!
scanf / printf is much faster than cin / cout (even with sync_with_stdio)
some snippets

  1. // 优化 cin cout
  2. cin.tie(0);
  3. ios::sync_with_stdio(false);
  4. // 保证 x 模 N 得到的是正整数
  5. (x % N + N) % N
  6. // 遍历链表的时候记得更新
  7. p = p->next;

存双向图的时候数组空间记得 x2

基础算法

排序

快速排序

思想是分治
算法流程如下:

  • 确定分界值 x :arr[l] / arr[(l+r)/2] / arr[r] / arr[random_index]
  • 将 arr 分为两个部分,左边的值 小于等于 x ,右边的值大于等于 x (重要)
  • 递归对两部分进行排序
  • 不稳定,可以用 pair(val, idx) 解决,问题不大。

模板:

  1. void quick_sort(int q[], int l, int r){
  2. if (l >= r) return;
  3. int x = q[l + r >> 1], i = l-1, j = r+1;
  4. while(i < j){
  5. do i++; while(q[i] < x);
  6. do j--; while(q[j] > x);
  7. if (i < j) swap(q[i], q[j]);
  8. }
  9. quick_sort(q, l, j);
  10. quick_sort(q, j+1, r);
  11. }

归并排序

思想也是分治
快排是选择分界值,归并是选择分界点
算法流程如下:

  • 选择分界点:mid = (l+r)/2
  • 递归排序左右两部分
  • 归并,合二为一 (重要)

模板:

  1. int tmp[N];
  2. void merge_sort(int q[], int l, int r){
  3. if (l >= r) return;
  4. int mid = l + r >> 1;
  5. merge_sort(q, l, mid);
  6. merge_sort(q, mid+1, r);
  7. int k = 0, i = l, j = mid + 1;
  8. while(i <= mid && j <= r){
  9. if (q[i] <= q[j]) tmp[k++] = q[i++];
  10. else tmp[k++] = q[j++];
  11. }
  12. while(i <= mid) tmp[k++] = q[i++];
  13. while(j <= r) tmp[k++] = q[j++];
  14. for(int i = l, j = 0; i <= r; i ++, j ++) q[i] = tmp[j];
  15. }

二分

整数二分

单调性是可以二分充分不必要条件
二分的本质是寻找满足某种性质的边界

算法可分为两种情况,寻找右边界和左边界。计算 mid 方法不同,详见下图
image.png

两个模板:

  1. // 找到小于等于 x 的最大(右)下标
  2. int bsearch_r(int q[], int l, int r, int x){
  3. if (x < q[l] || q[r] < x) return -1;
  4. while(l < r){
  5. int mid = l + r + 1>> 1;
  6. if (q[mid] <= x) l = mid;
  7. else r = mid-1;
  8. }
  9. return q[l] == x ? l : -1;
  10. }
  11. // 找到大于等于 x 的最小(左)下标
  12. int bsearch_l(int q[], int l, int r, int x){
  13. if (x < q[l] || q[r] < x) return;
  14. while(l < r){
  15. int mid = l + r >> 1;
  16. if (q[mid] >= x) r = mid;
  17. else l = mid + 1;
  18. }
  19. return q[l] == x ? l : -1;
  20. }

浮点数二分

不用考虑各种边界条件。
r - l <= 1e-6 / for(int i=0; i<100; i++)

注意题目中的书数字范围,需要用于 l, r 的初始化

example:

  1. int main(){
  2. double x;
  3. cin >> x;
  4. // printf('')
  5. double l = -10000, r = -l;
  6. while( r - l >= 1e-8){
  7. // for(int i=0; i<100; i++){
  8. double mid = (r + l) / 2;
  9. if (mid * mid * mid < x) l = mid;
  10. else r = mid;
  11. }
  12. printf("%.6f", l);
  13. return 0;
  14. }

高精度

java 和 python 不需要考虑。
常见四种:

  • 两个大整数相加
  • 两个大整数相减
  • 一个大整数 x 一个小整数
  • 一个大整数 / 一个小整数

tips: 使用 vector 存储大整数,并且将大整数的地位存到 vector 的低位,这样之后进位方便。

  1. // 读取大整数并放到数组中
  2. string a; vector<int> A;
  3. cin >> a;
  4. for(int i = a.size() - 1; i >= 0; i --) A.push_back(a[i] - '0');
  5. // 打印数组中的大整数
  6. for(int i = C.size() - 1; i >= 0; i --) printf("%d", C[i]);
  7. // 大整数比较
  8. // A >= B
  9. bool cmp_ge(vector<int> A, vector<int> B){
  10. if (A.size() != B.size()) return A.size() > B.size();
  11. for(int i = A.size() - 1; i >= 0; i --)
  12. if (A[i] != B[i]) return A[i] >= B[i];
  13. return true;
  14. }
  15. // 去除前导 0
  16. while(C.size() > 1 && C.back() == 0) C.pop_back();

大整数加法

读进大整数字符串之后存到数组中,大整数的低位存到数组的低位,类似小端序,计算和的过程就和人工计算一样。
模板如下:

  1. // C = A + B
  2. vector<int> add(vector<int> A, vector<int> B){
  3. vector<int> C;
  4. int t = 0;
  5. for (int i = 0; i < A.size() || i < B.size() || t; i ++){
  6. if (i < A.size()) t += A[i];
  7. if (i < B.size()) t += B[i];
  8. C.push_back(t % 10);
  9. t /= 10;
  10. }
  11. return C;
  12. }

大整数减法

需要先判断一下 A 和 B 的大小,用大的数减去小的数,避免考虑边界情况。
具体计算过程也是模拟人工计算减法的过程:
image.png

模板如下 :

  1. // A >= B
  2. bool cmp_ge(vector<int> A, vector<int> B){
  3. if (A.size() != B.size()) return A.size() > B.size();
  4. for(int i = A.size() - 1; i >= 0; i --)
  5. if (A[i] != B[i]) return A[i] >= B[i];
  6. return true;
  7. }
  8. // C = A - B (A >= B)
  9. vector<int> sub(vector<int> A, vector<int> B){
  10. vector<int> C;
  11. int t = 0;
  12. for (int i = 0; i < A.size(); i ++){
  13. t = A[i] - t;
  14. if (i < B.size()) t -= B[i];
  15. C.push_back((t + 10) % 10);
  16. if (t < 0) t = 1;
  17. else t = 0;
  18. }
  19. // 去除前导 0
  20. while(C.size() > 1 && C.back() == 0) C.pop_back();
  21. return C;
  22. }

大整数 x 小整数:A x b

思想就是分别计算 A 的每一位和 b 的乘法,然后进位相加。
image.png
模板如下:

  1. // C = A x b
  2. vector<int> mul(vector<int> A, int b){
  3. vector<int> C;
  4. int t = 0;
  5. for(int i = 0; i < A.size() || t; i ++){
  6. if (i < A.size()) t += A[i] * b;
  7. C.push_back(t % 10);
  8. t /= 10;
  9. }
  10. while(C.size() > 1 && C.back() == 0) C.pop_back();
  11. return C;
  12. }

大整数 / 小整数:A / b

模拟人工计算方法。但是除法是从大整数的高位开始算,不过为了一致性我们还是使用相同的方法表示大整数,所以计算除法的时候会多一个 reverse 操作。
r 为余数
r = r * 10 + A[i]
c = r / b
r /= 10

模板如下:

  1. // C = A / b
  2. // r = A % b
  3. vector<int> div(vector<int> A, int b, int &r){
  4. vector<int> C;
  5. r = 0;
  6. for(int i = A.size() - 1; i >= 0; i --){
  7. r = r * 10 + A[i];
  8. C.push_back(r / b);
  9. r %= b;
  10. }
  11. // 将大整数的低位放到数组的低位。
  12. reverse(C.begin(), C.end());
  13. while(C.size() > 1 && C.back() == 0) C.pop_back();
  14. return C;
  15. }

前缀和 and 差分

互为逆运算

一维数组前缀和

从 1 开始计算
算法基础笔记 - 图4

二维矩阵前缀和

算法基础笔记 - 图5

差分

对于一个区间进行多次操作的 时候可以使用差分数组的方法,降低时间复杂度。

一维数组差分

有一个数组 A,设 A 的差分为 B
则 A 为 B 的前缀和数组。
通过对 B 进行操作,可以等价于对 A 进行操作,同时复杂度可以从 O(n) 降到 O(1)。
如果想对 A 中 [l, r] 区间的值整体加上一个 c,那么就可以对比进行如下操作替代:

  • B[l] += c
  • B[r+1] -= c

初始化 B 数组我们也可以利用上面的思想

  1. 认为 A 中全为 0, 则 B 也全为 0
  2. A 在 [1, 1] 区间加上 A[1] -> B[1] += A[1], B[1+1] -= A[1]; 循环

插入操作

  1. void insert(int l, int r, int c){
  2. a[l] += c;
  3. a[r+1] -= c;
  4. }

二维差分

思想和一维类似,插入操作代码如下

  1. // b 为 a 的差分矩阵
  2. void insert(int x1, int x2, int y1, int y2){
  3. b[x1][x2] += c;
  4. b[x2+1][yy] -= c;
  5. b[x1][y2+1] -= c;
  6. b[x2+1][y2+1] += c;
  7. }

双指针算法

分类:

  • 两个指针分别指向两个序列
  • 两个指针指向同一个序列

常见写法

  1. for (int i = 0, j = 0; i < n; i ++){
  2. while( j < i && check(i, j) j ++;
  3. ////
  4. }
  5. // 核心思想
  6. for (int i = 0; i < n; i ++)
  7. for(int j = 0; j < n; j ++)
  8. // do sth

可以由朴素算法优化而来,通过寻找单调性来应用双指针算法,如题目最长单调子序列:

  1. // 朴素算法
  2. for (int i = 0; i < n; i ++)
  3. for (int j = 0; j <= i; j ++)
  4. if check(j, i) {
  5. res = max(res, i - j + 1;
  6. }
  7. }
  8. // 双指针
  9. for (int i = 0, j = 0; i < n; i ++)
  10. while (j <= i && !check(j, i) j ++;
  11. res = max(res, i - j + 1);
  12. }

位运算

  1. // n 的二进制表示中第 k 位是几
  2. (n >> k) & 1
  3. // x 的最后一位 1
  4. int lowbit(x){
  5. return x & -x;
  6. }

离散化

https://www.acwing.com/video/14/ 1:35:00

应用场景:值域很大,但是非常稀疏
将有序的离散的大整数序列 A 映射到连续的从1开始的自然数序列 B

  • 从 1 开始是为了和前缀和操作结合

重点:

  • A 中可能有重复元素,需要 去重

  • 如何算出 A[i] 离散化后的值
    • 下标,二分
  1. vector<int> alls;
  2. sort(alls.begin(), alls.end());
  3. alls.erase(unique(alls.begin(), alls.end()), alls.end());
  4. int find(int x){
  5. int l = 0, r = alls.size() - 1;
  6. while (l < r){
  7. int mid = l + r >> 1;
  8. if (alls[mid] >= x) r = mid;
  9. else l = mid + 1;
  10. }
  11. return l + 1; // 下标从 1 开始,方便计算前缀和
  12. }

image.png

区间合并

https://www.acwing.com/video/14/
01:57:45

数据结构

  1. 链表与邻接表
  2. 栈与队列
  3. kmp

用数组来模拟(笔试用,速度快)
如果用指针+结构体每次 new 都很耗时,可以面试时用

链表

  • 单链表
    • 邻接表, 存储树和图
  • 双链表
    • 优化某些问题

单链表

  1. #define N 10010
  2. // head 存储头结点, 用 ne[0] 表示,所以 idx 从 1 开始
  3. // e 存储第 k 个节点的值
  4. // ne 存储第 k 个节点的下个节点的下标
  5. // idx 为未使用的最小下标
  6. int e[N], ne[N], idx;
  7. void idx(){
  8. idx = 1;
  9. }
  10. // 插入到头结点
  11. void H(int x){
  12. e[idx] = x;
  13. ne[idx] = ne[0];
  14. ne[0] = idx ++;
  15. }
  16. // 删除第 k (k 从 1 开始)个插入的节点下一个节点, k = 0 时表示删除头节点
  17. void D(int k){
  18. ne[k] = ne[ne[k]];
  19. }
  20. // 第 k (k 从 1 开始)个插入的节点后插入一个节点,k >= 1
  21. void I(int k, int x){
  22. if (k == 0){
  23. H(x);
  24. } else {
  25. e[idx] = x;
  26. ne[idx] = ne[k];
  27. ne[k] = idx ++;
  28. }
  29. }
  30. // 遍历
  31. int cur = ne[0];
  32. while (cur) {
  33. // do sth
  34. cur = ne[cur];
  35. }

双链表

  1. const int N = 100010;
  2. // head = 0
  3. // tail = 1
  4. int e[N], l[N], r[N], idx;
  5. void init(){
  6. r[0] = 1, l[1] = 0, idx = 2;
  7. }
  8. // (1) “L x”,表示在链表的最左端插入数x
  9. void L(int x){
  10. e[idx] = x;
  11. r[idx] = r[0];
  12. l[idx] = 0;
  13. l[r[0]] = idx;
  14. r[0] = idx ++;
  15. }
  16. // (2) “R x”,表示在链表的最右端插入数x
  17. void R(int x){
  18. e[idx] = x;
  19. r[idx] = 1;
  20. l[idx] = l[1];
  21. r[l[1]] = idx;
  22. l[1] = idx ++;
  23. }
  24. // (3) “D k”,表示将第k个插入的数删除
  25. void D(int k){
  26. k ++;
  27. r[l[k]] = r[k];
  28. l[r[k]] = l[k];
  29. }
  30. // (4) “IL k x”,表示在第k个插入的数左侧插入一个数
  31. void IL(int k, int x){
  32. k ++;
  33. e[idx] = x;
  34. l[idx] = l[k];
  35. r[idx] = k;
  36. r[l[k]] = idx;
  37. l[k] = idx ++;
  38. }
  39. // (5) “IR k x”,表示在第k个插入的数右侧插入一个数。
  40. void IR(int k, int x){
  41. // if
  42. // IL(++ k, x);
  43. k ++;
  44. e[idx] = x;
  45. l[idx] = k;
  46. r[idx] = r[k];
  47. l[r[k]] = idx;
  48. r[k] = idx ++;
  49. }

邻接表

其实就是一堆单链表

栈和队列

  1. #define N 100010
  2. int stk[tt], tt;
  3. void push(int x){ stk[++ tt] = x;}
  4. void pop(){tt --;}
  5. bool empty(){return tt <= 0;}
  6. int top(){return stk[tt];}
  7. int size(){return hh;}

队列

  1. #define N 100010
  2. int a[N], q[N], hh, tt = -1, n, k;
  3. void push(int x){q[++ tt] = x;}
  4. void pop_front(){hh ++; }
  5. void pop_back(){ tt --; }
  6. bool empty(){return hh > tt;}
  7. int front(){return q[hh];}
  8. int back(){return q[tt];}
  9. int size(){return tt - hh + 1;}
  10. int clear(){hh = 0, tt = -1;}

单调栈

应用场景:找寻一个序列中每一个数的左边离它最近的且比它小的数的位置
example
3 4 2 7 5
-1 0 -1 2 2

  1. // naive method
  2. for (int i = 0; i < n; i ++){
  3. for (int j = i; j >= 0; j --)
  4. if (a[j] < a[i]) break;
  5. cout << j << endl;
  6. }

也可用于寻找每个数右边第一个比它大的数的位置。此时栈中的元素从栈底到栈顶单调不升。

单调队列

应用场景:

  • 求滑动窗口中的最大值 or 最小值
  1. #include <iostream>
  2. using namespace std;
  3. const int N = 1000010;
  4. int a[N], n, k, q[N], hh, tt = -1;
  5. int main(){
  6. scanf("%d%d", &n, &k);
  7. for (int i = 0; i < n; i ++) scanf("%d", &a[i]);
  8. // 递增单调队列,求最小值。
  9. for (int i = 0; i < n; i ++) {
  10. if (hh <= tt && q[hh] < i - k + 1) hh ++;
  11. while (hh <= tt && a[q[tt]] > a[i]) tt --;
  12. q[++ tt] = i;
  13. if (i >= k - 1) printf("%d ", a[q[hh]]);
  14. }
  15. puts("");
  16. // 递减单调队列,求最大值。
  17. hh = 0, tt = -1;
  18. for (int i = 0; i < n; i ++) {
  19. if (hh <= tt && q[hh] < i - k + 1) hh ++;
  20. while (hh <= tt && a[q[tt]] < a[i]) tt --;
  21. q[++ tt] = i;
  22. if (i >= k - 1) printf("%d ", a[q[hh]]);
  23. }
  24. return 0;
  25. }

kmp 算法

  1. #include <iostream>
  2. #include <cstdio>
  3. using namespace std;
  4. //string P, S;
  5. const int N = 100010, M = 1000010;
  6. int n, m;
  7. char p[N], s[M];
  8. int ne[N];
  9. int main() {
  10. cin >> n >> p + 1 >> m >> s + 1;
  11. // build next
  12. for (int i = 2, j = 0; i <= n; i ++){
  13. while (j && p[i] != p[j + 1]) j = ne[j];
  14. if (p[i] == p[j + 1]) j ++;
  15. ne[i] = j;
  16. }
  17. // kmp match
  18. for (int i = 1, j = 0; i <= m; i ++){
  19. while (j && s[i] != p[j + 1]) j = ne[j];
  20. if (s[i] == p[j + 1]) j ++;
  21. if (j == n){
  22. printf("%d ", i - n);
  23. j = ne[j];
  24. }
  25. }
  26. return 0;
  27. }

Trie 树

用于快速存储和查找字符串集合的数据结构
使用数组模拟:

  1. #define N 100010
  2. int son[N][26], cnt[N], idx, n;
  3. void insert(char s[]){
  4. int cur = 0;
  5. for (int i = 0; s[i]; i ++){
  6. int u = s[i] - 'a';
  7. if (son[cur][u]){
  8. cur = son[cur][u];
  9. } else {
  10. son[cur][u] = ++ idx;
  11. cur = son[cur][u];
  12. }
  13. }
  14. cnt[cur] ++;
  15. }
  16. int query(char s[]){
  17. int cur = 0;
  18. for (int i = 0; s[i]; i ++){
  19. int u = s[i] - 'a';
  20. if (son[cur][u]) cur = son[cur][u];
  21. else return 0;
  22. }
  23. return cnt[cur];
  24. }

并查集

面试常见。代码段,思想比较巧妙。
用途:

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

近乎 O(1) 时间复杂度完成上述两个操作
原理:

  • 每个集合用一颗树表示
  • 树根的编号就是集合的编号
  • 每个节点存储它的父节点 p[x]

问题1:如何判断树根:p[x] = x
问题2:如何求 x 的集合编号:while (p[x] != x) x = p[x];
问题3:如歌合并两个集合: p[x] = y
优化:

  • 路经压缩:找到根节点之后,将路径上所有节点都直接指向根节点。 ```cpp // 返回 x 的祖宗节点 + 路径压缩 int find(int x){ if (p[x] != x) p[x] = find(p[x]); return p[x]; }

// merge p[find(a)] = find(b);

// whether in the same set find(a) == find(b);

  1. <a name="znW64"></a>
  2. ## 堆
  3. 如何手写一个堆<br />支持的操作:
  4. - 插入一个数
  5. - 求集合中的最小值
  6. - 删除集合中的最小值
  7. - 删除任意一个元素
  8. - 修改任意一个元素
  9. 基本结构
  10. - 一颗完全二叉树
  11. - 任意一个节点的值小于等于两个字节点的值
  12. 使用二维数组存储:
  13. - left child : 2x
  14. - right child : 2x + 1
  15. 两个基本操作:
  16. - down(x)
  17. - up(x)
  18. ```cpp
  19. // heap starts at index 1
  20. // initialize heap
  21. for (int i = n / 2; i >= 1; i --) down(i);
  22. // up
  23. void up(int u){
  24. while ( u / 2 && h[u / 2] > h[u]){
  25. swap(h[u / 2], h[u]);
  26. u /= 2;
  27. }
  28. }
  29. // down
  30. void down(int u){
  31. int t = u;
  32. if (u * 2 <= hsize && h[u * 2] < h[t]) t = u * 2;
  33. if (u * 2 + 1 <= hsize && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
  34. if (u != t){
  35. swap(h[u], h[t]);
  36. down(t);
  37. }
  38. }
  39. // insert
  40. h[size] = x; size ++; up(x);
  41. // remove smallest x
  42. h[1] = h[size --]; down(1);
  43. // query smallest x
  44. h[1];
  45. // reomve k
  46. h[k] = h[size --]; down(k); up(k);
  47. // modify k
  48. h[k] = x; down(k); up(k);

哈希表

  • 存储方式:
    • 开放寻址法
    • 拉链法
  • 字符串哈希方法

算模的时候需要保证得到的结果是正整数 算法基础笔记 - 图7

拉链法

  1. const int N = 100019;
  2. int h[N], e[N], ne[N], idx;
  3. void insert(int x){
  4. int y = (x % N + N) % N;
  5. e[++ idx] = x;
  6. ne[idx] = h[y];
  7. h[y] = idx;
  8. ;
  9. }
  10. bool query(int x){
  11. int y = (x % N + N) % N;
  12. int cur = h[y];
  13. while(cur && e[cur] != x) cur = ne[cur];
  14. return e[cur] == x;
  15. }

开放寻址法

  1. // 开放寻址法
  2. const int N = 100019;
  3. int h[N * 3];
  4. int find(int x){
  5. int y = (x % N + N) % N;
  6. while (h[y] && h[y] != x) y ++;
  7. return y;
  8. }
  9. void insert(int x){
  10. int y = find(x);
  11. h[y] = x;
  12. }
  13. bool query(int x){
  14. if(h[find(x)] == x) return true;
  15. else return false;
  16. }

字符串哈希

image.png
通常 P 取 131 / 13331,hash 值对 2 ** 64 取模。

  1. #include <iostream>
  2. #include <cstring>
  3. using namespace std;
  4. const int N = 100019;
  5. typedef unsigned long long ULL;
  6. ULL h[N], ps[N], P = 131;
  7. char s[N];
  8. ULL get(int l, int r){
  9. return h[r] - h[l - 1] * ps[r - l + 1];
  10. }
  11. int main(){
  12. int n, m;
  13. scanf("%d%d", &n, &m);
  14. scanf("%s", s + 1);
  15. ps[0] = 1;
  16. for (int i = 1; i <= n; i ++){
  17. ps[i] = ps[i - 1] * P;
  18. h[i] = h[i - 1] * P + s[i];
  19. }
  20. while (m --){
  21. int l1, r1, l2, r2;
  22. scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
  23. if (get(l1, r1) == get(l2, r2)) printf("Yes\n");
  24. else printf("No\n");
  25. }
  26. return 0;
  27. }

stl

  1. vector, 变长数组,倍增的思想
  2. size() 返回元素个数
  3. empty() 返回是否为空
  4. clear() 清空
  5. front()/back()
  6. push_back()/pop_back()
  7. begin()/end()
  8. []
  9. 支持比较运算,按字典序
  10. pair<int, int>
  11. first, 第一个元素
  12. second, 第二个元素
  13. 支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
  14. string,字符串
  15. size()/length() 返回字符串长度
  16. empty()
  17. clear()
  18. substr(起始下标,(子串长度)) 返回子串
  19. c_str() 返回字符串所在字符数组的起始地址
  20. queue, 队列
  21. size()
  22. empty()
  23. push() 向队尾插入一个元素
  24. front() 返回队头元素
  25. back() 返回队尾元素
  26. pop() 弹出队头元素
  27. priority_queue, 优先队列,默认是大根堆
  28. size()
  29. empty()
  30. push() 插入一个元素
  31. top() 返回堆顶元素
  32. pop() 弹出堆顶元素
  33. 定义成小根堆的方式:priority_queue<int, vector<int>, greater<int>> q;
  34. stack,
  35. size()
  36. empty()
  37. push() 向栈顶插入一个元素
  38. top() 返回栈顶元素
  39. pop() 弹出栈顶元素
  40. deque, 双端队列
  41. size()
  42. empty()
  43. clear()
  44. front()/back()
  45. push_back()/pop_back()
  46. push_front()/pop_front()
  47. begin()/end()
  48. []
  49. set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
  50. size()
  51. empty()
  52. clear()
  53. begin()/end()
  54. ++, -- 返回前驱和后继,时间复杂度 O(logn)
  55. set/multiset
  56. insert() 插入一个数
  57. find() 查找一个数
  58. count() 返回某一个数的个数
  59. erase()
  60. (1) 输入是一个数x,删除所有x O(k + logn)
  61. (2) 输入一个迭代器,删除这个迭代器
  62. lower_bound()/upper_bound()
  63. lower_bound(x) 返回大于等于x的最小的数的迭代器
  64. upper_bound(x) 返回大于x的最小的数的迭代器
  65. map/multimap
  66. insert() 插入的数是一个pair
  67. erase() 输入的参数是pair或者迭代器
  68. find()
  69. [] 注意multimap不支持此操作。 时间复杂度是 O(logn)
  70. lower_bound()/upper_bound()
  71. unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
  72. 和上面类似,增删改查的时间复杂度是 O(1)
  73. 不支持 lower_bound()/upper_bound(), 迭代器的++,--
  74. bitset, 圧位
  75. bitset<10000> s;
  76. ~, &, |, ^
  77. >>, <<
  78. ==, !=
  79. []
  80. count() 返回有多少个1
  81. any() 判断是否至少有一个1
  82. none() 判断是否全为0
  83. set() 把所有位置成1
  84. set(k, v) 将第k位变成v
  85. reset() 把所有位变成0
  86. flip() 等价于~
  87. flip(k) 把第k位取反
  88. 作者:yxc
  89. 链接:https://www.acwing.com/blog/content/404/
  90. 来源:AcWing
  91. 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

数学知识

质数

试除法判定质数

O(sqrt(n)

  1. bool is_prime(int x) {
  2. if (x < 2) return false;
  3. for (int i = 2; i <= x / i; i ++)
  4. if (x % i == 0) return false;
  5. return true;
  6. }

分解质因数

O(sqrt(n))
注意处理最后剩的一个大于 sqrt(x) 的因数

  1. void divide(int x){
  2. if (x < 2) return ;
  3. for (int i = 2; i <= x / i; i ++) {
  4. if (x % i == 0) {
  5. int cnt = 0;
  6. while (x % i == 0) {
  7. x /= i;
  8. cnt ++;
  9. }
  10. printf("%d %d\n", i, cnt);
  11. }
  12. }
  13. if (x > 1) printf("%d %d\n", x, 1);
  14. printf("\n");
  15. }

筛质数

埃氏筛法
用每个质数去筛后面所有 <= n 的倍数。
O(n loglogn)

  1. void get_primes(int n) {
  2. for (int i = 2; i <= n; i ++)
  3. if (!st[i]) {
  4. // printf("%d ", i);
  5. primes[cnt ++] = i;
  6. for (int j = i + i; j <= n; j += i) st[j] = true;
  7. }
  8. }

线性筛法
对于每个数用最小的质因数筛掉。
因为每个数最多只会被标记一次,所以算法的时间复杂度是:O(n)

  1. //线性筛法-O(n), n = 1e7 的时候基本就比埃式筛法快一倍了
  2. //算法核心:x仅会被其最小质因子筛去
  3. void get_prime(int x) {
  4. for(int i = 2; i <= x; i++) {
  5. if(!st[i]) prime[cnt++] = i;
  6. for(int j = 0; prime[j] <= x / i; j++) {
  7. //对于任意一个合数 x ,假设 pj 为 x 最小质因子,当 i x / pj 时,一定会被筛掉
  8. st[prime[j]*i] = true;
  9. if(i % prime[j] == 0) break;
  10. /*
  11. 1.i % pj == 0, pj 定为 i 最小质因子,pj 也定为 pj * i 最小质因子
  12. 2.i % pj != 0, pj 定小于 i 的所有质因子,所以 pj 也为 pj * i 最小质因子
  13. */
  14. }
  15. }
  16. }

约数

试除法求约数

O(sqrt(n)
如果 n 是平分数需要避免输出两次 sqrt(n),所以需要增加一次判断。

  1. void get_divisors(int x) {
  2. vector<int> res;
  3. for (int i = 1; i < x / i; i ++)
  4. if (x % i == 0) {
  5. res.push_back(i);
  6. if (i != x / i) res.push_back(x / i);
  7. }
  8. sort(res.begin(), res.end());
  9. for (auto val : res) printf("%d ", val);
  10. pust("");
  11. }

约数个数/约数和

算法基础笔记 - 图9

  1. // hashmap tips:
  2. unordered_map<int, int> hash;
  3. hash[k ++]; // 若 k 不在 hash 的 key 中的话则等价于 hash[k] = 1;
  4. // 遍历 hash 表
  5. for (auto [k, v] : hash) {
  6. // ...
  7. }
  8. // 计算 p^0 + p^1 + ... + p^k
  9. typedef long long LL;
  10. const int mod = 1e9 + 7;
  11. LL t = 1;
  12. while (k --) t = (t * p + 1) % mod;
  13. // 分解质因数,并记录每个质因数的次数
  14. unordered_map<int, int> primes;
  15. void get_primes(int x) {
  16. for (int i = 2; i <= x / i; i ++)
  17. if (x % i == 0)
  18. while (x % i == 0) {
  19. x /= i;
  20. primes[i] ++;
  21. }
  22. if (x > 1) primes[x] ++;
  23. }

最大公约数

欧几里得算法

  1. int gcd(int a, int b) {
  2. return b ? gcd(b, a % b) : a;
  3. }

欧拉函数

欧拉函数 算法基础笔记 - 图10 的定义为:从 1 到 n 中所有和 n 互质的数的个数。特别地,对于质数 p,算法基础笔记 - 图11
一个 n 的欧拉函数的计算方法如下:
算法基础笔记 - 图12

求欧拉函数

O(sqrt(n))
和分解质因数代码类似。实现的时候为了避免出现小数误差所以需要调整一下计算顺序。

  1. int main() {
  2. int n;
  3. cin >> n;
  4. while (n --) {
  5. int x;
  6. cin >> x;
  7. int res = x;
  8. for (int i = 2; i <= x / i; i ++) {
  9. if (x % i == 0) {
  10. res = res / i * (i - 1);
  11. while (x % i == 0) x /= i;
  12. }
  13. }
  14. if (x > 1) res = res / x * (x - 1);
  15. cout << res << endl;
  16. }
  17. return 0;
  18. }

筛法求欧拉函数

可以一次性求 2 到 n 所有数的欧拉函数。
O(n)

  1. #include <iostream>
  2. using namespace std;
  3. const int N = 1000010;
  4. typedef long long LL;
  5. int primes[N], cnt, phi[N];
  6. bool st[N];
  7. void get_primes(int n) {
  8. phi[1] = 1;
  9. for (int i = 2; i <= n; i ++) {
  10. if (!st[i]) {
  11. primes[cnt ++] = i;
  12. // i 是质数,所以 \phi(i) = i - 1
  13. phi[i] = i - 1;
  14. }
  15. for (int j = 0; primes[j] <= n / i; j ++) {
  16. int t = i * primes[j];
  17. st[t] = true;
  18. if (i % primes[j] == 0) {
  19. // primes[j] 是 i 的(最小)质因数,因此计算 \phi(i) 的时候已经
  20. // 乘过一次 (primes[j] - 1 / primes[j])。所以在计算 t 的质因数的时候
  21. // 就不需要再乘 (primes[j] - 1 / primes[j]) 了。
  22. // \phi(x) = \phi(i) / i * t = \phi(i) * primes[j];
  23. phi[t] = (LL) phi[i] * primes[j];
  24. break;
  25. }
  26. // primes[j] 和 i 互质,因此 \phi(x) = \phi(i) / i * t * (primes[j] - 1) / primes[j]
  27. // = \phi(i) * (primes[j] - 1)
  28. phi[t] = (LL) phi[i] * (primes[j] - 1);
  29. }
  30. }
  31. }
  32. int main() {
  33. int n;
  34. cin >> n;
  35. get_primes(n);
  36. LL res = 0;
  37. for (int i = 1; i <= n; i ++) res += phi[i];
  38. cout << res << endl;
  39. return 0;
  40. }

欧拉定理

算法基础笔记 - 图13
算法基础笔记 - 图14

快速幂

算法基础笔记 - 图15
image.png

  1. typedef long long LL;
  2. int qmi(int a, int b, int p) {
  3. LL res = 1 % p;
  4. while (b) {
  5. if (b & 1)
  6. res = (LL) res * a % p;
  7. b >>= 1;
  8. a = (LL) a * a % p;
  9. }
  10. return res;
  11. }

应用:

  • 求逆元(欧拉定理)
    • 判断一下无解情况(a 是 p 的整数倍)

      扩展欧几里得

      裴蜀定理:对于任意一对正整数 a,b。一定存在两个非零整数 x,y。使得 ax + by = gcd(a, b),且 gcd(a, b) 是 a 和 b 能线性租的得到的最小正整数。

image.png

  1. int exgcd(int a, int b, int &x, int &y) {
  2. if (!b) {
  3. x = 1, y = 0;
  4. return a;
  5. }
  6. int d = exgcd(b, a % b, y, x);
  7. // x = x
  8. y -= a / b * x;
  9. return d;
  10. }

应用:

  • 线性同余方程
  • 求逆元

中国剩余定理

// todo

高斯消元

用途:解方程。O(n^3) 时间复杂度解 n 元线性方程组。
image.png算法基础笔记 - 图19

  1. #include <iostream>
  2. #include <cmath>
  3. using namespace std;
  4. const int N = 110;
  5. double q[N][N], eps = 1e-6;
  6. int n;
  7. /*
  8. return:
  9. 0 表示无解
  10. 1 表示有唯一解
  11. 2 表示有无穷多解
  12. */
  13. int gauss() {
  14. int c = 0, r = 0;
  15. for (c = 0; c < n; c ++) {
  16. int t = r;
  17. // 找到最大元素
  18. for (int i = r; i < n; i ++)
  19. if (fabs(q[i][c]) > fabs(q[t][c])) t = i;
  20. // 若均为 0,break;
  21. if(fabs(q[t][c]) < eps) continue;
  22. // 和最上面的行交换
  23. for (int i = c; i <= n; i ++) swap(q[r][i], q[t][i]);
  24. // 对最上面的行进行归一化处理, 注意要从后往前
  25. for (int i = n; i >= c; i --) q[r][i] /= q[r][c];
  26. // 消除下面的行
  27. for (int i = r + 1; i < n; i ++)
  28. if (fabs(q[i][c]) > eps)
  29. for (int j = n; j >= c; j --)
  30. q[i][j] -= q[r][j] * q[i][c];
  31. r ++;
  32. // log();
  33. }
  34. // log();
  35. // 此时我们已经得到了梯形矩阵,需要判断是否有解
  36. if (r < n) {
  37. for (int i = r; i < n; i ++)
  38. if (fabs(q[i][n]) > eps) return 0;
  39. return 2;
  40. }
  41. // 向上消除
  42. for (int i = n - 2; i >= 0; i --) {
  43. for (int j = n - 1; j > i; j --) {
  44. q[i][n] -= q[j][n] * q[i][j];
  45. q[i][j] = 0;
  46. }
  47. }
  48. return 1;
  49. }

组合数

算法基础笔记 - 图20
解法:

  • 预处理 C_i^j (a,b 较小 1e3 级别)
    • 时间:O(n),空间 O(n^2)
  • 预处理阶乘和对应的逆(快速幂)(a,b 较大 1e5 级别)
    • O(nlogn)
  • 卢卡斯定理(a,b 较大,1e18 级别)

  • 高精度

    • 分解质因数 + 高精度乘法
  • 卡特兰数

算法基础笔记 - 图21

容斥原理

时间复杂度:
算法基础笔记 - 图22
实现:位运算

博弈论

nim 游戏

必胜状态:可以转移到必败状态
必败状态:无法走到必败状态

sg 图

mex 函数

搜索与图论

  1. 普通 dfs
  2. 普通 bfs
  3. 树与图的存储
  4. 树与图的 dfs
  5. 树与图的 bfs
  6. 拓扑排序
  7. 最短路径

    DFS

    stack
    使用空间和高度成正比。
    不具有最短性。
    两个概念:
  • 回溯
  • 剪枝

关键是想清楚便利顺序
回溯的时候一定要注意恢复现场
经典题型

  • 全排列
  • N 皇后问题

N 皇后问题中判断对角线是否满足条件的技巧:

  1. const int N = 10;
  2. bool col[N], dg[N * 2], udg[N * 2];
  3. int x, y, n;
  4. if ( ! col[y] && ! dg[x + y] && ! dg[x - y + n]){
  5. // do sth
  6. ;
  7. }

BFS

queue
使用的空间和宽度成正比,指数级别。
包含最短路的性质。
只有在所有边权都为 1 时才能有 BFS 来解决最短路问题,否则需要使用专门的最短路算法。
典型例题:

  • 走迷宫

树与图的存储

树是无环联通图
无向图是特殊的有向图
有向图的存储方法:

  • 邻接矩阵
    • 用的比较少
    • 浪费空间 O(n ^ 2)
    • 用于稠密图
  • 邻接表
  1. const int N = 10010;
  2. int h[N], e[N * 2], ne[N * 2], idx;
  3. //add edge : a -> b
  4. void ae(int a, int b){
  5. e[++ idx] = b;
  6. ne[idx] = h[a];
  7. h[a] = idx;
  8. }
  9. // iterate edges os node a
  10. int cur = h[a];
  11. while (cur){
  12. b = e[cur];
  13. // do sth
  14. cur = ne[cur];
  15. }

拓扑排序

有向无环图一定存在拓扑序列,因此也被称作拓扑图
入度和出度
基于 bfs,但是需要考虑重边的情况,所以不要用 st 数组。

  • 比如有两条 a -> b 的边那么就需要两次才能让 b 的入度为 0。
  • 因为仅当一个节点入度为 0 的时候才会被放到队列里面,所以也不用担心环导致的死循环。

最短路问题

约定

  • n 表示点数量
  • m 表示边数量

稠密图:m ~ n^2
稀疏图:m ~ n

分类

  • 单源最短路
    • 所有边权重都为正数
      • 朴素 Dijkstra 算法:O(n^2)
      • 堆优化 Dijkstra 算法:O(m logn)
    • 存在负权遍
      • Bellman-Ford 算法: O(m n)
      • SPFA : 平均负责度为 O(m),最坏为 O(m n)
  • 多源(起点)汇(终点)最短路
    • Floyd 算法:O(n^3)

考察点:如何把问题抽象成最短路问题,并构造点和边的过程
image.png

Dijkstra

image.png

Bellman-Ford

image.png

SPFA

图中不能有负权回路
优化的 Bellman-Ford 算法:只有一个节点的距离被更新了,才有必要更新他的后续节点。
image.png
队列中包含所有已经更新的点
需要用一个数组保证队列中的元素唯一,因为重复的点没有意义

Floyd

可以有负权边,但是不能有负权回路。
算法思想是动态规划:
f[k, i, j] 表示从 i 到 j 经过 1..k 个点的最短距离。
复杂度为 n^3
直接在原矩阵上操作即可:d[a][b] 表示从节点 a 到节点 b 的最短距离。

  1. for k in 1..n {
  2. for i in 1..n {
  3. for j in 1..n {
  4. d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
  5. }
  6. }
  7. }
  8. for (int i = 1; i <= n; i ++ )
  9. for (int j = 1; j <= n; j ++ )
  10. if (i == j) d[i][j] = 0;
  11. else d[i][j] = INF;
  12. while (m --) {
  13. int a, b, c;
  14. scanf("%d%d%d", &a, &b, &c);
  15. d[a][b] = min(d[a][b], c);
  16. }

最小生成树

无向图
两种算法:

  • prime 算法
  • kruskal 算法

难点:建图
image.png

prim 算法

image.png
写法和 Dijkstra 很像。

  1. #include <iostream>
  2. #include <cstring>
  3. using namespace std;
  4. const int N = 510, M = 200010, INF = 0x3f3f3f3f;
  5. int g[N][N], n, m;
  6. void add_edge(int a, int b, int w) {
  7. if (a == b) return;
  8. g[a][b] = g[b][a] = min(g[a][b], w); // min !
  9. }
  10. bool st[N];
  11. int dist[N];
  12. int prim() {
  13. memset(dist, 0x3f, sizeof dist);
  14. int res = 0;
  15. for (int i = 0; i < n; i ++) {
  16. int t = -1;
  17. for (int j = 1; j <= n; j ++)
  18. if (!st[j] && (t == -1 || dist[j] < dist[t])) t = j;
  19. if (i && dist[t] == INF) return -1;
  20. if (i) res += dist[t];
  21. st[t] = true;
  22. for (int j = 1; j <= n; j ++) dist[j] = min(dist[j], g[t][j]);
  23. }
  24. return res;
  25. }
  26. int main() {
  27. scanf("%d%d", &n, &m);
  28. memset(g, 0x3f, sizeof g);
  29. for (int i = 0; i < m; i ++) {
  30. int a, b, w;
  31. scanf("%d%d%d", &a, &b, &w);
  32. add_edge(a, b, w);
  33. }
  34. int res = prim();
  35. if (res == -1) puts("impossible");
  36. else printf("%d\n", res);
  37. return 0;
  38. }

kruskal 算法

image.png

  1. #include <iostream>
  2. #include <cstring>
  3. #include <algorithm>
  4. using namespace std;
  5. const int N = 100010, M = 400010, INF = 0x3f3f3f3f;
  6. int n, m;
  7. struct Edge {
  8. int a, b, w;
  9. bool operator< (const struct Edge &rhs) {
  10. return w < rhs.w;
  11. }
  12. } edges[M];
  13. int p[N];
  14. int find(int x) {
  15. if (p[x] != x) p[x] = find(p[x]);
  16. return p[x];
  17. }
  18. int kruskal() {
  19. for (int i = 0; i <= n; i ++) p[i] = i;
  20. sort(edges, edges + m);
  21. int res = 0;
  22. int cnt = 0;
  23. for (int i = 0; i < m; i ++) {
  24. auto e = edges[i];
  25. int a = e.a, b = e.b, w = e.w;
  26. int pa = find(a), pb = find(b);
  27. if (pa == pb) continue;
  28. else {
  29. cnt ++;
  30. res += w;
  31. p[pa] = pb;
  32. }
  33. }
  34. if (cnt == n - 1) return res;
  35. else return -1;
  36. }
  37. int main() {
  38. scanf("%d%d", &n, &m);
  39. for (int i = 0; i < m; i ++) {
  40. int a, b, w;
  41. scanf("%d%d%d", &a, &b, &w);
  42. edges[i] = {a, b, w};
  43. }
  44. int res = kruskal();
  45. if (res == -1) puts("impossible");
  46. else printf("%d\n", res);
  47. return 0;
  48. }

二分图

image.png

染色法

判定一个图是否是二分图。
性质:一个图是二分图当且仅当这个图中不含奇数环。
dfs

  1. #include <iostream>
  2. // #incldue
  3. using namespace std;
  4. const int N = 200010;
  5. int h[N], e[N], ne[N], idx;
  6. int n, m;
  7. int add_edge(int a, int b) {
  8. e[++ idx] = b;
  9. ne[idx] = h[a];
  10. h[a] = idx;
  11. }
  12. int colors[N];
  13. bool dfs(int a, int color) {
  14. colors[a] = color;
  15. int next_color = 3 - color;
  16. for (int cur = h[a]; cur; cur = ne[cur]) {
  17. int b = e[cur];
  18. if (colors[b]) {
  19. if (colors[b] != next_color) return false;
  20. } else {
  21. dfs(b, next_color);
  22. }
  23. }
  24. return true;
  25. }
  26. int main() {
  27. scanf("%d%d", &n, &m);
  28. for (int i = 0; i < m; i ++) {
  29. int a, b;
  30. scanf("%d%d", &a, &b);
  31. add_edge(a, b);
  32. add_edge(b, a);
  33. }
  34. for (int i = 1; i <= n; i ++) {
  35. if (!colors[i]) {
  36. if (!dfs(i, 1)) {
  37. puts("No");
  38. return 0;
  39. }
  40. }
  41. }
  42. puts("Yes");
  43. return 0;
  44. }

匈牙利算法

给定一个二分图,求最大匹配。
先到先得,能让就让。
太巧妙了。

  1. #include <iostream>
  2. #include <cstring>
  3. using namespace std;
  4. const int N = 510, M = 200010;
  5. int h[N], e[M], ne[M], idx;
  6. int n1, n2, m;
  7. void add_edge(int a, int b) {
  8. e[++ idx] = b;
  9. ne[idx] = h[a];
  10. h[a] = idx;
  11. }
  12. int match[N];
  13. bool st[N];
  14. // int find(int a) {
  15. // for (int cur = h[a]; cur; cur = ne[cur]) {
  16. // int b = e[cur];
  17. // if (!st[b] && (!match[b] || find(match[b]))) {
  18. // st[b] = true;
  19. // match[b] = a;
  20. // return true;
  21. // }
  22. // }
  23. // return false;
  24. // }
  25. int find(int a) {
  26. for (int cur = h[a]; cur; cur = ne[cur]) {
  27. int b = e[cur];
  28. if (!st[b]) {
  29. st[b] = true;
  30. if (!match[b] || find(match[b])) {
  31. match[b] = a;
  32. return true;
  33. }
  34. }
  35. }
  36. return false;
  37. }
  38. int main() {
  39. scanf("%d%d%d", &n1, &n2, &m);
  40. for (int i = 0; i < m; i ++) {
  41. int a, b;
  42. scanf("%d%d", &a, &b);
  43. add_edge(a, b);
  44. }
  45. int res = 0;
  46. for (int i = 1; i <= n1; i ++) {
  47. memset(st, false, sizeof st);
  48. if (find(i)) res ++;
  49. }
  50. printf("%d\n", res);
  51. return 0;
  52. }

动态规划

image.png

背包问题

01背包问题

N 个物品,每个物品的体积为 vi,价值为 wi 。每个物品最多只能用一次。同时有一个容量为 V 的背包。
算法基础笔记 - 图32
image.png

完全背包问题

朴素做法很好理解:第 i 个物品选择 0 到 k 个。
但是这样时间复杂度比较高:n x m x k
可以通过找规律优化状态转移方程,使得时间复杂度降到:n x m
最后可以优化成一维数组存储状态,使得空间复杂度降为:m
image.png

多重背包问题

优化思路比较 nb。
状态表示还是 f(i, j) :前 i 个物品,容量为 j。
状态计算思想如下:

  • f(i, j) 表示可能的取法集合,该集合由以下子集构成:
    • 取 0 个第 i 个物品
    • 取 1 个第 i 个物品
    • 。。。
    • 取 s[i] 个地 i 个物品

所以朴素算法的时间复杂度就是:算法基础笔记 - 图35
但是我们可以对物品进行打包,比如第 i 个物品有 s 个,我们对其进行如下打包操作

  • 1个:v = vi, w = wi
  • 2个:v = vi 2,w = wi 2
  • 。。。
  • c 个:v = vi c,w = wi c

因为这些打包物品可以凑出 1 - s 其中任意多个物品,所以我们就把多重背包问题转化成了 01 背包问题。时间复杂度降到了 算法基础笔记 - 图36
实现的时候需要注意一下初始化空间的大小 算法基础笔记 - 图37
image.png

分组背包问题

f(i, j) 表示前 i 组,背包容量选 j
计算状态的思路就是遍历当前第 i 组中的每个物品,选或者不选。

线性dp

三角形权重最大路径

注意初始化问题

最长上升子序列问题

image.png
优化:

最长公共子序列

image.png

最短编辑距离

image.png

编辑距离

区间 dp

合并石子

image.png
也可以写成递归的形式(记忆化搜索)

数位统计 dp

分情况讨论

计数 dp

状态压缩 dp

使用二进制位表示状态
通常 n 比较小

蒙德里安的梦想

核心思想:

  • 先放横着的小方块
  • 总方案数等于所有横着小方块的合法摆放数
  • 如何判断是否合法:
    • 每一列不能有连续空奇数个

动态规划:

  • 状态表示:
    • f[i, j] 表示前 i - 1 列已经摆好,且从第 i-1 列伸到第 i 列的状态为 j 的所有摆放方式的集合
  • 状态计算

树形 dp

通常用 dfs 求解

没有上司的舞会

image.png

记忆优化搜索

递归求解每一个状态

滑雪

贪心算法

区间问题

相关题目

  • 111 畜栏
  • 112 雷达

按照点的左端点 or 右端点进行排序。
证明正确性:

  • Ans <= cnt
  • Ans >= cnt
  • Ans == cnt
  • 结合反证法

区间选点

image.png

最大不相交区间数量

image.png

区间分组

image.png

区间覆盖

image.png

Huffman 树

最小堆冲冲冲

合并果子问题

和“合并石子”的区别:石子只能合并相邻的两堆石子,合并果子可以合并任意两堆果子

排序不等式

绝对值不等式

推公式