自用:常见算法竞赛/刷题问题 & 模板
以下是我平常刷题遇到的部分常见问题,随手记录一下。(不定时更新)
基本算法
二分与最大值最小化
注意,这里的有序是广义的有序,如果一个数组中的左侧或者右侧都满足某一种条件,而另一侧都不满足这种条件,也可以看作是一种有序(如果把满足条件看做1 ,不满足看做 0,至少对于这个条件的这一维度是有序的)。换言之,二分搜索法可以用来查找满足某种条件的最大(最小)的值,也即“至少满足某条件的最小值 (最大化)”和“最多满足某条件的最大值(最小化)”
以下区间均假设为闭区间。
int binary_search_maximize_min(int L, int R) {
while (L < R) {
int mid = (L + R + 1) / 2; // 注意这里是 (L + R + 1) / 2
if (check(mid)) {
L = mid; // mid 满足条件,尝试更大的值
} else {
R = mid - 1; // mid 不满足条件,尝试更小的值
}
}
return L; // 或 R,此时 L == R,返回任意一个都可以
}
int binary_search_minimize_max(int L, int R) {
while (L < R) {
int mid = (L + R) / 2; // 注意这里是 (L + R) / 2
if (check(mid)) {
R = mid; // mid 满足条件,尝试更小的值
} else {
L = mid + 1; // mid 不满足条件,尝试更大的值
}
}
return L; // 或 R,此时 L == R,返回任意一个都可以
}
单调队列 与 滑动窗口
例见:
P1886 滑动窗口 /【模板】单调队列
P1725 琪露诺
单调队列用于维护区间 \([L, R]\) 之间的 \(Max/Min\) 值,通常存放的是索引而非值本身。
deque<int> dq;
// 维护 [i - R, i + L] 区间
for (int i = l; i <= n; ++i)
{
int idxToAddToMonoQueue = /* ... */ i - l;
while (!dq.empty() && f[dq.back()] <= f[idxToAddToMonoQueue]) dq.pop_back();
dq.push_back(idxToAddToMonoQueue);
while (!dq.empty() && dq.front() + r < i) dq.pop_front();
f[i] = f[dq.front()] + a[i];
if (i + r > n) ans = max(ans, f[i]);
}
单调栈
单调栈通常用于维护序列 \(a_{1...n}\) 内,对于每个元素,在该元素前/后满足特定比较关系的第一个元素的下标 \(f(i)\)
stack<int> s;
for (int i = n; i; --i)
{
while (!s.empty() && a[s.top()] <= a[i])
s.pop();
res[i] = s.empty() ? 0 : s.top();
s.push(i);
}
二维前缀和
for (int i = 1; i <= m; ++i)
{
for (int j = 1; j <= n; ++j)
{
pre[i][j] = pre[i - 1][j] + pre[i][j - 1] - pre[i - 1][j - 1] + nums[i][j];
}
}
// 异或版本
for (int i = 1; i <= m; ++i)
{
for (int j = 1; j <= n; ++j)
{
pre[i][j] = pre[i - 1][j] ^ pre[i][j - 1] ^ pre[i - 1][j - 1] ^ nums[i][j];
}
}
并查集
struct Dsu
{
int pa[N];
Dsu() {
iota(pa, pa + N, 0);
}
int find(int u) {
return pa[u] == u ? u : pa[u] = find(pa[u]);
}
void merge(int u, int v) {
pa[find(u)] = find(v);
}
} dsu;
动态规划
数字三角形模型:“多线程” 情况
一般使用 f[k][i1][i2]
表示 两个人从(1,1), (1,1) -> (i, k-i1), (i1, k - i2)
的最大值之和.
例见:
P1004 [NOIP2000 提高组] 方格取数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
P1006 [NOIP2008 提高组] 传纸条
// P1006 [NOIP2008 提高组] 传纸条
for (int k = 1; k <= n + m; ++k)
{
for (int i1 = 1; i1 <= n; ++i1)
{
for (int i2 = 1; i2 <= n; ++i2)
{
int j1 = k - i1, j2 = k - i2;
if (j1 < 1 || j1 > m || j2 < 1 || j2 > m)
continue;
int w = (j1 == j2) ? grid[i1][j1] : grid[i1][j1] + grid[i2][j2];
for (int fi : {f[k - 1][i1][i2], f[k - 1][i1 - 1][i2], f[k - 1][i1][i2 - 1], f[k - 1][i1 - 1][i2 - 1]})
{
f[k][i1][i2] = max(f[k][i1][i2], fi + w);
}
}
}
}
cout << f[n + m][n][n] << endl;
LIS 问题
\(dp_i\) 为所有的长度为 \(i\) 的不下降子序列的末尾元素的最小值,\(len\) 为子序列的长度。
memset(dp, 0x3f, sizeof(dp));
for (int i = 1; i <= n; ++i)
{
if (dp[len] < a[i])
dp[++len] = a[i];
else
*lower_bound(dp + 1, dp + len + 1, a[i]) = a[i];
}
cout << len << endl;
LCS:最长公共子序列到 LIS 的转化
现已知:
A:3 2 1 4 5
B:1 2 3 4 5
我们不妨给它们重新标个号:把3标成a,把2标成b,把1标成c……于是变成:
A: a b c d e
B: c b a d e
这样标号之后,LCS长度显然不会改变。但是出现了一个性质:两个序列的子序列,一定是A的子序列。而A本身就是单调递增的。因此这个子序列是单调递增的。换句话说,只要这个子序列在B中单调递增,它就是A的子序列。
#include <bits/stdc++.h>
using namespace std;
int n;
const int N = 100010;
int a[N], b[N], dp[N];
int main()
{
ios::sync_with_stdio(false); cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; ++i)
{
int ai; cin >> ai;
a[ai] = i;
}
for (int i = 1; i <= n; ++i)
{
int bi; cin >> bi;
b[i] = a[bi];
}
int len = 0;
memset(dp, 0x3f, sizeof(dp));
for (int i = 1; i <= n; ++i)
{
int pos = int(lower_bound(dp + 1, dp + len + 1, b[i]) - dp);
len = max(pos, len);
dp[pos] = b[i];
}
cout << len << endl;
}
背包问题: 多重背包的二进制优化问题
参考:OI WIKI
见例:P1833 樱花 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
使用二进制优化的基本思路在于:将每个物品按照其数量拆分成若干个不同的子物品,每个子物品的数量是2的幂次方。从而可以将多重背包问题转换为多个0/1背包问题进行处理。时间复杂度从 \(O(n \times m \times k)\) 降为 \(O(n \times m \times \log k)\)
for (int i = 1; i <= n; ++i)
{
int t, c, p; scanf("%lld %lld %lld", &t, &c, &p);
if (p == 0) p = 10000;
for (int k = 1; k <= p; k *= 2)
{
++cnt;
w[cnt] = t * k;
v[cnt] = c * k;
p -= k;
}
if (p > 0)
{
++cnt;
w[cnt] = t * p;
v[cnt] = c * p;
}
}
for (int i = 1; i <= cnt; ++i)
{
for (int j = T; j >= w[i]; --j)
{
f[j] = max(f[j], f[j - w[i]] + v[i]);
}
}
区间动态规划
例见:
https://www.luogu.com.cn/problem/P1775
https://www.luogu.com.cn/problem/P1880
https://leetcode.cn/problems/burst-balloons/description/
区间 DP 有以下特点:
- 合并:即将两个或多个部分进行整合,当然也可以反过来;
- 特征:能将问题分解为能两两合并的形式;
- 求解:对整个问题设最优值,枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值。
// 最外层枚举区间长度有小到大(对应上文小石子堆合成大石子堆),
// 第二层枚举区间起点,最后一层枚举中间点
for (int len = 2; len <= n; ++len)
{
for (int i = 1; i <= n - len + 1; ++i)
{
int j = i + len - 1;
for (int k = i; k < j; ++k)
{
dp[i][j] =
min(dp[i][j], dp[i][k] + dp[k + 1][j] + pre[j] - pre[i - 1]);
}
}
}
cout << dp[1][n] << endl;
图论
多源最短路
适用于任何图,不管有向无向,边权正负,但是最短路必须存在。(不能有个负环)
for (k = 1; k <= n; k++)
{
for (x = 1; x <= n; x++)
{
for (y = 1; y <= n; y++)
{
f[x][y] = min(f[x][y], f[x][k] + f[k][y]);
}
}
}
单源正权最短路
最短路一般使用 Dijkstra 的优先队列优化实现.
struct Node {
int u, dis;
bool operator>(const Node &rhs) const {
return this->dis > rhs.dis;
}
};
priority_queue<Node, vector<Node>, greater<Node>> pq;
int dist[N];
bool vis[N];
void dijkstra(int s)
{
memset(dist, 0x3f, sizeof(dist));
pq.push({s, 0});
dist[s] = 0;
while (!pq.empty())
{
int u = pq.top().u; pq.pop();
if (vis[u]) continue;
vis[u] = true;
for (Edge e : adj[u])
{
int v = e.v, w = e.w;
if (dist[v] > dist[u] + w)
{
dist[v] = dist[u] + w;
pq.push({v, dist[v]});
}
}
}
}
SPFA 与 负环
int dist[N], cnt[N];
bool vis[N];
bool spfa(int s)
{
memset(dist, 0x3f, sizeof(dist));
queue<int> q;
dist[s] = 0, vis[s] = true;
q.push(s);
while (!q.empty())
{
int u = q.front(); q.pop();
vis[u] = false;
for (Edge e : adj[u])
{
int v = e.v, w = e.w;
if (dist[v] > dist[u] + w)
{
dist[v] = dist[u] + w;
cnt[v] = cnt[u] + 1;
// 在不经过负环的情况下,最短路至多经过 n - 1 条边
// 因此如果经过了多于 n 条边,一定说明经过了负环
if (cnt[v] >= n) return false;
if (!vis[v])
{
q.push(v);
vis[v] = true;
}
}
}
}
return true;
}
Kruskal 与 最小生成树(MST)
#include <bits/stdc++.h>
using namespace std;
int n, m;
const int M = 200010, N = 5010;
struct Edge {
int u, v, w;
bool operator<(const Edge &e) const {
return this->w < e.w;
}
} es[M];
struct Dsu {
int pa[N];
Dsu() { iota(pa, pa + N, 0); }
int find(int x) { return x == pa[x] ? x : pa[x] = find(pa[x]); }
void merge(int x, int y) { pa[find(x)] = find(y); }
} dsu;
int main()
{
ios::sync_with_stdio(false); cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= m; ++i)
{
int u, v, w; cin >> u >> v >> w;
es[i] = {u, v, w};
}
sort(es + 1, es + m + 1);
int ans = 0, cnt = 0;
for (int i = 1; i <= m; ++i)
{
int u = dsu.find(es[i].u), v = dsu.find(es[i].v);
if (u != v)
{
dsu.merge(u, v);
ans += es[i].w;
++cnt;
}
}
if (cnt != n - 1) cout << "orz\n";
else cout << ans << endl;
}
LCA
倍增算法模板
const int MAXD = 20;
int depth[N];
int fa[N][MAXD];
void bfs(int s)
{
memset(depth, 0x3f, sizeof(depth));
queue<int> q;
q.push(s); depth[0] = 0, depth[s] = 1;
while (!q.empty())
{
int u = q.front(); q.pop();
for (int v : adj[u])
{
if (depth[v] > depth[u] + 1)
{
depth[v] = depth[u] + 1;
fa[v][0] = u;
for (int d = 1; d < MAXD; ++d)
fa[v][d] = fa[fa[v][d - 1]][d - 1];
q.push(v);
}
}
}
}
int lca(int x, int y)
{
if (depth[x] < depth[y]) swap(x, y);
for (int d = MAXD - 1; d >= 0; --d)
{
if (depth[fa[x][d]] >= depth[y])
x = fa[x][d];
}
if (x == y) return x;
for (int d = MAXD - 1; d >= 0; --d)
{
if (fa[x][d] != fa[y][d])
{
x = fa[x][d];
y = fa[y][d];
}
}
return fa[x][0];
}
Tarjan 离线 LCA
tarjan 离线做法很大程度取决问题所求,通常场景为需要根据多个询问指定的两个节点的 LCA 以推导结果的情况。
例如,在边权图内求解任意两点的最短路径:
\(dis_{root,u}\) 可以通过 DFS 预处理求出。而 \(lca(i, j)\) 则在 tarjan 递归中可以得知,见下:
struct Edge {
int u, w;
};
vector<Edge> adj[N];
struct Query {
int v, id;
};
vector<Query> qe[N];
struct Dsu
{
int pa[N];
Dsu() {
iota(pa, pa + N, 0);
}
int find(int u) {
return pa[u] == u ? u : pa[u] = find(pa[u]);
}
void merge(int u, int v) {
pa[find(u)] = find(v);
}
} dsu;
void dfs(int u, int fa)
{
for (auto [v, w] : adj[u])
{
if (v == fa) continue;
dis[v] = dis[u] + w;
dfs(v, u);
}
}
void tarjan(int u, int fa)
{
st[u] = true;
for (auto [u, w] : adj[u])
{
if (v != fa)
tarjan(v, u);
}
for (Query q : qe[u])
{
int v = q.v, id = q.id;
if (st[v])
{
int anc = dsu.find(v);
res[id] = dis[u] + dis[v] - 2 * dis[anc];
}
}
dsu.pa[u] = fa;
}
如果给定的为点权,则额外注意需要加上 LCA 的点权,因为在LCA本身也包含在 \(x \rightarrow y\) 的路径之中。
线段树
线段树可以在 \(O(\log N)\) 的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。
无懒标记的单点修改
struct Node
{
int l, r, mx;
};
struct Seg
{
Node tr[M * 4];
void build(int u, int l, int r)
{
tr[u].l = l, tr[u].r = r;
// 叶子节点
if (l == r)
return;
int mid = (l + r) >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
}
// 由子节点计算父节点
void pushup(int u)
{
tr[u].mx = max(tr[u << 1].mx, tr[u << 1 | 1].mx);
}
// 获取 [l, r] 的目标值
int query(int u, int l, int r)
{
// 当前节点的范围已被 [l, r] 包括在内
if (tr[u].l >= l && tr[u].r <= r)
return tr[u].mx;
// [l, r] 必定会与我们递归的节点 u 范围有交集
int res = INT_MIN, mid = (tr[u].l + tr[u].r) >> 1;
if (l <= mid)
res = query(u << 1, l, r);
if (r > mid)
res = max(res, query(u << 1 | 1, l, r));
return res;
}
// 将位置 x 的元素改为 v
void modify(int u, int x, int v)
{
// 已位于要修改的叶子节点
if (tr[u].l == x && tr[u].r == x)
tr[u].mx = v;
else
{
int mid = (tr[u].l + tr[u].r) >> 1;
if (x <= mid)
modify(u << 1, x, v);
else
modify(u << 1 | 1, x, v);
pushup(u);
}
}
} seg;
懒标记:区间修改
struct Node
{
int l, r;
// add 为懒标记
// 注意:add 没有包含本身节点
ll sum, add;
} tr[N * 4];
struct SegTree {
Node tr[N << 2];
void build(int u, int l, int r) {
tr[u].l = l, tr[u].r = r;
if (l == r) {
tr[u].sum = a[l];
tr[u].add = 0;
return;
}
int mid = l + r >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
pushup(u);
}
void pushup(int u) {
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
void pushdown(int u) {
Node &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];
if (root.add) {
left.add += root.add;
left.sum += (left.r - left.l + 1) * root.add;
right.add += root.add;
right.sum += (right.r - right.l + 1) * root.add;
root.add = 0;
}
}
void modify(int u, int l, int r, int d) {
auto &root = tr[u];
if (root.l >= l && root.r <= r) {
root.add += d;
root.sum += (root.r - root.l + 1) * d;
} else {
pushdown(u);
int mid = root.l + root.r >> 1;
if (l <= mid) modify(u << 1, l, r, d);
if (r > mid) modfiy(u << 1 | 1, l, r, d);
pushup(u);
}
}
ll query(int u, int l, int r) {
auto &root = tr[u];
if (root.l >= l && root.r <= r) return root.sum;
pushdown(u);
int mid = (root.l + root.r) >> 1;
ll res = 0;
if (l <= mid) res += query(u << 1, l, l);
if (r > mid) res += query(u << 1 | 1, l, r);
return res;
}
} tr;
数论
线性素数筛
bool isprime[N];
int prime[N], np, pmx[N];
void lineve()
{
for (int i = 2; i <= n; ++i) isprime[i] = true;
for (int i = 2; i <= n; ++i)
{
if (isprime[i]) prime[++np] = i;
for (int p = 1; p <= np && i * prime[p] <= n; ++p)
{
isprime[i * prime[p]] = false;
if (i % prime[p] == 0) break;
}
}
}
快速幕
ll fpow(ll x, ll y, ll mod)
{
x %= mod;
ll res = 1;
while (y > 0)
{
if (y & 1) res = res * x % mod;
x = x * x % mod;
y >>= 1;
}
return res;
}
其它 & 杂记
获取 \(a/b\) 小数点后第 n 位
ll getpp(ll a, ll b, ll n) {
return a * fpow(10, n - 1, b) * 10 / b % 10;
}