EdisonBa 的 XCPC 基础模板
\(\text{Update:}\)
2024.10.23
新增数位 dp 以及两个板子
删除 平衡树,主席树,珂朵莉树
删除 Tarjan (缩点,割点)
历史更新记录
2024.10.14
简要添加了树上背包的板子
2024.10.10
更新了整数二分的板子
添加了二分中位数的板子
添加了二维前缀和板子,二维差分板子
新建 “其他技巧” 大分类,后续可能会往里面加入一些有趣的小 Trick
2024.10.9
添加对顶堆维护中位数
2024.9.7
删了一堆没用的
2024.8.14
发现快速幂的板子是快寄幂,现在添加了对 a 进行取模,防止不必要的 bug 发生
2024.8.13
增加了数学 —— 手写 sqrt
2024.8.11
SPFA 判断负环被卡了,更新了可以 AC 的模板
2024.3.22
发现树链剖分模板中更新最大值出现错误,已更正
2021.9.29
新增 次短路
2021.9.25
新增 割点
2021.9.24
新增 哈希表
2021.9.17
STL 新增 vector
2021.9.16
新增 矩阵加速(数列)
更新了 进阶 dp
2021.9.15
STL 新增 list, multimap, bieset 等
2021.9.10
更新 LCA 为 不含 lg 数组
2021.9.8
更新 FHQ-Treap 为能看的指针代码
更新 线段树 为效率高的指针代码
2021.9.1
重写了 tarjan 并更改模板题为 缩点
新增 Fast-Output
2021.8.29
线性求逆元和线性求欧拉函数
2021.8.20
更新 ST 表 和 Kruskal 为阳间代码
2021.7.24
更新快速幂为zrt的快速幂
2021.7.22
新增 Splay
2021.7.7
新增树链剖分
2021.7.4
更新线段树区间为左闭右闭
新增可持久化数组、可持久化线段树
将“常用 STL 初步”改名为“STL”
新增 STL:rope
2021.7.3
新增 Tarjan 输出强连通分量
2021.7.2
新增图的 BFS 与 DFS 参考代码
2021.6.30
新增扩展 KMP 算法
2021.6.29
新增 KMP 和 LCA
新增 FHQ-Treap
新增 AC自动机
2021.6.28
新增 std::string
新增 Tarjan
2021.6.15
新增 Treap 和 左偏树
2021.6.14
新增 Kruskal 最小生成树
2021.5.30
单源最短路径新增 vector 存图的
2021.5.16
动态规划新增 LCIS (最长公共上升子序列)
2021.5.14
更新了区间dp的模板
2021.4.29
新增了珂朵莉树
快速幂的代码锅了,现已修复
格式化了全部代码!!!
2021.4.28
添加了单调队列,包含 “定长” 和 “不定长”。
2021.4.23
求 gcd 的部分锅了,重新写了下。
2021.4.20
新增线段树的区间加乘,区间修改。
重构了线段树的代码。
更新了部分标题名称。
考试必备
头文件
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define re read()
#define INF 9223372036854775800
#define fr(i, x, y) for (int i = x, _ = y; i <= _; ++i)
#define rp(i, x, y) for (int i = x, _ = y; i >= _; --i)
#define Timeok ((double)clock() / CLOCKS_PER_SEC < MAX_TIME)
const double MAX_TIME = 1.0 - 0.0032;
inline ll read()
{
ll x = 0, f = 0;
char ch = getchar();
while (!isdigit(ch))
f |= (ch == '-'), ch = getchar();
while (isdigit(ch))
x = (x << 1) + (x << 3) + (ch ^= 48), ch = getchar();
return f ? -x : x;
}
void write(ll x)
{
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
write(x / 10);
putchar(x % 10 + 48);
}
inline void W(ll x, char ch)
{
write(x);
putchar(ch);
}
对拍
数据结构
ST表
静态查询区间最值。
ll f[100001][20], lg[100001];
ll n, m, a[100001];
void pre()
{
lg[0] = -1;
for (int i = 1; i <= n; ++i)
lg[i] = lg[i >> 1] + 1;
}
void ST_make()
{
for (int i = 1; i <= n; ++i)
f[i][0] = a[i];
for (int j = 1; j < 22; ++j)
for (int i = 1; i + (1 << j) - 1 <= n; ++i)
f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
}
ll ST_check(ll l, ll r)
{
ll k = lg[r - l + 1];
return max(f[l][k], f[r - (1 << k) + 1][k]);
}
int main()
{
n = read(), m = read();
for (int i = 1; i <= n; ++i)
a[i] = read();
pre();
ST_make();
for (int i = 1; i <= m; ++i)
{
ll l = read(), r = read();
printf("%lld\n", ST_check(l, r));
}
return 0;
}
哈希表
不断插入元素,询问某元素出现次数。
struct node{
int nxt, key, cnt;
} t[N];
ll head[mod + 20], tot;
void add(ll x)
{
for(int i = head[x % mod]; i; i = t[i].nxt) // Already Have
{
if(t[i].key == x)
{
++t[i].cnt;
return;
}
}
t[++tot].key = x;
t[tot].cnt = 1;
t[tot].nxt = head[x % mod];
head[x % mod] = tot;
}
ll check(ll x)
{
for(int i = head[x % mod]; i; i = t[i].nxt)
if(t[i].key == x) return t[i].cnt;
return 0;
}
对顶堆
用于维护中位数。
维护两个 multiset
,分别为 \(s1\) 与 \(s2\)。
\(s1\) 中存小于等于中位数的权值,\(s2\) 中存大于等于中位数的权值,且钦定 \(\text{size}(s1)≥\text{size}(s2),|\text{size}(s1)−\text{size}(s2)|≤1\)。
\(s1\) 中最大的数即为整个集合的中位数。
维护步骤:
- 先在 \(s1\) 中插入极小值,\(s2\) 中插入极大值。
- 对于插入操作,若 \(x\leq \text{max\{s1\}}\),则将 \(x\) 插入 \(s1\),否则插入 \(s2\)。
- 对于删除操作,先查询 \(s1\) 中有无,再查询 \(s2\) 中有无。
- 对于每次插入删除操作,都进行调整:若 \(\text{size}(s1)>\text{size}(s2) + 1\),则不断取出 \(s1\) 的最大值插入 \(s2\);若 \(\text{size}(s2)>\text{size}(s1)\),则不断取出 \(s2\) 的最小值插入 \(s1\)。
- 调整完成后,\(s1\) 中最大的数即为集合的中位数。
multiset<ll> s1;
multiset<ll> s2;
void Init()
{
s1.clear();
s2.clear();
s1.insert(-INF);
s2.insert(INF);
}
void Change()
{
while (s1.size() > s2.size() + 1)
{
ll x = *(--s1.end());
s1.erase(--s1.end());
s2.insert(x);
}
while (s2.size() > s1.size())
{
ll x = *s2.begin();
s2.erase(s2.begin());
s1.insert(x);
}
}
void Insert(ll x)
{
if (x <= *s2.begin())
{
s1.insert(x);
}
else
{
s2.insert(x);
}
Change();
}
void Del(ll x)
{
auto it = s1.lower_bound(x);
if (it != s1.end())
{
s1.erase(it);
}
else
{
it = s2.lower_bound(x);
s2.erase(it);
}
Change();
}
ll Zhong()
{
return *s1.rbegin();
}
单调队列
有一个长为 \(n\) 的序列 \(a\),以及一个大小为 \(k\) 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。
代码1
const int N = 2e6 + 31222;
ll n, q, p;
ll a[N];
deque<ll> q1;
int main()
{
n = re;
ll k = re;
fr(i, 1, n) a[i] = re;
fr(i, 1, n + 1)
{
while (!q1.empty() && i - q1.front() > k)
q1.pop_front();
// dp
if(i > k)
W(a[q1.front()], ' '); // 以 i-1 个元素为开头的 前k个 最大值
while (!q1.empty() and a[q1.back()] < a[i])
q1.pop_back();
q1.push_back(i);
}
return 0;
}
代码2
ll n, k, cnt = 0;
ll ans[2][1000005];
struct node
{
ll sum, id;
};
deque<node> maxq;
deque<node> minq;
int main()
{
n = read();
k = read();
node t;
for (int i = 1; i <= n; ++i)
{
ll x = read();
t.id = i;
t.sum = x;
while (!minq.empty() && x <= minq.back().sum)
minq.pop_back();
while (!maxq.empty() && x >= maxq.back().sum)
maxq.pop_back();
minq.push_back(t);
maxq.push_back(t);
while (i - k >= minq.front().id)
minq.pop_front();
while (i - k >= maxq.front().id)
maxq.pop_front();
if (i >= k)
{
ans[0][++cnt] = minq.front().sum;
ans[1][cnt] = maxq.front().sum;
}
}
for (int i = 1; i <= n - k + 1; ++i)
printf("%lld ", ans[0][i]);
puts("");
for (int i = 1; i <= n - k + 1; ++i)
printf("%lld ", ans[1][i]);
}
最大不定长子段和问题。
在一段长为 \(n\) 的数列中,找出一个长度 \(≤m\) 的子段,使得它的和是最大的。子段长度不能为0。
const ll maxn = 5000005;
#define INF 9223372036854775800
ll sum[maxn], q[maxn];
ll n, m;
int main()
{
n = read();
m = read();
for (int i = 1; i <= n; ++i)
{
ll x = read();
sum[i] = sum[i - 1] + x;
}
ll h = 1, t = 1, ans = -INF;
q[1] = 0;
for (int i = 1; i <= n; ++i)
{
while (h <= t && q[h] < i - m)
h++;
ans = max(ans, sum[i] - sum[q[h]]);
while (h <= t && sum[i] <= sum[q[t]])
t--;
q[++t] = i;
}
printf("%lld\n", ans);
Edison Ba;
}
树状数组
支持单点修改,区间查询。
ll lowbit(ll x)
{
return x & (-x);
}
ll c[500002], n, m;
void add(ll x, ll y) //单点修改
{
for (; x <= n; x += lowbit(x))
c[x] += y;
}
ll sum(ll x) //前缀和
{
ll ans = 0;
for (; x; x -= lowbit(x))
ans += c[x];
return ans;
}
ll ask(ll l, ll r) //区间查询
{
return sum(r) - sum(l - 1);
}
int main()
{
n = read();
m = read();
for (int i = 1; i <= n; ++i) //初始化
{
ll x = read();
add(i, x);
}
for (int i = 1; i <= m; ++i)
{
ll opt = read();
if (opt == 1) //单点修改
{
ll x = read(), k = read();
add(x, k);
}
else if (opt == 2) //区间查询
{
ll x, y;
x = read();
y = read();
ll ans = ask(x, y);
printf("%lld\n", ans);
}
}
return 0;
}
线段树
注:本线段树使用链表形式(指针),每个结点都是左闭右闭区间。
操作为加或乘,最后答案模 \(mod\)。
打两个标记,分别为 add
和 mul
。
ll n, m, mod;
struct node{
ll L, R, add, mul, sum;
node *lc, *rc;
void makeadd(ll k)
{
sum = (sum + k * (R - L + 1)) % mod;
add = (add + k) % mod;
}
void makemul(ll k)
{
sum = (sum * k) % mod;
mul = (mul * k) % mod;
add = (add * k) % mod;
}
void pushup()
{
sum = (lc->sum + rc->sum) % mod;
}
void pushdown()
{
if(mul != 1)
{
lc->makemul(mul);
rc->makemul(mul);
mul = 1;
}
if(add)
{
lc->makeadd(add);
rc->makeadd(add);
add = 0;
}
}
};
const ll N = 1e5 + 4;
ll a[N];
void Build(node *now, ll l, ll r)
{
now->L = l;
now->R = r;
now->add = 0;
now->mul = 1;
now->sum = 0;
if(l < r)
{
ll mid = (l + r) >> 1;
now->lc = new node;
now->rc = new node;
Build(now->lc, l, mid);
Build(now->rc, mid + 1, r);
now->pushup();
}
else
{
now->sum = a[l];
now->lc = now->rc = NULL;
}
}
ll check(node *now, ll l, ll r)
{
if(l <= now->L and now->R <= r)
return now->sum % mod;
now->pushdown();
ll mid = (now->L + now->R) >> 1;
ll ans = 0;
if(l <= mid)
ans = (ans + check(now->lc, l, r)) % mod;
if(mid < r)
ans = (ans + check(now->rc, l , r)) % mod;
return ans % mod;
}
void add(node *now, ll l, ll r, ll k)
{
if(l <= now->L and now->R <= r)
now->makeadd(k);
else
{
now->pushdown();
ll mid = (now->L + now->R) >> 1;
if(l <= mid)
add(now->lc, l, r, k);
if(mid < r)
add(now->rc, l, r, k);
now->pushup();
}
}
void mul(node *now, ll l, ll r, ll k)
{
if(l <= now->L and now->R <= r)
now->makemul(k);
else
{
now->pushdown();
ll mid = (now->L + now->R) >> 1;
if(l <= mid)
mul(now->lc, l, r, k);
if(mid < r)
mul(now->rc, l, r, k);
now->pushup();
}
}
int main()
{
n = read(), m = read(), mod = read();
for(int i = 1; i <= n; ++i)
a[i] = read();
node *root;
root = new node;
Build(root, 1, n);
for(int i = 1; i <= m; ++i)
{
ll opt = read(), l = read(), r = read(), k;
if(opt == 1)
{
k = read();
mul(root, l, r, k);
}
if(opt == 2)
{
k = read();
add(root, l ,r, k);
}
if(opt == 3)
{
printf("%lld\n", check(root, l, r) % mod);
}
}
return 0;
}
树链剖分
\(n\) 个点, \(m\) 个操作数, 根结点为 \(R\), 取模数为 \(mod\)。
输入一颗树。
支持的操作:
- 把 \(x\) 点的点权增加(或修改)\(y\)。
- 将树从 \(x\) 到 \(y\) 结点最短路径上所有节点的值都加上 \(z\)。
- 询问某个节点 \(x\) 到 \(y\) 节点的路径中所有点的点权和 (或maxn)。
- 把 \(x\) 为根结点的子树中所有点的点权都增加 \(y\)。
- 求以 \(x\) 为根节点的子树内所有节点值之和
const ll N = 3e5 + 4;
ll n, m, tot, R, mod;
ll head[N], fa[N], son[N], siz[N], top[N], dep[N], seg[N], rev[N], a[N], lim[N];
struct nodee
{
ll to, nxt;
} t[2 * N];
void add(ll x, ll y)
{
t[++tot].to = y;
t[tot].nxt = head[x];
head[x] = tot;
}
void dfs1(ll u, ll father)
{
siz[u] = 1, fa[u] = father, dep[u] = dep[father] + 1;
for (ll i = head[u]; i; i = t[i].nxt)
{
ll y = t[i].to;
if (siz[y] == 0)
{
dfs1(y, u);
siz[u] += siz[y];
if (siz[y] > siz[son[u]])
{
son[u] = y;
}
}
}
}
void dfs2(ll u)
{
seg[u] = ++seg[0];
rev[seg[0]] = u;
if (son[u])
{
top[son[u]] = top[u];
dfs2(son[u]);
}
for (ll i = head[u]; i; i = t[i].nxt)
{
ll y = t[i].to;
if (y == son[u] || fa[u] == y)
{
continue;
}
dfs2(y);
}
lim[u] = seg[0];
}
struct node
{
ll L, R, sum, tag, maxn;
node *lc, *rc;
};
void pushup(node *now)
{
now->sum = (now->lc->sum + now->rc->sum) % mod;
now->maxn = max(now->lc->maxn, now->rc->maxn);
}
inline void maketag(node *now, ll w)
{
now->tag = (now->tag + w) % mod;
now->sum = (now->sum + (now->R - now->L + 1) * w) % mod;
now->maxn += w;
}
inline void pushdown(node *now)
{
if (now->tag == 0)
return;
maketag(now->lc, now->tag);
maketag(now->rc, now->tag);
now->tag = 0;
}
void build(node *now, ll l, ll r)
{
now->L = l;
now->R = r;
now->tag = 0;
if (l < r)
{
ll mid = (l + r) >> 1;
now->lc = new node;
now->rc = new node;
build(now->lc, l, mid);
build(now->rc, mid + 1, r);
pushup(now);
}
else
{
now->maxn = a[rev[l]];
now->sum = a[rev[l]];
now->lc = now->rc = NULL;
}
}
void change(node *now, ll l, ll r, ll w)
{
if (l <= now->L and now->R <= r)
{
maketag(now, w);
}
else if (!((now->L > r) || (now->R < l)))
{
pushdown(now);
change(now->lc, l, r, w);
change(now->rc, l, r, w);
pushup(now);
}
}
ll check1(node *now, ll l, ll r)
{
if (l <= now->L and now->R <= r)
return now->sum;
if ((now->L > r) || (now->R < l))
return 0;
pushdown(now);
return (check1(now->lc, l, r) + check1(now->rc, l, r)) % mod;
}
ll check2(node *now, ll l, ll r)
{
if (l <= now->L and now->R <= r)
return now->maxn;
if ((now->L > r) || (now->R < l))
return -INF;
pushdown(now);
return max(check2(now->lc, l, r), check2(now->rc, l, r));
}
int main()
{
n = read(), m = read();
R = 1, mod = INF; // R为根结点序号
for (ll i = 1; i <= n; i++)
top[i] = i;
for (ll i = 1; i <= n; i++)
a[i] = read();
for (ll i = 1; i <= n - 1; i++)
{
ll x = read(), y = read();
add(x, y);
add(y, x);
}
dfs1(R, 0);
dfs2(R);
node *root;
root = new node;
build(root, 1, n);
for (int i = 1; i <= m; ++i)
{
ll opt = read(), x = read(), y, z;
// 把 x 点的点权增加 y
// 如果想要修改,更改上面的 maketag
if (opt == 0)
{
y = read();
change(root, seg[x], seg[x], y);
}
// 表示将树从 x 到 y 结点最短路径上所有节点的值都加上 z。
else if (opt == 1)
{
y = read(), z = read();
while (top[x] != top[y])
{
if (dep[top[x]] < dep[top[y]])
swap(x, y);
change(root, seg[top[x]], seg[x], z);
x = fa[top[x]];
}
if(seg[x] > seg[y]) swap(x, y);
change(root, seg[x], seg[y], z);
}
// 询问某个节点 x 到 y 节点的路径中所有点的点权和 (或maxn)
else if (opt == 2)
{
y = read();
ll sum = 0;
// ll maxn = -2147483647;
while (top[x] != top[y])
{
if (dep[top[x]] < dep[top[y]])
swap(x, y);
sum = (sum + check1(root, seg[top[x]], seg[x])) % mod;
// maxn = max(maxn, check2(root, seg[top[x]], seg[x]));
x = fa[top[x]];
}
if (seg[x] > seg[y])
swap(x, y);
sum = (sum + check1(root, seg[x], seg[y])) % mod;
// maxn = max(maxn, check2(root, seg[x], seg[y]));
printf("%lld\n", sum);
}
// 把 x 为根结点的子树中所有点的点权都增加 y
else if (opt == 3)
{
y = read();
change(root, seg[x], lim[x], y);
}
// 求以 x 为根节点的子树内所有节点值之和
else if (opt == 4)
{
ll ans = check1(root, seg[x], lim[x]) % mod;
printf("%lld\n", ans);
}
}
return 0;
}
左偏树
一开始有 \(n\) 个小根堆,每个堆包含且仅包含一个数。接下来需要支持两种操作:
1 x y
:将第 \(x\) 个数和第 \(y\) 个数所在的小根堆合并(若第 \(x\) 或第 \(y\) 个数已经被删除或第 \(x\) 和第 \(y\) 个数在用一个堆内,则无视此操作)。2 x
:输出第 \(x\) 个数所在的堆最小数,并将这个最小数删除(若有多个最小数,优先删除先输入的;若第 \(x\) 个数已经被删除,则输出 \(-1\) 并无视删除操作)。
第一行包含两个正整数 \(n,m\),分别表示一开始小根堆的个数和接下来操作的个数。
第二行包含 \(n\) 个正整数,其中第 \(i\) 个正整数表示第 \(i\) 个小根堆初始时包含且仅包含的数。
接下来 \(m\) 行每行 \(2\) 个或 \(3\) 个正整数,表示一条操作。
输出包含若干行整数,分别依次对应每一个操作 \(2\) 所得的结果。
#define M 150010
#define swap my_swap
#define ls S[x].Son[0]
#define rs S[x].Son[1]
struct Tree
{
ll dis, val, Son[2], rt;
} S[M];
ll N, T, A, B, C, i;
inline ll Merge(ll x, ll y);
ll my_swap(ll &x, ll &y) { x ^= y ^= x ^= y; }
inline ll Get(ll x) { return S[x].rt == x ? x : S[x].rt = Get(S[x].rt); }
inline void Pop(ll x) { S[x].val = -1, S[ls].rt = ls, S[rs].rt = rs, S[x].rt = Merge(ls, rs); }
inline ll Merge(ll x, ll y)
{
if (!x || !y)
return x + y;
if (S[x].val > S[y].val || (S[x].val == S[y].val && x > y))
swap(x, y);
rs = Merge(rs, y);
if (S[ls].dis < S[rs].dis)
swap(ls, rs);
S[ls].rt = S[rs].rt = S[x].rt = x, S[x].dis = S[rs].dis + 1;
return x;
}
int main()
{
N = read(), T = read();
S[0].dis = -1;
for (i = 1; i <= N; ++i)
S[i].rt = i, S[i].val = read();
for (i = 1; i <= T; ++i)
{
A = read(), B = read();
if (A == 1)
{
C = read();
if (S[B].val == -1 || S[C].val == -1)
continue;
ll f1 = Get(B), f2 = Get(C);
if (f1 != f2)
S[f1].rt = S[f2].rt = Merge(f1, f2);
}
else
{
if (S[B].val == -1)
puts("-1");
else
printf("%lld\n", S[Get(B)].val), Pop(Get(B));
}
}
return 0;
}
数学
手写 sqrt
ll Sq(ll n)
{
ll x = n;
ll y = (x + 1) >> 1;
while (y < x) {
x = y;
y = (x + n / x) >> 1;
}
return x;
}
线性筛素数
给定一个整数 \(n\) ,求出 $[2,n] $ 之间的所有素数。
思路:prime
数组存放已经筛出的素数, \(m\) 代表素数个数(也就是说遍历时从 \(1\) 遍历到 \(m\) 即可),v
数组代表有没有被标记,避免重复筛。
int v[maxn], prime[maxn], n, k, t, m;
void primes(int n)
{
memset(v, 0, sizeof(v)); //清空标记数组
m = 0; //质数个数
for (int i = 2; i <= n; i++)
{
if (!v[i]) //未被标记,i为质数
v[i] = i, prime[++m] = i; //记录
for (int j = 1; j <= m; j++)
{
if (prime[j] > v[i] || prime[j] > n / i)
break; //i有更小的质因子,或者超出n的范围
v[i * prime[j]] = prime[j]; //prime[j]为合数 i*prime[j]的最小质因子
}
}
}
int main()
{
scanf("%d", &n);
primes(n);
for (int i = 1; i <= m; ++i)
printf("%d\n", prime[i]);
}
线性求逆元
inv[0] = inv[1] = 1;
for (register int i(2); i <= n; i++)
inv[i] = (1ll * (mod - mod / i) * inv[mod % i]) % mod;
递推求组合数
for(int i = 0; i <= 5010; i ++ )
for(int j = 0; j <= i; j ++ )
if(!j) C[i][j] = 1;
else C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
线性求欧拉函数
int prime[N], phi[N];
bool isprime[N];
void pre()
{
ll cnt = 0;
isprime[1] = 1;
phi[1] = 1;
for (register int i(2); i <= n; ++i)
{
if (!isprime[i])
{
prime[++cnt] = i;
phi[i] = i - 1;
}
for (register int j(1); j <= cnt and i * prime[j] <= n; ++j)
{
isprime[i * prime[j]] = 1;
if (i % prime[j])
phi[i * prime[j]] = phi[i] * phi[prime[j]];
else
{
phi[i * prime[j]] = phi[i] * prime[j];
break;
}
}
}
}
矩阵加速(数列)
\(F_n = \begin{cases} 1 & (n \leq 2)\\ F_{n-1} + F_{n-2} & (n \geq 3 )\end{cases}\)
请你求出 \(F_n \text{ mod } 10^9+7\) 的值。
\(\begin{cases} F_{n+1} = 1 × F_{n} + 1 × F_{n-1} \\ F_n = 1× F_n + 0× F_{n - 1} \end{cases}\)
\( \Rightarrow \left[\begin{array}{ccc} F_{n+1}\\ F_n \end{array}\right] = \left[\begin{array}{ccc} 1 & 1\\ 1 & 0 \end{array}\right] × \left[\begin{array}{ccc} F_n\\ F_{n- 1} \end{array}\right] \)
\( \Rightarrow \left[\begin{array}{ccc} F_{n+1}\\ F_n \end{array}\right] = \left[\begin{array}{ccc} 1 & 1\\ 1 & 0 \end{array}\right] ^ {n - 1} × \left[\begin{array}{ccc} F_2\\ F_1 \end{array}\right] \)
\( \Rightarrow \left[\begin{array}{ccc} F_{n+1}\\ \color{red}{F_n} \end{array}\right] = \left[\begin{array}{ccc} 1 & 1\\ 1 & 0 \end{array}\right] ^ {n - 1} × \left[\begin{array}{ccc} 1\\ 1 \end{array}\right] \)
则初始化矩阵为 \(\left[\begin{array}{ccc}1\\1\end{array}\right]\),快速幂 \(n-1\) 次转移矩阵 \(\left[\begin{array}{ccc} 1 & 1\\ 1 & 0 \end{array}\right]\),得到的 \(Ans_{2, 1}\) 即为答案。
创建
struct STU{
ll m[3][3];
STU(){memset(m, 0, sizeof m);}
STU operator*(const STU &b) const{
STU x;
for(int i = 1; i <= 2; ++i)
for(int j = 1; j <= 2; ++j)
for(int k = 1; k <= 2; ++k)
x.m[i][j] = (x.m[i][j] + m[i][k] * b.m[k][j]) % mod;
return x;
}
} Ans, Base;
初始化
void Pre()
{
Ans.m[1][1] = Ans.m[2][1] = 1;
Base.m[1][1] = Base.m[1][2] = Base.m[2][1] = 1;
}
计算
void ksm(ll b)
{
while(b)
{
if(b & 1)
Ans = Ans * Base;
Base = Base * Base;
b >>= 1;
}
}
int main()
{
ll n = read();
Pre();
ksm(n - 1);
printf("%lld\n", Ans.m[2][1] % mod);
return 0;
}
分治
整数二分
模板
求满足条件的最大值:
while (l < r)
{
mid = (l + r) / 2 + 1;
if (满足条件)
l = mid; // 答案应该变大
else
r = mid - 1;
}
ans = l;
求满足条件的最小值
while (l < r)
{
mid = (l + r) / 2;
if (满足条件)
r = mid; // 答案应该变小
else
l = mid + 1;
}
ans = r;
二分中位数
在一个序列中,小于等于 \(x\) 的元素个数如果大于等于 \(\frac{n+1}{2}\),那么中位数一定小于等于 \(x\)。
Check 的判出条件为小于等于 \(x\) 的元素个数。
const int N = 1e5 + 32;
ll n;
ll a[N];
bool Check(ll x)
{
ll cnt = 0, en = (n + 1) / 2;
fr(i, 1, n)
{
if (a[i] <= x) ++cnt;
if (cnt >= en) return 1;
}
return 0;
}
int main()
{
n = re;
fr(i, 1, n) a[i] = re;
ll l = 0, r = 1e9;
while (l < r)
{
ll mid = (l + r) >> 1;
if (Check(mid)) r = mid;
else l = mid + 1;
}
W(r, '\n');
return 0;
}
/*
输入
4
1 2 3 4
输出:2
输入
5
1 2 3 4 5
输出:3
*/
快速乘法取余
给定三个整数 \(a,n,mod\) ,求 \(a \times n ~\%~mod\) 的值。
inline int mult_mod(int a, int n, int mod)
{
int ans = 0;
while (n > 0)
{
if (n & 1)
ans = (ans + a) % mod;
a = (a + a) % mod;
n >>= 1;
}
return ans;
}
这个东西好像没有必要的样子,貌似只需要 \((a~\%~mod)×(n~\%~mod)~\%~mod\) 即可。
快速幂
给定三个整数 \(a,b,mod\) ,求 \(a^b~\%~mod\) 的值。
ll ksm(ll a, ll b, ll mod)
{
a %= mod;
ll ret = 1 % mod;
while(b)
{
if(b & 1)
ret = ret * a % mod;
b >>= 1;
a = a * a % mod;
}
return ret;
}
LIS
求一个序列的最长上升子序列个数。
本程序采用边读边处理 + 二分法。
ll f[maxn], ans = 1; //注意答案个数初始化为1
int main()
{
ll n = read();
for (int i = 1; i <= n; ++i)
{
int x = read();
if (i == 1)
{
f[1] = x;
continue;
}
int l = 1, r = ans, mid;
while (l <= r)
{
mid = (l + r) >> 1;
if (x <= f[mid])
r = mid - 1;
else
l = mid + 1;
}
f[l] = x;
if (l > ans)
++ans;
}
printf("%lld\n", ans);
return 0;
}
lower_bound
使用前提:数列为有序数列。
①数组内
//查找范围:[ begin , end ) ,左闭右开区间。
*lower_bound(begin, end, num); //返回第一个 >= num 的数的数值
lower_bound(begin, end, num) - begin; // 返回下标
实际操作:
int a[100] = {0, 1, 3, 5, 7, 9, 10};
// 下标:0 1 2 3 4 5 6
int main()
{
int x = lower_bound(a + 1, a + 6 + 1, 6) - a; //输出下标
int y = *lower_bound(a + 1, a + 6 + 1, 6); //输出值
printf("%d %d", x, y);
return 0;
}
输出结果:4 7
②结构体内
结构体内使用 lower_bound 需要重载,下面我们主要对结构体中的 \(a\) 进行操作。
struct node //开结构体
{
int a, id; //定义结构体内的两个变量
node() {}
node(int x, int y) : a(x), id(y) {}
bool operator<(const node t) const //重载
{
return a < t.a;
}
} t[1001];
bool cmp(node x, node y) //快排 cmp 比较函数
{
if (x.a < y.a)
return 1; //结构体内按 a 由小到大排序。
return 0;
}
int main()
{
int n = read(); //数列中数的个数
for (int i = 1; i <= n; ++i)
{
t[i].a = read(); //读入数列
t[i].id = i;
}
sort(t + 1, t + n + 1, cmp); //按小到大排序
int x, xiabiao, ans;
x = read(); //需要查找的数字
xiabiao = lower_bound(t + 1, t + n + 1, node(x, 0)) - t; //这样求下标
ans = (*lower_bound(t + 1, t + n + 1, node(x, 0))).a; //这样求值
printf("%d %d\n", xiabiao, ans);
return 0;
}
输入:
5
20 40 30 10 50
35
输出:
4 40
另:upper_bound 的使用与 lower_bound 的使用类似,只不过是严格大于(>)。
动态规划
二维前缀和
求二维前缀和后,能够实现 \(O(1)\) 求原数组二维区间和,但是不支持修改。
ll n, m, sum2[N][N], c[N][N];
void Sum2_pre()
{
fr(i, 1, n)
fr(j, 1, m)
sum2[i][j] = sum2[i-1][j] + sum2[i][j-1] - sum2[i-1][j-1] + c[i][j];
}
ll Sum2(ll x1, ll y1, ll x2, ll y2)
{
return sum2[x2][y2] + sum2[x1-1][y1-1] - sum2[x1-1][y2] - sum2[x2][y1-1];
}
二维差分
先预处理出差分数组,之后可以实现 \(O(1)\) 原数组二维区间修改。
将差分数组还原为原数组的复杂度是 \(O(n^2)\) 的。
ll n, m, cha2[N][N], c[N][N];
void Cha2_pre() // 预处理差分数组
{
fr(i, 1, n)
fr(j, 1, m)
cha2[i][j] = c[i][j] - c[i][j-1] - c[i-1][j] + c[i-1][j-1];
}
void Cha2(ll x1, ll y1, ll x2, ll y2, ll K) // 原数组二维区间修改
{
cha2[x1][y1] += K;
cha2[x2 + 1][y1] -= K;
cha2[x1][y2 + 1] -= K;
cha2[x2 + 1][y2 + 1] += K;
}
void Cha2_nxt() // 还原差分数组
{
fr(i, 1, n)
fr(j, 1, m)
{
cha2[i][j] = cha2[i-1][j] + cha2[i][j-1] - cha2[i-1][j-1] + cha2[i][j];
c[i][j] = cha2[i][j];
}
}
int main()
{
n = re; m = re;
Cha2_pre();
fr(i, 1, 5)
{
ll a = re, b = re, c = re, d = re, e = re;
Cha2(a, b, c, d, e);
}
Cha2_nxt();
fr(i, 1, n)
{
fr(j, 1, m) W(c[i][j], ' ');
puts("");
}
return 0;
}
输入
10 10
1 1 10 10 1
2 2 9 9 1
3 3 8 8 -1
4 4 7 7 2
5 5 6 6 1
输出
1 1 1 1 1 1 1 1 1 1
1 2 2 2 2 2 2 2 2 1
1 2 1 1 1 1 1 1 2 1
1 2 1 3 3 3 3 1 2 1
1 2 1 3 4 4 3 1 2 1
1 2 1 3 4 4 3 1 2 1
1 2 1 3 3 3 3 1 2 1
1 2 1 1 1 1 1 1 2 1
1 2 2 2 2 2 2 2 2 1
1 1 1 1 1 1 1 1 1 1
基础模型
数字金字塔
f[i][j] = max((f[i][j] + f[i + 1][j]), (f[i][j] + f[i][j + 1]));
LCS
操作对象:两个长度不一定相等的字符串。
string s, t;
int f[maxn][maxn];
int main()
{
cin >> s >> t;
int ls = s.length(), lt = t.length();
for (int i = 1; i <= ls; i++)
for (int j = 1; j <= lt; j++)
{
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
if (s[i - 1] == t[j - 1])
f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
}
cout << f[ls][lt] << endl;
return 0;
}
LCIS
操作对象:两个长度不一定相等的数列。
const int maxn = 1005;
ll n, m, a[maxn], b[maxn], ans;
ll f[maxn][maxn], lcis[maxn][maxn];
int main()
{
n = read();
for (int i = 1; i <= n; ++i)
a[i] = read();
m = read();
for (int i = 1; i <= m; ++i)
b[i] = read();
for (int i = 1; i <= n; ++i)
{
for (int j = 1, k = 0; j <= m; ++j)
{
if (a[i] == b[j])
{
f[i][j] = f[i - 1][k] + 1;
for (int p = 1; p <= f[i - 1][k]; ++p)
lcis[j][p] = lcis[k][p];
lcis[j][f[i][j]] = a[i];
}
else
f[i][j] = f[i - 1][j];
if (b[j] < a[i] && f[i][j] > f[i][k])
k = j;
}
}
for (int i = 1; i <= m; ++i)
if (f[n][i] > f[n][ans])
ans = i;
printf("%lld\n", f[n][ans]);
for (int p = 1; p <= f[n][ans]; ++p)
printf("%lld ", lcis[ans][p]);
puts("");
return 0;
}
基础背包
01背包
背包数量为 \(V\),有 \(n\)件物品,重量为 \(w_i\),价值为 \(c_i\)。求能获得最大价值。
ll V, n, w[10000], c[10000], f[10000];
int main()
{
V = read();
n = read();
for (int i = 1; i <= n; ++i)
{
w[i] = read();
c[i] = read();
}
for (int i = 1; i <= n; ++i)
for (int v = V; v >= w[i]; --v)
{
if (f[v - w[i]] + c[i] > f[v])
f[v] = f[v - w[i]] + c[i];
}
printf("%lld\n", f[V]);
return 0;
}
01-方案数
一种物品只能选一次,组合出固定价值的方案数问题。
例题:ybt1291:数字组合
ll a[21], f[1003], n, t;
int main()
{
n = read();
t = read();
for (int i = 1; i <= n; ++i)
{
a[i] = read();
}
f[0] = 1;
for (int i = 1; i <= n; ++i)
{
for (int j = t; j >= a[i]; --j)
{
f[j] += f[j - a[i]];
}
}
printf("%lld\n", f[t]);
return 0;
}
完全背包
一种物品可以选无限次。
只需要改一下第二层循环的循环顺序就行了。
for (int i = 1; i <= n; ++i)
for (int v = w[i]; v <= V; ++v)
{
if (f[v - w[i]] + c[i] > f[v])
f[v] = f[v - w[i]] + c[i];
}
完全-方案数
一种物品可以选无限次,组合出固定价值的方案数问题。
例题:ybt1293:买书
ll a[5], f[10002], m;
int main()
{
m = read();
a[1] = 10, a[2] = 20, a[3] = 50, a[4] = 100;
f[0] = 1;
for (int i = 1; i <= 4; ++i)
{
for (int j = a[i]; j <= m; ++j)
{
f[j] += f[j - a[i]];
}
}
printf("%lld\n", f[m]);
return 0;
}
混合背包
一种物品可以选 \(p\) 次。
ll w[31], c[31], p[31];
ll f[201], n, m;
int main()
{
m = read();
n = read();
for (int i = 1; i <= n; ++i)
{
w[i] = read();
c[i] = read();
p[i] = read();
}
for (int i = 1; i <= n; ++i)
{
if (p[i] == 0) //完全
{
for (int j = w[i]; j <= m; ++j)
f[j] = max(f[j], f[j - w[i]] + c[i]);
}
else //01和多重
{
for (int j = 1; j <= p[i]; ++j)
{
for (int k = m; k >= w[i]; --k)
{
f[k] = max(f[k], f[k - w[i]] + c[i]);
}
}
}
}
printf("%lld\n", f[m]);
return 0;
}
二维费用背包
再加一重循环,多开一维数组即可。
ll v, u, k;
ll a[1091], b[1021], c[1092];
ll f[101][101];
int main()
{
memset(f, 127, sizeof f);
f[0][0] = 0;
v = read();
u = read();
k = read();
for (int i = 1; i <= k; ++i)
{
a[i] = read();
b[i] = read();
c[i] = read();
}
for (int i = 1; i <= k; ++i)
{
for (int j = v; j >= 0; --j)
{
for (int l = u; l >= 0; --l)
{
ll t1 = j + a[i];
ll t2 = l + b[i];
t1 = min(t1, v);
t2 = min(t2, u);
f[t1][t2] = min(f[t1][t2], f[j][l] + c[i]);
}
}
}
printf("%lld\n", f[v][u]);
return 0;
}
进阶dp
区间 dp
以 f[i][j]
中的 \(i\) 为起点,\(j\) 为终点进行 dp。
核心代码:
for (int l = 2; l <= n; ++l) // 枚举区间长度
{
for (int i = 1; i + l - 1 <= n; ++i)
{
int j = i + l - 1;
for (int k = i; k < j; ++k)
{
dp;
}
}
}
print();
树形 dp
基础
f[u][i]
表示以 \(u\) 为根节点的子树选择 \(i\) 条边,至多保留的苹果数目。
\(f[u][i]=max(f[u][i],f[u][i−j−1]+f[v][j]+e[i].w)\)
$ 1≤i≤min(m,sz[u]),0≤j≤min(sz[v],i−1) $
\(u\) 表示当前节点,\(v\) 是 \(u\) 的一个子节点,\(sz[u]\) 表示 \(u\) 的子树上的边数,\(m\) 就是题目中要求的最多保留边数。
void dfs(ll now, ll fath)
{
for(int i = head[now]; i; i = t[i].nxt)
{
ll y = t[i].to;
if(y == fath) continue;
dfs(y, now);
sz[now] += sz[y] + 1;
for(int j = min(sz[now], m); j >= 1; --j)
for(int k = min(1LL * j - 1, sz[y]); k >= 0; --k)
f[now][j] = max(f[now][j], f[now][j - k - 1] + f[y][k] + t[i].w);
}
}
树上背包
一般 \(f[i][j]\) 表示第 \(i\) 个节点下,选择 \(j\) 个节点的答案。
\(f[x][v]\) 可以由 \(x\) 的所有儿子转移过来。对每个儿子 \(y\) 分别考虑,每次做一次 \(01\) 背包即可。
void dfs(ll x)
{
for(int i = head[x]; i; i = t[i].nxt)
{
ll y = t[i].to;
dfs(y);
}
for(int i = head[x]; i; i = t[i].nxt)
{
ll y = t[i].to;
rp(v, m, 0) // 倒序枚举重量
{
fr(k, 0, v-1) // 枚举 y 的重量有 k 个给了 x
f[x][v] = max(f[x][v], f[x][v-k] + f[y][k]);
}
}
}
数位 dp
本质是记忆化搜索。
\(lim\) 为 \(1\),表示当前位之前都是最大的数,当前位的大小受限制,不是 1~9,是 1~up。
\(zero\) 为 \(0\),表示这一位之前为前导 0。
\(lim\) 的转移:lim && (i == up)
。
\(zero\) 的转移:zero || i
。
例题1
给定 \(a\) 和 \(b\),求在 \([a,b]\) 的所有整数中,\(0 \sim 9\) 各出现了几次。\(1 \leq a \leq b \leq 10^{12}\)。
思路:
从高位到低位枚举,对于每一位,当前位的答案 \(ans\) 初始为 0,然后枚举这一位数字的种类,用更新后的状态来处理答案累加。
当位数为 0 时说明处理完了,返回当前的答案。
处理的过程中开 \(dp\) 数组存储 dfs 时的状态,实现记忆化。
ll n, m, k;
ll num[20], len;
ll f[20][20][2][2];
ll dfs(ll x, ll lim, ll zero, ll sum, ll d)
{
if(x == 0) return sum;
ll ans = 0;
if(f[x][sum][lim][zero] != -1) return f[x][sum][lim][zero];
ll up = 9;
if(lim) up = num[x];
fr(i, 0, up)
{
ans += dfs(x-1, lim && (i == up), zero || i, sum + ((zero || i) && (i == d)), d);
}
return f[x][sum][lim][zero] = ans;
}
ll Calc(ll x, ll d)
{
len = 0;
while(x)
{
num[++len] = x % 10;
x /= 10;
}
memset(f, -1, sizeof(f));
return dfs(len, 1, 0, 0, d);
}
int main()
{
ll l = re, r = re;
fr(i, 0, 9)
W(Calc(r, i) - Calc(l-1, i), ' ');
return 0;
}
例题2
https://codeforces.com/gym/101982/attachments
给定两个整数 \(k\) 和 \(b\),\(1 \leq k \leq 1000\),\(1 \leq b \leq 128\)。
求 \([0,2^b-1]\) 内的所有整数中,为 \(k\) 的倍数的数在二进制表示下 \(1\) 的数量的总和。答案对 \(1e9+9\) 取模。
思路:
以二进制按位考虑。发现对于任意一个二进制数,左移一位,\(1\) 的数量不会被改变。
开三个状态。\(x\) 为当前位数,\(num1\) 为当前 \(1\) 的个数,\(mod\) 为当前这个数除以 \(k\) 的余数。
对于每一位,枚举当前位是 \(0\) 还是 \(1\),转移答案和余数,转移过程比较简单。
ll n, m, k, b;
ll f[130][130][1010];
ll Mo = 1e9 + 9;
ll dfs(ll x, ll num1, ll mod)
{
if(x <= 0)
{
if(mod != 0) return 0;
return num1;
}
if(f[x][num1][mod] != -1) return f[x][num1][mod];
ll ans = 0;
fr(i, 0, 1)
{
ans += dfs(x-1, num1 + i, (mod * 2 + i) % k);
ans %= Mo;
}
f[x][num1][mod] = ans;
return ans;
}
int main()
{
k = re, b = re;
memset(f, -1, sizeof(f));
W(dfs(b, 0, 0), '\n');
return 0;
}
斜率优化 dp
未完成,待补充。
const ll N = 5e4 + 2;
ll n, L;
ll h, t, Q[N];
double sum[N], f[N];
double A(ll i) { return (double)sum[i] + i; }
double B(ll i) { return (double)sum[i] + i + 1.0 * L + 1.0; }
double X(ll i) { return (double)B(i); }
double Y(ll i) { return (double)f[i] + B(i) * B(i); }
double K(ll i, ll j)
{
return ((Y(i) - Y(j))) / ((X(i) - X(j)));
}
void Main()
{
n = read(), L = read();
for (int i = 1; i <= n; ++i)
{
scanf("%lf", &sum[i]);
sum[i] += sum[i - 1];
}
h = t = 1;
for (int i = 1; i <= n; ++i)
{
while (h < t and K(Q[h], Q[h + 1]) < 2 * A(i))
++h;
f[i] = f[Q[h]] + (A(i) - B(Q[h])) * (A(i) - B(Q[h]));
while (h < t and K(i, Q[t - 1]) < K(Q[t - 1], Q[t]))
--t;
Q[++t] = i;
}
printf("%.0lf\n", f[n]);
}
图论
前置
链式前向星存图
ll head[maxn];
struct node
{
ll nxt, to, w;
} t[maxn];
void add(const ll u, const ll v, const ll w)
{
t[++tot].to = v;
t[tot].w = w;
t[tot].nxt = head[u];
head[u] = tot;
}
vector 存图
struct node{
ll to, w;
};
vector<node> t[maxn];
void add(const int u, const int v, const int w)
{
t[u].push_back((node){v, w});
}
图的遍历
存图形式采用 \(\text{vector}\) 存图
ans
存储遍历顺序。
DFS
void dfs(ll s)
{
vis[s] = 1;
ans.push_back(s);
for (int i = 0; i < t[s].size(); ++i)
if (!vis[t[s][i]])
dfs(t[s][i]);
}
BFS
void bfs(ll s)
{
queue<ll> q;
q.push(s);
vis[s] = 1;
while (!q.empty())
{
ll x = q.front();
ans.push_back(x);
q.pop();
for (ll i = 0; i < t[x].size(); ++i)
{
if (!vis[t[x][i]])
{
q.push(t[x][i]);
vis[t[x][i]] = 1;
}
}
}
}
Dijkstra 最短路
求单源 \(s\) 到任意一点的最短路径。最短路径保存在数组 dis
中。
链式前向星
#include <queue>
priority_queue<pair<ll, ll>> q;
void dijkstra(int s)
{
memset(dis, 0x3f, sizeof(dis)); //初始边无限大
memset(vis, 0, sizeof(vis)); //结点初始均为访问
dis[s] = 0; //起点到自己距离为0
q.push(make_pair(0, s)); //起点进队
while (!q.empty())
{
x = q.top().second;
q.pop(); //初始结点入队
if (vis[x])
continue; //如果走过,直接跳过
vis[x] = 1; //标记已访问
for (ll i = head[x]; i != -1; i = t[i].nxt)
{
ll y = t[i].to, z = t[i].w;
if (dis[y] > dis[x] + z)
{
dis[y] = dis[x] + z; //更新起点到y最短路
q.push(make_pair(-dis[y], y)); //d[y]相反数入队,转小根堆
}
}
}
}
int main()
{
for (int i = 1; i <= n; ++i)
head[i] = -1;
...
}
//后面省略
vector
void dj(int s)
{
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof vis);
dis[s] = 0;
q.push(make_pair(0, s));
while (!q.empty())
{
ll x = q.top().second;
q.pop();
if (vis[x])
continue;
vis[x] = 1;
for (int i = 0; i < t[x].size(); ++i)
{
ll y = t[x][i].to, z = t[x][i].w;
if (dis[y] > dis[x] + z)
{
dis[y] = dis[x] + z;
q.push(make_pair(-dis[y], y));
}
}
}
}
SPFA
SPFA能处理负边权,可以判断负环。也可以求最长路。
最短路
#include <queue>
queue<int> q;
void SPFA(int s)
{
fill(dis + 1, dis + 1 + n, 2147483647); //初始边无限大
memset(vis, 0, sizeof vis);
dis[s] = 0;
q.push(s);
vis[s] = 1;
while (!q.empty())
{
int x = q.front();
q.pop();
vis[x] = 0;
for (int i = head[x]; i != -1; i = t[i].nxt)
{
int y = t[i].to, z = t[i].w;
if (dis[y] > dis[x] + z)
{
dis[y] = dis[x] + z;
if (vis[y] == 0)
{
q.push(y);
vis[y] = 1;
}
}
}
}
}
最长路
可依据最短路代码进行修改。
1. `dis` 数组赋初值时,如果没有负边权就赋 $-1$ ,如果有负边权就赋无限小。
- 把
dis[y]>dis[x]+z
中的>
改成<
。
判断负环
可在最短路代码基础上进行修改。需新加入一个数组 cnt
,专门记录负环。
补充代码:
ll cnt[maxn]; //专门记录负环
void SPFA().....if (dis[y] > dis[x] + z)
{
dis[y] = dis[x] + z;
cnt[y] = cnt[x] + 1;
if (cnt[y] >= n) //出现超过n次表示就有负环
{
ans = 1; //ans=1代表有负环。
return;
}
if (vis[y] == 0)
{
q.push(y);
vis[y] = 1;
}
}
Floyd 全源最短路
inline void floyd()
{
for (k = 1; k <= n; k++)
for (i = 1; i <= n; i++)
for (j = 1; j <= n; j++)
if (e[i][j] > e[i][k] + e[k][j])
e[i][j] = e[i][k] + e[k][j];
}
次短路
严格次短路
ll dis[N][2];
bool vis[N];
queue<ll> q;
void SPFA(ll s)
{
memset(dis, 0x3f, sizeof dis);
q.push(s);
vis[s] = 1;
dis[s][0] = 0;
while(!q.empty())
{
ll x = q.front();
q.pop();
vis[x] = 0;
for(int i = head[x]; i; i= t[i].nxt)
{
ll y = t[i].to, z = t[i].w;
if(dis[y][0] > dis[x][0] + z)
{
dis[y][1] = dis[y][0];
dis[y][0] = dis[x][0] + z;
if(vis[y] == 0)
vis[y] = 1, q.push(y);
}
if(dis[y][1] > dis[x][0] + z and dis[x][0] + z > dis[y][0])
{
dis[y][1] = dis[x][0] + z;
if(vis[y] == 0)
vis[y] = 1, q.push(y);
}
if(dis[y][1] > dis[x][1] + z)
{
dis[y][1] = dis[x][1] + z;
if(vis[y] == 0)
vis[y] = 1, q.push(y);
}
}
}
}
非严格次短路
queue<ll> q;
ll fr[N], fro[N], dis[N];
bool vis[N];
void SPFA(ll s, ll u, ll to)
{
for (int i = 1; i <= n; ++i)
dis[i] = INF, vis[i] = 0;
dis[s] = 0;
vis[s] = 1;
q.push(s);
while (!q.empty())
{
ll x = q.front();
q.pop();
vis[x] = 0;
for (int i = head[x]; i; i = t[i].nxt)
{
ll y = t[i].to, z = t[i].w;
if ((x == u and y == to) || (x == to and y == u))
continue;
if (dis[y] > dis[x] + z)
{
dis[y] = dis[x] + z;
fr[y] = x;
if (vis[y] == 0)
vis[y] = 1, q.push(y);
}
}
}
}
int main()
{
存边;
SPFA(1, 0, 0);
ll now = n, ans = INF;
for (int i = 1; i <= n; ++i)
fro[i] = fr[i];
while (fro[now])
{
SPFA(1, now, fro[now]);
ans = min(ans, dis[n]);
now = fro[now];
}
W((ans == INF) ? -1 : ans, '\n');
return 0;
}
并查集
\(n\) 代表元素个数,\(m\) 为操作数。
\(opt=1\) 时,合并集合 \(a,b\) ;\(opt=2\) 时,如果 \(a,b\) 在同一集合,输出 Y
否则输出 N
。
int find(int k)
{
if (f[k] == k)
return k;
return f[k] = find(f[k]);
}
int main()
{
n = read();
m = read();
for (int i = 1; i <= n; ++i)
f[i] = i; //初始化自己的老大是自己
for (int i = 1; i <= m; ++i)
{
int opt, a, b;
opt = read();
a = read();
b = read();
if (opt == 1)
f[find(a)] = find(b);
else
{
if (find(a) == find(b))
printf("Y\n");
else
printf("N\n");
}
}
return 0;
}
LCA
邻接表存图。
struct node{...};
void add(...){}
ll dep[500010], fa[500010][25];
ll head[500010], tot;
ll n, m, s;
ll dep[N], fa[N][23];
void dfs(ll now, ll fath)
{
dep[now] = dep[fath] + 1;
fa[now][0] = fath;
for(int i = 1; i <= 22; ++i)
fa[now][i] = fa[fa[now][i - 1]][i - 1];
for(int i = head[now]; i; i = t[i].nxt)
if(t[i].to != fath)
dfs(t[i].to, now);
}
ll LCA(ll x, ll y)
{
if(dep[x] < dep[y]) swap(x, y);
for(int k = 22; k >= 0; --k)
if(dep[fa[x][k]] >= dep[y])
x = fa[x][k];
if(x == y) return x;
for(int k = 22; k >= 0; --k)
if(fa[x][k] != fa[y][k])
x = fa[x][k], y = fa[y][k];
return fa[x][0];
}
int main()
{
n = read();
m = read();
s = read();
for (int i = 1; i <= n; ++i)
lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
for (int i = 1; i < n; ++i)
{
ll x = read(), y = read();
add(x, y);
add(y, x);
}
dfs(s, 0);
for (int i = 1; i <= m; ++i)
{
ll x = read(), y = read();
printf("%lld\n", LCA(x, y));
}
return 0;
}
最小生成树
Kruskal
前置:并查集
const ll N = 2e5 + 3;
ll n, m, fa[N], ans;
struct node{
ll x, y, w;
}t[N];
bool cmp(node a, node b)
{
return a.w < b.w;
}
ll F(ll x)
{
if(fa[x] == x) return x;
return fa[x] = F(fa[x]);
}
int main()
{
n = read(), m = read();
for(int i = 1; i <= m; ++i)
t[i].x = read(), t[i].y = read(), t[i].w = read();
sort(t + 1, t + m + 1, cmp);
for(int i = 1; i <= n; ++i)
fa[i] = i;
for(int i = 1; i <= m; ++i)
{
if(F(t[i].x) == F(t[i].y)) continue;
fa[F(t[i].x)] = F(t[i].y);
ans += t[i].w;
}
for(int i = 2; i <= n; ++i)
{
if(F(i) != F(1)) // if Lian Tong
{
puts("orz");
return 0;
}
}
printf("%lld\n", ans);
return 0;
}
Prim
int ans, cnt, now = 1; //Prim
void prim()
{
for (int i = 2; i <= n; ++i)
dis[i] = MAXN;
for (int i = head[1]; i; i = t[i].nxt)
dis[t[i].to] = min(dis[t[i].to], t[i].w);
while (++cnt < n)
{
int minn = MAXN;
vis[now] = 1;
for (int i = 1; i <= n; ++i)
{
if (vis[i])
continue;
if (minn > dis[i])
{
minn = dis[i];
now = i;
}
}
ans += minn;
for (int i = head[now]; i; i = t[i].nxt)
{
int y = t[i].to, z = t[i].w;
if (vis[y])
continue;
if (dis[y] > z)
{
dis[y] = z;
}
}
}
}
拓扑排序
ll ans[100] ,cnt; //拓扑序列及其元素个数
ll deg[100]; //所有点的入度
void topsort()
{
queue<ll> q;
for (int i = 1; i <= n; ++i)
if (deg[i] == 0) //寻找最开始入度就为0的点
q.push(i); //入队
while (!q.empty())
{
ll x = q.front();
q.pop();
ans[++cnt] = x; //把队首的元素放进拓扑序列
for (int i = head[x]; i; i = t[i].nxt)
{
ll y = t[i].to; //寻找邻边
if (--deg[y] == 0) //邻边的入度-1,并且判断减后的入度是否为0
q.push(y); //如果为0就入队
}
}
}
int main()
{
n = read();
m = read(); //点,边
for (int i = 1; i <= m; ++i)
{
ll x = read(), y = read();
add(x, y);
deg[v]++; //入度++
}
topsort(); //拓扑排序
if (cnt < n) //拓扑序列的元素个数小于点数,说明有环
puts("有环");
else
puts("无环");
for (int i = 1; i <= cnt; ++i)
printf("%lld ", ans[i]); //输出拓扑序列
return 0;
}
字符串
快速读入
可以根据题目描述自行修改。
void Init()
{
char ch;
ch = getchar();
while (ch < 'A' || ch > 'Z')
ch = getchar();
while (ch >= 'A' && ch <= 'Z')
{
A[++lena] = ch;
ch = getchar();
}
while (ch < 'A' || ch > 'Z')
ch = getchar();
while (ch >= 'A' && ch <= 'Z')
{
B[++lenb] = ch;
ch = getchar();
}
}
KMP
\(A\) 为大串,\(B\) 为小串。
求 \(next\) 数组
void make_nxt()
{
j = 0;
for (int i = 2; i <= lenb; ++i)
{
while (B[i] != B[j + 1] and j)
j = nxt[j];
if (B[i] == B[j + 1])
++j;
nxt[i] = j;
}
}
匹配
void check()
{
j = 0;
for (int i = 1; i <= lena; i++)
{
while (A[i] != B[j + 1] and j)
j = nxt[j];
if (A[i] == B[j + 1])
++j;
if (j == lenb)
{
printf("%lld\n", i - lenb + 1);
j = nxt[j];
}
}
}
AC自动机
模板1
判断多个模式串 \(s_i\) 是否在样本串 \(t\) 里出现过。
const int N = 1000005;
int n, len, tot, ch[N][30], End[N], fail[N];
char s[N];
queue<int> q;
void insert()
{
len = strlen(s + 1);
int now = 0;
for (int i = 1; i <= len; i++)
{
int c = s[i] - 'a';
if (!ch[now][c])
ch[now][c] = ++tot;
now = ch[now][c];
}
End[now]++;
}
void make_AC()
{
for (int i = 0; i < 26; i++)
{
if (ch[0][i])
{
fail[ch[0][i]] = 0; //特殊处理,避免跳回自己
q.push(ch[0][i]);
}
}
while (!q.empty())
{
int x = q.front();
q.pop();
for (int i = 0; i < 26; i++)
{
if (ch[x][i])
{
fail[ch[x][i]] = ch[fail[x]][i];
q.push(ch[x][i]);
}
else
ch[x][i] = ch[fail[x]][i];
}
}
}
int check_tot()
{
len = strlen(s + 1);
int now = 0, ans = 0;
for (int i = 1; i <= len; i++)
{
now = ch[now][s[i] - 'a'];
for (int t = now; t > 0 && End[t] != -1; t = fail[t])
{
ans += End[t];
End[t] = -1;
}
}
return ans;
}
int main()
{
n = read();
for (int i = 1; i <= n; i++)
{
scanf("%s", s + 1);
insert();
}
make_AC();
scanf("%s", s + 1);
printf("%d\n", check_tot());
return 0;
}
模板2
输出模式串最多出现的次数,输出出现次数最多的模式串。
const int N = 1000005;
int n, len, cnt, tot, maxn;
int ch[N][30], End[N], fail[N];
int num[210], sum[210];
char t[N], s[210][210];
queue<int> q;
void C()
{
while (q.size())
q.pop();
maxn = 0;
cnt = 0;
tot = 0;
memset(ch, 0, sizeof(ch));
memset(sum, 0, sizeof(sum));
memset(End, 0, sizeof(End));
memset(ch, 0, sizeof(ch));
}
void insert(ll k)
{
len = strlen(s[k] + 1);
int now = 0;
for (int i = 1; i <= len; ++i)
{
int c = s[k][i] - 'a';
if (!ch[now][c])
ch[now][c] = ++tot;
now = ch[now][c];
}
End[now] = k;
}
void make_AC()
{
for (int i = 0; i < 26; i++)
{
if (ch[0][i])
{
fail[ch[0][i]] = 0; //特殊处理,避免跳回自己
q.push(ch[0][i]);
}
}
while (!q.empty())
{
int x = q.front();
q.pop();
for (int i = 0; i < 26; i++)
{
if (ch[x][i])
{
fail[ch[x][i]] = ch[fail[x]][i];
q.push(ch[x][i]);
}
else
ch[x][i] = ch[fail[x]][i];
}
}
}
void check()
{
len = strlen(t + 1);
int now = 0;
for (int i = 1; i <= len; i++)
{
now = ch[now][t[i] - 'a'];
for (int t = now; t > 0; t = fail[t])
{
++sum[End[t]];
}
}
}
int main()
{
while (1)
{
n = read();
if (n == 0)
return 0;
for (int i = 1; i <= n; ++i)
{
scanf("%s", s[i] + 1);
insert(i);
}
make_AC();
scanf("%s", t + 1);
check();
for (int i = 1; i <= n; ++i)
{
if (sum[i] > maxn)
{
maxn = sum[i];
cnt = 1;
num[cnt] = i;
}
else if (sum[i] == maxn)
{
num[++cnt] = i;
}
}
printf("%d\n", maxn);
for (int i = 1; i <= cnt; i++)
{
len = strlen(s[num[i]] + 1);
for (int j = 1; j <= len; j++)
{
printf("%c", s[num[i]][j]);
}
puts("");
}
C();
}
return 0;
}
模板3
求出多个模式串 \(s_i\) 是否在样本串 \(t\) 里出现的次数。
const int N = 2000005;
int n, len, cnt, tot, maxn;
int ch[N][30], End[N], fail[N], in[N], ans[N];
int num[210], sum[210], Map[N], f[N];
char t[N], t2[N];
queue<int> q, q2;
vector<char> s[N];
void insert(int k)
{
int now = 0;
for (int i = 0; i < s[k].size(); i++)
{
int c = s[k][i] - 'a';
if (!ch[now][c])
ch[now][c] = ++tot;
now = ch[now][c];
}
if (!End[now])
End[now] = k;
Map[k] = End[now];
}
void make_AC()
{
for (int i = 0; i < 26; i++)
{
if (ch[0][i])
{
fail[ch[0][i]] = 0;
q.push(ch[0][i]);
}
}
while (!q.empty())
{
int x = q.front();
q.pop();
for (int i = 0; i < 26; i++)
{
if (ch[x][i])
{
fail[ch[x][i]] = ch[fail[x]][i];
in[fail[ch[x][i]]]++;
q.push(ch[x][i]);
}
else
ch[x][i] = ch[fail[x]][i];
}
}
}
void check()
{
int now = 0, len = strlen(t + 1);
for (int i = 1; i <= len; i++)
{
int c = t[i] - 'a';
now = ch[now][c];
f[now]++;
}
}
void top()
{
for (int i = 1; i <= tot; i++)
{
if (in[i] == 0)
q2.push(i);
}
while (!q2.empty())
{
int x = q2.front();
q2.pop();
in[fail[x]]--;
ans[End[x]] += f[x];
f[fail[x]] += f[x];
if (in[fail[x]] == 0)
{
q2.push(fail[x]);
}
}
}
int main()
{
n = read();
for (int i = 1; i <= n; ++i)
{
scanf("%s", t2 + 1);
len = strlen(t2 + 1);
for (int j = 1; j <= len; ++j)
s[i].push_back(t2[j]);
insert(i);
}
make_AC();
scanf("%s", t + 1);
check();
top();
for (int i = 1; i <= n; i++)
printf("%d\n", ans[Map[i]]);
return 0;
}
扩展 KMP
对于一个长度为 \(n\) 的字符串 \(s\),定义函数 \(z[i]\) 表示 \(s\) 和 \(s[i,n-1]\) (即以 \(s[i]\) 开头的后缀)的最长公共前缀(LCP)的长度。\(z\) 被称作为 \(s\) 的 Z 函数。特别地,\(z[0]=[0]\)。
本代码中 \(z[0]=|B|\)。
const int MAXN = 2e7 + 5;
char B[MAXN], A[MAXN];
int lenb, lena, z[MAXN], ext[MAXN];
void getZ() //求 z 数组
{
z[0] = lenb;
int now = 0;
while (now + 1 < lenb && B[now] == B[now + 1])
now++;
z[1] = now;
int p0 = 1;
for (int i = 2; i < lenb; ++i)
{
if (i + z[i - p0] < p0 + z[p0])
{
z[i] = z[i - p0]; //第一种情况
}
else
{
now = p0 + z[p0] - i;
now = max(now, 0);
while (now + i < lenb && B[now] == B[now + i])
now++;
z[i] = now;
p0 = i;
}
}
}
void exkmp() //求 B 与 A 的每一个后缀的 LCP 长度数组
{
int now = 0;
while (now < lenb && now < lena && B[now] == A[now])
now++;
ext[0] = now;
int p0 = 0;
for (int i = 1; i < lena; ++i)
{
if (i + z[i - p0] < p0 + ext[p0])
ext[i] = z[i - p0];
else
{
now = p0 + ext[p0] - i;
now = max(now, 0);
while (now < lenb && now + i < lena && B[now] == A[now + i])
now++;
ext[i] = now;
p0 = i;
}
}
}
int main()
{
scanf("%s%s", A, B);
lena = strlen(A); //文本串
lenb = strlen(B); //模式串
getZ();
exkmp();
得到 z[], p[];
}
STL
string
#include<cstring>
string s1,s2;
s1 + s2; // 将两个字符串拼接
[cur]; // 访问下标
s1.size(); // 返回字符串长度
s1.append(s2); // 将 s2 添加到 s1 末尾
s1.replace(pos, n, s2); // 删除从 pos 开始的 n 个字符,然后在 pos 处插入串 s
s1.erase(pos, n); // 删除从 pos 开始的 n 个字符
s1.insert(pos, s2); // 在 pos 位置插入字符串 s
s1.substr(start, len); // 从 start 截取一个长度为 len 的字符串
s1.find(char,st = 0); // 查找并返回从 start 开始的字符 ch 的位置
s1.rfind(ch); //从末尾开始,查找并返回第一个找到的字符 ch 的位置
// 找不到则返回 -1
queue
先进先出
#include<queue>
queue<int> q;
// priority_queue<int> q(从大到小排序);
q.empty(); // 判断队列是否为空
q.size(); // 返回队列长度
q.push(item); // 对于 queue,在队尾压入一个新元素
// 对于 priority_queue,在基于优先级的适当位置插入新元素
q.pop(); // 弹出队首元素
// queue only:
q.front(); //返回队首元素的值,但不删除该元素
q.back(); //返回队尾元素的值,但不删除该元素
// priority_queue only:
q.top(); //返回具有最高优先级的元素值,但不删除该元素
stack
先进后出
#include<set>
stack<int> s;
stack<int, vector<int> > stk; // 覆盖基础容器类型,使用vector实现stk
s.empty(); // 判断 stack 是否为空,为空返回 true,否则返回 false
s.size(); // 返回 stack 中元素的个数
s.pop(); // 删除栈顶元素,但不返回其值
s.top(); // 返回栈顶元素的值,但不删除此元素
s.push(item); // 在栈顶压入新元素 item
vector
#include <vector>
vector<int> v;
/* 操作 */
v.clear(); // 清空
v.swap(v1); // 和另一个 vector 进行交换,常数复杂度
v.push_back(k); // 从末尾插入
v.emplace_back(k); // 从末尾插入,更快
v.pop_back(); // 弹出末尾元素
v.insert(v.begin() + pos, k); // 在下标为 pos 之前插入 k
v.emplace(it, k); // 同上面的 insert,更快,但只能插入一个元素
v.insert(it, n, k); // 在 it 之前插入 n 个 k
v.insert(it, v1.begin(), v1.end()); // 在 it 之前插入另一个 vector 中的一段(左闭右开)
v.insert(it, {1, 2, 3, 4, 5}); // 显而易见
v.erase(it); // 删除 it 指的元素,合并左右部分
v.erase(it, it2); // 删除 [it, it2) 的元素,合并左右部分
/* 查询 */
v[k]; // 访问第 k 个元素
v.front(); // 返回首元素
v.back(); // 返回尾元素
v.begin(); // 返回首元素的迭代器
v.end(); // 返回数组尾端占位符的迭代器(空)
v.empty(); // 返回 vector 是否为空
v.size(); // 返回 vector 元素个数
/* 遍历 */
for(int i = 0; i < v.size(); ++i)
v[i];
list
链表
#include <list>
list<ll> l, l2;
list<ll>::iterator it;
/* 操作 */
l.clear(); // 清空
l.insert(it, 0);// 在迭代器前面插入一个元素,迭代器指向原来的元素
l.erase(it); // 删除迭代器指向的元素
l.remove(0); // 在 list 删除某一种元素(全删)
l.push_back(0); // 在 list 的末尾添加一个元素
l.push_front(0);// 在 list 的头部添加一个元素
l.pop_back(); // 删除最后一个元素
l.pop_front(); // 删除第一个元素
l.merge(l2); // 合并两个 list
l.reverse(); // 把 list 的元素倒转
l.sort(); // 给 list 排序
l.swap(l2); // 交换两个 list
l.unique(); // 删除 list 中重复的元素
/* 查询 */
l.begin(); // 返回指向第一个元素的迭代器
l.end(); // 返回末尾的迭代器
l.front(); // 返回第一个元素
l.back(); // 返回最后一个元素
l.empty(); // 如果 list 是空的则返回 1
l.size(); // 返回 list 中的元素个数
/* 遍历 */
for(it = l.begin(); it != l.end(); ++it)
*it;
set
自动从小到大排序,自动去重。
set<int> s;
// multiset<int> s (不去重)
set<int>::const_iterator iter; // 迭代器
s.insert(); // 插入
s.erase(); // 若参数为元素值,则删除所有该元素值
// 若参数为迭代器,则删除该迭代器指向的值
s.empty(); // 判断 set 是否为空,为空返回 true,否则返回 false
s.count(); // 返回某个值元素的个数
s.clear(); // 清除所有元素
s.find(); // 查找某元素,找到则返回其迭代器,否则返回 s.end()
s.begin(); // 返回指向第一个元素的迭代器
--s.end(); // 返回指向最后一个元素的迭代器
*s.begin(); // 返回指向第一个元素的值
*--s.end(); // 返回指向最后一个元素的值
// 区间形式为 [ begin , end ) ,所以 end 要自减
s.size(); // 返回集合中元素的个数
*s.lower_bound(k); // 返回第一个大于等于k的元素值
*s.upper_bound(k); // 返回第一个大于k的元素值 (后继)
// 如果没有符合条件的值,则输出 s.size()
/* 遍历 */
for(iter = s.begin() ; iter != s.end() ; ++iter)
*iter; // 使用迭代器遍历
unordered_set
不排序,\(O(1)\) 查找。
#include <unordered_set>
// #include <unordered_multiset>
unordered_set<ll> s;
// unordered_multiser<ll> s;
s.insert(); // 在开头插入某元素
s.find(); // 查找,返回迭代器
s.count(); // 返回某个值元素的个数
/* 遍历 */
for(iter = s.begin() ; iter != s.end() ; ++iter)
*iter;
// 注意:新插入的元素遍历时先被扫到。
map
#include<map>
map<string, int> m;// string 是 key,int 是 value。
/* 基本操作 */
m.size(); // 输出元素个数
m.empty(); // 如果 map 为空则返回 true
m.clear(); // 删除所有元素
/* 插入 */
m["AKIOI"] = 10;
m.insert(make_pair("AKIOI", 10));
/* 修改 */
m["AKIOI"] = 10;
m.find("AKIOI")->second = ...;
/* 查找 */
m["AKIOI"];
m.find("AKIOI")->second;
/* 遍历 */
它会按照 key 排序
for(auto it = mp.begin(); it != mp.end(); ++it)
cout << it->second << ' ';
multimap
#include <map>
multimap<int, string> mp; // 可重
mp.size(), mp.empty(), mp.clear(); // 常规操作
mp.count(k) // 找 key 为 k 的个数
mp.find(k) // 返回第一个插入的 k 的迭代器
mp.erase(k) // 删除所有键值为 k 的元素
/* 插入 */
mp.insert(make_pair(int, string)); // 只能用 make_pair 构造键值对
/* 修改 */
m.find(k)->second = ...; // 修改第一个插入的 key 为 k 的
for(auto it = mp.find(k); it->first == k; ++it) // 修改 key 为 k,值为自己选的
if(it->second == "I") it->second = "Love";
/* 查找 */
mp.find(k)->second;
/* 遍历 */
for(auto it = mp.begin(); it != mp.end(); ++it)
cout << it->second << ' ';
unordered_map
#include<unordered_map>
用法:与 map 差别不大。
优点:因为内部实现了哈希表,因此其查找速度非常的快
缺点:哈希表的建立比较耗费时间
适用处:对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用 unordered_map
bitset
bitset<100> b;
bitset<100> f;
b = 10, f = 11;
b[0]; // 访问某一位
/* 相同大小的 bitset 可以进行运算符操作 */
/* == != & &= | |= ^ ^= ~ << <<= >> >>= */
b.count(); // 返回 1 的数量
b.size(); // 返回 bitset 的大小
b.any(); // 若有 1, 返回 1
b.none(); // 若无 1, 返回 1
b.all(); // 若全是 1, 返回 1
b.set(); // 全部设为 1
b.set(0, 0);// 将第 pos 位设为 k
b.reset(); // 全部设为 0
b.flip(); // 翻转每一位
b.flip(0); // 翻转某一位
rope
rope
的复制操作是 \(O(\log n)\) 的,可以较轻松地实现可持久化。
想要使用 rope
,需要在头文件中加入两行:
#include <ext/rope>
using namespace __gnu_cxx;
定义字符串类型的 rope
,叫做 crope
,要这样定义:
crope a;
支持的操作:
a.push_back(x); // 在 a 的末尾添加字符串 x
a.insert(k, x); // 在 a 的第 k 个字符后加入字符串 x
a.erase(k, x); // 在 a 的第 k 个字符后删除 x 个字符
a.replace(k, x); // 将 a 的第 k 个字符后 x 的长度个字符删除,并插入 x
a.substr(k, x); // 获取 a 的第 k 个字符后的长度为 x 的字符串
a.at(k); // 获取 a 的第 k 个字符(从 0 开始)
位运算
二进制状态压缩
位运算操作
二进制状态压缩,是指将一个长度为 m 的 bool 数组用一个 m 位二进制整数表示并存储的方法。利用下列位运算操作可以实现原 bool 数组中对应下标元素的存取。
操作 | 运算 |
---|---|
取出 n 的第 k 位 | ( n >> k ) & 1 |
取出 n 的后 k 位( 0 ~ k-1 位) | n & ( ( 1 << k ) - 1 ) |
将第 k 位取反,赋值到 n | n = n xor ( 1 << k ) |
将第 k 位变为 1,赋值到 n | n |= ( 1 << k ) |
将第 k 位变为 0,赋值到 n | n &= ( ~ ( 1 << k ) ) |
这种方法运算简便,比暴力从十进制算出二进制的每一位节省了大量的时间和空间。
推荐题目:CSP-S2020 动物园
运算符优先级
从高到低排列:
加减 | 移位 | 比较大小 | 按位与 | 按位异或 | 按位或 |
---|---|---|---|---|---|
+ , - | << , >> | > , < , == , != | & | xor ( C++ ^ ) | | |
如果不确定优先级,则可以加一些括号,以保证运算顺序的正确性。
\(lowbit\) 计算
lowbit(n)
定义为非负整数 n 在二进制表示下 “最低位的 1 及其后边所有的 0 构成的数值”。
推导
设 \(n>0\) ,\(n\) 的第 \(k\) 位是 \(1\),第 \(0\) ~ \(k-1\) 位都是 \(0\)。
为了实现 lowbit 运算,先把 \(n\) 取反,此时第 \(k\) 位变为 0, 第 \(0\) ~ \(k-1\) 位都是 1。再令 \(n=n+1\) ,此时因为要进位,第 k 位变为 1 ,第 \(0\) ~ \(k-1\) 位都是 0。
在上面的取反加 1 操作后,\(n\) 的第 \(k+1\) 到最高位恰好与原来相反,所以 \(n ~\& ~(\sim n+1)\) 仅有第 \(k\) 位为 1,其余位都是 0。而在补码表示下,\(\sim n = -1-n\),因此:
Code
配合 Hash 的 \(\log\) 运算,可以使复杂度降低。
const maxn = 1 << 20;
int H[maxn];
for(int i=0; i<=20; ++i)
H[1 << i] = i; //预处理
while(cin >> n) //对多组数据进行求解
{
while(n > 0)
{
cout << H[n & -n] << ' ';
n -= n & -n;
}
puts("");
}
内置函数
下面这些仙术可以高效计算 lowbit 以及二进制中 1 的个数。但是,部分竞赛禁止 使用下划线开头的库函数,所以这些内置函数在竞赛里不要使用。(OI不让用)
-
返回 \(x\) 的二进制表示下最低位的 1 后面有多少个 0:
int __builtin_ctz (unsigned int x)
long long __builtin_ctzll (unsigned long long x)
-
返回 \(x\) 的二进制表示下有多少位为 1:
int __builtin_popcount (unsigned int x)
int __builtin_popcountll (unsigned long long x)
其他技巧
两种物品凑数求最小花费
有两种物品。第一种权重 \(A\),花费 \(B\)。第二种权重 \(C\),花费 \(D\)。每种物品均可以买任意个,求权重总和至少为 \(n\) 的情况下,最小的花费 \(ans\)。
结论:设第一种为性价比高的物品。第二种物品购买的数量一定在 \(0 \sim A\) 之间。其中 \(A\) 是性价比高的物品的权重。于是枚举第二种物品的数量即可。时间复杂度 \(O(A)\)。
ll Trick1(ll a, ll b, ll c, ll d, ll n)
{
ld A = 1.0 * a / (1.0 * b), B = 1.0 * c / (1.0 * d);
if (A < B)
{
swap(a, c);
swap(b, d);
}
ll ans = INF;
fr(i, 0, a)
{
ll sum = 0;
ll cha = n - i * c;
sum = i * d;
if (cha >= 0)
sum += ((cha + a - 1) / a) * b;
ans = min(ans, sum);
}
return ans;
}