C++ 模板合集(持续更新)
简介
MGJ 在 \(2025/7/14\) 灵感迸发,就创造了他的蜜汁小凉菜——C++ 模板合集。
模板分模块,有一些模板会有注释、描述、解析、拓展,除注释的文字一般在代码后。
如果这个算法没有模板(如 dp),就会选取有代表性、模板性的题。
观看效果或描述可能不佳,敬请谅解。
字符串
KMP + 失配树
【KMP】
给出两个字符串 \(s_1\) 和 \(s_2\),若 \(s_1\) 的区间 \([l, r]\) 子串与 \(s_2\) 完全相同,则称 \(s_2\) 在 \(s_1\) 中出现了,其出现位置为 \(l\)。
现在请你求出 \(s_2\) 在 \(s_1\) 中所有出现的位置。
定义一个字符串 \(s\) 的 border 为 \(s\) 的一个非 \(s\) 本身的子串 \(t\),满足 \(t\) 既是 \(s\) 的前缀,又是 \(s\) 的后缀。
对于 \(s_2\),你还需要求出对于其每个前缀 \(s'\) 的最长 border \(t'\) 的长度。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 1e6 + 10;
int n, m, ne[kMaxN];
string s, t;
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> s >> t;
n = s.size(), m = t.size();
s = '#' + s, t = '#' + t;
for (int i = 2, j = 0; i <= m; ++ i) {
for (; j && t[i] != t[j + 1]; j = ne[j]);
if (t[i] == t[j + 1]) {
++ j;
}
ne[i] = j;
}
for (int i = 1, j = 0; i <= n; ++ i) {
for (; j && s[i] != t[j + 1]; j = ne[j]);
if (s[i] == t[j + 1]) {
++ j;
}
if (j == m) {
cout << i - m + 1 << '\n';
}
}
for (int i = 1; i <= m; ++ i) {
cout << ne[i] << ' ';
}
cout << '\n';
return 0;
}
思想:在每次失配时,不是把 \(s\) 串往后移一位,而是把 \(s\) 串往后移动至下一次可以和前面部分匹配的位置这样就可以跳过大多数的失配步骤。而每次 \(s\) 串移动的步数就是通过查找 \(\text{next}\) 数组确定的。
核心:\(\text{next}\) 数组。\(\text{next}_i\) 表示前 \(i\) 个字符的最长公共前缀后缀,当匹配失败的时候,短的字符串通过 \(\text{next}\) 数组跳过一些一定会失败的点,减少时间复杂度。
(拓展)循环节:通过对 \(\text{next}\) 数组进行模拟,发现 \(i - \text{next}_i\) 就是前 \(i\) 个字母的伪循环节(通过前 \(i - \text{next}_i\) 个字符循环可以覆盖掉前 \(i\) 个字符),当 \(i \bmod (i - \text{next}_i) = 0\) 的时候,就会变成真的循环节(通过这几个字母循环可以恰好得到这个字符串)。
【失配树】(失配树本质上为 LCA + KMP,并不是真正的数据结构)
给定一个字符串 \(s\),定义它的 \(k\) 前缀 \(\mathit{pre}_k\) 为字符串 \(s_{1\dots k}\),\(k\) 后缀 \(\mathit{suf}_k\) 为字符串 \(s_{|s|-k+1\dots |s|}\),其中 \(1 \le k \le |s|\)。
定义 \(\bold{Border}(s)\) 为对于 \(i \in [1, |s|)\),满足 \(\mathit{pre}_i = \mathit{suf}_i\) 的字符串 \(\mathit{pre}_i\) 的集合。\(\bold{Border}(s)\) 中的每个元素都称之为字符串 \(s\) 的 \(\operatorname{border}\)。
有 \(m\) 组询问,每组询问给定 \(p,q\),求 \(s\) 的 \(\boldsymbol{p}\) 前缀 和 \(\boldsymbol{q}\) 前缀 的 最长公共 \(\operatorname{border}\) 的长度。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 1e6 + 10;
string s;
int n, m, p, q, ne[kMaxN], dep[kMaxN], fa[kMaxN][22];
int LCA(int x, int y) {
if (dep[x] < dep[y]) {
swap(x, y);
}
for (int i = 20; i >= 0; -- i) {
if (dep[fa[x][i]] >= dep[y]) {
x = fa[x][i];
}
}
if (x == y) {
return fa[x][0];
}
for (int i = 20; i >= 0; -- i) {
if (fa[x][i] != fa[y][i]) {
x = fa[x][i];
y = fa[y][i];
}
}
return fa[x][0];
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> s >> m;
n = s.size();
s = '#' + s;
for (int i = 2, j = 0; i <= n; ++ i) {
for (; j && s[i] != s[j + 1]; j = ne[j]);
if (s[i] == s[j + 1]) {
++ j;
}
ne[i] = j;
dep[i] = dep[j] + 1;
fa[i][0] = j;
}
for (int k = 1; k <= 20; ++ k) {
for (int i = 1; i <= n; ++ i) {
fa[i][k] = fa[fa[i][k - 1]][k - 1];
}
}
for (; m; -- m) {
cin >> p >> q;
cout << LCA(p, q) << '\n';
}
return 0;
}
字典树
给定 \(n\) 个模式串 \(s_1, s_2, \dots, s_n\) 和 \(q\) 次询问,每次询问给定一个文本串 \(t_i\),请回答 \(s_1 \sim s_n\) 中有多少个字符串 \(s_j\) 满足 \(t_i\) 是 \(s_j\) 的前缀。
一个字符串 \(t\) 是 \(s\) 的前缀当且仅当从 \(s\) 的末尾删去若干个(可以为 0 个)连续的字符后与 \(t\) 相同。
输入的字符串大小敏感。例如,字符串 Fusu 和字符串 fusu 不同。
注意:字符串包含大小写字母和数字。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 3e6 + 10;
int n, q, son[kMaxN][65], cnt[kMaxN * 65], idx;
int Get(char c) {
if (c >= 'a' && c <= 'z') {
return c - 'a';
} else if (c >= 'A' && c <= 'Z') {
return c - 'A' + 26;
} else {
return c - '0' + 52;
}
}
void Ins(string s) {
int p = 0;
for (int i = 0, x; i < s.size(); ++ i) {
x = Get(s[i]);
if (!son[p][x]) { // 没有节点就创造一个新的节点,然后跳过去
son[p][x] = ++ idx;
}
// 如果有节点,就直接跳过去
p = son[p][x];
++ cnt[p];
}
}
int F(string s) {
int p = 0;
for (int i = 0, x; i < s.size(); ++ i) {
x = Get(s[i]);
if (!son[p][x]) {
return 0;
}
p = son[p][x];
}
return cnt[p];
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t;
for (cin >> t; t; -- t) {
cin >> n >> q, idx = 0;
string s;
for (int i = 1; i <= n; ++ i) {
cin >> s;
Ins(s);
}
for (; q; -- q) {
cin >> s;
cout << F(s) << '\n';
}
// 清空数组
for (int i = 0; i <= idx; ++ i) {
for (int j = 0; j <= 62; ++ j) {
son[i][j] = 0;
}
}
for (int i = 0; i < idx; ++ i) {
cnt[i] = 0;
}
}
return 0;
}
数据结构
单调栈
给出项数为 \(n\) 的整数数列 \(a_{1 \dots n}\)。
定义函数 \(f(i)\) 代表数列中第 \(i\) 个元素之后第一个大于 \(a_i\) 的元素的下标,即 \(f(i)=\min_{i<j\leq n, a_j > a_i} \{j\}\)。若不存在,则 \(f(i)=0\)。
试求出 \(f(1\dots n)\)。
【第一种】(从右往左遍历)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 3e6 + 10;
int n, a[kMaxN], ans[kMaxN];
stack<int> stk;
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <= n; ++ i) {
cin >> a[i];
}
for (int i = n; i; -- i) {
while (!stk.empty() && a[stk.top()] <= a[i]) {
stk.pop();
}
if (!stk.empty()) {
ans[i] = stk.top();
}
stk.push(i);
}
for (int i = 1; i <= n; ++ i) {
cout << ans[i] << ' ';
}
cout << '\n';
return 0;
}
【第二种】(从左往右遍历)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 3e6 + 10;
int n, a[kMaxN], ans[kMaxN];
stack<int> stk;
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <= n; ++ i) {
cin >> a[i];
}
for (int i = 1; i <= n; ++ i) {
while (!stk.empty() && a[stk.top()] < a[i]) {
ans[stk.top()] = i;
stk.pop();
}
stk.push(i);
}
for (int i = 1; i <= n; ++ i) {
cout << ans[i] << ' ';
}
cout << '\n';
return 0;
}
单调递增栈规律:对于将要入栈的元素来说,在对栈进行更新(即弹出了所有比白己大的元素),此时栈顶元素就是数组中左侧第一个比自己小的元素。
单调递减栈规律:对于将要入栈的元素来说,在对栈进行更新后(即弹出了所有比自己小的元素),此时栈顶元素就是数组中左侧第一个比自己大的元,
什么时候使用单调栈:给定一个序列,求序列中的每一个数左边或右边第一个比他大或比他小的数在什么地方。
ST 表
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 1e6 + 10, kLog = 18;
int n, m, a[kMaxN], f[kMaxN][kLog + 1];
void Init() {
for (int j = 0; j <= kLog; ++ j) {
for (int i = 1; i <= 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]);
}
}
}
}
int Query(int l, int r) {
int len = r - l + 1, k = log(len) / log(2);
return max(f[l][k], f[r - (1 << k) + 1][k]);
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; ++ i) {
cin >> a[i];
}
Init();
for (int l, r; m; -- m) {
cin >> l >> r;
cout << Query(l, r) << '\n';
}
return 0;
}
ST 表是利用倍增思想来缩短时间的。而倍增就体现在他数组的定义中。
预处理思想:将两个小区间的答案合并,即为这个大区间的值。
线段树
【单点、区间加减+查询】
守墓人把墓地分为主要墓碑和次要墓碑, 主要墓碑只能有 \(1\) 个, 守墓人把他记为 \(1\) 号, 而次要墓碑有 \(n-1\) 个,守墓人将之编号为 \(2, 3\dots n\),所以构成了一个有 \(n\) 个墓碑的墓地。
而每个墓碑有一个初始的风水值,这些风水值决定了墓地的风水的好坏,所以守墓人需要经常来查询这些墓碑。风水也不是不可变,除非遭遇特殊情况,已知在接下来的 \(2147483647\) 年里,会有 \(f\) 次灾难,守墓人会有几个操作:
- 将 \([l,r]\) 这个区间所有的墓碑的风水值增加 \(k\)。
- 将主墓碑的风水值增加 \(k\)。
- 将主墓碑的风水值减少 \(k\)。
- 统计 \([l,r]\) 这个区间所有的墓碑的风水值之和。
- 求主墓碑的风水值。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 2e5 + 20;
int n, f;
ll a[kMaxN];
struct node {
int l, r;
ll sum, add;
} tr[kMaxN << 2];
int ls(int u) { return (u << 1); }
int rs(int u) { return (u << 1 | 1); }
void pushup(int u) {
tr[u].sum = tr[ls(u)].sum + tr[rs(u)].sum;
}
void pushdown(int u) {
if (tr[u].add) {
tr[ls(u)].sum += (tr[ls(u)].r - tr[ls(u)].l + 1) * tr[u].add;
tr[ls(u)].add += tr[u].add;
tr[rs(u)].sum += (tr[rs(u)].r - tr[rs(u)].l + 1) * tr[u].add;
tr[rs(u)].add += tr[u].add;
tr[u].add = 0;
}
}
void Build(int u, int l, int r) {
if (l == r) {
tr[u] = {l, r, a[l], 0};
return;
}
tr[u] = {l, r, 0, 0};
int mid = l + r >> 1;
Build(ls(u), l, mid);
Build(rs(u), mid + 1, r);
pushup(u);
}
void Modify(int u, int l, int r, ll k) {
if (l <= tr[u].l && tr[u].r <= r) {
tr[u].sum += (tr[u].r - tr[u].l + 1) * k;
tr[u].add += k;
return;
}
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) {
Modify(ls(u), l, r, k);
}
if (r > mid) {
Modify(rs(u), l, r, k);
}
pushup(u);
}
ll Query(int u, int l, int r) {
if (l <= tr[u].l && tr[u].r <= r) {
return tr[u].sum;
}
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
ll res = 0;
if (l <= mid) {
res = Query(ls(u), l, r);
}
if (r > mid) {
res += Query(rs(u), l, r);
}
return res;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> f;
for (int i = 1; i <= n; ++ i) {
cin >> a[i];
}
Build(1, 1, n);
for (ll op, l, r, k; f; -- f) {
cin >> op;
if (op == 1) {
cin >> l >> r >> k;
Modify(1, l, r, k);
} else if (op == 2) {
cin >> k;
Modify(1, 1, 1, k);
} else if (op == 3) {
cin >> k;
Modify(1, 1, 1, -k);
} else if (op == 4) {
cin >> l >> r;
cout << Query(1, l, r) << "\n";
} else {
cout << Query(1, 1, 1) << "\n" ;
}
}
return 0;
}
【区间乘+查询】
已知一个数列,你需要进行下面三种操作:
- 将某区间每一个数乘上 \(x\);
- 将某区间每一个数加上 \(x\);
- 求出某区间每一个数的和。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 1e6 + 20;
ll n, q, m, a[kMaxN];
struct node {
ll l, r, sum, add, mul;
} tr[kMaxN << 2];
ll ls(ll u) { return (u << 1);}
ll rs(ll u) { return (u << 1 | 1);}
void pushup(ll u) {
tr[u].sum = (tr[ls(u)].sum + tr[rs(u)].sum) % m;
}
void pd(node &t, ll mul, ll add) {
t.sum = (t.sum * mul + (t.r - t.l + 1) * add) % m;
t.add = (t.add * mul + add) % m;
t.mul = t.mul * mul % m;
}
void pushdown(ll u) {
pd(tr[u << 1], tr[u].mul, tr[u].add);
pd(tr[u << 1 | 1], tr[u].mul, tr[u].add);
tr[u].add = 0, tr[u].mul = 1;
}
void Build(ll u, ll l, ll r) {
if (l == r) {
tr[u] = {l, r, a[l], 0, 1};
return;
}
tr[u] = {l, r, 0, 0, 1};
ll mid = l + r >> 1;
Build(ls(u), l, mid);
Build(rs(u), mid + 1, r);
pushup(u);
}
void Modify1(ll u, ll l, ll r, ll k) {
if (l <= tr[u].l && tr[u].r <= r) {
pd(tr[u], k, 0);
return;
}
pushdown(u);
ll mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) {
Modify1(ls(u), l, r, k);
}
if (r > mid) {
Modify1(rs(u), l, r, k);
}
pushup(u);
}
void Modify2(ll u, ll l, ll r, ll k) {
if (l <= tr[u].l && tr[u].r <= r) {
pd(tr[u], 1, k);
return;
}
pushdown(u);
ll mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) {
Modify2(ls(u), l, r, k);
}
if (r > mid) {
Modify2(rs(u), l, r, k);
}
pushup(u);
}
ll Query(ll u, ll l, ll r) {
if (l <= tr[u].l && tr[u].r <= r) {
return tr[u].sum;
}
pushdown(u);
ll mid = tr[u].l + tr[u].r >> 1;
ll res = 0;
if (l <= mid) {
res = Query(ls(u), l, r);
}
if (r > mid) {
res += Query(rs(u), l, r);
}
return res % m;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> q >> m;
for (ll i = 1; i <= n; ++ i) {
cin >> a[i];
}
Build(1, 1, n);
for (ll op, l, r, x; q; -- q) {
cin >> op >> l >> r;
if (op == 1) {
cin >> x;
Modify1(1, l, r, x);
} else if (op == 2) {
cin >> x;
Modify2(1, l, r, x);
} else {
cout << Query(1, l, r) << '\n';
}
}
return 0;
}
树状数组
树状数组思想:树状数组的本质思想是使用树结构维护 “前缀和”,从而把时间复杂度降为 \(\text{O}(\log n)\)。
每个结点 \(t_x\) 保存以 \(x\) 为根的子树中叶结点值的和每个结点覆盖的长度为 \(\text{lowbit}(x)\)。\(t_x\) 结点的父结点为 \(t_{x + \text{lowbit(x)}}\),树的深度为 \(\log n + 1\)。
树状数组原理:利用二进制凑数的思想。
【单点修改+区间查询】
已知一个数列,你需要进行下面两种操作:
- 将某一个数加上 \(x\)
- 求出某区间每一个数的和
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 5e5 + 10;
int n, m, a[kMaxN], tr[kMaxN];
int lowbit(int x) {
return x & (-x);
}
void Modify(int x, int k) {
for (int i = x; i <= n; i += lowbit(i)) {
tr[i] += k;
}
}
int Query(int x) {
int ans = 0;
for (int i = x; i; i -= lowbit(i)) {
ans += tr[i];
}
return ans;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; ++ i) {
cin >> a[i];
Modify(i, a[i]);
}
for (int op, x, y; m; -- m) {
cin >> op;
if (op == 1) {
cin >> x >> y;
Modify(x, y);
} else {
cin >> x >> y;
cout << Query(y) - Query(x - 1) << '\n';
}
}
return 0;
}
【区间修改+单点查询】
已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上 \(x\);
- 求出某一个数的值。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 5e5 + 10;
int n, m, a[kMaxN], tr[kMaxN];
int lowbit(int x) {
return x & (-x);
}
void Modify(int x, int k) {
for (int i = x; i <= n; i += lowbit(i)) {
tr[i] += k;
}
}
int Query(int x) {
int ans = 0;
for (int i = x; i; i -= lowbit(i)) {
ans += tr[i];
}
return ans;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; ++ i) {
cin >> a[i];
Modify(i, a[i]);
Modify(i + 1, -a[i]);
}
for (int op, x, y, k; m; -- m) {
cin >> op;
if (op == 1) {
cin >> x >> y >> k;
Modify(x, k);
Modify(y + 1, -k);
} else {
cin >> x;
cout << Query(x) << '\n';
}
}
return 0;
}
图论
链式前向星
链式前向星的本质是手动模拟链表。
// 点的数量记为 kMaxN,边的数量记为 kMaxM
int h[kMaxN], e[kMaxM], ne[kMaxM], w[kMaxM], idx;
// 加边
void Add(int u, int v, int w) {
e[idx] = v;
ne[idx] = h[u];
w[idx] = w;
h[u] = idx ++; // 注意:不能写 ++ idx
}
// 遍历邻边
for (int i = h[x]; i != -1; i = ne[i]) {
int v = e[i];
// e[i] 为这条边另外一个点,w[i] 为边的长度,i 为这条边的编号
}
欧拉回路
给定一张图,请你找出欧拉回路,即在图中找一个环使得每条边都在环上出现恰好一次。输入的 \(t\):\(t = 1\) 为无向图,\(t = 2\) 为有向图。对于每个数据,输出路径编号(输入时按照顺序编号),若为有向图则输出相反数表示 \(v \to u\) 的路径。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 5e5 + 10, kMaxM = 5e5 + 50;
int t, n, m, h[kMaxN], e[kMaxM], ne[kMaxM], idx, ind[kMaxN], outd[kMaxN], f[kMaxM], ans[kMaxN], cnt;
void Add(int u, int v) {
e[idx] = v;
ne[idx] = h[u];
h[u] = idx ++;
}
void Dfs1(int x) {
for (int &i = h[x]; i != -1; ) {
if (f[i]) { // 如果这条边使用了
i = ne[i]; // 这里的 i 等价于 h[x],这里等于删除这条边
continue;
}
f[i] = f[i ^ 1] = 1;
int t = i / 2 + 1;
if (i % 2) {
t = -t;
}
int v = e[i];
i = ne[i]; // 走过这条边也要删除
Dfs1(v);
ans[++ cnt] = t;
}
}
void Dfs2(int x) {
for (int &i = h[x]; i != -1; ) {
int t = i + 1;
int v = e[i];
i = ne[i];
Dfs2(v);
ans[++ cnt] = t;
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> t;
memset(h, -1, sizeof h);
if (t == 1) {
cin >> n >> m;
for (int i = 1, u, v; i <= m; ++ i) {
cin >> u >> v;
Add(u, v), Add(v, u);
++ ind[v], ++ ind[u];
}
for (int i = 1; i <= n; ++ i) {
if (ind[i] & 1) {
return cout << "NO\n", 0;
}
}
for (int i = 1; i <= n; ++ i) {
if (h[i] != -1) {
Dfs1(i);
break;
}
}
if (cnt < m) {
return cout << "NO\n", 0;
}
cout << "YES\n";
for (int i = cnt; i; -- i) {
cout << ans[i] << " ";
}
} else {
cin >> n >> m;
for (int i = 1, u, v; i <= m; ++ i) {
cin >> u >> v;
Add(u, v);
++ ind[v], ++ outd[u];
}
for (int i = 1; i <= n; ++ i) {
if (ind[i] != outd[i]) {
return cout << "NO\n", 0;
}
}
for (int i = 1; i <= n; ++ i) {
if (h[i] != -1) {
Dfs2(i);
break;
}
}
if (cnt < m) {
return cout << "NO\n", 0;
}
cout << "YES\n";
for (int i = cnt; i; -- i) {
cout << ans[i] << " ";
}
}
return 0;
}
- 对于无向图,所有边都是连通的。
- 存在欧拉路径的充分必要条件:度数为奇数的点只能有 \(0\) 或 \(2\) 个。
- 存在欧拉回路的充分必要条件:度数为奇数的点只能有 \(0\) 个。
- 对于有向图,所有边都是连通的。
- 存在欧拉路径的充分必要条件:要么所有点的出度均等于入度;要么除了两个点之外,其余所有点的出度等于入度,剩余的两个点:一个满足出度比入度多 \(1\)(起点),另一个满足入度比出度多 \(1\)(终点)。
- 存在欧拉回路的充分必要条件:所有点的出度均等于入度。
最短路
如题,给出一个有向图,请输出从某一点出发到所有点的最短路径长度。
【Dijkstra】
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 1e5 + 10;
int n, m, s, dis[kMaxN];
vector<pair<int, int>> e[kMaxN];
void Dijkstra() {
fill(dis, dis + n + 1, 2147483647);
priority_queue<pair<int, int>> q;
dis[s] = 0;
for (q.push({s, 0}); q.size(); ) {
auto x = q.top();
q.pop();
for (auto i : e[x.first]) {
if (dis[i.first] > dis[x.first] + i.second) { // 如果更优,则更新距离
dis[i.first] = dis[x.first] + i.second;
q.push({i.first, dis[i.first]}); // 放入队列
}
}
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m >> s;
for (int i = 1, u, v, w; i <= m; ++ i) {
cin >> u >> v >> w;
e[u].push_back({v, w});
}
Dijkstra();
for (int i = 1; i <= n; ++ i) {
cout << dis[i] << ' ';
}
cout << '\n';
return 0;
}
【SPFA】
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 1e5 + 10;
int n, m, s, dis[kMaxN], v[kMaxN];
vector<pair<int, int>> e[kMaxN];
void SPFA() {
fill(dis, dis + n + 1, 2147483647);
queue<int> q;
dis[s] = 0, v[s] = 1;
for (q.push(s); q.size(); ) {
int x = q.front();
q.pop();
v[x] = 0; // 要记得
for (auto i : e[x]) {
if (dis[i.first] > dis[x] + i.second) {
dis[i.first] = dis[x] + i.second;
if (v[i.first]) {
continue;
}
v[i.first] = 1;
q.push(i.first);
}
}
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m >> s;
for (int i = 1, u, v, w; i <= m; ++ i) {
cin >> u >> v >> w;
e[u].push_back({v, w});
}
SPFA();
for (int i = 1; i <= n; ++ i) {
cout << dis[i] << ' ';
}
cout << '\n';
return 0;
}
SPFA 算法为即队列优化的 \(\text{bellman-ford}\) 算法。
SPFA 算法可以处理带有负权边的图,但不能处理带有负权环的图。算法的基本思想是使用一个队列来存储所有待优化的节点,并通过不断的松弛操作来逼近最短路径。
拓扑排序
有个人的家族很大,辈分关系很混乱,请你帮整理一下这种关系。给出每个人的后代的信息。输出一个序列,使得每个人的后辈都比那个人后列出。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 110;
int n, d[kMaxN];
vector<int> e[kMaxN];
void TSort() {
queue<int> q;
for (int i = 1; i <= n; ++ i) {
if (!d[i]) {
q.push(i);
}
}
for (; q.size(); ) {
int x = q.front();
q.pop();
for (auto i : e[x]) {
if (!(-- d[i])) {
q.push(i);
}
}
cout << x << ' ';
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1, x; i <= n; ++ i) {
while (cin >> x) {
if (!x) {
break;
}
e[i].push_back(x);
++ d[x];
}
}
TSort();
cout << '\n';
return 0;
}
在图论中,拓扑排序是一个有向无环图的所有顶点的线性序列。且该序列必须满足下面两个条件:
- 每个顶点出现且只出现一次。
- 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
最小生成树
如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出 orz。
最小生成树(Minimum Spanning Tree,MST)是指在一个带权无向连通图中,选择若干条边,使得这些边构成的子图不仅包含图中的所有顶点,而且边的权值总和最小。
【kruskal】
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 2e5 + 20;
int n, m, ans, cnt, fa[kMaxN];
struct Node {
int u, v, w;
bool operator< (const Node &b) const {
return w < b.w;
}
} a[kMaxN];
// 并查集
int F(int x) {
return fa[x] == x? x : fa[x] = F(fa[x]);
}
void U(int x, int y) {
x = F(x), y = F(y);
if (fa[x] != y) {
fa[x] = y;
++ cnt;
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; ++ i) {
fa[i] = i;
}
for (int i = 1; i <= m; ++ i) {
cin >> a[i].u >> a[i].v >> a[i].w;
}
sort(a + 1, a + m + 1);
for (int i = 1, u, v; i <= m; ++ i) { // 枚举每一条边
u = F(a[i].u), v = F(a[i].v);
if (u != v) {
U(u, v);
ans += a[i].w;
}
}
if (cnt < n - 1) {
cout << "orz\n";
} else {
cout << ans << '\n';
}
return 0;
}
【Prim】
作者水平较低(bushi),且不常用,所以直接粘贴别人的代码。
#include <bits/stdc++.h>
using namespace std;
#define re register
#define il inline
il int read()
{
re int x = 0, f = 1;
char c = getchar();
while (c < '0' || c > '9')
{
if (c == '-')
f = -1;
c = getchar();
}
while (c >= '0' && c <= '9')
x = (x << 3) + (x << 1) + (c ^ 48), c = getchar();
return x * f;
} // 快读,不理解的同学用cin代替即可
#define inf 123456789
#define maxn 5005
#define maxm 200005
struct edge
{
int v, w, next;
} e[maxm << 1];
// 注意是无向图,开两倍数组
int head[maxn], dis[maxn], cnt, n, m, tot, now = 1, ans;
// 已经加入最小生成树的的点到没有加入的点的最短距离,比如说1和2号节点已经加入了最小生成树,那么dis[3]就等于min(1->3,2->3)
bool vis[maxn];
// 链式前向星加边
il void add(int u, int v, int w)
{
e[++cnt].v = v;
e[cnt].w = w;
e[cnt].next = head[u];
head[u] = cnt;
}
// 读入数据
il void init()
{
n = read(), m = read();
for (re int i = 1, u, v, w; i <= m; ++i)
{
u = read(), v = read(), w = read();
add(u, v, w), add(v, u, w);
}
}
il int prim()
{
// 先把dis数组附为极大值
for (re int i = 2; i <= n; ++i)
{
dis[i] = inf;
}
// 这里要注意重边,所以要用到min
for (re int i = head[1]; i; i = e[i].next)
{
dis[e[i].v] = min(dis[e[i].v], e[i].w);
}
while (++tot < n) // 最小生成树边数等于点数-1
{
re int minn = inf; // 把minn置为极大值
vis[now] = 1; // 标记点已经走过
// 枚举每一个没有使用的点
// 找出最小值作为新边
// 注意这里不是枚举now点的所有连边,而是1~n
for (re int i = 1; i <= n; ++i)
{
if (!vis[i] && minn > dis[i])
{
minn = dis[i];
now = i;
}
}
ans += minn;
// 枚举now的所有连边,更新dis数组
for (re int i = head[now]; i; i = e[i].next)
{
re int v = e[i].v;
if (dis[v] > e[i].w && !vis[v])
{
dis[v] = e[i].w;
}
}
}
return ans;
}
int main()
{
init();
printf("%d", prim());
return 0;
}
强连通分量(Tarjan)
【强连通分量】
给定一张 \(n\) 个点 \(m\) 条边的有向图,求出其所有的强连通分量。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 1e5 + 10;
// scc 记录当前的强连通分量是第几个,low 数组记录每个点能到的最早时间戳,dfn 是自己的时间戳
// ans 为每个强连通分量内的点,is(instack)来存这个点是否在栈中,tim 是时间戳,栈(stk)存经过的点
// col 存每个点的颜色(scc) ,st 数组判断强连通分量是否输出过
int n, m, tim, low[kMaxN], dfn[kMaxN], col[kMaxN], scc;
bool is[kMaxN], st[kMaxN];
vector<int> e[kMaxN], ans[kMaxN];
stack<int> stk;
void Tarjan(int x) {
stk.push(x);
dfn[x] = low[x] = ++ tim;
is[x] = 1;
for (auto i : e[x]) {
if (!dfn[i]) { // i 还未搜到过
Tarjan(i);
low[x] = min(low[x], low[i]);
} else if (is[i]) {
low[x] = min(low[x], low[i]);
}
}
if (low[x] == dfn[x]) { // 能到的最早时间戳等于自己的时间戳,说明走到了强连通分量最早的那个点,也就是找到了一个强连通分量
++ scc;// 连通分量个数 +1
for (int v; stk.size(); ) {
v = stk.top();
stk.pop();
ans[scc].push_back(v);
col[v] = scc;
is[v] = 0;
if (x == v) { // 如果找到了强连通分量最开始进来的那个点,结束
break;
}
}
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1, u, v; i <= m; ++ i) {
cin >> u >> v;
e[u].push_back(v);
}
for (int i = 1; i <= n; ++ i) {
if (!dfn[i]) { // 如果未搜过
Tarjan(i);
}
}
cout << scc << '\n';
for (int i = 1; i <= n; ++ i) {
if (!st[col[i]]) { // 如果这个颜色(scc)未输出过
st[col[i]] = 1; // 标记
sort(ans[col[i]].begin(), ans[col[i]].end());
for (auto j : ans[col[i]]) {
cout << j << ' ';
}
cout << '\n';
}
}
return 0;
}
强连通分量就是极大的强连通(在有向图中的连通图)子图。
Tarjan 算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
【缩点】
给定一个 \(n\) 个点 \(m\) 条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。
允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 5e5 + 10;
int n, m, tim, a[kMaxN], low[kMaxN], dfn[kMaxN], col[kMaxN], scc, cnt[kMaxN], d[kMaxN], u[kMaxN], v[kMaxN], sccw[kMaxN], f[kMaxN], ans;
bool is[kMaxN], st[kMaxN];
vector<int> e[kMaxN], g[kMaxN];
stack<int> s;
void Tarjan(int x) {
s.push(x);
dfn[x] = low[x] = ++ tim;
is[x] = 1;
for (auto i : e[x]) {
if (!dfn[i]) {
Tarjan(i);
low[x] = min(low[x], low[i]);
} else if (is[i]) {
low[x] = min(low[x], dfn[i]);
}
}
if (dfn[x] == low[x]) {
++ scc;
for (int v; s.size(); ) {
v = s.top();
s.pop();
is[v] = 0;
col[v] = scc;
sccw[scc] += a[v];
if (x == v) {
break;
}
}
}
}
void Dfs(int x) {
if (f[x] != -114514) {
return;
}
f[x] = sccw[x];
int mx = 0;
for (auto i : g[x]) {
if (f[i] != -114514) {
Dfs(i);
}
mx = max(mx, f[i]);
}
f[x] += mx;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; ++ i) {
cin >> a[i];
}
for (int i = 1; i <= m; ++ i) {
cin >> u[i] >> v[i];
e[u[i]].push_back(v[i]);
}
for (int i = 1; i <= n; ++ i) {
if (!dfn[i]) {
Tarjan(i);
}
}
for (int i = 1; i <= m; ++ i) {
if (col[u[i]] != col[v[i]]) {
g[col[u[i]]].push_back(col[v[i]]);
}
}
fill(f, f + scc + 1, -114514);
for (int i = 1; i <= scc; ++ i) {
if (f[i] == -114514) {
Dfs(i);
ans = max(ans, f[i]);
}
}
cout << ans << '\n';
return 0;
}
缩点通过染色(col[v] = scc)将每个强连通分量看(缩)成一个点,编号为 \(\text{col}_\text{scc 中的点}\)。然后重新建图将图变成有向无环图,在进行计算时将整个强连通分量的数据缩到一起。
C++ 模板合集(持续更新)的更多相关文章
- 安装 Ubuntu 21.04 后必备的绝佳应用大合集(持续更新中)
@ 目录 一.Google Chrome 浏览器 1.下载 2.安装 3.设置搜索引擎 二.火焰截图(替代QQ截图) 1.简介: 2.安装: 3.设置快捷键: 三.VLC视频播放器(替代Potplay ...
- ubuntu相关软件合集(持续更新中)
本人使用的是Ubuntu-Kylin14.04,自带了日历.输入法.优客助手等易于上手的应用.省的每次安装完原生的系统再麻烦的安装,下面介绍默认应用外的相关常用软件: 一.Keylock Applic ...
- DataStage 错误集(持续更新)
DataStage 错误集(持续更新) DataStage序列文章 DataStage 一.安装 DataStage 二.InfoSphere Information Server进程的启动和停止 D ...
- 学渣乱搞系列之Tarjan模板合集
学渣乱搞系列之Tarjan模板合集 by 狂徒归来 一.求强连通子图 #include <iostream> #include <cstdio> #include <cs ...
- ACM算法模板 · 一些常用的算法模板-模板合集(打比赛专用)
ACM算法模板 · 一些常用的算法模板-模板合集(打比赛专用)
- 最短路算法模板合集(Dijkstar,Dijkstar(优先队列优化), 多源最短路Floyd)
再开始前我们先普及一下简单的图论知识 图的保存: 1.邻接矩阵. G[maxn][maxn]; 2.邻接表 邻接表我们有两种方式 (1)vector< Node > G[maxn]; 这个 ...
- 有趣的线段树模板合集(线段树,最短/长路,单调栈,线段树合并,线段树分裂,树上差分,Tarjan-LCA,势能线段树,李超线段树)
线段树分裂 以某个键值为中点将线段树分裂成左右两部分,应该类似Treap的分裂吧(我菜不会Treap).一般应用于区间排序. 方法很简单,就是把分裂之后的两棵树的重复的\(\log\)个节点新建出来, ...
- Vue-小demo、小效果 合集(更新中...)
(腾讯课堂学习小demo:https://ke.qq.com/course/256052) 一.简单的指令应用 --打击灭火器 图片素材点击腾讯课堂的链接获取 html: <!DOC ...
- mongodb管理副本集(持续更新中)
许多维护工作不能在备份节点上完成 因为要写操作,也不能在主节点上进行,这就需要单机模式启动服务器, 是指重启成员服务器,让他成为一个单机运行的服务器,而不再是副本集中的一员(临时的) 在单机 ...
- C# net core程序调试错误集(持续更新)
目录 C#程序调试错误集 1.依赖注入错误System.InvalidOperationException: Unable to resolve service for type 'xxx' whil ...
随机推荐
- [转自洛谷]洛谷KateX公式大全【LateX】
前言 由于在洛谷,有很多人对于\(\KaTeX\)和\(\LaTeX\)之间的关系并不清楚,导致很多人去搜\(\LaTeX\)的资料,然后发现有许多指令无法在洛谷运行. 但是事实上,\(\KaTeX\ ...
- 『Plotly实战指南』--交互功能基础篇
在数据可视化领域,静态图表早已无法满足用户对深度分析与探索的需求. Plotly作为新一代交互式可视化工具,通过其强大的交互功能重新定义了"数据叙事"的边界. 通过精心设计的交互功 ...
- 27.7K star!这个SpringBoot+Vue人力资源管理系统,让企业开发事半功倍!
嗨,大家好,我是小华同学,关注我们获得"最新.最全.最优质"开源项目和高效工作学习方法 "只需一个脚手架,轻松搭建企业级人事管理系统!" 微人事(vhr)是一款 ...
- 电脑ocr软件
天若ocr 体积小,可以隐藏任务栏,但有时候识别度不好,停止更新了,新项目为树洞ocr github: https://github.com/AnyListen/tianruoocr/releases ...
- K8s新手系列之namespace
概述 官方文档地址:https://kubernetes.io/zh-cn/docs/tasks/administer-cluster/namespaces/ namespace是K8s系统中的一种非 ...
- window-docker的容器使用宿主机音频设备
目录 前言 操作配置 前言 你有没有遇到过这种情况? 你兴冲冲地在Windows上用Docker搭了个语音识别项目,准备让容器高歌一曲,或者至少"Hey Docker"一下.结果- ...
- Winform C#多显示器窗口控制详解
写Winform程序的时候,有将一个窗口放置到特定的显示器,并且全屏的需求.于是借此机会,好好研究了一番这个Screen类[1],总结了一些方法. Windows的窗口逻辑 首先我们需要知道窗口定位的 ...
- codeup之进制转换(大数的进制转换
题目描述 将一个长度最多为30位数字的十进制非负整数转换为二进制数输出. 输入 多组数据,每行为一个长度不超过30位的十进制非负整数. (注意是10进制数字的个数可能有30个,而非30bits的整数) ...
- sass中@use 的用法
前言在上一篇中,我们深入探讨了 Sass 中 @import 语法的局限性,正是因为这些问题,Sass 在 1.80 版本 后逐步弃用 @import,推出了更现代化的 @use 语法作为替代.在本文 ...
- 信息工程大学第五届超越杯程序设计竞赛(同步赛)A遗失的旋律
题目链接 :A-遗失的旋律_信息工程大学第五届超越杯程序设计竞赛(同步赛) (nowcoder.com) 本场比赛的数据都很水,导致很多题暴力都能过,(出题人背大锅, 说实话,如果数据不水, 这场感觉 ...