算法与数据结构|算法与数据结构——AcWing算法题常用代码模板

一、基础算法 1、常用代码模板 快速排序算法模板
—— 模板题 AcWing 785. 快速排序
算法步骤:
? ①确定分界点
? ②调整区间
? ③递归处理左右两段
Tips:
? ①如果输入数据量比较大的话,用scanf输入更快,不要使用cin。
? ②只要背诵一种万能模板即可避开所有边界问题。
? ③考试时一般不会用到快排,面试时喜欢考察。
? ④ #include包含了目前c++所包含的所有头文件 。
? ⑤分治一般用不到,只要掌握快速排序和归并排序就行。

void quick_sort(int q[], int l, int r) { if (l >= r) return; int i = l - 1, j = r + 1, x = q[l + 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); }

归并排序算法模板
—— 模板题 AcWing 787. 归并排序
算法步骤:
? ①确定分界点
? ②递归排序
? ③归并——合二为一
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]; }

整数二分算法模板
—— 模板题 AcWing 789. 数的范围
Tips:
? ①二分区间一点包含答案。
? ②二分的时候一定是有解的,如果无解就是和题目有关,二分的模板一定保证有解。
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; }

浮点数二分算法模板
—— 模板题 AcWing 790. 数的三次方根
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; }

高精度加法
—— 模板题 AcWing 791. 高精度加法
Tips:
? ①高精度只对C++有要求
? ②在笔试时偶尔会出现
// C = A + B, A >= 0, B >= 0 // 用引用传参会快很多 vector add(vector &A, vector &B) { if (A.size() < B.size()) return add(B, A); vector C; int t = 0; for (int i = 0; i < A.size(); i ++ ) { 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; }

高精度减法
—— 模板题 AcWing 792. 高精度减法
// C = A - B, 满足A >= B, A >= 0, B >= 0 vector sub(vector &A, vector &B) { vector 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; } // 去掉前导0 while (C.size() > 1 && C.back() == 0) C.pop_back(); return C; }

高精度乘低精度
—— 模板题 AcWing 793. 高精度乘法
// C = A * b, A >= 0, b >= 0 vector mul(vector &A, int b) { vector C; int t = 0; // 进位 for (int i = 0; i < A.size() || t; i ++ ) { if (i < A.size()) t += A[i] * b; C.push_back(t % 10); t /= 10; }while (C.size() > 1 && C.back() == 0) C.pop_back(); return C; }

高精度除以低精度
—— 模板题 AcWing 794. 高精度除法
// A / b = C ... r, A >= 0, b > 0 vector div(vector &A, int b, int &r) { vector C; r = 0; for (int i = A.size() - 1; i >= 0; i -- ) { r = r * 10 + A[i]; C.push_back(r / b); r %= b; }r= reverse(C.begin(), C.end()); while (C.size() > 1 && C.back() == 0) C.pop_back(); return C; }

一维前缀和
—— 模板题 AcWing 795. 前缀和
前缀和的作用:快速计算某一段数的和
tips:
? ①ios::sync_with_stdio(false); 提高cin的读取速度、不能再使用scanf了,但依旧没有scanf快。数据输入>=100w,建议使用scanf,否则建议使用cin。
? ②只要定义了i - 1,则下标一般从1开始。
S[i] = a[1] + a[2] + ... a[i] a[l] + ... + a[r] = S[r] - S[l - 1]

二维前缀和
—— 模板题 AcWing 796. 子矩阵的和
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]

一维差分
—— 模板题 AcWing 797. 差分
给区间[l, r]中的每个数加上c:B[l] += c, B[r + 1] -= c

