乱七八糟的模板集合

字符串

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 }

 

posted on 2022-11-11 17:04  loser_kugua  阅读(54)  评论(0)    收藏  举报