算法模板
一)基础算法
split
vector<string> split(string& s, char c) {
vector<string> res;
int i = 0;
while (i<s.size() && s[i] == c) i++;
if (i >= s.size()) return {};
for (int j=i; j<s.size(); ) {
while(j<s.size() && s[j] != c) j++;
res.push_back(s.substr(i, j-i));
while(j<s.size() && s[j] == c) j++;
i=j;
}
return res;
}
高精度加法
// C = A + B, A >= 0, B >= 0
for(int i=a.size()-1; i>=0; i--) A.push_back(a[i] - '0');//输入
vector<int> add(vector<int> &A, vector<int> &B)
{
if (A.size() < B.size()) return add(B, A);
vector<int> 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;
}
高精度减法
// C = A - B, 满足A >= B, A >= 0, B >= 0
vector<int> sub(vector<int> &A, vector<int> &B) {
if(cmp(A,B) == false) {// A >= B ?
printf("-");
return sub(B,A);
}
vector<int> C;
for(int i=0, t=0; i<A.size(); ++i) {
t = A[i] - t;
if(i < B.size()) t -= B[i];
C.push_back((t+10)%10);
if(t < 0) t = 1;
else t = 0;
}
//去前导0
while(C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
高精度乘低精度
// C = A * b, A >= 0, b > 0
vector<int> mul(vector<int> &A, int b)
{
vector<int> 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;
}
高精度除以低精度
// A / b = C ... r, A >= 0, b > 0
vector<int> div(vector<int> &A, int b, int &r)
{
vector<int> C;
r = 0;
for (int i = A.size() - 1; i >= 0; i -- )
{
r = r * 10 + A[i];
C.push_back(r / b);
r %= b;
}
reverse(C.begin(), C.end());
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
二维前缀和
S[i, j] = 第i行j列格子左上部分所有元素的和
s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];//预处理:(1,1)开始
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
一维差分
给区间[l, r]中的每个数加上c:a[l] += c, a[r + 1] -= c
二维差分
给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
a[x1, y1] += c, a[x2 + 1, y1] -= c, a[x1, y2 + 1] -= c, a[x2 + 1, y2 + 1] += c
离散化
vector<int> alls; // 存储所有待离散化的值(查询和修改的)
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
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
}
二)数据结构
单调栈-O(n)
常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for(int i=1; i<=n; ++i) {//小的
while(tt && a[stk[tt]] >= a[i]) tt--;
if(tt) printf("%d ", a[stk[tt]]);
else printf("-1 ");
stk[++tt] = i;//下标
}
单调队列-O(n)
常见模型:找出滑动窗口中的最大值/最小值
int hh=0, tt=-1;//找最小值
for(int i=0;i<n; ++i) {
while(hh<=tt && q[hh]<i-k+1) hh++;
while(hh<=tt && a[q[tt]]>=a[i]) tt--;
q[++tt] = i;
if(i >= k-1) printf("%d ", a[q[hh]]);
}
KMP-O(n)
寻找字符串p在文本s中出现的位置
// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度, s和p从下标1开始存
for (int i = 2, j = 0; i <= m; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++ ;
ne[i] = j;
}
for (int i = 1, j = 0; i <= n; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == m)
{
j = ne[j];
// 匹配成功后的逻辑
// 匹配位置:i-m+1 (s下标从1开始)
}
}
Trie树-O(strlen)
询问字符串在字符串集合中出现的次数
int son[N][26], cnt[N], idx;// N为所有字符串的总长度
// cnt[]存储以每个节点结尾的单词数量
void insert(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if (!son[p][u]) son[p][u] = ++ idx;
p = son[p][u];
}
cnt[p] ++ ;
}
int query(char *str)//返回次数
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if (!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}
并查集
int cnt[N]; // cnt[px] 根才有效
int p[N], d[N]; // d[x]存储x到p[x]的距离
int find(int x) {
if (p[x] != x)
{
int px = p[x];
p[x] = find(p[x]);//p[x] = root
d[x] += d[px];
}
return p[x];
}
// 合并a和b所在的两个集合:
if(pa != pb) {
p[pa] = pb;
d[pa] = 画图确定距离 a->pa b->pb a与b关系
}
字符串哈希
判断字符串子串是否相等,预处理O(strlen),查询O(1)
typedef unsigned long long ULL;
ULL h[N]; // h[k]存储字符串前k个字母的哈希值
ULL p[N]; // p[k]存储 P^k mod 2^64
int P = 131; p[0] = 1;
// str从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) {//画图 h[r]-h[l-1]*(r-l+1)
return h[r] - h[l - 1] * p[r - l + 1];
}
STL
reverse_iterator: rbegin rend
set/multiset
erase()
(1) 输入是一个数x,删除所有x st.erase(x)
(2) 输入一个迭代器,删除这个迭代器 st.erase(it)
lower_bound()/upper_bound()
lower_bound(x) 返回>=x的最小数的迭代器 st.lower_bound(x)
upper_bound(x) 返回>x的最小数的迭代器 st.upper_bound(x)
map/multimap
erase() 输入的参数是pair或者迭代器
mp.erase(it)
mp.erase({x,y})
lower_bound()/upper_bound()
lower_bound(x) 返回>=x的最小数的迭代器 mp.lower_bound(x)
upper_bound(x) 返回>x的最小数的迭代器 mp.upper_bound(x)
bitset, 圧位
bitset<10000> s;
~, &, |, ^
>>, <<
==, !=
[]
count() 返回有多少个1
any() 判断是否至少有一个1
none() 判断是否全为0
set() 把所有位置成1
set(k, v) 将第k位变成v
reset() 把所有位变成0
flip() 等价于~
flip(k) 把第k位取反
三)图论
拓扑排序-O(n+m)
bool topsort() {
int hh = 0, tt = -1;
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;
}
BF算法-O(km)
边数限制,最多经过 k条边的最短距离
struct Edge {
int a, b, w;
}edges[M];
int d[N], backup[N];
// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford() {// 最多k条边到达n
memset(d,0x3f,sizeof d);
d[1] = 0;
for(int i=0; i<k; ++i) {
memcpy(backup, d, sizeof d);
for(int i=0; i<m; ++i) {
int a = edge[i].a, b= edge[i].b, w = edge[i].w;
d[b] = min(d[b], backup[a]+w);
}
}
if(d[n] > 0x3f3f3f3f/2) return -1;
return d[n];
}
spfa判断图中是否存在负环 -O(nm)
int dist[N], cnt[N]; // cnt[x]存储1到x的最短路中经过的点数
bool spfa() {
int hh=0, tt=0;
for (int i = 1; i <= n; i ++ ) {
q[tt++] = i;
st[i] = true;
}
while (q.size()) {
int u = q[hh++];
if(hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; 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;
if (!st[j]) {
st[j] = true;
q[tt++] = j;
if(tt == N) tt = 0;
}
return false;
}
染色法判断二分图-O(n+m)
// color[u] stores node u belong to set 1 or 0
bool check() {
memset(color,-1,sizeof color);
for(int i=1; i<=n; ++i)
if(color[i]==-1) {
if(dfs(i,0) == false) return false;
}
return true;
}
bool dfs(int u, int c) {//邻接表存储
color[u] = c;
for(int i=h[u]; ~i; i=ne[i]) {
int j=e[i];
if(color[j] == -1) {
if(dfs(j,!c) == false) return false;
}else {
if(color[j] == c) return false;//same color
}
}
return true;//dif color
}
二分图最大匹配-O(nm)
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 ++ ;
}
差分约束
求最小值->最长路,求最大值->最短路
以最小值为例子:
i->j
转化成 s[j] >= s[i] + c: add(i,j,c)
1.找到定值(原点)
2.确保从原点能走到所有边
3.根据限定条件建图
4.用 *SPFA* 求每个点最长路
dist[i]即为所求的最小值
注意:若用SPFA存在环,则表明无解
[结合前缀和]
s[0] = 0 原点
a[i]非负-> s[i] >= s[i-1]
其他限制a[i]的条件 or 限制区间和的条件
欧拉路径
1)无向图:
充要条件:除了起点和终点,其余点度数为偶数。度数为奇数的点有0个或2个
从1号点到n号点的欧拉路径 O(mlogm+n)
set<int> g[N];
vector<int> ans;//起点在最后,终点在开头
void dfs(int u) {
while (g[u].size()) {
int t = *g[u].begin();
g[u].erase(t), g[t].erase(u);
dfs(t);
}
ans.push_back(u);
}
2)有向图:
充要条件:要么所有点出度=入度,要么除起点和终点,其余点入度=出度。起点出度比入度多1,终点入度比出度多1
欧拉回路
1)无向图:所有点度数为偶数
2)有向图:所有点入度=出度
LCA
1.求树的两点之间的最短路
2.求树的两点之间的最小边
void dfs(int u, int faa) {//O(32n)
fa[u][0] = faa;
dep[u] = dep[faa]+1;
for(int i=1; i<=P; ++i) {
fa[u][i] = fa[fa[u][i-1]][i-1];
dis[u][i] = dis[u][i-1] + dis[fa[u][i-1]][i-1];
}
for(int i=h[u]; ~i; i=ne[i]) {
int j=e[i];
if(j==faa) continue;
dis[j][0] = w[i];
dfs(j,u);
}
}
int lca(int a, int b) {//O(32)
int res=0;
if(dep[a] < dep[b]) swap(a,b);
//跳到同一层
for(int i=P; i>=0; --i)
if(dep[fa[a][i]] >= dep[b])
res+=dis[a][i], a=fa[a][i];
if(a == b) return res;
for(int i=P; i>=0; --i)
if(fa[a][i] != fa[b][i]) {//跳到LCA的下一层
res += dis[a][i]+dis[b][i];
a = fa[a][i];
b = fa[b][i];
}
res += dis[a][0]+dis[b][0];
return res;
}
缩点tarjan-O(n+m)
for (int i = 1; i <= n; i ++ )
if (!dfn[i])
tarjan(i);
void tarjan(int u) {
dfn[u] = low[u] = ++ timestamp;
stk[ ++ top] = u, in_stk[u] = true;
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if (in_stk[j]) low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u]) {
++ scc_cnt;
int y;
do {
y = stk[top -- ];
in_stk[y] = false;
id[y] = scc_cnt;
Size[scc_cnt] ++ ;
} while (y != u);
}
}
四)数论
试除法分解质因数-O(sqrt(x))
unordered_map<int,int> primes;
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 ++ ;
primes[i] += s;
}
if (x > 1) primes[x] ++;
}
朴素筛质数-O(nloglogn)
void get_primes(int n) {
for(int i=2; i<=n; ++i) {
if(!st[i]) {
prime[++siz] = i;
for(int j=i; j<=n; j+=i) st[j] = true;
}
试除法求所有约数(因子)-O(sqrt(x))
vector<int> get_divisors(int x){
vector<int> res;
for (int i = 1; i <= x / i; i ++ )
if (x % i == 0) {
res.push_back(i);
if (i != x / i) res.push_back(x / i);
}
sort(res.begin(), res.end());
return res;
}
约数个数和约数之和
如果 N = p1^c1 * p2^c2 * ... *pk^ck // 结合质因数分解
约数个数: (c1 + 1) * (c2 + 1) * ... * (ck + 1)
约数之和: (p1^0 + p1^1 + ... + p1^c1) * ... * (pk^0 + pk^1 + ... + pk^ck)
快速幂-O(logk)
//求 a^k mod p,时间复杂度 O(logk)。a,b,p无限制关系
int qmi(int a, int k, int p) {//初始a^(2^0) = a
int res = 1;
while(k) {
if(k&1) res = (LL)res * a % p;
k >>= 1;
a = (LL)a*a % p;
}
return res;
}
①递归法求组合数-O(n^2)
for (int i = 0; i < N; i ++ )//c(2000,2000)
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;
②逆元求组合数-O(nlogp)
// C(a,b) = a!/(b!*(a-b)!)
// C(1e5,1e5) mod为质数
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求组合数-单次O(plogp)
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;
}
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 ++ ) {//a*(a-1)*...和b!
x = (LL)x * i % p;
y = (LL) y * j % p;
}
return x * (LL)qmi(y, p - 2, p) % p;
}
卡特兰数
给定n个0和n个1,它们按照某种顺序排成长度为2n的序列,满足任意前缀中0的个数都不少于1的个数的序列的数量为:
Cat(n) = C(2n, n) / (n + 1)
Nim游戏
NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ … ^ An != 0
五)数据结构二
树状数组-O(logn)
动态查找区间和,即查找与修改穿插。或者 区间修改,单点查询(差分)
int tr[N], n;//1~n
memset(tr,0,sizeof tr);
void add(int x, int c) {//(位置,要加的值)
while(x <= n) {
tr[x] += c;
x += lowbit(x);
}
}
int sum(int x) {//前1~x个位置的和
int res = 0;
while(x > 0) {
res += tr[x];
x -= lowbit(x);
}
return res;
}
线段树-O(logn)
struct Node {//1~n
LL l, r;
LL sum, add;
}tr[N * 4];
LL w[N];
void pushup(LL u) {// 孩子更新父亲
tr[u].sum = tr[u<<1].sum + tr[u<<1|1].sum;
}
void eval(Node& u, LL add) {
u.sum += add*(u.r-u.l+1);
u.add += add;
}
void pushdown(LL u) {// 父亲更新孩子懒标记
eval(tr[u<<1], tr[u].add);
eval(tr[u<<1|1], tr[u].add);
tr[u].add = 0;
}
void build(LL u, LL l, LL r) {
if (l == r) tr[u] = {l, r, w[r], 0};
else {
tr[u] = {l, r, 0, 0};
LL mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
pushup(u);
}
}
void update(LL u, LL l, LL r, LL add) {
if (tr[u].l >= l && tr[u].r <= r) {
eval(tr[u], add);//打懒标记,并更新区间值
}
else {
pushdown(u);
LL mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) update(u << 1, l, r, add);
if (r > mid) update(u << 1 | 1, l, r, add);
pushup(u);
}
}
LL query(LL u, LL l, LL r) {//注意LL
if (tr[u].l >= l && tr[u].r <= r) {
return tr[u].sum; // TODO 需要补充返回值
} else {
pushdown(u);
LL mid = tr[u].l + tr[u].r >> 1;
LL res = 0;
if (l <= mid ) res = query(u << 1, l, r);
if (r > mid) res += query(u << 1 | 1, l, r);
return res;
}
}
st表RMQ-O(nlogn)
静态求区间最值:预处理 O(nlogn),查询 O(1)
// 数组a从1开始存
int f[N][M];//表示i开始,长度为2^j的最大值
void init() {
for(int j=0; j<M; ++j)//长度从小到大枚举
for(int i=1; i+(1<<j)-1<=n; ++i) {
if(!j) f[i][j] = a[i];
else f[i][j] = max(f[i][j-1], f[i+(1<<j-1)][j-1]);//前2^j-1个数,后2^j-1个数
}
}
int query(int l, int r) {//下表为l~r中返回最大的数
int len = r - l + 1;
int k = log2(len);
return max(f[l][k], f[r-(1<<k)+1][k]);//前2^k个数,后2^k个数
}
六)提高算法
双端队列BFS-O(n+m)
//求边权为01的最短路, 即经过边权为1的最小边数
bool check(int mid) {//类似堆优化版dijkstra
deque<int> q;
dist[1] = 0;
q.push_back(1);
while(q.size()) {
int u = q.front();
q.pop_front();
if(st[u]) continue;
st[u] = true;
for(int i=h[u]; ~i; i=ne[i]) {
int j = e[i], d = w[i] > mid;
if(dist[u] + d < dist[j]) {
dist[j] = dist[u] + d;
if(!d) q.push_front(j);
else q.push_back(j);
}
return dist[n] <= k;
}
双向BFS
int bfs() {
queue<string> qa, qb;
unordered_map<string,int> da, db;//存到a或b的最短距离
qa.push(A), qb.push(B);
da[A] = 0, db[B] = 0;
while(qa.size() && qb.size()) {
int t;
if(qa.size() <= qb.size()) t = extend(qa,da,db);
else t = extend(qb,db,da);
if(t <= 10) return t;//不超过10层
}
return INF;//还未扩展成功
int extend(queue<string>& q, unordered_map<string,int>& da, unordered_map<string,int>& db,
string a[], string b[]) {
for(int k=0,sk=q.size(); k<sk; ++k) {
auto t = q.front();
q.pop();
//枚举可到达的状态继续搜索
if(da.count(nw)) continue;
if(db.count(nw)) return da[t] + db[nw] + 1;
da[nw] = da[t] + 1;
q.push(nw);
return INF;
}
A*算法-k短路
启发式搜索,bfs队列换优先队列,取(真实距离+估计距离)小的入队
k短路的启发函数:每个点到终点的最短距离(建反边从终点开始dij)
int astar() {
priority_queue<PIII,vector<PIII>,greater<PIII>> heap;
heap.push({dist[S],{0,S}});
while(heap.size()) {
auto t = heap.top();
heap.pop();
int u = t.y.y, distance = t.y.x;
cnt[u]++;
if(cnt[T] == K) return distance;
for(int i = h[u]; ~i; i=ne[i]) {
int j = e[i];
if(cnt[j] < K)
heap.push({distance+w[i]+dist[j], {distance+w[i], j}});
}
}
return -1;
}