二维差分
—— 模板题 AcWing 798. 差分矩阵
给以(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

位运算
—— 模板题 AcWing 801. 二进制中1的个数
tips:
? ①把第k位移到最后一位 n>>k
? ②看个位是几 x&1
? ③lowbit(x):x的二进制表达式中最低位的1所对应的值
? ④x&-x = x&(~x+1) ~x:x取反 简单应用:统计x中有多少个1
? ⑤x = 1010 原码:1010 反码:0101 补码:0110
求n的第k位数字: n >> k & 1 返回n的最后一位1:lowbit(n) = n & -n

双指针算法
—— 模板题 AcWIng 799. 最长连续不重复子序列
—— 模板题 AcWing 800. 数组元素的目标和
tips:
? ①算法复杂度都是O(n)
? ②先想一个朴素算法,观察单调关系,再想双指针算法
for (int i = 0, j = 0; i < n; i ++ ) { while (j < i && check(i, j)) j ++ ; // 具体问题的逻辑 } 常见问题分类: (1) 对于一个序列,用两个指针维护一段区间 (2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作

整数离散化
—— 模板题 AcWing 802. 区间和
数据离散化处理
定义:
离散化,把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。
通俗的说,离散化是在不改变数据相对大小的条件下,对数据进行相应的缩小。例如:
原数据:1,999,100000,15;处理后:1,3,4,2;
原数据:{100,200},{20,50000},{1,400};
处理后:{3,4},{2,6},{1,5};
例如有时需要将数据直接作为数组下标,但因为数据太大而无法开数组时,若数据的总量相对较少时,就可以对其进行离散化处理
tips:
? ①pair将一对值(T1和T2)组合成一个值,这一对值可以具有不同的数据类型(T1和T2),两个值可以分别用pair的两个公有函数first和second访问。
? ②pair排序默认先按左端点排序,后按右端点排序。
? ③c++用vector做离散化
vector 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 }

区间合并
—— 模板题 AcWing 803. 区间合并
// 将所有存在交集的区间合并 void merge(vector &segs) { vector 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; }

上课:明白主要思想
课下:代码整个流程弄懂、大致的背过、题目默写相关模板、写出来并调试通过
如何提高熟练度:重复写代码(删、写)、一个题目写3-5遍
二、数据结构 1、常用代码模板 单链表
—— 模板题 AcWing 826. 单链表
【算法与数据结构|算法与数据结构——AcWing算法题常用代码模板】Tips:
? ①scanf每次%c之前都要加上空格,防止读取空格
struct Node { int val; Node *next; }; //在面试题中用的多,在笔试中用一般不会用,因为new Node()非常慢,用数组模拟链表很快

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

双链表
—— 模板题 AcWing 827. 双链表
// 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]; }


—— 模板题 AcWing 828. 模拟栈
——模板题 AcWing 3302. 表达式求值
// tt表示栈顶 int stk[N], tt = 0; // 向栈顶插入一个数 stk[ ++ tt] = x; // 从栈顶弹出一个数 tt -- ; // 栈顶的值 stk[tt]; // 判断栈是否为空 if (tt > 0) {}

队列
—— 模板题 AcWing 829. 模拟队列
1、普通队列: // hh 表示队头,tt表示队尾 int q[N], hh = 0, tt = -1; // 向队尾插入一个数 q[ ++ tt] = x; // 从队头弹出一个数 hh ++ ; // 队头的值 q[hh]; // 判断队列是否为空 if (hh <= tt) {}

2、循坏队列 // hh 表示队头,tt表示队尾的后一个位置 int q[N], hh = 0, tt = 0; // 向队尾插入一个数 q[tt ++ ] = x; if (tt == N) tt = 0; // 从队头弹出一个数 hh ++ ; if (hh == N) hh = 0; // 队头的值 q[hh]; // 判断队列是否为空 if (hh != tt) {}

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

单调队列
—— 模板题 AcWing 154. 滑动窗口
? 1、用普通队列该怎么做
? 2、将队列中的没有用的元素删掉——>具有了单调性
? 3、可以用O(1)时间从队头/队尾取出最值
Tips:
? ①适用场景非常有限,但面试笔试喜欢考察
常见模型:找出滑动窗口中的最大值/最小值 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
—— 模板题 AcWing 831. KMP字符串
KMP算法详解
KMP算法配图详解
? ne数组作用有两个:一是之前提到的,ne[i]的值表示下标为i的字符前的字符串最长相等前后缀的长度;
? 二是表示该处字符不匹配时应该回溯到的字符的下标。
// 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]; //当主串S与模式串P失配时,j=next[j],P向右移动j - next[j] if (s[i] == p[j + 1]) j ++ ; if (j == m) { j = ne[j]; // 匹配成功后的逻辑 } }

Trie字典树
—— 模板题 AcWing 835. Trie字符串统计
int son[N][26], cnt[N], idx; // 0号点既是根节点,又是空节点 // son[][]存储树中每个节点的子节点 // cnt[]存储以每个节点结尾的单词数量 // idx存储当前用到的节点下标// 插入一个字符串 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]; }

并查集
—— 模板题 AcWing 836. 合并集合
—— 模板题 AcWing 837. 连通块中点的数量
Tips:
? 分类问题用并查集做
(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);

(2)维护size的并查集: 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所在的两个集合: size[find(b)] += size[find(a)]; p[find(a)] = find(b);

(3)维护到祖宗节点距离的并查集: 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)的偏移量


—— 模板题 AcWing 838. 堆排序
—— 模板题 AcWing 839. 模拟堆
// 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);

一般哈希
—— 模板题 AcWing 840. 模拟散列表
(1) 拉链法 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; }

(2) 开放寻址法 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; }

