乱七八糟的模板集合
字符串
1.字符串哈希
可以用于直接比较字符串相同,找循环节,hash(l, r - x) == hash(l + x, r) 可判定x为一个循环节(x整除长度)
1 // 采用自然溢出 2 typedef unsigned long long ull; 3 const short base = 131; 4 ull hashs[maxn],bases[maxn]; 5 void init() { 6 bases[0] = 1; 7 for(int i = 1; i <= n; ++i) { 8 bases[i] = bases[i - 1] * base; 9 hashs[i] = hashs[i - 1] * base + str[i] - 'a' + 1; 10 } 11 } 12 ull get_hash(int l, int r) { 13 return hashs[r] - hashs[l - 1] * bases[r - l + 1]; 14 }
2.KMP
单模式串匹配,着重理解前缀数组的应用,比如n - next[n]为最小循环节
1 for(int i = 2, j = 0; i <= len2; i++) { 2 while(j && s2[i] != s2[j + 1]) j = next[j]; 3 if(s2[i] == s2[j + 1]) j++; 4 next[i] = j; 5 } 6 for(int i = 1, j = 0; i <= len1; i++) {// 单模式串匹配 7 while(j && s1[i] != s2[j + 1]) j = next[j]; 8 if(s1[i] == s2[j + 1]) j++; 9 if(j == len2) {// 匹配成功 10 printf("%d\n", i - len2 + 1); 11 } 12 }
3.AC自动机
多模式串匹配,一般多串跑不了
1 struct AC_automation { 2 int ch[maxn][26],tot,fail[maxn],ending[maxn]; 3 //子节点,结点数,失配指针,结束标记 4 void insert(char *str, int len) {// trie插入 5 int p = 0; 6 char c; 7 for(int i = 0; i < len; ++i) { 8 c = str[i] - 'a'; 9 if(!ch[p][c]) ch[p][c] = ++tot; 10 p = ch[p][c]; 11 } 12 ending[p]++; 13 } 14 void build() {// 构建失配指针 15 int head = 1, tail = 0, q[maxn], p; 16 for(int i = 0; i < 26; ++i){ 17 if(!ch[0][i]) continue; 18 q[++tail] = ch[0][i]; 19 fail[ch[0][i]] = 0; 20 } 21 while(head <= tail) { 22 p = q[head++]; 23 for(int i = 0; i < 26; ++i) { 24 if(ch[p][i]){ 25 fail[ch[p][i]] = ch[fail[p]][i];//error#1 26 q[++tail] = ch[p][i]; 27 } 28 else ch[p][i] = ch[fail[p]][i];// error#2 29 } 30 } 31 } 32 void match(char *str, int len) {// 匹配文本 33 int p = 0, ret = 0; 34 for(int i = 0; i < len; ++i) { 35 p = ch[p][str[i] - 'a']; 36 for(int j = p; j; j = fail[j]) { 37 // 根据题目要求自行变通 38 } 39 } 40 } 41 }AC;
4.trie
(不要问我为什么trie在ACAM后面,问就是忘加了。。)下面代码以查询一个串是几个串的前缀为例
1 struct trie { 2 int ch[maxn][63],tot,val[maxn];// 可以用val标记这个点有多少个单词经过 3 void insert(char *str, int len) { 4 int p = 0; 5 for(int i = 1; i <= len; ++i) { 6 int c = trans(str[i]); 7 if(!ch[p][c]) ch[p][c] = ++tot; 8 p = ch[p][c]; 9 ++val[p]; 10 } 11 } 12 int match(char *str, int len) { 13 int p = 0; 14 for(int i = 1; i <= len; ++i) { 15 p = ch[p][trans(str[i])]; 16 if(!p) return 0; 17 } 18 return val[p]; 19 } 20 void clear() {// 别用memset要T飞的 21 for(int i = 0; i <= tot; ++i) 22 for(int j = 0; j <= 62; ++j) 23 ch[i][j] = 0; 24 for(int i = 0; i <= tot; ++i) 25 val[i] = 0; 26 tot = 0; 27 } 28 }t;
5.后缀数组
倍增后缀数组,整了一长串注释方便自己理解
1 int n,m,sa[maxn],rk[maxn],tp[maxn],tax[maxn]; 2 // 通俗易懂(当前长度len): 3 // sa[i]:上次排序排i的后缀哪个 4 // rk[i]:上次排序第i个后缀排第几 rk[sa[i]] = i 5 // tp[i]:辅助排序 第二关键字 跟sa[i]描述的差不多 不过是后半段 6 // tax[i]:基数排序的桶 7 // m:字符集大小 8 void radix_sort() { 9 for(int i = 1; i <= m; ++i) tax[i] = 0;// 清空 10 for(int i = 1; i <= n; ++i) ++tax[rk[i]];// 存 11 for(int i = 1; i <= m; ++i) tax[i] += tax[i - 1];// 做前缀和可以得到比i小的后缀有多少 12 for(int i = n; i >= 1; --i) sa[tax[rk[tp[i]]]--] = tp[i]; 13 // 最阴间的一句话 14 // 我们考虑len -> 2*len会如何变化 15 // 第一关键字相对位置不变 rk即排名相同的后缀会变化 16 // 这个时候我们通过tp把这些后缀区分开 tp保证两两不同 17 // 由于tax存的第一关键字桶 而且做前缀和之后是这个段最大的排名 18 // 我们倒序枚举i rk[tp[i]]定位到第一关键字 这个时候桶的值就是真的排名 19 } 20 void suffix_sort() { 21 for(int i = 1; i <= n; ++i) rk[i] = str[i], tp[i] = i, m = max(m, (int)str[i]); 22 // 一开始莫得第二关键字就拿自己位置了 23 radix_sort();// 先排一次 24 for(int p, len = 1; p < n; len <<= 1, m = p) {// p就是个排名计数器 25 p = 0; 26 for(int i = 1; i <= len; ++i) tp[++p] = n - len + i;// 这些没有第二关键字 当0了 27 for(int i = 1; i <= n; ++i) 28 if(sa[i] > len) tp[++p] = sa[i] - len;// 这些有第二关键字 丢进去 29 radix_sort(); swap(tp, rk); rk[sa[1]] = p = 1; 30 // 排序 然后tp由于没用 我们就相当于拿tp来存rk的拷贝来更新新的rk 31 for(int i = 2; i <= n; ++i) 32 rk[sa[i]] = (tp[sa[i]] == tp[sa[i - 1]] && tp[sa[i] + len] == tp[sa[i - 1] + len]) ? p : ++p; 33 // 两个关键字都相同的排名相同 34 } 35 for(int i = 1, k = 0; i <= n; ++i) {// 获取height数组 36 if(!rk[i]) continue; 37 if(k) --k; 38 while(str[i + k] == str[sa[rk[i] - 1] + k]) ++k; 39 height[rk[i]] = k; 40 } 41 }
6.后缀自动机
虽然但是这玩意应该不考吧,以防万一
1 struct Suffix_AutoMaton { 2 int link[maxn],len[maxn],tot,last; 3 map<char, int>ch[maxn]; 4 Suffix_AutoMaton() { 5 tot = last = 1; 6 link[1] = len[1] = 0; 7 ch[1].clear(); 8 } 9 void insert(char c) { 10 int cur = ++tot, p = last; sz[cur] = 1; 11 len[cur] = len[last] + 1; 12 while(p && ch[p].find(c) == ch[p].end()) { 13 ch[p][c] = cur; 14 p = link[p]; 15 } 16 if(!p) link[cur] = 1; 17 else { 18 int q = ch[p][c]; 19 if(len[p] + 1 == len[q]) link[cur] = q; 20 else { 21 int cl = ++tot; 22 ch[cl] = ch[q]; 23 link[cl] = link[q]; 24 len[cl] = len[p] + 1; 25 while(p && ch[p][c] == q) { 26 ch[p][c] = cl; 27 p = link[p]; 28 } 29 link[q] = link[cur] = cl; 30 } 31 } 32 last = cur; 33 } 34 }SAM;
数据结构
1.并查集
短小且精悍,灵活运用
1 int fa[maxn]; 2 void init() {// 初始化 一定要记得 3 for(int i = 1; i <= n; ++i) fa[i] = i; 4 } 5 int getfa(int x) {// 查询所属于集合 路径压缩优化 6 return fa[x] == x ? x : fa[x] = getfa(fa[x]); 7 } 8 void merge(int x, int y) {// 合并 9 x = getfa(x), y = getfa(y); 10 fa[x] = y; 11 } 12 bool same(int x, int y) {// 查询是否在同一集合 13 return getfa(x) == getfa(y); 14 }
2.树状数组
一样短小精悍,高效维护动态前缀和
1 #define lowbit(x) ((x) & (-x)) 2 int t[maxn]; 3 void add(int x, int a) { 4 while(x <= n) { 5 t[x] += a; 6 x += lowbit(x); 7 } 8 } 9 int sum(int l, int r) { 10 --l; 11 int suml = 0, sumr = 0; 12 while(l) { 13 suml += t[l]; 14 l -= lowbit(l); 15 } 16 while(r) { 17 sumr += t[r]; 18 r -= lowbit(r); 19 } 20 return sumr - suml; 21 }
求逆序对
其还有各种变体,例如求逆序对(看见什么冒泡排序,就可以想想逆序对)
1 struct num { 2 int v,id; 3 friend bool operator < (const num &n1, const num &n2) { 4 // 处理重复情况 5 if(n1.v == n2.v) return n1.id < n2.id; 6 return n1.v < n2.v; 7 } 8 }a[maxn];// a[i].v为真实值 9 int main() { 10 //输入 11 sort(a + 1, a + n + 1); 12 for(int i = 1; i <= n; ++i) b[a[i].id] = i;// 离散化 13 for(int i = 1; i <= n; ++i) { 14 add(b[i]); 15 ans += i - sum(b[i]); 16 } 17 return 0; 18 }
值域树状数组
离线的弱化平衡树(?)
1 int sum(int x) { 2 int ret = 0; 3 while(x){ 4 ret += t[x]; 5 x -= lowbit(x); 6 } 7 return ret; 8 } 9 void add(int x, int v) { 10 if(!(sum(x) - sum(x - 1)) && v < 0) return;// 虚空减法 11 while(x <= n){ 12 t[x] += v; 13 x += lowbit(x); 14 } 15 } 16 int rnk(int x) {// 查询x排名 17 return sum(x - 1) + 1; 18 } 19 int kth(int x) {// 倍增查询第k小数 编号 20 int ret = 0,cnt = 0; 21 for(int i = lg; ~i; i--) {// lg为log2n 22 ret += 1 << i; 23 if(ret > n || cnt + t[ret] >= x) ret -= 1 << i; 24 else cnt += t[ret]; 25 } 26 return ret + 1; 27 } 28 int id(int x) { 29 return lower_bound(b + 1,b + lim + 1, x) - b; 30 } 31 int pre(int x) {// x前驱编号 32 return kth(rnk(x) - 1); 33 } 34 int next(int x) {// x后继编号 35 return kth(rnk(x + 1)); 36 } 37 /* 38 假设a为原数组(除了kth操作存下来的数),b为排序去重后数组 39 插入add(id(a[i]), 1); 40 删除add(id(a[i]), -1); 41 排名rnk(id(a[i])); 42 第k大b[kth(a[i])]; 43 前驱b[pre(id(a[i]))] 44 后继b[next(id(a[i]))] 45 */
3.线段树
维护区间等,还有各种变体
1 struct segment_tree { 2 int l,r; 3 long long tag; 4 long long sum; 5 }t[maxn << 2];// 注意开4倍空间 6 void pushup(int p) { 7 t[p].sum = t[p << 1].sum + t[p << 1 | 1].sum; 8 } 9 void pushdown(int p) {// 有取模的要注意取模问题 10 if(t[p].tag) { 11 t[p << 1].tag += t[p].tag; 12 t[p << 1 | 1].tag += t[p].tag; 13 t[p << 1].sum += t[p].tag * (t[p << 1].r - t[p << 1].l + 1); 14 t[p << 1 | 1].sum += t[p].tag * (t[p << 1 | 1].r - t[p << 1 | 1].l + 1); 15 t[p].tag = 0; 16 } 17 } 18 void build(int p, int l, int r) {// 调用build(1, 1, n) 19 t[p].l = l; t[p]. r = r; 20 if(l == r) { 21 t[p].sum = a[l]; 22 return; 23 } 24 int mid = l + r >> 1; 25 build(p << 1, l, mid); 26 build(p << 1 | 1, mid + 1, r); 27 pushup(p); 28 } 29 void change(int p, int l, int r, long long k) {// k要开long long 30 if(t[p].l >= l && t[p].r <= r) { 31 t[p].tag += k; 32 t[p].sum += k * (t[p].r - t[p].l + 1); 33 return; 34 } 35 pushdown(p); 36 int mid = t[p].l + t[p].r >> 1; 37 if(l <= mid) change(p << 1, l, r, k);// 注意这里不要把l,r改了 38 if(r > mid) change(p << 1 | 1, l, r, k); 39 pushup(p); 40 } 41 long long ask(int p, int l, int r) { 42 if(t[p].l >= l && t[p].r <= r) return t[p].sum; 43 pushdown(p); 44 int mid = t[p].l + t[p].r >> 1; 45 long long ret = 0; 46 if(l <= mid) ret += ask(p << 1, l, r, k);// 注意这里不要把l,r改了 47 if(r > mid) ret += ask(p << 1 | 1, l, r, k); 48 return ret;// 有取模就要注意 49 }
可持久化权值线段树(主席树)
查询区间第k小
1 int build(int p, int l, int r) {// 版本0建树 2 if(!p) p = ++tot; 3 if(l == r) return p; 4 int mid = (l + r) >> 1; 5 ls[p] = build(ls[p], l, mid); 6 rs[p] = build(rs[p], mid + 1, r); 7 return p; 8 } 9 int copy_node(int p) { 10 ls[++tot] = ls[p]; 11 rs[tot] = rs[p]; 12 sum[tot] = sum[p] + 1; 13 return tot; 14 } 15 int change(int p, int l, int r, int pos) { //插入操作 16 p = copy_node(p); 17 if(l == r) return p; 18 int mid = l + r >> 1; 19 if(pos <= mid) ls[p] = change(ls[p], l, mid, pos); 20 else rs[p] = change(rs[p], mid + 1, r, pos); 21 return p; 22 } 23 int query(int ql, int qr, int l, int r, int k) { 24 // 通过区间减法去掉序列1~ql范围存储的左子树区间的权值 25 int x = sum[ls[qr]] - sum[ls[ql]]; 26 if(l == r) return l; 27 int mid = (l + r) >> 1; 28 if(k <= x) return query(ls[ql], ls[qr], l, mid, k); 29 return query(rs[ql], rs[qr], mid + 1, r, k - x); 30 } 31 // 插入要一个一个插入 离散化后 rt[i] = change(rt[i - 1], 1, len, lsh[a[i]]) lsh为离散化 len为值域上限 32 // 查询调用 id[query(rt[l - 1], rt[r], 1, len, k)];
可持久化线段树
可持久化数组,可以搞可持久化并查集之类的东西(本质是fa数组可持久化)
1 int num[maxn];// 初始值 2 struct persistence_segment_tree { 3 int ls[limit],rs[limit],val[limit],rt[maxn],tot,nrt; 4 int build(int l, int r) { 5 int p = ++tot; 6 if(l == r) { 7 val[p] = num[l]; 8 return p; 9 } 10 int mid = l + r >> 1; 11 ls[p] = build(l, mid); 12 rs[p] = build(mid + 1, r); 13 return p; 14 } 15 int copy_node(int p) { 16 ls[++tot] = ls[p]; 17 rs[tot] = rs[p]; 18 return tot; 19 } 20 int change(int p, int l, int r, int pos, int k) { 21 p = copy_node(p); 22 if(l == r) { 23 val[p] = k; 24 return; 25 } 26 int mid = l + r >> 1; 27 if(l <= mid) ls[p] = change(ls[p], l, mid, pos, k); 28 else rs[p] = change(rs[p], mid + 1, r, pos, k); 29 return p; 30 } 31 int query(int p, int l, int r, int pos) { 32 if(l == r) return val[p]; 33 int mid = l + r >> 1; 34 if(pos <= mid) return query(ls[p], l, mid, pos); 35 else return query(rs[p], mid + 1, r, pos); 36 } 37 }; 38 /* 39 这是可持久化线段树,非权值 40 建树:先把num初始化完,调用t.rt[0] = t.build(1, n); 41 每次操作(存版本)开始就t.rt[i] = t.rt[i - 1]; 42 更改版本 t.rt[i] = t.rt[version]; 43 更改数 t.rt[i] = t.change(t.rt[i], 1, n, pos, k) 真实数组t[pos] = k; 44 其中t.rt[i] 也可改为基于的版本 45 查询数t.query(t.rt[i], 1, n, pos); 查询真实数组在版本i的t[pos]值; 46 */
线段树合并
统计子树大小就dfs一遍从子树合并上来即可(先递归再合并)
1 int merge_segment_tree(int p, int q, int l, int r) { 2 if(!p) return q; 3 if(!q) return p;// 一个空了返回另一个 4 if(l == r) { 5 // 根据题目要求修改 6 return p; 7 } 8 int mid = (l + r) >> 1; 9 ls[p] = merge_segment_tree(ls[p], ls[q], l, mid);// 权值线段树 10 rs[p] = merge_segment_tree(rs[p], rs[q], mid + 1, r); 11 pushup(p); 12 return p; 13 }
4.树链剖分
用于树上修改、查询等问题,核心思想为给树上每个结点重新编号存到线段树里面,使得根节点到每个点经过链条数均摊logn,虽然(logn)^2但是常数小,除非完全二叉树,不然叠不满,还可以拿来求LCA(CSP-S2022 树剖LCA打挂 警钟长鸣)编号过程通过两次dfs实现,修改就是两个结点一起跳重链,直到到LCA,在线段树里一段一段地修改就可以了
1 int son[maxn],size[maxn],fa[maxn],dep[maxn],rev[maxn],seg[maxn].top[maxn]; 2 // 重儿子编号,子树大小,父节点,深度,线段树中结点在原树中编号,原树中结点在线段树中编号,某个点所在重链链顶结点编号 3 void dfs1(int p, int last) { 4 dep[p] = dep[last] +1; 5 fa[p] = last; 6 size[p] = 1; 7 for(int i = head[p]; i; i = e[i].next) {// 邻接表 8 if(e[i].to == last) continue; 9 dfs1(e[i].to, p); 10 size[p] += size[e[i].to]; 11 if(size[e[i].to] > size[son[p]]) { 12 son[p] = e[i].to; 13 } 14 } 15 } 16 // seg[0]存当前编号数,如果有从零开始编号的出题人你。。。 17 void dfs2(int p) {// 主函数用 seg[0] = seg[root] = 1; top[root] = rev[root] = root; root为1时直接拉到底 18 if(son[p]) { 19 top[son[p]] = top[p]; 20 seg[son[p]] = ++seg[0]; 21 rev[seg[0]] = son[p];// 线段树建树调用的rev 22 dfs2(son[p]); 23 } 24 for(int i = head[p]; i; i = e[i].next) { 25 if(top[e[i].to]) continue; 26 top[e[i].to] = e[i].to; 27 seg[e[i].to] = ++seg[0]; 28 rev[seg[0]] = e[i].to; 29 dfs2(e[i].to); 30 } 31 }
树剖求LCA
同修改过程,一段一段跳,然后两个结点在一条重链上的时候,深度较小的那个点就是LCA了,比又慢又难写的倍增好多了
1 int lca(int x, int y) { 2 while(top[x] != top[y]) { 3 if(dep[top[x]] < dep[top[y]]) swap(x, y);// 深度大的链向上跳,注意是链顶深度,考场写挂。。 4 x = fa[top[x]]; 5 } 6 return dep[x] < dep[y] ? x : y;// 深度小的就是LCA了,同一条链上了 7 }
修改/查询大同小异,就是在每次跳链之前调用change/ask(1, seg[top[x]], seg[x], ......)(链顶编号较小),最后同一条链上时再调用一次就行了
5.ST表
ST表用于解决静态RMQ问题,基于倍增思想,O(nlogn)预处理,O(1)查询,就是细节有点多(CSP-S2022 T2 ST表忘初始化 60 -> 15 警钟长鸣)
1 // 下回最好写函数版不然太乱 2 int st[log2(maxn)][maxn],lg2[maxn],a[maxn];// lg2要预处理才能做到高效,a原数组,st[i][j]表示第j个数后面2^i个数(包括本身)中的最大最小数 3 void init() { 4 lg2[0] = -1; 5 for(int i = 1; i <= n; ++i)// 记得初始化 6 lg2[i] = lg2[i >> 1] + 1; 7 st[0][i] = a[i]; 8 } 9 for(int i = 1; i <= lg2[n]; ++i)// 注意细节 10 for(int j = 1; j <= n - (1 << i) + 1; ++j) { 11 st[i][j] = max(st[i - 1][j], st[i - 1][j + (1 << (i - 1))];// 可根据实际更改 12 } 13 } 14 int query(int l, int r) { 15 int temp = lg2[r - l + 1]; 16 return max(st[temp][l], st[temp][r - (1 << temp) + 1]);// 要+1 17 }
6.单调队列/单调栈
可以用来优化dp等但是我要是能设计出来状态就磕头了,也可以变个形变成单调栈,既然有单调性也可以考虑二分等
1 struct dandiao_queue {// 以保留较大值为例 2 int q[maxn],head,tail; 3 dandiao_queue() { 4 head = 1; 5 tail = 0; 6 } 7 int front() { 8 if(head > tail) return 998244353;// 自行改错误代码 有用 9 return q[head]; 10 } 11 void pop() { 12 if(head > tail) return; 13 ++head; 14 } 15 void push(int x) { 16 while(tail >= head && x > q[tail]) --tail;// 记得要判队列非空 自行调整比较方式 17 q[++tail] = x; 18 } 19 void bl() {// debug 用 20 printf("now queue:"); 21 for(int i = head; i <= tail; ++i) printf("%d ", q[i]); 22 printf("\n"); 23 } 24 }q;
7.分块
核心思想是大段整体维护,局部暴力,大多数题目需要离散化等几步转化,贴个简单板子。利用基本不等式得出块长为$\sqrt{n}$时最优,大多数时候时间复杂度为$O(n\sqrt{n})$,大概可以卡常过1e5
int n,a[maxn],tot,sq,id[maxn];// sq块长 tot块数 struct kuai { int l,r,add;// add区间加标记 }blo[sqrt(maxn)]; void add(int l, int r, int z) { int x = id[l], y = id[r]; if(x == y) { for(int i = l; i <= r; ++i) a[i] += z; return; } for(int i = l; i <= blo[x].r; ++i) a[i] += z; for(int i = x + 1; i <= y - 1; ++i) blo[i].add += z; for(int i = blo[y].l; i <= r; ++i) a[i] += z; } int query(int x) { return blo[id[x]].add + a[x]; } int main() { sq = sqrt(n); for(int i = 1; 1; ++i) { if(i * sq > n) { tot = i - 1; break; } blo[i].l = blo[i - 1].r + 1; blo[i].r = blo[i].l + sq - 1; } if(blo[tot].r < n) { ++tot; blo[tot].l = blo[tot - 1].r + 1; blo[tot].r = n; } for(int i = 1; i <= tot; ++i) for(int j = blo[i].l; j <= blo[i].r; ++j) id[j] = i; return 0; }
8.莫队
其实准确来说莫队算是一种技巧(?)吧,但是跟分块思想有点像我就放一起了,莫队基于给询问按一定方式排序,然后将已知答案区间一个一个移动就行
看见可离线,区间询问,就可以想到莫队。大多数莫队题目还是要转化一下,由于修改很多,通常配合值域分块,来进行$O(1)$修改$O(\sqrt{n})$查询
背框架主要是背个奇偶化排序,另外注意莫队询问区间先扩张后收缩
1 struct query{// 存询问的 2 int l,r,id; 3 friend bool operator < (const query &q1,const query &q2) { 4 if(q1.l / sq != q2.l / sq) return q1.l < q2.l; 5 else { 6 if(q1.l / sq & 1) return q1.r < q2.r; 7 return q1.r > q2.r; 8 } 9 } 10 }ask[maxn];// sq为询问块长自行定 11 sort(ask + 1, ask + q + 1); 12 for(int l = 1,r = 0,i = 1; i <= q; i++) { 13 while(l < ask[i].l)del(l++);// del、add就是删/增统计,自行更改 14 while(l > ask[i].l)add(--l); 15 while(r < ask[i].r)add(++r); 16 while(r > ask[i].r)del(r--); 17 ans[ask[i].id] = ;// 统计答案 18 }
9.平衡树(fhq-treap)
fhq_treap好写好用
1 mt19937 rnd(time(0)); 2 struct fhq_treap { 3 int size[maxn],ls[maxn],rs[maxn],val[maxn],tot,wei[maxn],root; 4 int New(int v) { 5 size[++tot] = 1; 6 val[tot] = v; 7 wei[tot] = rnd(); 8 return tot; 9 } 10 void pushup(int p) {size[p] = size[ls[p]] + size[rs[p]] + 1;} 11 void Split(int p, int v, int &x, int &y) { 12 if(!p) {x = 0; y = 0; return;} 13 if(val[p] <= v){x = p; Split(rs[p], v, rs[p], y);} 14 else {y = p; Split(ls[p], v, x, ls[p]);} 15 pushup(p); 16 } 17 int Merge(int x, int y) { 18 if((!x) || (!y)) return x + y; 19 if(wei[x] < wei[y]) { 20 rs[x] = Merge(rs[x], y); 21 pushup(x); 22 return x; 23 } 24 else { 25 ls[y] = Merge(x, ls[y]); 26 pushup(y); 27 return y; 28 } 29 } 30 void insert(int v) { 31 int x,y; 32 Split(root, v, x, y); 33 root = Merge(Merge(x, New(v)), y); 34 } 35 void del(int v) { 36 int x,y,z; 37 Split(root, v, x, z); 38 Split(x, v - 1, x, y); 39 root = Merge(Merge(x, Merge(ls[y], rs[y])), z); 40 } 41 int find(int p, int kth) { 42 if(size[ls[p]] + 1 == kth) return p; 43 if(kth < size[ls[p]] + 1) return find(ls[p], kth);// 又写反了。。 44 return find(rs[p], kth - size[ls[p]] - 1); 45 } 46 int rnk(int v) { 47 int x,y,ret; 48 Split(root, v - 1, x, y); 49 ret = size[x] + 1; 50 root = Merge(x, y); 51 return ret; 52 } 53 int pre(int v) { 54 int x,y,ret; 55 Split(root, v - 1, x, y); 56 ret = val[find(x, size[x])]; 57 root = Merge(x, y); 58 return ret; 59 } 60 int nxt(int v) { 61 int x,y,ret; 62 Split(root, v, x, y); 63 ret = val[find(y, 1)]; 64 root = Merge(x, y); 65 return ret; 66 } 67 }t;
10.Link-Cut-Tree
LCT反正不考,但是万一有更优解可以用,留一个板子在这
1 struct mypair { 2 int x,y; 3 friend bool operator < (const mypair &m1, const mypair &m2) { 4 return m1.x < m2.x; 5 } 6 }; 7 struct link_cut_tree { 8 mypair mx[maxn << 2],val[maxn << 2]; 9 int fa[maxn << 2],ch[maxn << 2][2]; 10 bool rev[maxn << 2]; 11 #define isroot(p) ((ch[fa[p]][0] != p) && (ch[fa[p]][1] != p)) 12 #define get(p) (ch[fa[p]][1] == p) 13 #define ls ch[x][0] 14 #define rs ch[x][1] 15 void pushup(int x) {mx[x] = max(val[x], max(mx[ls], mx[rs]));} 16 void rever(int x) {swap(ls, rs); rev[x] ^= 1;} 17 void pushdown(int x) { 18 if(rev[x]) { 19 if(ls) rever(ls); 20 if(rs) rever(rs); 21 rev[x] = 0; 22 } 23 } 24 void update(int x) { 25 if(!isroot(x)) update(fa[x]); 26 pushdown(x); 27 } 28 void rotate(int x) { 29 int y = fa[x], z = fa[y], k = get(x); 30 if(!isroot(y)) ch[z][get(y)] = x; 31 ch[y][k] = ch[x][!k], fa[ch[x][!k]] = y; 32 ch[x][!k] = y; fa[y] = x; fa[x] = z; 33 pushup(x); pushup(y); 34 } 35 void splay(int x) { 36 update(x); 37 for(int y = fa[x]; !isroot(x); rotate(x), y = fa[x]) 38 if(!isroot(y)) rotate(get(y) == get(x) ? y : x); 39 } 40 void access(int x) { 41 for(int y = 0; x; y = x, x = fa[x]) { 42 splay(x); 43 ch[x][1] = y; 44 pushup(x); 45 } 46 } 47 void makeroot(int x) { 48 access(x); 49 splay(x); 50 rever(x); 51 } 52 int findroot(int x) {// 找根 连通性 53 access(x); 54 splay(x); 55 pushdown(x); 56 while(ch[x][0]) {// 找深度最浅的那个 57 x = ch[x][0]; 58 pushdown(x); 59 } 60 splay(x);// 旋上去保证复杂度 61 return x; 62 } 63 void split(int x, int y) { 64 makeroot(x); 65 access(y); 66 splay(y); 67 } 68 void link(int x, int y) { 69 makeroot(x); 70 fa[x] = y; 71 } 72 void cut(int x, int y) { 73 makeroot(x); 74 access(y); 75 splay(y);// 上面三行可以直接换成split(x, y) 76 fa[x] = ch[y][0] = 0; 77 pushup(y); 78 } 79 }LCT;
11.树套树
写法是权值线段树套平衡树,空间复杂度$O((n+m)\log{V})$,时间复杂度$O((n+m)\log{n}\log{V})$
但是这玩意是真难写啊
1 struct fhq_treap { 2 int tot,ls[maxn],rs[maxn],wei[maxn],sz[maxn],ps[maxn]; 3 int New(int v) { 4 ps[++tot] = v; 5 wei[tot] = rnd(); 6 sz[tot] = 1; 7 return tot; 8 } 9 void pushup(int p) {sz[p] = sz[ls[p]] + sz[rs[p]] + 1;} 10 void split(int p, int v, int &x, int &y) {// 偶尔压行一下 11 if(!p) {x = y = 0; return;} 12 if(ps[p] <= v) {x = p; split(rs[p], v, rs[p], y);} 13 else {y = p; split(ls[p], v, x, ls[p]); } 14 pushup(p); 15 } 16 int Merge(int x, int y) { 17 if(!x || !y) return x + y; 18 if(wei[x] < wei[y]) { 19 rs[x] = Merge(rs[x], y); 20 pushup(x); 21 return x; 22 } 23 else { 24 ls[y] = Merge(x, ls[y]); 25 pushup(y); 26 return y; 27 } 28 } 29 void insert(int &p, int v) { 30 int x, y; 31 split(p, v, x, y); 32 p = Merge(x, Merge(New(v), y)); 33 } 34 void del(int &p, int v) { 35 int x, y, z; 36 split(p, v, x, z); 37 split(x, v - 1, x, y); 38 p = Merge(x, z); 39 } 40 int getsum(int &p, int l, int r) { 41 int x, y, z, res = 0; 42 split(p, l - 1, x, y); 43 split(y, r, y, z); 44 res = sz[y]; 45 p = Merge(x, Merge(y, z)); 46 return res; 47 } 48 void bl(int p) { 49 if(ls[p]) bl(ls[p]); 50 cout << ps[p] << ' '; 51 if(rs[p]) bl(rs[p]); 52 } 53 }T1; 54 struct segment_tree { 55 int tot = 1,ls[maxn],rs[maxn],id[maxn]; 56 int getrnk(int p, int l, int r, int ql1, int qr1, int ql2, int qr2) { 57 // ql1,qr1是外层区间(值域) ql2 qr2是内层区间(位置) 58 if(!p || ql1 > qr1) return 0; 59 if(l >= ql1 && r <= qr1) return T1.getsum(id[p], ql2, qr2); 60 int mid = (l + r) >> 1, res = 0; 61 if(ql1 <= mid) res = getrnk(ls[p], l, mid, ql1, qr1, ql2, qr2); 62 if(qr1 > mid) res += getrnk(rs[p], mid + 1, r, ql1, qr1, ql2, qr2); 63 return res; 64 } 65 int kth(int p, int l, int r, int ql2, int qr2, int k) { 66 if(l == r) return l; 67 int tp = T1.getsum(id[ls[p]], ql2, qr2); 68 int mid = (l + r) >> 1; 69 if(tp >= k) return kth(ls[p], l, mid, ql2, qr2, k); 70 return kth(rs[p], mid + 1, r, ql2, qr2, k - tp); 71 } 72 int insert(int p, int l, int r, int pos, int k) { 73 if(!p) p = ++tot; 74 T1.insert(id[p], pos); 75 if(l == r) return p; 76 int mid = (l + r) >> 1; 77 if(k <= mid) ls[p] = insert(ls[p], l, mid, pos, k); 78 else rs[p] = insert(rs[p], mid + 1, r, pos, k); 79 return p; 80 } 81 void del(int p, int l, int r, int pos, int k) { 82 T1.del(id[p], pos); 83 if(l == r) return; 84 int mid = (l + r) >> 1; 85 if(k <= mid) del(ls[p], l, mid, pos, k); 86 else del(rs[p], mid + 1, r, pos, k); 87 } 88 int pre(int l, int r, int k) { 89 int rk = getrnk(1, 0, V, 0, k - 1, l, r) + 1; 90 if(rk == 1) return -inf; 91 return kth(1, 0, V, l, r, rk - 1); 92 } 93 int nxt(int l, int r, int k) { 94 int rk = getrnk(1, 0, V, 0, k, l, r) + 1; 95 if(rk > r - l + 1) return inf; 96 return kth(1, 0, V, l, r, rk); 97 } 98 }T2;
图论
1.最短路
dijkstra
时间复杂度$O(nlogn)$,目前最快的单源最短路径算法,没有负边权时可用
1 struct node { 2 int id,dis; 3 friend bool operator > (const node &n1, const node &n2) { 4 return n1.dis > n2.dis; 5 } 6 }; 7 priority_queue<node, vector<node>, greater<node> >q; 8 void dijkstra() { 9 memset(dis, 0x3f, sizeof(dis)); 10 dis[s] = 0; 11 q.push((node) {s, 0}); 12 while(q.size()) { 13 int p = q.top().id; q.pop(); 14 if(vis[p]) continue; 15 vis[p] = 1; 16 for(int i = head[p]; i; i = e[i].next) { 17 int v1 = e[i].to, w1 = e[i].w; 18 if(dis[v1] > dis[p] + w1) { 19 dis[v1] = dis[p] + w1; 20 q.push((node) {v1, dis[v1]}); 21 } 22 } 23 } 24 }
SPFA(队列优化的bellman-ford算法)
虽然SPFA已死,但是其在判定负环、处理负权边、费用流等方面有着不可替代的地位
下面代码以SPFA判定负环同时求最短路为例给出代码
1 bool SPFA() {// 返回true就是无负环 也可用于差分约束 2 dist[n + 1] = 0; 3 cnt[n + 1] = 1; 4 inq[n + 1] = 1; 5 for(int i = 1; i <= n; ++i) dist[i] = 0x3f3f3f3f; 6 q.push(n + 1); 7 while(q.size()) { 8 int p = q.front(); q.pop(); 9 inq[p] = 0; 10 for(int i = head[p]; i; i = e[i].next) { 11 int v1 = e[i].to, w1 = e[i].w; 12 if(dist[v1] > dist[p] + w1) { 13 dist[v1] = dist[p] + w1; 14 if(!inq[v1]) { 15 if(++cnt[v1] > n) return false; 16 q.push(v1); 17 inq[v1] = 1; 18 } 19 } 20 } 21 } 22 return true; 23 }
floyd
floyd用于求全源最短路,本质是动态规划,所以阶段中转点k作为动态规划的阶段需要放在最外层
且由于min、+运算满足结合律,floyd也可用于重定义矩阵乘法,算出经过n条路径的最短路径长度
1 // 记得初始化dist为inf 2 for(int k = 1; k <= n; ++k) 3 for(int i = 1; i <= n; ++i) 4 for(int j = 1; j <= n; ++j) 5 dist[i][j] = min(dist[i][k] + dist[k][j], dist[i][k];
2.LCA
树链剖分求LCA,详见树链剖分部分
3.tarjan系列
tarjan系列都离不开时间戳(dfn)和追溯值(low)两个数组的更新,tarjan时都在主函数中把没有遍历过的节点再dfs一遍(!dfn[i])
强连通分量
多用于有向图的缩点,缩点之后可以变成一个DAG,进行dp等操作。强联通分量判定法则:dfs回溯时,若dfn[x] == low[x],则x及其当前搜索子树处于同一个强连通分量内,为了存下搜索树中所有节点,我们使用一个栈来模拟,每次dfs都存下当前节点
1 void tarjan(int p) { 2 st[++top] = p; ins[p] = 1; 3 dfn[p] = low[p] = ++num; 4 for(int i = head[p]; i; i = e[i].next) { 5 if(!dfn[e[i].to]) { 6 tarjan(e[i].to); 7 low[p] = min(low[p], low[e[i].to]); 8 } 9 else if(ins[e[i].to]) { 10 low[p] = min(low[p], dfn[e[i].to]); 11 } 12 } 13 if(dfn[p] == low[p]) { 14 ++scc; int y; 15 do { 16 y = st[top--], ins[y] = 0; 17 belong[y] = scc;// 属于哪个强连通分量 18 a2[scc] += a[y];// 缩点后统计点信息 19 }while(p != y);// 主要循环方式 20 } 21 }
边双联通分量 割边
$\exists$y$\in$subtree(x)(搜索树),且low[y] > dfn[x],则<x,y>为割边(无向边) 边双联通分量就是不含割边的子图
1 bool bridge[maxe];// 割边 2 void tarjan(int p, int in_edge) {// tarjan求割边 3 dfn[p] = low[p] = ++num; 4 for(int i = head[p]; i; i = e[i].next) { 5 int v1 = e[i].to; 6 if(!dfn[v1]) { 7 tarjan(v1, i); 8 low[p] = min(low[p], low[v1]); 9 if(low[v1] > dfn[p]) {// 割边判定法则 10 bridge[i] = bridge[i ^ 1] = true; 11 } 12 } 13 else if(i != (in_edge ^ 1)) { 14 low[p] = min(low[p], dfn[v1]); 15 } 16 } 17 } 18 int belong[maxn];// 属于哪个分量 19 vector<int>e_dcc[maxn];// 存储每个双联通分量编号和点数 20 void dfs(int p) {// 边双联通分量dfs 21 if(!p) return;// 防止虚无边 22 belong[p] = e_dcc_cnt; 23 e_dcc[e_dcc_cnt].push_back(p); 24 ++e_dcc[e_dcc_cnt][0]; 25 for(int i = head[p]; i; i = e[i].next) { 26 if(belong[e[i].to] || bridge[i]) continue; 27 dfs(e[i].to); 28 } 29 } 30 // 下面代码在主函数中 31 for(int i = 1; i <= n; ++i) { 32 if(!dfn[i]) { 33 tarjan(i, 0); 34 } 35 } 36 for(int i = 1; i <= n; ++i) { 37 if(!belong[i]) { 38 e_dcc[++e_dcc_cnt].push_back(0); 39 dfs(i); 40 } 41 }
4.网络流
dinic最大流
不会EK算法,所以写dinic了反正dinic效率比较高。网络流注重于建模,算法为其次。
容易用反证法证明最大流=最小割,且通过建源点汇点的方式可以解决二分图最大匹配问题,等等......
1 bool bfs() {// 残量网络分层图 2 memset(d, 0, sizeof(d)); 3 qhead = 1, tail = 0; 4 q[++tail] = s;d[s] = 1; now[s] = head[s];// 从源点开始 5 while(tail >= qhead) { 6 int p = q[qhead++]; 7 for(int i = head[p]; i; i = e[i].next) { 8 if(e[i].v > 0 && !d[e[i].to]) {// 在网络内,没分过层 9 q[++tail] = e[i].to; 10 now[e[i].to] = head[e[i].to];// 当前弧优化 11 d[e[i].to] = d[p] + 1; 12 if(e[i].to == t) return 1;// 到汇点 分层成功 13 } 14 } 15 } 16 return 0;// 记得 17 } 18 int dinic(int p, int flow) {// 增广 19 if(p == t) return flow; 20 int rest = flow, temp, i; 21 for(int i = now[p]; i && rest; i = e[i].next) { 22 if(e[i].v > 0 && d[e[i].to] == d[p] + 1) { 23 temp = dinic(e[i].to, min(rest, e[i].v));// 继续增广,取当前剩余和边限制最小值 24 if(!temp) d[e[i].to] = 0;// 剪枝,增广完不遍历 25 e[i].v -= temp; 26 e[i ^ 1].v += temp; 27 rest -= temp; 28 } 29 } 30 now[p] = i;// 当前弧优化即避免遍历不可遍历边 31 return flow - rest; 32 } 33 // 下面代码在主函数中 34 while(bfs()) { 35 while(temp = dinic(s, inf)) { 36 maxflow += temp; 37 } 38 }
ISAP最大流
虽然复杂度理论上界相比dinic没有变,但是随机数据下跑得比dinic快,也不是很难写,hlpp没有必要
1 void bfs() {// 只用bfs一遍 2 qhead = 1, tail = 0; 3 q[++tail] = t; dep[t] = 1;// 从t开始搜索 4 gap[1] = 1; 5 while(tail >= qhead) { 6 int p = q[qhead++]; 7 for(int i = head[p]; i; i = e[i].next) { 8 if(!dep[e[i].to]) {// 少了有流量的限制 dfs时判定就好 9 dep[e[i].to] = dep[p] + 1; 10 ++gap[dep[e[i].to]]; 11 q[++tail] = e[i].to; 12 } 13 } 14 } 15 } 16 int dfs(int p, int flow) { 17 if(p == t) return flow; 18 int rest = flow, temp; 19 for(int &i = now[p]; i; i = e[i].next) {// 因为中间要return不便修改 加& 20 if(e[i].flow && dep[p] == dep[e[i].to] + 1) {// 注意深度反过来 21 temp = dfs(e[i].to, min(e[i].flow, rest)); 22 e[i].flow -= temp; 23 e[i ^ 1].flow += temp; 24 rest -= temp; 25 if(!rest) return flow;// 这里表示没得增广了 26 } 27 } 28 // 到这里说明 1.前面传来流量还有 2.能往下传的都传完了 表明该点不能再增广 要改深度 29 // 分隔开该点出点 30 --gap[dep[p]]; 31 if(!gap[dep[p]]) dep[s] = n + 1;// 就为这句话 出现断层不用再增广 32 ++dep[p]; 33 ++gap[dep[p]]; 34 return flow - rest; 35 } 36 // 以下在主函数中 37 bfs(); 38 while(dep[s] <= n) {// 只要没有断层 39 memcpy(now, head, sizeof(head));// 当前弧优化 40 maxflow += dfs(s, inf);// 就一直增广 41 }
没比dinic多几行
dinic费用流
费用流每次以费用为权值找最短路来增广,注意由于反向边用于撤销,费用要设为负数。具体实现就是bfs分层改成SPFA求最短路。
消圈暂时搁置。
1 bool SPFA() { 2 memset(dis, inf, sizeof(dis)); 3 memcpy(now, head, sizeof(head)); 4 qhead = 1, tail = 0, q[++tail] = s; 5 dis[s] = 0, inq[s] = 1; 6 // printf("%d %d\n", t, dis[t]); 7 while(tail >= qhead) { 8 int p = q[qhead++]; 9 inq[p] = 0; 10 for(int i = head[p]; i; i = e[i].next) { 11 if(e[i].flow > 0 && dis[e[i].to] > dis[p] + e[i].cost) { 12 dis[e[i].to] = dis[p] + e[i].cost; 13 if(!inq[e[i].to]) { 14 q[++tail] = e[i].to; 15 inq[e[i].to] = 1; 16 } 17 } 18 } 19 } 20 return dis[t] != inf; 21 } 22 int dinic(int p, int flow) { 23 if(p == t) return flow; 24 int rest = flow, i, temp; 25 inq[p] = 1;// 必要的增广队列 26 for(i = now[p]; i && rest > 0; i = e[i].next) { 27 if(!inq[e[i].to] && dis[e[i].to] == dis[p] + e[i].cost && e[i].flow > 0) { 28 temp = dinic(e[i].to, min(rest, e[i].flow)); 29 if(!temp) dis[e[i].to] = -1; 30 e[i].flow -= temp; 31 e[i ^ 1].flow += temp; 32 mincost += e[i].cost * temp; 33 rest -= temp; 34 } 35 } 36 inq[p] = 0; 37 now[p] = i; 38 return flow - rest; 39 } 40 // 下面代码在主函数中 41 while(SPFA()) { 42 while((nowflow = dinic(s, inf)) != 0) { 43 maxflow += nowflow; 44 } 45 }
5.点分治
处理树上路径统计问题。日后有时间简化一下寻找重心部分。
1 void get_g(int p, int last) { 2 sz[p] = 1; 3 int max_part = 0; 4 for(int i = head[p]; i; i = e[i].next) { 5 if(del[e[i].to] || e[i].to == last) continue; 6 get_g(e[i].to, p); 7 sz[p] += sz[e[i].to]; 8 max_part = max(max_part, sz[e[i].to]); 9 } 10 max_part = max(max_part, allsz - sz[p]); 11 if(max_part < gpart) { 12 g = p; 13 gpart = max_part; 14 } 15 } 16 void dfs(int p, int last) { 17 a[++top] = p; 18 for(int i = head[p]; i; i = e[i].next) { 19 if(e[i].to == last || del[e[i].to]) continue; 20 dis[e[i].to] = dis[p] + e[i].w; 21 bel[e[i].to] = bel[p]; 22 dfs(e[i].to, p); 23 } 24 } 25 void divide(int p) { 26 gpart = 0x3f3f3f3f; 27 get_g(p, 0); 28 bel[g] = g; dis[g] = top = 0; 29 a[++top] = g; cnt[g] = 0; 30 for(int i = head[g]; i; i = e[i].next) { 31 if(del[e[i].to]) continue; 32 dis[e[i].to] = e[i].w; 33 bel[e[i].to] = e[i].to; 34 cnt[e[i].to] = 0; 35 dfs(e[i].to, g); 36 } 37 sort(a + 1, a + top + 1, cmp); 38 for(int i = 1; i <= top; ++i) ++cnt[bel[a[i]]]; 39 int l = 1, r = top; 40 while(l < r) {// 例子是双指针统计 路径长度<=k路径数 41 while(dis[a[l]] + dis[a[r]] > k) --cnt[bel[a[r--]]]; 42 ans += r - l + 1 - cnt[bel[a[l]]]; 43 --cnt[bel[a[l++]]]; 44 } 45 del[g] = 1; 46 for(int i = head[g]; i; i = e[i].next) { 47 if(del[e[i].to]) continue; 48 allsz = sz[e[i].to]; 49 divide(e[i].to); 50 } 51 }
数学
1.exgcd 求二元一次不定方程
首先ax+by = c有整数解的充要条件是 gcd(a,b)|c (裴蜀定理,显然) 用这个可以判定方程无解情况,令d=gcd(a,b),解方程时可以先把方程化为 (a/d)*x + (b/d) * y = c/d,再调用exgcd的函数,最后特解x2 *= c,y2 *= c即可,可以构造通解形式 x = x2 + t * b, y = y2 - t * a, 进行值域分析容易得到 $\lceil \frac{-x2+1}{b} \rceil$ <= t <= $\lfloor \frac{y2-1}{a} \rfloor$
对于同余方程ax $\equiv$ 1 (mod p),构造二元一次不定方程ax + py = 1(y为 负数未知数),解出x即可得到a在模p意义下的乘法逆元,且比费马小定理更通用,a和p互质即可
1 typedef long long ll; 2 void exgcd(ll a, ll b, ll &x, ll &y) {// 同时也相当于求解了gcd(a,b) 记得写& 3 if(!b) { 4 x = 1; 5 y = 0; 6 return; 7 } 8 exgcd(b, a % b, x, y); 9 int temp = x; 10 x = y; 11 y = temp - a / b * y;// 推个式子就好了 12 }
2.快速幂&龟速乘
没啥好说的,(a ^ b) % m,核心思想是二进制拆分,但是注意可能幂运算过多可能要预处理次幂,龟速乘一般用于模数比int大的情况,模数为质数时候可以用快速幂+费马小定理求乘法逆元
1 typedef long long ll; 2 ll qpow(ll a, ll b, ll m) {// 快速幂 3 ll ret = 1; 4 while(b) { 5 if(b & 1) ret = ret * a % m; 6 a = a * a % m; 7 b >>= 1; 8 } 9 return ret; 10 } 11 ll mul(ll a, ll b, ll m) {// 龟速乘, a*b % m 12 ll ret = 0; 13 while(b) { 14 if(b & 1) ret = (ret + a) % m; 15 a = (a + a) % m; 16 b >>= 1; 17 } 18 return ret; 19 }
3.线性筛 & 积性函数
线性筛质数,还可以拿来搞积性函数递推,下面约定phi为欧拉函数,miu为莫比乌斯函数,d为约数个数函数
1 // prime大小要自行估计,开大点一般不会爆 2 phi[0] = phi[1] = 1;// 千万记得初始化 3 miu[1] = 1; 4 d[1] = 1; 5 for(int i = 2; i <= n; ++i) { 6 if(!isntp[i]) { 7 prime[++cnt] = i; 8 phi[i] = i - 1; 9 miu[i] = -1; 10 d[i] = 2;// 对于约数函数,可以考虑使用唯一分解+乘法原理 11 num[i] = 1;// num[i]为i当前质因数次幂 12 } 13 for(int j = 1; j <= cnt; ++j) { 14 int k = i * prime[j]; 15 if(k > n) break; 16 isntprime[k] = 1; 17 if(i % prime[j] == 0) { 18 phi[k] = phi[i] * prime[j];// k包含i所有质因子 19 miu[k] = 0; 20 num[k] = num[i] + 1; 21 d[k] = d[i] / num[k] * (num[k] + 1); 22 break; 23 } 24 // 互质 积性递推 25 phi[k] = phi[i] * phi[prime[j]]; 26 miu[k] = -miu[i]; 27 d[k] = d[i] << 1; 28 num[k] = 1; 29 } 30 }
4.乘法逆元
乘法逆元一般多个数递推比较有用,可以拿来算组合数之类的
1 void exgcd(int a, int b, int &x, int &y) { 2 if(!b) {x = 1; y = 0; return;} 3 exgcd(b, a % b, x, y); 4 int temp = x; 5 x = y; 6 y = temp -a / b * x; 7 } 8 int inv(int a,int p) { 9 int x0,y0; 10 exgcd(a, p, x0, y0); 11 return (x0 % p + p) % p;// 防止负数 12 } 13 // 下面在主函数内 14 s[0] = 1;// 前缀积 15 for(int i = 1; i <= n; ++i) { 16 scanf("%d", a + i);// 特别是a[i] = i 时可以预处理阶乘逆元 17 s[i] = 1ll * s[i-1] * a[i] % p; 18 } 19 vs[n] = inv(s[n],p); 20 for(int i = n - 1; i >= 1; --i) vs[i] = 1ll * vs[i+1] * a[i+1] % p; 21 // 考虑把式子写成倒数形式就可以了 22 for(int i = 1; i <= n; ++i) { 23 inva[i] = 1ll * vs[i] * s[i-1] % p; 24 }
5.CRT & EXCRT
crt就是基于基于一个公式 x = $\sum_{i=1}^n a_i M_i t_i$ Mi就是所有$m_i$(每个方程模数)乘起来除以当前mi,ti就是 $M_i t_i \equiv 1$(mod $m_i$) 的解,$a_i$就是原方程右边的数
要求mi两两之间互质,显然有限制
excrt就是推式子,用exgcd将方程之间两两合并,维护前缀最大公约数,原本的解加上t * M, 解出t然后回带,有些题换汤不换药,就比如NOI2018 屠龙勇士
1 // CRT 2 for(int i = 1; i <= n; ++i) { 3 M[i] = Mm / m[i]; 4 exgcd(M[i], m[i], x0, y0); 5 t[i] = (x0 % m[i] + m[i]) % m[i]; 6 } 7 for(int i = 1; i <= n; ++i) x += a[i] * M[i] * t[i]; 8 //EXCRT 9 for(int i = 2; i <= n; ++i) { 10 tempgcd = gcd(prelcm, m[i]); 11 tempc = (a[i] - nowx % m[i] + m[i]) % m[i]; 12 exgcd(prelcm, m[i], tempx, tempy); 13 tempx = mul(tempx, tempc / tempgcd, m[i] / tempgcd); 14 nowx = nowx + tempx * prelcm; 15 prelcm = prelcm / tempgcd * m[i]; 16 nowx = (nowx % prelcm + prelcm) % prelcm; 17 }
6.BSGS ex待补
拿来解方程 $ a^x \equiv b$ (mod p),a、p互质,实质是枚举法的改进,根号根号地枚举,最后验证
我好像也不太知道这玩意能拿来干嘛
1 int BSGS(int a, int b, int p) {// 无解返回-1 2 b %= p; 3 int t = (int)sqrt(p) + 1; 4 for(int j = 0; j < t; ++j) { 5 hs.insert(1ll * b * ksm(a, j, p) % p, j);// 拉链法哈希表 考试就写unordered_map吧 6 } 7 a = ksm(a, t, p); 8 if(!a) return b == 0 ? 1 : -1; 9 for(int i = 0; i <= t; ++i) { 10 int val = ksm(a, i, p); 11 int j = hs.find(val); 12 if(j >= 0 && i * t - j >= 0) return i * t - j; 13 } 14 return -1; 15 }
7.矩阵乘法
什么年代了还在考传统矩阵乘法,看见有min{},+,&,|,这种有结合律的东西就考虑下矩阵乘法(GZOI2017 河神)
1 struct matrix { 2 ll mat[11][11]; 3 void clear() { 4 for(int i = 1; i <= 10; ++i) 5 memset(mat[i], 0, sizeof(mat[i])); 6 } 7 ll *operator [] (int index) { 8 return mat[index]; 9 } 10 matrix operator * (const matrix &m2) { 11 matrix ret; ret.clear(); 12 for(int i = 1; i <= 10; ++i) { 13 for(int k = 1; k <= 10; ++k) { 14 ll temp = mat[i][k]; 15 for(int j = 1; j <= 10; ++j) { 16 ret[i][j] = (ret[i][j] + temp * m2.mat[k][j] % mod) % mod; 17 } 18 } 19 } 20 return ret; 21 } 22 friend matrix operator ^ (matrix m1, ll k) { 23 matrix ret; ret.clear(); 24 for(int i = 1; i <= 10; ++i) 25 ret[i][i] = 1; 26 while(k) { 27 if(k & 1) ret = ret * m1; 28 m1 = m1 * m1; 29 k >>= 1; 30 } 31 return ret; 32 } 33 }a1,ans;
8.高斯消元
拿来解线性方程组,还有矩阵求逆、求行列等作用,还可以优化成环的期望dp
都不想吐槽之前写的高斯消元什么答辩玩意,换了高斯-约旦消元法,精度更好,代码更短
1 bool gauss() { 2 for(short i = 1; i <= n; ++i) { 3 int mxj = i; 4 for(short j = i; j <= n; ++j) if(fabs(a[j][i]) > fabs(a[mxj][i])) mxj = j;// 这里可以减小误差 5 swap(a[mxj], a[i]); 6 if(fabs(a[i][i]) < eps) { 7 printf("No Solution"); 8 return false; 9 } 10 for(short j = 1; j <= n; ++j) { 11 if(j == i) continue; 12 double rate = a[j][i] / a[i][i]; 13 for(short k = i; k <= n + 1; ++k) a[j][k] = a[j][k] - a[i][k] * rate; 14 } 15 } 16 return true; 17 }
高斯消元求行列式
就是消成只剩主对角线,乘起来即可,注意行列式交换两行值要取反
模数为质数,保证逆元存在的版本
1 int getdet() { 2 int ret = 1, fh = 1; 3 for(short i = 1; i <= li; ++i) { 4 for(short j = i; j <= li; ++j) 5 if(mat[j][i]) { 6 if(i != j) swap(mat[i], mat[j]), fh = -fh; 7 break; 8 } 9 for(short j = 1; j <= li; ++j) { 10 if(j == i || !mat[j][i]) continue; 11 int rate = 1ll * mat[j][i] * ksm(mat[i][i], mod - 2) % mod; 12 for(short k = 1; k <= li; ++k) mat[j][k] = (mat[j][k] - 1ll * mat[i][k] * rate % mod) % mod; 13 } 14 } 15 for(short i = 1; i <= li; ++i) ret = 1ll * ret * mat[i][i] % mod; 16 return (ret * fh + mod) % mod; 17 }
模数不保证是质数的版本
1 for(short i = 1; i <= n; ++i) { 2 for(short j = i + 1; j <= n; ++j) { 3 while(a[i][i]) { 4 int rate = a[j][i] / a[i][i]; 5 for(short k = 1; k <= n && rate; ++k) a[j][k] = (a[j][k] - 1ll * rate * a[i][k] % p) % p; 6 swap(a[i], a[j]); 7 fh = -fh; 8 } 9 swap(a[i], a[j]); 10 fh = -fh; 11 } 12 } 13 for(short i = 1; i <= n; ++i) ans = 1ll * ans * a[i][i] % p; 14 ans *= fh;
9.线性基
线性基可以搞比如一堆数异或起来可得到的最大,最小,第k大值,判断一个数能不能由某些数异或得到
核心思想是每次高斯消元消掉最高位
异或版
查询存不存在就遍历一遍,当前数如果与当前基底的这一位都是1就异或掉这个基底,最后看是不是0
1 void insert(ll x) {// 插入 2 for(int i = maxbit; i >= 0; --i) {// maxbit最高位 3 if(x >> i & 1) { 4 if(!lb[i]) { 5 lb[i] = x; 6 ++cnt; 7 break; 8 } 9 else x ^= lb[i]; 10 if(!x) { 11 zero = 1; 12 break; 13 } 14 } 15 } 16 } 17 void work() {// 如果查询第k大要高斯消元要化为最简形 18 for(int i = 0; i <= 60; ++i) { 19 if(!lb[i]) continue; 20 for(int j = i + 1; j <= 60; ++j) { 21 if(lb[j] >> i & 1) lb[j] ^= lb[i]; 22 } 23 } 24 } 25 ll kth(ll k) { 26 ll ret = 0; 27 if(zero) --k; 28 if(k >= (1ll << cnt)) return -1; 29 for(int i = 0; i <= cnt - 1; ++i) 30 if(k >> i & 1) ret ^= lb[i]; 31 return ret; 32 } 33 ll max_num(ll x) {// 最大值 34 for(int i = maxbit; i >= 0; --i) { 35 if((x ^ lb[i]) > x) x ^= lb[i]; 36 } 37 return x; 38 } 39 // 最小值就是线性基中最小的数
实数版
1 struct real_vector { 2 int v[505],c; 3 friend real_vector operator * (real_vector v1, const int &x) { 4 for(short i = 1; i <= m; ++i) { 5 if(!v1.v[i]) continue; 6 v1.v[i] = 1ll * v1.v[i] * x % mod; 7 } 8 return v1; 9 } 10 friend real_vector operator - (real_vector v1, const real_vector &v2) { 11 for(short i = 1; i <= m; ++i) { 12 v1.v[i] = ((v1.v[i] - v2.v[i]) % mod + mod) % mod;// 由于JLOI那个题过于鬼畜所以要上取模 13 } 14 return v1; 15 } 16 }b[505],lb[505]; 17 void insert(int x) { 18 bool flag = 0; 19 for(short i = m; i >= 1; --i) { 20 if(b[x].v[i]) { 21 if(!lb[i].v[i]) { 22 lb[i] = b[x]; 23 flag = 1; 24 ++ans1; 25 ans2 += b[x].c; 26 break; 27 } 28 else b[x] = b[x] - (lb[i] * (1ll * b[x].v[i] * inv(lb[i].v[i]) % mod));// 核心思想是消去最高位 inv逆元 29 if(ept(x)) break;// ept表示0向量 30 } 31 } 32 }
10.多项式
生成函数的基础。闲的没事偶尔抽点时间完善一下多项式全家桶。
FFT
注意的细节:
1.逆变换除以长度
2.不要三次变两次,精度小心被卡爆,同时能用NTT的题绝对不用FFT
3.用于蝴蝶操作的r/rev数组递推不要写错
4.最高次为m,n的两个多项式相乘,最高次数m+n,我们要变化到$2^{x} >= m + n, x \in \mathbb{Z}$
1 void fft(int lim, complex *f, int type){// 1系数变点,-1点变系数,complex手写复数类 2 for(int i = 0; i < lim; ++i) if(i < r[i]) swap(f[i], f[r[i]]); 3 for(int len = 2; len <= lim; len <<= 1) { 4 complex wn = {cos(2 * pi / len), type * sin(2 * pi / len)}; 5 for(int i = 0; i < lim; i += len) { 6 complex wnk = {1, 0}; 7 for(int j = i; j < i + (len >> 1); ++j, wnk = wnk * wn) { 8 complex u = f[j], t = f[j + (len >> 1)] * wnk; 9 f[j] = u + t; 10 f[j + (len >> 1)] = u - t; 11 } 12 } 13 } 14 } 15 // 注意这个版本是没有预处理单位根的 16 // 对于每个len处理出所有对应单位根会更快
NTT
注意的细节:
1.该取模的一个都不能少,该乘1ll的一个都不能忘
2.gp[0] = 1
1 void ntt(int lim, int *f, int type) {// 1系数变点,-1点变系数 2 for(int i = 0; i < lim; ++i) 3 if(i < r[i]) swap(f[i], f[r[i]]); 4 for(int len = 2; len <= lim; len <<= 1) { 5 gp[1] = ksm(type == 1 ? g : g_1, (p - 1) / len, p); 6 for(int i = 2; i < (len >> 1); ++i) gp[i] = 1ll * gp[i - 1] * gp[1] % p;// 预处理单位根 忘取模草 7 for(int i = 0; i < lim; i += len) { 8 for(int j = i; j < i + (len >> 1); ++j) {// 这里要反复使用单位原根 可以预处理 9 int u = f[j],t = 1ll * f[j + (len >> 1)] * gp[j - i] % p; 10 f[j] = (u + t) % p; 11 f[j + (len >> 1)] = (u - t + p) % p; 12 } 13 } 14 } 15 if(type == -1) {//逆变换除以长度 费马小定理乘法逆元 16 int inv = ksm(lim, p - 2, p); 17 for(int i = 0; i < lim; ++i) f[i] = 1ll * f[i] * inv % p; 18 } 19 }
封装多项式乘法
1 void init() { 2 sx = 1, l = -1; 3 while(sx <= n + m) sx <<= 1, ++l; 4 for(int i = 0; i < sx; ++i) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << l); 5 gp[0] = 1; 6 } 7 int A[maxn],B[maxn]; 8 void mul(int *a, int *b, int lim) {// a(x) * b(x) 存在a(x)上 9 memset(A, 0, sizeof(A)); 10 memset(B, 0, sizeof(B)); 11 for(int i = 0; i < lim; ++i) A[i] = a[i], B[i] = b[i]; 12 ntt(A, lim, 1); ntt(B, lim, 1); 13 for(int i = 0; i < lim; ++i) A[i] = 1ll * A[i] * B[i] % p; 14 ntt(A, lim, -1); 15 for(int i = 0; i < lim; ++i) a[i] = A[i]; 16 }
多项式乘法逆
考虑倍增计算,简单数学推导一下即可,自认为常数卡得还可以(?)。就是细节有点多
1.rev数组重新计算
2.注意多项式的界,超过清零
1 void polyinv(int *a, int *b, int lim) { 2 if(lim == 1) { 3 b[0] = ksm(a[0], p - 2); 4 return; 5 } 6 polyinv(a, b, (lim + 1) >> 1); 7 int sx = 1, l = -1; 8 while(sx <= (lim << 1)) sx <<= 1, ++l; 9 for(int i = 0; i < sx; ++i) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << l);//error2 10 for(int i = 0; i < lim; ++i) c[i] = a[i]; 11 for(int i = lim; i < sx; ++i) c[i] = 0; 12 ntt(c, sx, 1); ntt(b, sx, 1); 13 for(int i = 0; i < sx; ++i)// 求逆的高级操作 不用卷两次 14 b[i] = 1ll * b[i] * (2 - 1ll * c[i] * b[i] % p + p) % p; 15 ntt(b, sx, -1); 16 for(int i = lim; i < sx; ++i) b[i] = 0; 17 }
多项式ln
先求导再积分 解决了
1 void qiudao(int *a, int *b, int lim) { 2 for(int i = 0; i < lim; ++i) b[i] = 1ll * a[i + 1] * (i + 1) % p; 3 b[lim - 1] = 0; 4 } 5 void jifen(int *a, int *b, int lim) { 6 for(int i = 1; i < lim; ++i) b[i] = 1ll * a[i - 1] * ksm(i, p - 2) % p; 7 b[0] = 0; 8 } 9 int d[maxn],e[maxn]; 10 void polyln(int *a, int *b, int lim) { 11 qiudao(a, d, lim); polyinv(a, e, lim); 12 int sx = 1, l = -1; 13 while(sx <= (lim << 1)) sx <<= 1, ++l; 14 for(int i = 0; i < sx; ++i) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << l); 15 ntt(d, sx, 1); ntt(e, sx, 1); 16 for(int i = 0; i < sx; ++i) d[i] = 1ll * d[i] * e[i] % p; 17 ntt(d, sx, -1); 18 jifen(d, b, lim); 19 }