字符串哈希
—— 模板题 AcWing 841. 字符串哈希
核心思想:将字符串看成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 first, 第一个元素 second, 第二个元素 支持比较运算,以first为第一关键字,以second为第二关键字(字典序)string,字符串 size()/length()返回字符串长度 empty() clear() substr(起始下标,(子串长度))返回子串 c_str()返回字符串所在字符数组的起始地址 支持 a += “def“ a += ‘c’queue, 队列 size() empty() push()向队尾插入一个元素 front()返回队头元素 back()返回队尾元素 pop()弹出队头元素priority_queue, 优先队列,默认是大根堆 size() empty() push()插入一个元素 top()返回堆顶元素 pop()弹出堆顶元素 定义成小根堆的方式:priority_queue, greater> q; 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,删除所有xO(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()返回有多少个1any()判断是否至少有一个1 none()判断是否全为0set()把所有位置成1 set(k, v)将第k位变成v reset()把所有位变成0 flip()等价于~ flip(k) 把第k位取反

三、搜索与图论 1、常用代码模板 树与图的存储
树是一种特殊的图,与图的存储方式相同。
对于无向图中的边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 表示边数
(1) 深度优先遍历 —— 模板题 AcWing 846. 树的重心
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); } }

(2) 宽度优先遍历 —— 模板题 AcWing 847. 图中点的层次
queue 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); } } }

拓扑排序
—— 模板题 AcWing 848. 有向图的拓扑序列
时间复杂度 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算法
—— 模板题 AcWing 849. Dijkstra求最短路 I
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算法
—— 模板题 AcWing 850. Dijkstra求最短路 II
时间复杂度 O(mlogn), n 表示点数,m 表示边数
typedef pair 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, greater> 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算法
—— 模板题 AcWing 853. 有边数限制的最短路
时间复杂度 O(nm), n 表示点数,m 表示边数
注意在模板题中需要对下面的模板稍作修改,加上备份数组,详情见模板题。
int n, m; // n表示点数,m表示边数 int dist[N]; // dist[x]存储1到x的最短路距离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的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。 // 假设 1 号点到 n 号点是可达的,每一个点同时向指向的方向出发,更新相邻的点的最短距离,通过循环 n-1 次操作,若图中不存在负环,则 1 号点一定会到达 n 号点,若图中存在负环,则在 n-1 次松弛后一定还会更新 for (int i = 0; i < n; i ++ ) { for (int j = 0; j < m; j ++ ) { int a = edges[j].a, b = edges[j].b, w = edges[j].w; if (dist[b] > dist[a] + w) dist[b] = dist[a] + w; } }if (dist[n] > 0x3f3f3f3f / 2) return -1; return dist[n]; }

spfa 算法(队列优化的Bellman-Ford算法)
—— 模板题 AcWing 851. spfa求最短路
时间复杂度 平均情况下 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 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判断图中是否存在负环
—— 模板题 AcWing 852. 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 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算法
—— 模板题 AcWing 854. Floyd求最短路
初始化: 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算法
—— 模板题 AcWing 858. Prim算法求最小生成树
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算法
—— 模板题 AcWing 859. 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; }

染色法判别二分图
—— 模板题 AcWing 860. 染色法判定二分图
时间复杂度是 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; }

匈牙利算法
—— 模板题 AcWing 861. 二分图的最大匹配
时间复杂度是 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 ++ ; }

四、数学知识 1、常用代码模板 试除法判定质数
—— 模板题 AcWing 866. 试除法判定质数
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; }

试除法分解质因数
—— 模板题 AcWing 867. 分解质因数
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; }

朴素筛法求素数
—— 模板题 AcWing 868. 筛质数
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; } }

线性筛法求素数
—— 模板题 AcWing 868. 筛质数
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; } } }

试除法求所有约数
—— 模板题 AcWing 869. 试除法求约数
vector get_divisors(int x) { vector 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; }

约数个数和约数之和
—— 模板题 AcWing 870. 约数个数, AcWing 871. 约数之和
如果 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)

欧几里得算法
—— 模板题 AcWing 872. 最大公约数
int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }

求欧拉函数
—— 模板题 AcWing 873. 欧拉函数
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; }

筛法求欧拉函数
—— 模板题 AcWing 874. 筛法求欧拉函数
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); } } }

快速幂
—— 模板题 AcWing 875. 快速幂
求 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; }

扩展欧几里得算法
—— 模板题 AcWing 877. 扩展欧几里得算法
// 求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; }

中国剩余定理
// 根据中国剩余定理,如果m1,m2,…,mn不是两两互质的,那么就不存在这样的x // 如果满足上述条件,则x = a1·M1·M1^(-1) + a2·M2·M2^(-1) + …… + an·Mn·Mn^(-1), // 其中Mi = M / mi,M = m1·m2·…·mn,Mi^(-1)表示Mi模mi的逆

高斯消元
—— 模板题 AcWing 883. 高斯消元解线性方程组
// 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; // 有唯一解 }

递归法求组合数
—— 模板题 AcWing 885. 求组合数 I
// 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;

通过预处理逆元的方式求组合数
—— 模板题 AcWing 886. 求组合数 II
首先预处理出所有阶乘取模的余数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定理
—— 模板题 AcWing 887. 求组合数 III
若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; }

分解质因数法求组合数
—— 模板题 AcWing 888. 求组合数 IV
当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用: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 mul(vector a, int b)// 高精度乘低精度模板 { vector 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 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]);

卡特兰数
—— 模板题 AcWing 889. 满足条件的01序列
给定n个0和n个1,它们按照某种顺序排成长度为2n的序列,满足任意前缀中0的个数都不少于1的个数的序列的数量为: Cat(n) = C(2n, n) / (n + 1)

NIM游戏
—— 模板题 AcWing 891. Nim游戏
给定N堆物品,第i堆物品有Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。
我们把这种游戏称为NIM博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。
所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。
NIM博弈不存在平局,只有先手必胜和先手必败两种情况。
定理: NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ … ^ An != 0
公平组合游戏ICG
若一个游戏满足:
由两名玩家交替行动;
在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
不能行动的玩家判负;
则称该游戏为一个公平组合游戏。
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)。
有向图游戏的和
—— 模板题 AcWing 893. 集合-Nim游戏
设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。
五、动态规划 1、课堂笔记 DP时间复杂度 = 状态数量 * 转移计算量
一般做法: 分析状态表示(集合、属性),写出状态计算(转移方程)
01背包问题
——模板题 AcWing 2. 01背包问题
每件物品最多只用一次
结合题目理解
状态表示 f(i, j):
? 集合:所有选法的集合 ,只从前 i 个物品中,总体积 <= j
? 属性:集合最大值,集合最小值,数量
状态计算:
? 集合划分:不含 i ,包含 i ,不重复(不一定要满足),不漏(一定满足)
完全背包问题
——模板题 AcWing 3. 完全背包问题
每件物品有无限个
朴素算法:
? 可能超时
? f[i, j] = f[i - 1, j - v[i] * k] + w[i] * k
? k不能无限大,超过背包容量终止
优化算法:
之后与01背包问题类似优化
多重背包问题
——模板题 AcWing 4. 多重背包问题
——模板题 AcWing 5. 多重背包问题 II
每个物品有 Si 个
朴素算法与完全背包问题类似
不能直接用完全背包问题的优化方法进行优化
采用二进制进行优化
分组背包问题
——模板题 AcWing 9. 分组背包问题
每组物品有若干个,每组最多只能选一个
线性DP
数字三角形
——模板题 AcWing 898. 数字三角形
最长上升子序列
——模板题 AcWing 895. 最长上升子序列
求最长上升子序列:
——模板题 AcWing 896. 最长上升子序列 II
最长公共子序列
——模板题 AcWing 897. 最长公共子序列
最短编辑距离
——模板题 AcWing 902. 最短编辑距离
编辑距离
——模板题 AcWing 899. 编辑距离
思路与最短编辑距离类似
区间DP
——模板题 AcWing 282. 石子合并
计数类DP
——模板题 AcWing 900. 整数划分
整数问题
可以看作一个完全背包问题
数位统计DP
——模板题 AcWing 338. 计数问题
分情况讨论!
状态压缩DP
用一个二进制整数来表示某种状态
蒙德里安的梦想
——模板题 AcWing 291. 蒙德里安的梦想
j 存储i - 1列哪些行横放了1 * 2的矩形
①和②判断状态是否能够转移过来
只枚举了横放的矩形,所以列空白必须不存在连续奇数个0
最短Hamiltan路径
——模板题 AcWing 91. 最短Hamilton路径
树形DP
——模板题 AcWing 285. 没有上司的舞会
记忆化搜索
要求不存在环
代码复杂度低
已经算过的值不会重复计算
滑雪游戏
——模板题 AcWing 901. 滑雪
六、回溯
// 回朔代码模板 void backward(): if (回朔点):// 这条路走到底的条件。也是递归出口 保存该结果 return; else: for (route : all_route_set)// 逐步选择当前节点下的所有可能route if 剪枝条件: 剪枝前的操作 return; // 不继续往下走了,退回上层,换个路再走 else:// 当前路径可能是条可行路径 保存当前数据// 向下走之前要记住已经走过这个节点了。例如push当前节点 backward(); // 递归发生,继续向下走一步了。 回朔清理// 该节点下的所有路径都走完了,清理堆栈,准备下一个递归。例如弹出当前节点

    推荐阅读