写在前面
好困啊,不想写题,来写板子了。
基础
快读
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar()) {
if (ch == '-') {
f = -1;
}
}
for (; isdigit(ch); ch = getchar()) {
w = (w << 3) + (w << 1) + (ch ^ '0');
}
return f * w;
}
随机树生成器
#include <bits/stdc++.h>
const int kTreeSize = 100000;
int main() {
srand(time(0));
int n = kTreeSize;
printf("%d\n", n);
for (int i = 2; i <= n; ++ i) {
printf("%d %d\n", i, rand() % (i - 1) + 1);
}
return 0;
}
随机图生成器
保证无自环重边。
#include <bits/stdc++.h>
#define pr std::pair
#define mp std::make_pair
const int kNodeSize = 10;
const int kEdgeSize = 20;
std::set <pr <int, int> > has;
int main() {
srand(time(0));
int n = kNodeSize, m = kEdgeSize;
printf("%d %d\n", n, m);
for (int i = 1; i <= m; ++ i) {
int u_ = rand() % n + 1, v_ = rand() % n + 1;
while (u_ == v_ || has.count(mp(u_, v_))) {
u_ = rand() % n + 1, v_ = rand() % n + 1;
}
has.insert(mp(u_, v_));
printf("%d %d\n", u_, v_);
}
return 0;
}
Checker
#include <bits/stdc++.h>
int main() {
for (int i = 1; ; i ++) {
printf("%d:", i);
system("datamaker.exe");
system("force.exe");
system("std.exe");
if (system("fc std.in force.in")) {
break;
}
}
}
数据结构
并查集
将集合关系转化为图论中连通块的方式进行维护。
路径压缩 + 按秩合并
均摊复杂度 \(O(m)\)。
//知识点:并查集
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e4 + 10;
//=============================================================
int n, m, fa[kMaxn], size[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
int Find(int x_) {
return x_ == fa[x_] ? x_ : fa[x_] = Find(fa[x_]);
}
void Union(int u_, int v_) {
int fu = Find(u_), fv = Find(v_);
if (fu == fv) return ;
if (size[fu] > size[fv]) std::swap(fu, fv);
fa[fu] = fv;
size[fv] += size[fu];
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) {
fa[i] = i;
size[i] = 1;
}
for (int i = 1; i <= m; ++ i) {
int opt = read(), x = read(), y = read();
if (opt == 1) {
Union(x, y);
} else {
printf("%c\n", Find(x) == Find(y) ? 'Y' : 'N');
}
}
return 0;
}
可撤销并查集
记录操作的变化,压入栈中。
为保证回退次数,只能使用按秩合并。
//知识点:可撤销并查集
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e4 + 10;
const int kMaxm = 2e5 + 10;
//=============================================================
int n, m, top, fa[kMaxn], size[kMaxn];
struct Stack {
int u, v, fa, size;
} st[kMaxm];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
int Find(int x_) {
return x_ == fa[x_] ? x_ : fa[x_] = Find(fa[x_]);
}
void Union(int u_, int v_) {
int fu = Find(u_), fv = Find(v_);
if (size[fu] > size[fv]) std::swap(fu, fv);
st[++ top] = (Stack) {fu, fv, fa[fu], size[fu]};
if (fu != fv) {
fa[fu] = fv;
size[fv] += size[fu];
size[fu] = 0;
}
}
void Restore() {
Stack now = st[top];
if (now.u != now.v) {
fa[now.u] = now.fa;
size[now.v] -= now.size;
size[now.u] = now.size;
}
top --;
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) {
fa[i] = i;
size[i] = 1;
}
for (int i = 1; i <= m; ++ i) {
int opt = read(), x = read(), y = read();
if (opt == 1) {
Union(x, y);
} else {
printf("%c\n", Find(x) == Find(y) ? 'Y' : 'N');
}
}
return 0;
}
单调栈
求每个元素右侧第一个比它大的数的位置。
维护一个单调递减的栈。
//知识点:单调栈
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 3e6 + 10;
//=============================================================
int n, top, a[kMaxn], st[kMaxn], ans[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
//=============================================================
int main() {
n = read();
for (int i = 1; i <= n; ++ i) a[i] = read();
for (int i = 1; i <= n; ++ i) {
while (top && a[st[top]] < a[i]) {
ans[st[top]] = i;
top --;
}
st[++ top] = i;
}
for (int i = 1; i <= n; ++ i) printf("%d ", ans[i]);
return 0;
}
单调队列
滑动窗口求区间最大值。
//知识点:单调队列
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e6 + 10;
//=============================================================
int n, k, a[kMaxn];
int h, t, q[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Init() {
h = t = 0;
}
void InsertMin(int pos_) {
while (t >= h && a[q[t]] >= a[pos_]) {
t --;
}
q[++ t] = pos_;
}
void InsertMax(int pos_) {
while (t >= h && a[q[t]] <= a[pos_]) {
t --;
}
q[++ t] = pos_;
}
int Query(int pos_) {
while (t >= h && q[h] + k <= pos_) ++ h;
return a[q[h]];
}
//=============================================================
int main() {
n = read(), k = read();
for (int i = 1; i <= n; ++ i) a[i] = read();
for (int i = 1; i < k; ++ i) InsertMin(i);
for (int i = k; i <= n; ++ i) {
InsertMin(i);
printf("%d ", Query(i));
}
printf("\n");
Init();
for (int i = 1; i < k; ++ i) InsertMax(i);
for (int i = k; i <= n; ++ i) {
InsertMax(i);
printf("%d ", Query(i));
}
return 0;
}
树状数组
二进制优化前缀和,维护前缀信息。
若想维护区间信息必须具有可减性。
单点加区间和
//知识点:树状数组
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e6 + 10;
//=============================================================
int n, m, a[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
namespace Bit {
#define lowbit(x) (x&-x)
int Lim;
LL t[kMaxn];
void Init(int lim_) {
Lim = lim_;
}
void Insert(int pos_, LL val_) {
for (int i = pos_; i <= Lim; i += lowbit(i)) {
t[i] += val_;
}
}
LL Sum(int pos_) {
LL ret = 0;
for (int i = pos_; i; i -= lowbit(i)) {
ret += t[i];
}
return ret;
}
#undef lowbit
}
//=============================================================
int main() {
n = read(), m = read();
Bit::Init(n);
for (int i = 1; i <= n; ++ i) {
a[i] = read();
Bit::Insert(i, a[i]);
}
while (m --) {
int opt = read();
if (opt == 1) {
int x = read(), k = read();
Bit::Insert(x, 1ll * k);
} else {
int x = read(), y = read();
printf("%lld\n", Bit::Sum(y) - Bit::Sum(x - 1));
}
}
return 0;
}
ST 表
用长度为 2 的幂的区间覆盖查询区间。
维护不具有可减性信息。
静态区间最大值。
//
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e5 + 10;
//=============================================================
int n, m, a[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
namespace ST {
int Log2[kMaxn], f[kMaxn][22];
void MakeST () {
Log2[1] = 0;
for (int i = 1; i <= n; ++ i) {
f[i][0] = a[i];
if (i > 1) Log2[i] = Log2[i >> 1] + 1;
}
for (int i = 1; i <= 22; ++ i) {
for (int j = 1; j + (1 << i) - 1 <= n; ++ j) {
f[j][i] = std::max(f[j][i - 1],
f[j + (1 << (i - 1))][i - 1]);
}
}
}
int Query(int l_, int r_) {
int lth = Log2[r_ - l_ + 1];
return std::max(f[l_][lth], f[r_ - (1 << lth) + 1][lth]);
}
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) a[i] = read();
ST::MakeST();
while (m --) {
int l = read(), r = read();
printf("%d\n", ST::Query(l, r));
}
return 0;
}
分块
想了想还是把分块扔到数据结构里了= =
分块的基本思想是:通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。
//知识点:分块
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cmath>
#include <cstring>
#define LL long long
const int kN = 5e4 + 10;
//=============================================================
int n, m;
int block_size, block_num, L[kN], R[kN], bel[kN];
LL a[kN], sum[kN], tag[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmin(LL &fir, LL sec) {
if (sec < fir) fir = sec;
}
void PrepareBlock() {
block_size = sqrt(n); //根据实际要求选择一个合适的大小。
block_num = n / block_size;
for (int i = 1; i <= block_num; ++ i) { //分配块左右边界。
L[i] = (i - 1) * block_size + 1;
R[i] = i * block_size;
}
if (R[block_num] < n) { //最后的一个较小的块。
++ block_num;
L[block_num] = R[block_num - 1] + 1;
R[block_num] = n;
}
//分配元素所属的块编号。
for (int i = 1; i <= block_num; ++ i) {
for (int j = L[i]; j <= R[i]; ++ j) {
bel[j] = i;
sum[i] += a[j]; //预处理区间和
}
}
}
void Modify(int L_, int R_, int val_) {
int bell = bel[L_], belr = bel[R_];
if (bell == belr) {
for (int i = L_; i <= R_; ++ i) {
a[i] += val_;
sum[bell] += val_;
}
return ;
}
for (int i = bell + 1; i <= belr - 1; ++ i) tag[i] += val_;
for (int i = L_; i <= R[bell]; ++ i) {
a[i] += val_;
sum[bell] += val_;
}
for (int i = L[belr]; i <= R_; ++ i) {
a[i] += val_;
sum[belr] += val_;
}
}
LL Query(int L_, int R_, int mod_) {
int bell = bel[L_], belr = bel[R_], ret = 0;
if (bell == belr) {
for (int i = L_; i <= R_; ++ i) ret = (ret + a[i] + tag[bell]) % mod_;
return ret;
}
for (int i = bell + 1; i <= belr - 1; ++ i) {
ret = (ret + sum[i] + (R[i] - L[i] + 1) * tag[i] % mod_) % mod_;
}
for (int i = L_; i <= R[bell]; ++ i) ret = (ret + a[i] + tag[bell]) % mod_;
for (int i = L[belr]; i <= R_; ++ i) ret = (ret + a[i] + tag[belr]) % mod_;
return ret;
}
//=============================================================
int main() {
n = read();
for (int i = 1; i <= n; ++ i) a[i] = read();
PrepareBlock();
for (int i = 1; i <= n; ++ i) {
int opt = read(), l = read(), r = read(), c = read();
if (opt == 0) {
Modify(l, r, c);
} else {
printf("%lld\n", Query(l, r, c + 1));
}
}
return 0;
}
线段树
区间加区间和
//知识点:线段树
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e5 + 10;
//=============================================================
int n, m, a[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
namespace Seg {
#define ls (now_<<1)
#define rs (now_<<1|1)
#define mid ((L_+R_)>>1)
LL sum[kMaxn << 2], tag[kMaxn << 2];
void Pushup(int now_) {
sum[now_] = sum[ls] + sum[rs];
}
void Pushdown(int now_, int L_, int R_) {
sum[ls] += 1ll * tag[now_] * (mid - L_ + 1);
sum[rs] += 1ll * tag[now_] * (R_ - mid);
tag[ls] += tag[now_];
tag[rs] += tag[now_];
tag[now_] = 0ll;
}
void Build(int now_, int L_, int R_) {
if (L_ == R_) {
sum[now_] = a[L_];
tag[now_] = 0ll;
return ;
}
Build(ls, L_, mid);
Build(rs, mid + 1, R_);
Pushup(now_);
}
void Modify(int now_, int L_, int R_, int l_, int r_, LL val_) {
if (l_ <= L_ and R_ <= r_) {
sum[now_] += 1ll * (R_ - L_ + 1) * val_;
tag[now_] += val_;
return ;
}
Pushdown(now_, L_, R_);
if (l_ <= mid) Modify(ls, L_, mid, l_, r_, val_);
if (r_ > mid) Modify(rs, mid + 1, R_, l_, r_, val_);
Pushup(now_);
}
LL Query(int now_, int L_, int R_, int l_, int r_) {
if (l_ <= L_ and R_ <= r_) return sum[now_];
Pushdown(now_, L_, R_);
LL ret = 0;
if (l_ <= mid) ret += Query(ls, L_, mid, l_, r_);
if (r_ > mid) ret += Query(rs, mid + 1, R_, l_, r_);
return ret;
}
#undef ls
#undef rs
#undef mid
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) a[i] = read();
Seg::Build(1, 1, n);
while (m --) {
int opt = read();
if (opt == 1) {
int l = read(), r = read(), k = read();
Seg::Modify(1, 1, n, l, r, 1ll * k);
} else {
int l = read(), r = read();
printf("%lld\n", Seg::Query(1, 1, n, l, r));
}
}
return 0;
}
可持久化线段树
单点修改,单点历史版本查询。
//知识点:可持久化线段树
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e6 + 10;
//=============================================================
int n, m, a[kMaxn], root[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
namespace Seg {
#define ls (lson[now_])
#define rs (rson[now_])
#define mid ((L_+R_)>>1)
int node_num, lson[kMaxn << 5], rson[kMaxn << 5], val[kMaxn << 5];
void Build(int &now_, int L_, int R_) {
now_ = ++ node_num;
if (L_ == R_) {
val[now_] = a[L_];
return ;
}
Build(ls, L_, mid);
Build(rs, mid + 1, R_);
}
void Modify(int &now_, int pre_, int L_, int R_, int pos_, int val_) {
now_ = ++ node_num;
if (L_ == R_) {
val[now_] = val_;
return ;
}
ls = lson[pre_];
rs = rson[pre_];
if (pos_ <= mid) Modify(ls, lson[pre_], L_, mid, pos_, val_);
else Modify(rs, rson[pre_], mid + 1, R_, pos_, val_);
}
int Query(int now_, int L_, int R_, int pos_) {
if (L_ == R_) return val[now_];
if (pos_ <= mid) return Query(ls, L_, mid, pos_);
return Query(rs, mid + 1, R_, pos_);
}
#undef ls
#undef rs
#undef mid
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) a[i] = read();
Seg::Build(root[0], 1, n);
for (int i = 1; i <= m; ++ i) {
int pre = read(), opt = read(), pos = read();
if (opt == 1) {
int val = read();
Seg::Modify(root[i], root[pre], 1, n, pos, val);
} else {
printf("%d\n", Seg::Query(root[pre], 1, n, pos));
root[i] = root[pre];
}
}
return 0;
}
动态开点线段树合并
动态开点 的线段树的合并。
总复杂度是 \(O(n\log n)\),\(n\) 是节点个数。
P4556 [Vani有约会]雨天的尾巴 /【模板】线段树合并
树上路径涂色,修改完后单点查询最多涂色数。
//知识点:线段树合并
/*
By:Luckyblock
*/
#include <cstdio>
#include <ctype.h>
#include <cstring>
#include <algorithm>
#define LL long long
const int kMaxn = 3e5 + 10;
const int kInf = 1e5 + 5;
//=============================================================
int n, m, root[kMaxn];
int edge_num, head[kMaxn], v[kMaxn << 1], ne[kMaxn << 1];
int dep[kMaxn], fa[kMaxn], son[kMaxn], top[kMaxn], size[kMaxn];
int ans[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void GetMax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void GetMin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
void AddEdge(int u_, int v_) {
v[++ edge_num] = v_;
ne[edge_num] = head[u_];
head[u_] = edge_num;
}
namespace Cut {
void Dfs1(int u_, int fat_) {
fa[u_] = fat_;
size[u_] = 1;
dep[u_] = dep[fat_] + 1;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ == fat_) continue ;
Dfs1(v_, u_);
size[u_] += size[v_];
if (size[v_] > size[son[u_]]) son[u_] = v_;
}
}
void Dfs2(int u_, int topn_) {
top[u_] = topn_;
if (son[u_]) Dfs2(son[u_], topn_);
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ == son[u_] || v_ == fa[u_]) continue ;
Dfs2(v_, v_);
}
}
int GetLca(int x_, int y_) {
for (; top[x_] != top[y_]; x_ = fa[top[x_]]) {
if (dep[top[x_]] < dep[top[y_]]) std :: swap(x_, y_);
}
return dep[x_] < dep[y_] ? x_ : y_;
}
}
namespace Seg {
#define ls (lson[now_])
#define rs (rson[now_])
#define mid ((L_+R_)>>1)
int node_num, lson[kMaxn << 5], rson[kMaxn << 5], sum[kMaxn << 5];
void Pushup(int now_) {
sum[now_] = std::max(sum[ls], sum[rs]);
}
void Insert(int &now_, int L_, int R_, int pos_, int val_) {
if (! now_) now_ = ++ node_num;
if (L_ == R_) {
sum[now_] += val_;
return ;
}
if (pos_ <= mid) Insert(ls, L_, mid, pos_, val_);
else Insert(rs, mid + 1, R_, pos_, val_);
Pushup(now_);
}
//不新建结点写法,now 直接连上了 y。
//若之后 now 还和其他的合并,就破坏了 y 的结构。
int Merge(int now_, int y_, int L_, int R_) {
if (! now_ || ! y_) return now_ + y_;
if (L_ == R_) {
sum[now_] += sum[y_];
return now_;
}
ls = Merge(ls, lson[y_], L_, mid);
rs = Merge(rs, rson[y_], mid + 1, R_);
Pushup(now_);
return now_;
}
int Query(int now_, int L_, int R_) {
if (L_ == R_) return L_;
if (sum[ls] >= sum[rs]) return Query(ls, L_, mid);
else return Query(rs, mid + 1, R_);
}
#undef ls
#undef rs
#undef mid
}
void Dfs(int u_) {
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ == fa[u_]) continue ;
Dfs(v_);
root[u_] = Seg::Merge(root[u_], root[v_], 1, kInf);
}
if (! Seg::sum[root[u_]]) ans[u_] = 0;
else ans[u_] = Seg::Query(root[u_], 1, kInf);
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i < n; ++ i) {
int u = read(), v = read();
AddEdge(u, v), AddEdge(v, u);
}
Cut::Dfs1(1, 0), Cut::Dfs2(1, 1);
for (int i = 1; i <= m; ++ i) {
int u = read(), v = read(), z = read();
int lca = Cut::GetLca(u, v);
Seg::Insert(root[u], 1, kInf, z, 1);
Seg::Insert(root[v], 1, kInf, z, 1);
Seg::Insert(root[lca], 1, kInf, z, - 1);
if (lca != 1) Seg::Insert(root[fa[lca]], 1, kInf, z, - 1);
}
Dfs(1);
for (int i = 1; i <= n; ++ i) printf("%d\n", ans[i]);
return 0;
}
主席树
动态开点权值线段树。
静态区间最 k 大。
//知识点:主席树
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 2e5 + 10;
//=============================================================
int n, m, d_num, a[kMaxn], data[kMaxn], map[kMaxn];
int root[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void Prepare() {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) {
a[i] = data[i] = read();
}
std::sort(data + 1, data + n + 1);
data[0] = -0x3f3f3f3f;
for (int i = 1; i <= n; ++ i) {
if (data[i] != data[i - 1]) d_num ++;
data[d_num] = data[i];
}
for (int i = 1; i <= n; ++ i) {
int ori = a[i];
a[i] = std :: lower_bound(data + 1, data + d_num + 1, ori) - data;
map[a[i]] = ori;
}
}
namespace Hjt {
#define ls lson[now_]
#define rs rson[now_]
#define mid ((L_+R_)>>1)
int node_num, lson[kMaxn << 5], rson[kMaxn << 5], size[kMaxn << 5];
void Insert(int &now_, int pre_, int L_, int R_, int val_) {
now_ = ++ node_num;
size[now_] = size[pre_] + 1;
ls = lson[pre_], rs = rson[pre_];
if (L_ == R_) return ;
if (val_ <= mid) Insert(ls, lson[pre_], L_, mid, val_);
else Insert(rs, rson[pre_], mid + 1, R_, val_);
}
int Query(int r_, int l_, int L_, int R_, int k_) {
if (L_ == R_) return L_;
int sz = size[lson[r_]] - size[lson[l_]];
if (k_ <= sz) return Query(lson[r_], lson[l_], L_, mid, k_);
else return Query(rson[r_], rson[l_], mid + 1, R_, k_ - sz);
}
#undef ls
#undef rs
#undef mid
}
//=============================================================
int main() {
Prepare();
for (int i = 1; i <= n; ++ i) {
Hjt::Insert(root[i], root[i - 1], 1, d_num, a[i]);
}
for (int i = 1; i <= m; ++ i) {
int l = read(), r = read(), k = read();
printf("%d\n", map[Hjt::Query(root[r], root[l - 1], 1, d_num, k)]);
}
return 0;
}
带修主席树
树状数组套主席树。
//知识点:主席树
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <vector>
#define LL long long
const int kN = 1e6 + 10;
//=============================================================
struct Opt {
bool type;
int l, r, k, pos, val;
} q[kN];
int n, m, d_num, data[kN], num[kN];
std::vector <int> tmp1, tmp2;
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
namespace Hjt {
#define ls lson[now_]
#define rs rson[now_]
#define mid ((L_+R_)>>1)
int node_num, root[kN], sz[kN << 5], lson[kN << 5], rson[kN << 5];
void Modify(int &now_, int L_, int R_, int pos_, int val_) {
if (!now_) now_ = ++ node_num;
sz[now_] += val_;
if (L_ == R_) return ;
if (pos_ <= mid) Modify(ls, L_, mid, pos_, val_);
else Modify(rs, mid + 1, R_, pos_, val_);
}
int Query(int L_, int R_, int k_) {
if (L_ == R_) return L_;
int sizel = 0;
for (int i = 0; i < tmp1.size(); ++ i) sizel -= sz[lson[tmp1[i]]];
for (int i = 0; i < tmp2.size(); ++ i) sizel += sz[lson[tmp2[i]]];
if (k_ <= sizel) {
for (int i = 0; i < tmp1.size(); ++ i) tmp1[i] = lson[tmp1[i]];
for (int i = 0; i < tmp2.size(); ++ i) tmp2[i] = lson[tmp2[i]];
return Query(L_, mid, k_);
}
for (int i = 0; i < tmp1.size(); ++ i) tmp1[i] = rson[tmp1[i]];
for (int i = 0; i < tmp2.size(); ++ i) tmp2[i] = rson[tmp2[i]];
return Query(mid + 1, R_, k_ - sizel);
}
}
namespace Bit {
#define low(x) (x&-x)
void Add(int pos_, int val_) {
int p = std::lower_bound(data + 1, data + d_num + 1, num[pos_]) - data;
for (int i = pos_; i <= n; i += low(i)) {
Hjt::Modify(Hjt::root[i], 1, d_num, p, val_);
}
}
int Query(int l_, int r_, int k_) {
tmp1.clear(), tmp2.clear();
for (int i = l_ - 1; i; i -= low(i)) tmp1.push_back(Hjt::root[i]);
for (int i = r_; i; i -= low(i)) tmp2.push_back(Hjt::root[i]);
return Hjt::Query(1, d_num, k_);
}
}
void Init() {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) data[++ d_num] = num[i] = read();
for (int i = 1; i <= m; ++ i) {
char opt[5]; scanf("%s", opt + 1);
q[i].type = (opt[1] == 'Q');
if (q[i].type) {
q[i].l = read(), q[i].r = read(), q[i].k = read();
} else {
q[i].pos = read(), data[++ d_num] = q[i].val = read();
}
}
std::sort(data + 1, data + d_num + 1);
d_num = std::unique(data + 1, data + d_num + 1) - data - 1;
for (int i = 1; i <= n; ++ i) Bit::Add(i, 1);
}
//=============================================================
int main() {
Init();
for (int i = 1; i <= m; ++ i) {
if (q[i].type) {
int l = q[i].l, r = q[i].r, k = q[i].k;
printf("%d\n", data[Bit::Query(l, r, k)]);
} else {
int pos = q[i].pos, val = q[i].val;
Bit::Add(pos, -1);
num[pos] = val;
Bit::Add(pos, 1);
}
}
return 0;
}
线段树分治
有这样的一种模型:
- 给定一些仅在 给定时间范围 内有效的操作。
- 询问某个时间点所有操作的贡献。
考虑离线操作,对时间轴建立线段树,将每个操作转化为线段树上的区间标记操作。
查询时遍历整棵线段树,到达每个节点时执行相应的操作,到达叶节点统计贡献,回溯时撤销操作的影响
//知识点:线段树分治,可撤销并查集
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <map>
#include <vector>
#define pr std::pair
#define mp std::make_pair
#define LL long long
const int kMaxn = 5e4 + 10;
const int kMaxm = 5e5 + 10;
//=============================================================
struct Operation {
int type, u, v;
} opt[kMaxm << 1];
struct Stack {
int u, v, fa, size;
} st[kMaxm];
int n, m, k, top, fa[kMaxn], size[kMaxn];
std::map <pr <int, int>, int> tim;
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
int Find(int x_) {
while (x_ != fa[x_]) x_ = fa[x_];
return x_;
}
void Union(int u_, int v_) {
int fu = Find(u_), fv = Find(v_);
if (size[fu] > size[fv]) std :: swap(fu, fv);
st[++ top] = (Stack) {fu, fv, fa[fu], size[fu]};
if (fu != fv) {
fa[fu] = fv;
size[fv] += size[fu];
size[fu] = 0;
}
}
void Restore() {
Stack now = st[top];
if (now.u != now.v) {
fa[now.u] = now.fa;
size[now.v] -= now.size;
size[now.u] = now.size;
}
top --;
}
namespace Seg {
#define ls (now_<<1)
#define rs (now_<<1|1)
#define mid ((L_+R_)>>1)
std::vector <int> t[kMaxm << 2];
void Modify(int now_, int L_, int R_, int l_, int r_, int val_) {
if (l_ <= L_ && R_ <= r_) {
t[now_].push_back(val_);
return ;
}
if (l_ <= mid) Modify(ls, L_, mid, l_, r_, val_);
if (r_ > mid) Modify(rs, mid + 1, R_, l_, r_, val_);
}
void Solve(int now_, int L_, int R_) {
for (int i = 0, lim = t[now_].size(); i < lim; ++ i) {
Operation nowo = opt[t[now_][i]];
Union(nowo.u, nowo.v);
}
if (L_ == R_) {
if (opt[L_].type == 2) {
int u = opt[L_].u, v = opt[L_].v;
printf("%c\n", Find(u) == Find(v) ? 'Y' : 'N');
}
for (int i = 0, lim = t[now_].size(); i < lim; ++ i) {
Restore();
}
return ;
}
Solve(ls, L_, mid);
Solve(rs, mid + 1, R_);
for (int i = 0, lim = t[now_].size(); i < lim; ++ i) {
Restore();
}
}
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= m; ++ i) {
int nowo = read(), u = read(), v = read();
opt[i] = (Operation) {nowo, u, v};
if (u > v) std::swap(u, v);
if (nowo == 0) {
tim[mp(u, v)] = i;
} else if (nowo == 1) {
Seg::Modify(1, 1, m, tim[mp(u, v)], i - 1, i);
tim[mp(u, v)] = 0;
}
}
for (std::map <pr<int, int>, int>::iterator it = tim.begin(); it != tim.end(); ++ it) {
if (it->second) {
Seg::Modify(1, 1, m, it->second, m, it->second);
}
}
for (int i = 1; i <= n; ++ i) {
fa[i] = i;
size[i] = 1;
}
Seg::Solve(1, 1, m);
return 0;
}
轻重链剖分
将一棵树分为多条重链,将树信息转化为序列信息。
维护路径信息时用多条重链信息覆盖路径。
路径加,路径和,子树加,子树和,树上路径问题转化为 dfn 序上的序列问题,线段树维护序列信息。
我八十年没写树剖了居然 1A 了= =
//知识点:重链剖分,线段树
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e5 + 10;
//=============================================================
int n, m, root, p, ori[kMaxn], a[kMaxn];
int e_num, head[kMaxn], v[kMaxn << 1], ne[kMaxn << 1];
int fa[kMaxn], son[kMaxn], dep[kMaxn], size[kMaxn], top[kMaxn];
int d_num, dfn[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
v[++ e_num] = v_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
namespace Seg {
#define ls (now_<<1)
#define rs (now_<<1|1)
#define mid ((L_+R_)>>1)
LL sum[kMaxn << 2], tag[kMaxn << 2];
void Pushup(int now_) {
sum[now_] = (sum[ls] + sum[rs]) % p;
}
void Pushdown(int now_, int L_, int R_) {
sum[ls] += 1ll * tag[now_] * (mid - L_ + 1) % p;
sum[rs] += 1ll * tag[now_] * (R_ - mid) % p;
tag[ls] += tag[now_] % p;
tag[rs] += tag[now_] % p;
sum[ls] %= p, sum[rs] %= p;
tag[ls] %= p, tag[rs] %= p;
tag[now_] = 0ll;
}
void Build(int now_, int L_, int R_) {
if (L_ == R_) {
sum[now_] = a[L_] % p;
tag[now_] = 0ll;
return ;
}
Build(ls, L_, mid);
Build(rs, mid + 1, R_);
Pushup(now_);
}
void Modify(int now_, int L_, int R_, int l_, int r_, LL val_) {
if (l_ <= L_ and R_ <= r_) {
sum[now_] += 1ll * (R_ - L_ + 1) * val_ % p;
tag[now_] += val_ % p;
sum[now_] %= p, tag[now_] %= p;
return ;
}
Pushdown(now_, L_, R_);
if (l_ <= mid) Modify(ls, L_, mid, l_, r_, val_);
if (r_ > mid) Modify(rs, mid + 1, R_, l_, r_, val_);
Pushup(now_);
}
LL Query(int now_, int L_, int R_, int l_, int r_) {
if (l_ <= L_ and R_ <= r_) return sum[now_];
Pushdown(now_, L_, R_);
LL ret = 0;
if (l_ <= mid) ret += Query(ls, L_, mid, l_, r_);
if (r_ > mid) ret += Query(rs, mid + 1, R_, l_, r_);
return ret % p;
}
#undef ls
#undef rs
#undef mid
}
namespace Cut {
void Dfs1(int u_, int fa_) {
fa[u_] = fa_;
size[u_] = 1;
dep[u_] = dep[fa_] + 1;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ == fa_) continue ;
Dfs1(v_, u_);
if (size[v_] > size[son[u_]]) son[u_] = v_;
size[u_] += size[v_];
}
}
void Dfs2(int u_, int top_) {
dfn[u_] = ++ d_num;
a[d_num] = ori[u_];
top[u_] = top_;
if (son[u_]) Dfs2(son[u_], top_);
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ == fa[u_] or v_ == son[u_]) continue ;
Dfs2(v_, v_);
}
}
void Modify(int u_, int v_, int val_) {
for (; top[u_] != top[v_]; u_ = fa[top[u_]]) {
if (dep[top[u_]] < dep[top[v_]]) {
std::swap(u_, v_);
}
Seg::Modify(1, 1, n, dfn[top[u_]], dfn[u_], val_);
}
if (dep[u_] < dep[v_]) std::swap(u_, v_);
Seg::Modify(1, 1, n, dfn[v_], dfn[u_], val_);
}
LL Query(int u_, int v_) {
LL ret = 0;
for (; top[u_] != top[v_]; u_ = fa[top[u_]]) {
if (dep[top[u_]] < dep[top[v_]]) {
std::swap(u_, v_);
}
ret += Seg::Query(1, 1, n, dfn[top[u_]], dfn[u_]);
ret %= p;
}
if (dep[u_] < dep[v_]) std::swap(u_, v_);
ret += Seg::Query(1, 1, n, dfn[v_], dfn[u_]);
ret %= p;
return ret;
}
}
//=============================================================
int main() {
n = read(), m = read(), root = read(), p = read();
for (int i = 1; i <= n; ++ i) ori[i] = read();
for (int i = 1; i < n; ++ i) {
int u_ = read(), v_ = read();
AddEdge(u_, v_), AddEdge(v_, u_);
}
Cut::Dfs1(root, 0), Cut::Dfs2(root, root);
Seg::Build(1, 1, n);
while (m --) {
int opt = read();
if (opt == 1) {
int u_ = read(), v_ = read(), val_ = read() % p;
Cut::Modify(u_, v_, val_);
} else if (opt == 2) {
int u_ = read(), v_ = read();
printf("%lld\n", Cut::Query(u_, v_));
} else if (opt == 3) {
int x_ = read(), val_ = read() % p;
Seg::Modify(1, 1, n, dfn[x_], dfn[x_] + size[x_] - 1, val_);
} else if (opt == 4) {
int x_ = read();
printf("%lld\n", Seg::Query(1, 1, n, dfn[x_], dfn[x_] + size[x_] - 1));
}
}
return 0;
}
Splay
普通平衡树
Splay 是一种二叉搜索树的维护方式。它通过不断将某节点旋转到根节点,使得整棵树仍满足二叉搜索树的性质,且保持平衡不至于退化为链,以保证复杂度。
//知识点:Splay
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
namespace Splay {
#define f fa[now_]
#define ls son[now_][0]
#define rs son[now_][1]
const int kMaxNode = 1e6 + 10;
int root, node_num, fa[kMaxNode], son[kMaxNode][2];
int val[kMaxNode], cnt[kMaxNode], siz[kMaxNode];
int top, bin[kMaxNode];
void Pushup(int now_) { //更新节点大小信息
if (!now_) return ;
siz[now_] = cnt[now_];
if (ls) siz[now_] += siz[ls];
if (rs) siz[now_] += siz[rs];
}
int WhichSon(int now_) { //获得儿子类型
return now_ == son[f][1];
}
void Clear(int now_) { //清空节点,同时进行垃圾回收
f = ls = rs = val[now_] = cnt[now_] = siz[now_] = 0;
bin[++ top] = now_;
}
int NewNode(int fa_, int val_) { //建立新节点
int now_ = top ? bin[top --] : ++ node_num; //优先调用垃圾堆
f = fa_, val[now_] = val_, siz[now_] = cnt[now_] = 1;
return now_;
}
void Rotate(int now_) { //旋转操作
int fa_ = f, whichson = WhichSon(now_);
if (fa[f]) son[fa[f]][WhichSon(f)] = now_;
f = fa[f];
son[fa_][whichson] = son[now_][whichson ^ 1];
fa[son[fa_][whichson]] = fa_;
son[now_][whichson ^ 1] = fa_;
fa[fa_] = now_;
Pushup(fa_), Pushup(now_);
}
void Splay(int now_) { //旋转到根操作
for (; f != 0; Rotate(now_)) {
if (fa[f]) Rotate(WhichSon(now_) == WhichSon(f) ? f : now_);
}
root = now_;
}
void Insert(int now_, int fa_, int val_) { //插入操作
if (now_ && val[now_] != val_) {
Insert(son[now_][val[now_] < val_], now_, val_);
return ;
}
if (val[now_] == val_) ++ cnt[now_];
if (!now_) {
now_ = NewNode(fa_, val_);
if (f) son[f][val[f] < val_] = now_;
}
Pushup(now_), Pushup(f), Splay(now_); //注意旋转到根
return ;
}
int Find(int now_, int val_) { //将权值 val 对应节点旋转至根。在平衡树上二分。
if (!now_) return false; //不存在
if (val_ < val[now_]) return Find(ls, val_);
if (val_ == val[now_]) {
Splay(now_);
return true;
}
return Find(rs, val_);
}
void Delete(int val_) { //删除一个权值 val
if (!Find(root, val_)) return ; //将 val 转到根
if (cnt[root] > 1) {
-- cnt[root];
Pushup(root);
return ;
}
int oldroot = root;
if (!son[root][0] && !son[root][1]) {
root = 0;
} else if (!son[root][0]) {
root = son[root][1], fa[root] = 0;
} else if (!son[root][1]) {
root = son[root][0], fa[root] = 0;
} else if (son[root][0] && son[root][1]) {
//将中序遍历中 root 前的一个元素作为新的 root。该元素即为 root 左子树中最大的元素。
int leftmax = son[root][0];
while (son[leftmax][1]) leftmax = son[leftmax][1];
Splay(leftmax); //转到根
son[root][1] = son[oldroot][1], fa[son[root][1]] = root; //继承信息
}
Clear(oldroot), Pushup(root);
}
int QueryRank(int val_) { //查询 val_ 的排名
Insert(root, 0, val_); //先插入,将其转到根,查询左子树大小
int ret = siz[son[root][0]] + 1;
Delete(val_);
return ret;
}
int QueryVal(int rk_) { //查询排名为 rk 的权值。在平衡树上二分。
int now_ = root;
while (true) {
if (!now_) return -1;
if (ls && siz[ls] >= rk_) { //注意 =
now_ = ls;
}else {
rk_ -= ls ? siz[ls] : 0;
if (rk_ <= cnt[now_]) { //该权值即为 val[now_]
Splay(now_);
return val[now_];
}
rk_ -= cnt[now_];
now_ = rs;
}
}
}
int QueryPre(int val_) { //查询前驱
Insert(root, 0, val_); //插入 val_,将其旋转到根,前驱(中序遍历中前一个严肃)即为左子树中最大的元素。
int now_ = son[root][0];
while (rs) now_ = rs;
Delete(val_);
return val[now_];
}
int QueryNext(int val_) { //查询后继
Insert(root, 0, val_);
int now_ = son[root][1];
while (ls) now_ = ls;
Delete(val_);
return val[now_];
}
}
//=============================================================
int main() {
int n = read();
while (n --) {
int opt = read(), x = read();
if (opt == 1) {
Splay::Insert(Splay::root, 0, x);
} else if (opt == 2) {
Splay::Delete(x);
} else if (opt == 3) {
printf("%d\n", Splay::QueryRank(x));
} else if (opt == 4) {
printf("%d\n", Splay::QueryVal(x));
} else if (opt == 5) {
printf("%d\n", Splay::QueryPre(x));
} else if (opt == 6) {
printf("%d\n", Splay::QueryNext(x));
}
}
return 0;
}
文艺平衡树
将被操作区间放置到一棵单独的子树内,对整棵子树打标记实现区间修改。注意标记下放的写法,反转标记取反并交换左右儿子。
//知识点:Splay
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, m;
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
namespace Splay {
#define f fa[now_]
#define ls son[now_][0]
#define rs son[now_][1]
const int kMaxNode = kN;
int root, node_num, fa[kMaxNode], son[kMaxNode][2];
int val[kMaxNode], siz[kMaxNode], cnt[kMaxNode];
bool tag[kN];
int top, bin[kMaxNode];
void Pushup(int now_) {
if (!now_) return ;
siz[now_] = cnt[now_];
if (ls) siz[now_] += siz[ls];
if (rs) siz[now_] += siz[rs];
}
void Pushdown(int now_) {
if (!now_ || !tag[now_]) return ;
tag[ls] ^= 1, tag[rs] ^= 1;
std::swap(ls, rs);
tag[now_] = false;
}
void Clear(int now_) {
f = ls = rs = val[now_] = cnt[now_] = siz[now_] = 0;
bin[++ top] = now_;
}
int NewNode(int fa_, int val_) {
int now_ = top ? bin[top --] : ++ node_num;
f = fa_, val[now_] = val_, cnt[now_] = siz[now_] = 1;
return now_;
}
bool WhichSon(int now_) {
return now_ == son[f][1];
}
void Rotate(int now_) {
int fa_ = f, whichson = WhichSon(now_);
if (fa[f]) son[fa[f]][WhichSon(f)] = now_;
f = fa[f];
son[fa_][whichson] = son[now_][whichson ^ 1];
fa[son[fa_][whichson]] = fa_;
son[now_][whichson ^ 1] = fa_;
fa[fa_] = now_;
Pushup(fa_), Pushup(now_);
}
void Splay(int now_, int fa_) {
for (; f != fa_; Rotate(now_)) {
if (fa[f] != fa_) Rotate(WhichSon(f) == WhichSon(now_) ? f : now_);
}
if (!fa_) root = now_;
}
void Build(int &now_, int fa_, int L_, int R_) {
if (L_ > R_) return;
int mid = (L_ + R_) >> 1;
now_ = NewNode(fa_, mid);
Build(ls, now_, L_, mid - 1);
Build(rs, now_, mid + 1, R_);
Pushup(now_);
}
int Kth(int now_, int rk_) {
Pushdown(now_);
if (ls && rk_ <= siz[ls]) return Kth(ls, rk_);
if (rk_ <= siz[ls] + cnt[now_]) return now_;
return Kth(rs, rk_ - siz[ls] - cnt[now_]);
}
void Modify(int L_, int R_) {
int x = Kth(root, L_ - 1 + 1), y = Kth(root, R_ + 1 + 1); //偏移量
Splay(x, 0), Splay(y, x);
tag[son[son[root][1]][0]] ^= 1;
}
void Print(int now_) {
Pushdown(now_);
if (ls) Print(ls);
if (val[now_] && val[now_] <= n) printf("%d ", val[now_]);
if (rs) Print(rs);
}
}
//=============================================================
int main() {
n = read(), m = read();
Splay::Build(Splay::root, 0, 0, n + 1);
while (m --) {
int l = read(), r = read();
Splay::Modify(l ,r);
}
Splay::Print(Splay::root);
return 0;
}
Link-Cut Tree
用于解决动态树问题,通过 Splay 动态维护树链剖分结构,从而将树上问题转化为 Splay 上的序列问题的一种数据结构。
支持动态加删除边,修改路径信息,查询路径、子树信息。不支持修改子树。
//知识点:LCT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e5 + 10;
const LL mod = 51061;
//=============================================================
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
namespace LCT {
#define f fa[now_]
#define ls son[now_][0]
#define rs son[now_][1]
const int kMaxNode = kN;
int fa[kMaxNode], son[kMaxNode][2], val[kMaxNode]; //Splay 的结构信息
LL siz[kMaxNode], sum[kMaxNode], tagplus[kMaxNode], tagprod[kMaxNode]; //子树大小,子树和,两种标记
bool tagrev[kMaxNode]; //Splay 的子树反转标记
void Pushup(int now_) { //维护子树和 和 子树大小
sum[now_] = (sum[ls] + sum[rs] + val[now_]) % mod;
siz[now_] = siz[ls] + siz[rs] + 1;
}
void PushReverse(int now_) { //子树反转标记,使得 Splay 节点的中序遍历反向。若原 splay 表示一条自顶向下的链,反转相当于将 splay 表示的链的边反向,父子关系互换。
if (!now_) return;
std::swap(ls, rs);
tagrev[now_] ^= 1;
}
void PushPlus(int now_, LL val_) {
if (!now_) return;
val[now_] = (val[now_] + val_) % mod;
sum[now_] = (sum[now_] + siz[now_] * val_ % mod) % mod;
tagplus[now_] = (tagplus[now_] + val_) % mod;
}
void PushProd(int now_, LL val_) {
if (!now_) return;
val[now_] = val[now_] * val_ % mod;
sum[now_] = sum[now_] * val_ % mod;
tagplus[now_] = tagplus[now_] * val_ % mod;
tagprod[now_] = tagprod[now_] * val_ % mod;
}
void Pushdown(int now_) { //注意下放顺序
LL plus = tagplus[now_], prod = tagprod[now_], rev = tagrev[now_];
if (prod != 1) PushProd(ls, prod), PushProd(rs, prod);
if (plus) PushPlus(ls, plus), PushPlus(rs, plus);
if (rev) PushReverse(ls), PushReverse(rs);
tagprod[now_] = 1, tagplus[now_] = 0, tagrev[now_] = 0;
}
bool IsRoot(int now_) { //判断 now_ 是否为当前 Splay 的根
return son[f][0] != now_ && son[f][1] != now_;
}
bool WhichSon(int now_) {
return son[f][1] == now_;
}
void Rotate(int now_) {
int fa_ = f, w = WhichSon(now_);
if (!IsRoot(f)) son[fa[f]][WhichSon(f)] = now_;
f = fa[f];
son[fa_][w] = son[now_][w ^ 1];
fa[son[fa_][w]] = fa_;
son[now_][w ^ 1] = fa_;
fa[fa_] = now_;
Pushup(fa_), Pushup(now_);
}
void Update(int now_) { //将 Splay 路径上的所有标记下放
if (!IsRoot(now_)) Update(f);
Pushdown(now_);
}
void Splay(int now_) {
Update(now_);
for (; !IsRoot(now_); Rotate(now_)) {
if (!IsRoot(f)) Rotate(WhichSon(f) == WhichSon(now_) ? f : now_);
}
}
void Access(int now_) { //使得树中由根->now 的链成为实链,构造出由它们组成的 Splay,满足 Splay 的中序遍历深度递减的性质。
//自下向上构建,舍弃父亲的原有右儿子,换成 -> last 的链。
for (int last_ = 0; now_; last_ = now_, now_ = f) {
Splay(now_), rs = last_;
Pushup(now_);
}
}
void MakeRoot(int now_) { //使 now 成为原树的根
Access(now_); //先使得树中由根->now 的链成为实链,构造出由它们组成的 Splay。
Splay(now_); //使 now 成为 splay 的根节点
PushReverse(now_); //将根->now 的链反转,使 now 成为原树的根。原理参考 PushReverse 函数的注释。
}
int Find(int now_) { //找到 now_ 所在原树的根
Access(now_);
Splay(now_);
while (ls) Pushdown(now_), now_ = ls; //使得树中由根->now 的链成为实链,构造出由它们组成的 Splay,再找到 Splay 中序遍历的第一个元素,即为原树的根。
Splay(now_); //为了下一步操作,把根再转回去
return now_;
}
void Split(int x_, int y_) { //构造由路径 x->y 组成的 Splay
MakeRoot(x_); //使 x 成为根,构造出根 -> y 的 Splay 即得,Splay 根的子树信息即为路径信息。
Access(y_);
Splay(y_);
}
void Link(int x_, int y_) { //加边 (x,y)
MakeRoot(x_); //使 x 成为根,再给根一个父亲
if (Find(y_) != x_) fa[x_] = y_;
}
void Cut(int x_, int y_) { //删边 (x,y)
MakeRoot(x_); //使 x 成为根
//Find(y_) != x_ 保证 y 与 x 连通
//在 Find 函数中 Access(y) 之后,y 所在的 splay 由 x->y 的链构成。又 x 是 splay 的根,y 的中序遍历在 x 后,则若边 (x,y) 存在,则 y 的位置只有一种可能:
//y 是 x 的右儿子,且 y 没有左儿子。从而保证 y 在中序遍历中是 x 的后一个。
if (Find(y_) != x_ || fa[y_] != x_ || son[y_][0]) return ;
fa[y_] = son[x_][1] = 0; //断绝父子关系
Pushup(x_);
}
void Modify(int x_, int y_, int val_, int type) { //路径修改
Split(x_, y_); //给路径构成的 Splay 打标记
if (!type) PushPlus(y_, val_);
else PushProd(y_, val_);
}
LL Query(int x_, int y_) { //路径查询
Split(x_, y_); //返回路径构成的 Splay 的信息
return sum[y_];
}
}
//=============================================================
signed main() {
int n = read(), m = read();
for (int i = 1; i <= n; ++ i) {
LCT::val[i] = LCT::tagprod[i] = LCT::siz[i] = 1;
}
for (int i = 1; i < n; ++ i) {
int u_ = read(), v_ = read();
LCT::Link(u_, v_);
}
for(int i = 1; i <= m; i ++) {
char opt[2]; scanf("%s", opt);
int u1 = read(), v1 = read();
if(opt[0] == '+') LCT::Modify(u1, v1, read(), 0);
if(opt[0] == '*') LCT::Modify(u1, v1, read(), 1);
if(opt[0] == '/') printf("%lld\n", LCT::Query(u1, v1));
if(opt[0] == '-') {
int u2 = read(), v2 = read();
LCT::Cut(u1, v1), LCT::Link(u2, v2);
}
}
return 0;
图论
多源最短路
Floyd
//知识点:多源最短路
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e3 + 10;
//=============================================================
int n, m, s, f[kMaxn][kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void Floyd() {
for (int i = 1; i <= n; ++ i) f[i][i] = 0;
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
for (int k = 1; k <= n; ++ k) {
Chkmin(f[j][k], f[j][i] + f[i][k]);
}
}
}
}
//=============================================================
int main() {
n = read(), m = read(), s = read();
memset(f, 63, sizeof (f));
for (int i = 1; i <= m; ++ i) {
int u_ = read(), v_ = read(), w_ = read();
Chkmin(f[u_][v_], w_);
}
Floyd();
for (int i = 1; i <= n; ++ i) {
printf("%d ", f[s][i]);
// printf("%d ", f[s][i] >= 0x3f3f3f3f ? 2147483647 : f[s][i]);
}
return 0;
}
单源最短路三合一
Dijkstra,Bellman-Ford,Spfa 三合一代码。
//知识点:单源最短路
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kMaxn = 1e5 + 10;
const int kMaxm = 5e5 + 10;
//=============================================================
int n, m, s;
int e_num, head[kMaxn], v[kMaxm], w[kMaxm], ne[kMaxm];
int dis[kMaxn];
bool vis[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_, int w_) {
v[++ e_num] = v_;
w[e_num] = w_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
void Dijkstra(int s_) {
std::priority_queue <pr <int, int> > q;
memset(dis, 63, sizeof (dis));
memset(vis, 0, sizeof (vis));
dis[s_] = 0;
q.push(mp(0, s_));
while (! q.empty()) {
int u_ = q.top().second;
q.pop();
if (vis[u_]) continue ;
vis[u_] = true;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i], w_ = w[i];
if (dis[u_] + w_ < dis[v_]) {
dis[v_] = dis[u_] + w_;
q.push(mp(-dis[v_], v_));
}
}
}
}
void BellmanFord(int s_) {
memset(dis, 63, sizeof (dis));
dis[s_] = 0;
while (1) {
bool flag = true;
for (int u_ = 1; u_ <= n; ++ u_) {
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i], w_ = w[i];
if (dis[u_] + w_ < dis[v_]) {
dis[v_] = dis[u_] + w_;
flag = false;
}
}
}
if (flag) break ;
}
}
void Spfa(int s_) {
std::queue <int> q;
memset(vis, 0, sizeof (vis));
memset(dis, 63, sizeof (dis));
q.push(s_);
dis[s_] = 0;
vis[s_] = true;
while (! q.empty()) {
int u_ = q.front();
q.pop();
vis[u_] = false;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i], w_ = w[i];
if (dis[u_] + w_ < dis[v_]) {
dis[v_] = dis[u_] + w_;
if (! vis[v_]) {
q.push(v_);
vis[v_] = true;
}
}
}
}
}
//=============================================================
int main() {
n = read(), m = read(), s = read();
for (int i = 1; i <= m; ++ i) {
int u_ = read(), v_ = read(), w_ = read();
AddEdge(u_, v_, w_);
}
Dijkstra(s);
// BellmanFord(s);
// Spfa(s);
for (int i = 1; i <= n; ++ i) {
printf("%d ", dis[i]);
// printf("%d ", dis[i] >= 0x3f3f3f3f ? 2147483647 : dis[i]);
}
return 0;
}
最小生成树
Kruscal
克鲁斯卡尔大战克苏鲁。
基于排序与并查集的贪心加边。
//知识点:最小生成树
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kMaxn = 5e4 + 10;
const int kMaxm = 4e5 + 10;
//=============================================================
int n, m, ans, fa[kMaxn], size[kMaxn];
struct Edge {
int u, v, w;
} e[kMaxm];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
bool CompareEdge(Edge f_, Edge s_) {
return f_.w < s_.w;
}
int Find(int x_) {
return x_ == fa[x_] ? x_ : fa[x_] = Find(fa[x_]);
}
void Union(int u_, int v_) {
int fu = Find(u_), fv = Find(v_);
if (fu == fv) return ;
if (size[fu] > size[fv]) std::swap(fu, fv);
fa[fu] = fv;
size[fv] += size[fu];
}
void Kruscal() {
int tot = 0;
std::sort(e + 1, e + m + 1, CompareEdge);
for (int i = 1; i <= n; ++ i) {
fa[i] = i;
size[i] = 1;
}
for (int i = 1; i <= m && tot < n - 1; ++ i) {
int u_ = e[i].u, v_ = e[i].v, w_ = e[i].w;
if (Find(u_) == Find(v_)) continue ;
Union(u_, v_);
ans += w_;
++ tot;
}
if (tot < n - 1) ans = -1;
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= m; ++ i) {
int u_ = read(), v_ = read(), w_ = read();
e[i] = (Edge) {u_, v_, w_};
}
Kruscal();
if (ans < 0) {
printf("orz\n");
} else {
printf("%d\n", ans);
}
return 0;
}
Prim
类似 Dijkstra,贪心地找将每一个点加入连通块的边的最小值。
用了堆优化。
//知识点:单元最短路
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kMaxn = 5e4 + 10;
const int kMaxm = 4e5 + 10;
//=============================================================
int n, m, ans;
int e_num, head[kMaxn], v[kMaxm], w[kMaxm], ne[kMaxm];
int dis[kMaxn];
bool vis[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_, int w_) {
v[++ e_num] = v_;
w[e_num] = w_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
void Prim() {
std::priority_queue <pr <int, int> > q;
memset(dis, 63, sizeof (dis));
memset(vis, 0, sizeof (vis));
dis[1] = 0;
q.push(mp(0, 1));
int tot = 0;
while (! q.empty() && tot < n) {
int u_ = q.top().second;
q.pop();
if (vis[u_]) continue ;
++ tot;
vis[u_] = true;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i], w_ = w[i];
if (w_ < dis[v_] && !vis[v_]) {
dis[v_] = w_;
q.push(mp(-dis[v_], v_));
}
}
}
if (tot < n) ans = -1;
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= m; ++ i) {
int u_ = read(), v_ = read(), w_ = read();
AddEdge(u_, v_, w_);
AddEdge(v_, u_, w_);
}
Prim();
if (ans < 0) {
printf("orz\n");
} else {
for (int i = 1; i <= n; ++ i) ans += dis[i];
printf("%d\n", ans);
}
return 0;
}
最近公共祖先
倍增
优化暴力上跳,用2 的幂的和拼出跳的深度。
//知识点:最近公共祖先
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 5e5 + 10;
const int kMaxm = kMaxn << 1;
//=============================================================
int n, m, root;
int e_num, head[kMaxn], v[kMaxm], ne[kMaxm];
int dep[kMaxn], fa[kMaxn][21];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
v[++ e_num] = v_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
void Dfs(int u_, int fa_) {
fa[u_][0] = fa_;
dep[u_] = dep[fa_] + 1;
for (int i = 1; i <= 20; ++ i) {
fa[u_][i] = fa[fa[u_][i - 1]][i - 1];
}
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ == fa_) continue ;
Dfs(v_, u_);
}
}
int Lca(int u_, int v_) {
if (dep[u_] < dep[v_]) std::swap(u_, v_);
for (int i = 20; i >= 0; -- i) {
if (dep[fa[u_][i]] >= dep[v_] ) {
u_ = fa[u_][i];
}
}
if (u_ == v_) return u_;
for (int i = 20; i >= 0; -- i) {
if (fa[u_][i] != fa[v_][i]) {
u_ = fa[u_][i];
v_ = fa[v_][i];
}
}
return fa[u_][0];
}
//=============================================================
int main() {
n = read(), m = read(), root = read();
for (int i = 1; i < n; ++ i) {
int u_ = read(), v_ = read();
AddEdge(u_, v_);
AddEdge(v_, u_);
}
Dfs(root, 0);
while (m --) {
int u_ = read(), v_ = read();
printf("%d\n", Lca(u_, v_));
}
return 0;
}
重链剖分
暴力上跳转化为跳重链。
//知识点:重链剖分
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 5e5 + 10;
const int kMaxm = kMaxn << 1;
//=============================================================
int n, m, root;
int e_num, head[kMaxn], v[kMaxm], ne[kMaxm];
int fa[kMaxn], son[kMaxn], dep[kMaxn], size[kMaxn], top[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
v[++ e_num] = v_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
namespace Cut {
void Dfs1(int u_, int fa_) {
fa[u_] = fa_;
size[u_] = 1;
dep[u_] = dep[fa_] + 1;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ == fa_) continue ;
Dfs1(v_, u_);
if (size[v_] > size[son[u_]]) son[u_] = v_;
size[u_] += size[v_];
}
}
void Dfs2(int u_, int top_) {
top[u_] = top_;
if (son[u_]) Dfs2(son[u_], top_);
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ == fa[u_] or v_ == son[u_]) continue ;
Dfs2(v_, v_);
}
}
int Lca(int u_, int v_) {
for (; top[u_] != top[v_]; u_ = fa[top[u_]]) {
if (dep[top[u_]] < dep[top[v_]]) {
std::swap(u_, v_);
}
}
return dep[u_] < dep[v_] ? u_ : v_;
}
}
//=============================================================
int main() {
n = read(), m = read(), root = read();
for (int i = 1; i < n; ++ i) {
int u_ = read(), v_ = read();
AddEdge(u_, v_), AddEdge(v_, u_);
}
Cut::Dfs1(root, 0), Cut::Dfs2(root, root);
while (m --) {
int u_ = read(), v_ = read();
printf("%d\n", Cut::Lca(u_, v_));
}
return 0;
}
RMQ
dfs 维护一个序列。
每当一个点被访问到,就将它加入到序列的尾部。
维护每个节点的深度,在序列中第一次出现的位置。
根据 dfs 的性质,对于节点 x、y,它们第一次出现位置这一段区间内深度最浅的点即为其 LCA。
预处理时间空间复杂度实际上是 \(𝑂(2𝑛 \log(2𝑛))\) 的,单次查询复杂度 \(O(1)\)。
使用局限性比较大,在某些题目中会有作用。
//知识点:LCA
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 5e5 + 10;
const int kM = kN << 1;
//=============================================================
int n, m, root;
int e_num, head[kN], v[kM], ne[kM], dep[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
v[++ e_num] = v_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
namespace ST {
int num, Log2[kN << 1], f[kN << 1][22], fir[kN];
void Dfs(int u_, int fa_) {
dep[u_] = dep[fa_] + 1;
fir[u_] = ++ num;
f[num][0] = u_;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ == fa_) continue ;
Dfs(v_, u_);
f[++ num][0] = u_;
}
}
void Prepare() {
Dfs(root, root);
Log2[1] = 0;
for (int i = 2; i <= num; ++ i) {
Log2[i] = Log2[i >> 1] + 1;
}
for (int i = 1; i <= 21; ++ i) {
for (int j = 1; j + (1 << i) - 1 <= num; ++ j) {
if (dep[f[j][i - 1]] < dep[f[j + (1 << (i - 1))][i - 1]]) {
f[j][i] = f[j][i - 1];
} else {
f[j][i] = f[j + (1 << (i - 1))][i - 1];
}
}
}
}
int Lca(int u_, int v_) {
int l = fir[u_], r = fir[v_];
if (l > r) std::swap(l, r);
int lth = Log2[r - l + 1];
if (dep[f[l][lth]] < dep[f[r - (1 << lth) + 1][lth]]) {
return f[l][lth];
}
return f[r - (1 << lth) + 1][lth];
}
}
//=============================================================
int main() {
n = read(), m = read(), root = read();
for (int i = 1; i < n; ++ i) {
int u_ = read(), v_ = read();
AddEdge(u_, v_), AddEdge(v_, u_);
}
ST::Prepare();
for (int i = 1; i <= n; ++ i) {
int u_ = read(), v_ = read();
printf("%d\n", ST::Lca(u_, v_));
}
return 0;
}
树的直径
图中所有最短路径的最大值即为「直径」,可以用两次 DFS 或者树形 DP 的方法在 O(n) 时间求出树的直径。
之前写的垃圾文章:「笔记」树的直径。
树形 DP
对每个节点维护最长链和次长链,相加取最大值。
//知识点:树的直径
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e4 + 10;
const int kMaxm = kMaxn << 1;
//=============================================================
int n, m, ans;
int e_num, head[kMaxn], v[kMaxm], ne[kMaxm];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
v[++ e_num] = v_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
int Dfs(int u_, int fa_) {
int f1 = 0, f2 = 0;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ == fa_) continue ;
int f = Dfs(v_, u_) + 1;
if (f > f1) {
f2 = f1;
f1 = f;
} else if (f > f2) {
f2 = f;
}
}
Chkmax(ans, f1 + f2);
return f1;
}
//=============================================================
int main() {
n = read();
for (int i = 1; i < n; ++ i) {
int u_ = read(), v_ = read();
AddEdge(u_, v_);
AddEdge(v_, u_);
}
Dfs(1, 0);
printf("%d\n", ans);
}
2 次DFS
距离一个点最远的点一定是直径的一个端点。
一次 DfS 找到一个端点,第二次找到另一个。
由于 dis 初值是 0,所以需要 -1。
//知识点:树的直径
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e4 + 10;
const int kMaxm = kMaxn << 1;
//=============================================================
int n, m, ans;
int e_num, head[kMaxn], v[kMaxm], ne[kMaxm];
int dis[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
v[++ e_num] = v_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
int Dfs(int u_, int fa_) {
dis[u_] = dis[fa_] + 1;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ == fa_) continue ;
Dfs(v_, u_);
}
}
//=============================================================
int main() {
n = read();
for (int i = 1; i < n; ++ i) {
int u_ = read(), v_ = read();
AddEdge(u_, v_);
AddEdge(v_, u_);
}
Dfs(1, 0);
int max_dis = 0, u_ = 0;
for (int i = 1; i <= n; ++ i) {
if (dis[i] > max_dis) {
max_dis = dis[i];
u_ = i;
}
dis[i] = 0;
}
Dfs(u_, 0);
max_dis = 0, u_ = 0;
for (int i = 1; i <= n; ++ i) {
if (dis[i] > max_dis) {
max_dis = dis[i];
u_ = i;
}
dis[i] = 0;
}
printf("%d\n", max_dis - 1);
}
判断负权环
Spfa,判断到达某点最短路上的点数是否 \(>n\)。
若为真则有负环。
//知识点:负环
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kMaxn = 1e5 + 10;
const int kMaxm = 5e5 + 10;
//=============================================================
int n, m;
int e_num, head[kMaxn], v[kMaxm], w[kMaxm], ne[kMaxm];
int dis[kMaxn], cnt[kMaxn];
bool vis[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_, int w_) {
v[++ e_num] = v_;
w[e_num] = w_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
void Init() {
e_num = 0;
memset(head, 0, sizeof (head));
}
bool SpfaBfs(int s_) {
std::queue <int> q;
memset(cnt, 0, sizeof (cnt));
memset(vis, 0, sizeof (vis));
memset(dis, 63, sizeof (dis));
q.push(s_);
dis[s_] = 0;
vis[s_] = true;
while (! q.empty()) {
int u_ = q.front();
q.pop();
vis[u_] = false;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i], w_ = w[i];
if (dis[u_] + w_ < dis[v_]) {
dis[v_] = dis[u_] + w_;
cnt[v_] = cnt[u_] + 1;
if (cnt[v_] > n) return true;
if (! vis[v_]) {
q.push(v_);
vis[v_] = true;
}
}
}
}
return false;
}
//=============================================================
int main() {
int T = read();
while (T --) {
Init();
n = read(), m = read();
for (int i = 1; i <= m; ++ i) {
int u_ = read(), v_ = read(), w_ = read();
AddEdge(u_, v_, w_);
if (w_ >= 0) AddEdge(v_, u_, w_);
}
printf("%s\n", SpfaBfs(1) ? "YES" : "NO");
}
return 0;
}
差分约束
有 \(x_i -x_j\le k\) 的一堆不等式。
转化为单源最短路中三角不等式 \(dis_i\le dis_j + k\) 的形式,从 \(j\) 向 \(i\) 连一条权值为 \(k\) 的单向边。
建立超级源点,与各点距离为 0,跑最短路。
若有负环则无解,否则最短路即为一组合法解。
合法解同加 \(\Delta\) 仍为合法解。
//知识点:负环
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kMaxn = 1e5 + 10;
const int kMaxm = 5e5 + 10;
//=============================================================
int n, m;
int e_num, head[kMaxn], v[kMaxm], w[kMaxm], ne[kMaxm];
int dis[kMaxn], cnt[kMaxn];
bool vis[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_, int w_) {
v[++ e_num] = v_;
w[e_num] = w_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
void Init() {
e_num = 0;
memset(head, 0, sizeof (head));
}
bool SpfaBfs(int s_) {
std::queue <int> q;
memset(cnt, 0, sizeof (cnt));
memset(vis, 0, sizeof (vis));
memset(dis, 63, sizeof (dis));
q.push(s_);
dis[s_] = 0;
vis[s_] = true;
while (! q.empty()) {
int u_ = q.front();
q.pop();
vis[u_] = false;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i], w_ = w[i];
if (dis[u_] + w_ < dis[v_]) {
dis[v_] = dis[u_] + w_;
cnt[v_] = cnt[u_] + 1;
if (cnt[v_] > n) return true;
if (! vis[v_]) {
q.push(v_);
vis[v_] = true;
}
}
}
}
return false;
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= m; ++ i) {
int v_ = read(), u_ = read(), w_ = read();
AddEdge(u_, v_, w_);
}
for (int i = 1; i <= n; ++ i) {
AddEdge(0, i, 0);
}
if (SpfaBfs(0)) {
printf("NO\n");
return 0;
}
for (int i = 1; i <= n; ++ i) {
printf("%d ", dis[i]);
}
return 0;
}
强连通相关
缩点
缩点 + Topsort DP。
//知识点:缩点
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#include <stack>
#include <vector>
#define LL long long
const int kMaxn = 1e4 + 10;
const int kMaxm = 2e5 + 10;
//=============================================================
int n, m, ans, ori[kMaxn];
int e_num, head[kMaxn], v[kMaxm], ne[kMaxm];
int d_num, b_num, dfn[kMaxn], low[kMaxn], bel[kMaxn], val[kMaxn];
int into[kMaxn], f[kMaxn];
std::vector <int> newv[kMaxn];
std::stack <int> st;
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
v[++ e_num] = v_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
void Tarjan(int u_) {
dfn[u_] = low[u_] = ++ d_num;
st.push(u_);
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (! dfn[v_]) {
Tarjan(v_);
Chkmin(low[u_], low[v_]);
} else if (! bel[v_]) {
Chkmin(low[u_], dfn[v_]);
}
}
if (dfn[u_] == low[u_]) { //
++ b_num;
for (int now = 0; now != u_; st.pop()) {
now = st.top();
bel[now] = b_num;
val[b_num] += ori[now];
}
}
}
void Topsort() {
std::queue <int> q;
for (int i = 1; i <= b_num; ++ i) {
if (! into[i]) {
f[i] = val[i];
q.push(i);
}
}
while (! q.empty()) {
int u_ = q.front();
q.pop();
for (int i = 0, lim = newv[u_].size(); i < lim; ++ i) {
int v_ = newv[u_][i];
Chkmax(f[v_], f[u_] + val[v_]);
into[v_] --;
if (! into[v_]) {
q.push(v_);
}
}
}
for (int i = 1; i <= b_num; ++ i) {
Chkmax(ans, f[i]);
}
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) ori[i] = read();
for (int i = 1; i <= m; ++ i) {
int u_ = read(), v_ = read();
if (u_ == v_) continue ;
AddEdge(u_, v_);
}
for (int i = 1; i <= n; ++ i) {
if (! dfn[i]) Tarjan(i);
}
for (int u_ = 1; u_ <= n; ++ u_) {
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (bel[u_] == bel[v_]) continue ;
newv[bel[u_]].push_back(bel[v_]);
into[bel[v_]] ++;
}
}
Topsort();
printf("%d\n", ans);
return 0;
}
割点
以下是几个月前写的垃圾文字:
应用Tarjan求割点。
按照dfs的遍历顺序,将图看成一棵树,图便有了层次性,将dfs回溯的边当做回边。
dfn[]
记录每一个点的dfs序,low[]
记录能连通的dfs序最小的点的编号。
初始时dfn[i] = low[i]
,在不断dfs递归过程中进行更新。对于搜索树的根节点,若它有两个 或 两个以上的子树,去掉此点后, 其各子树便不连通,说明它是一个割点。
对于其他的节点,更新完成后, 若有一条边, 满足
low[v] >= dfn[u]
。
说明 v 点, 一定到达不了 u 的祖先 (dfs 树意义下的祖先),则删去此点后,u 与 v 不连通,u 是一个割点。
//知识点:割点
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#include <stack>
#include <vector>
#define LL long long
const int kMaxn = 2e4 + 10;
const int kMaxm = 2e5 + 10;
//=============================================================
int n, m, ans;
int e_num, head[kMaxn], v[kMaxm], ne[kMaxm];
int d_num, dfn[kMaxn], low[kMaxn];
bool cut[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
v[++ e_num] = v_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
void Tarjan(int u_, int root_) {
int son = 0;
dfn[u_] = low[u_] = ++ d_num;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (! dfn[v_]) {
Tarjan(v_, root_);
Chkmin(low[u_], low[v_]);
if (u_ == root_) ++ son;
if (low[v_] >= dfn[u_] && u_ != root_) {
ans += (! cut[u_]);
cut[u_] = true;
}
} else {
Chkmin(low[u_], dfn[v_]);
}
}
if (u_ == root_ && son >= 2) {
ans += (!cut[u_]);
cut[u_] = true;
}
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= m; ++ i) {
int u_ = read(), v_ = read();
AddEdge(u_, v_);
AddEdge(v_, u_);
}
for (int i = 1; i <= n; ++ i) {
if (! dfn[i]) {
Tarjan(i, i);
}
}
printf("%d\n", ans);
for (int i = 1; i <= n; ++ i) {
if (cut[i]) {
printf("%d ", i);
}
}
return 0;
}
2-SAT
有 \(n\) 个 01 变量 \(x_1\sim x_n\),另有 \(m\) 个变量取值需要满足的限制。
每个限制是一个 \(k\) 元组 \((x_{p_1}, x_{p_2}, \cdots, x_{p_3})\),满足 \(x_{p_1}\oplus x_{p_2}\oplus\cdots\oplus x_{p_3} = a\)。其中 \(a\) 是 \(0/1\),\(\oplus\) 是某种二元 bool 运算。
要求构造一种满足所有限制的变量的赋值方案。
建立图论模型,使用强连通算法实现。
//知识点:2-SAT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <stack>
#define LL long long
const int kN = 2e6 + 10;
//=============================================================
int n, m, e_num, head[kN], v[kN << 1], ne[kN << 1];
int dfn_num, bel_num, dfn[kN], low[kN], bel[kN];
std::stack <int> st;
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
void Add(int u_, int v_) {
v[++ e_num] = v_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
void Tarjan(int u_) {
dfn[u_] = low[u_] = ++ dfn_num;
st.push(u_);
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (!dfn[v_]) {
Tarjan(v_);
Chkmin(low[u_], low[v_]);
} else if (!bel[v_]) {
Chkmin(low[u_], dfn[v_]);
}
}
if (dfn[u_] == low[u_]) {
++ bel_num;
while (true) {
int top = st.top(); st.pop();
bel[top] = bel_num;
if (top == u_) break;
}
}
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= m; ++ i) {
int x = read(), a = read(), y = read(), b = read();
if (a && b) Add(x + n, y), Add(y + n, x);
if (!a && b) Add(x, y), Add(y + n, x + n);
if (a && !b) Add(x + n, y + n), Add(y, x);
if (!a && !b) Add(x, y + n), Add(y, x + n);
}
for (int i = 1; i <= 2 * n; ++ i) {
if (!dfn[i]) Tarjan(i);
if (i <= n && bel[i] == bel[i + n]) {
printf("IMPOSSIBLE\n");
return 0;
}
}
printf("POSSIBLE\n");
for (int i = 1; i <= n; ++ i) printf("%d ", bel[i] < bel[i + n]); //Tarjan 算法赋给强连通分量的编号顺序与拓扑序相反
return 0;
}
虚树
对于树 \(T=(V,E)\),给定关键点集 \(S\subseteq V\),则可定义虚树 \(T'=(V',E')\)。
对于点集 \(V'\subseteq V\),使得 \(u\in V'\) 当且仅当 \(u\in S\),或 \(\exist x,y\in S,\operatorname{lca}(x,y)=u\)。
对于边集,\((u,v)\in E'\),当且仅当 \(u,v\in V'\),且 \(u\) 为 \(v\) 在 \(V'\) 中深度最深的祖先。
个人理解:
仅保留关键点及其 \(\operatorname{lca}\),缩子树成边,仅保留分叉点,可能删去一些不包含关键点的子树。
压缩了树的信息,同时也丢失了部分树的信息。
一个分叉点会合并至少两个关键点,虚树节点数最多为 \(2k-1\) 个。节点数变为了 \(O(k)\) 级别。
建树考虑增量法,每次向虚树中添加一个关键点。考虑先求得 关键节点 的 dfs 序,按照 dfs 序添加关键节点。这样可以保证相邻两个关键点的 \(\operatorname{lca}\) 深度不小于不相邻关键点的深度。
考虑单调栈维护虚树最右侧的链(上一个关键点与根的链),单调栈中节点深度递增,栈顶一定为上一个关键点。钦定 1 号节点为根,先将其压入栈中。
每加入一个关键点 \(a_i\),令 \(\operatorname{lca}(a_{i-1},a_i)=w\)。将栈顶 \(\operatorname{dep}_x > \operatorname{dep}_w\) 的弹栈,加入 \(w,a_i\),即为新的右链。特别地,若栈顶存在 \(\operatorname{dep}_x=\operatorname{dep}_w\),不加入 \(w\) 节点。
在此过程中维护每个节点的父节点,在弹栈时进行连边并维护信息,即得虚树。单次建虚树复杂度 \(O(kw)\) 级别,其中 \(w\) 为单次求 \(\operatorname{lca}\) 的复杂度。
其中 \(\operatorname{Cut}\) 为封装后的树链剖分。
虚树上 DP 板题:「SDOI2011」消耗战
namespace VT { //Virtual Tree
#define dep Cut::dep
const int kMaxNode = kN;
int top, node[kMaxNode], st[kMaxNode]; //栈
int tag[kMaxNode]; //标记是否为关键点
std::vector <int> newv[kMaxNode]; //虚树
bool CMP(int fir_, int sec_) { //按 dfs 序比较
return Cut::dfn[fir_] < Cut::dfn[sec_];
}
void Push(int u_) { //向虚树中加入 u_
int lca = Cut::Lca(u_, st[top]);
//为什么这里要写成 top - 1?因为 lca 可能出现在栈中,这样写便于处理。
for (; dep[st[top - 1]] > dep[lca]; -- top) {
newv[st[top - 1]].push_back(st[top]);
}
if (lca != st[top]) {
newv[lca].push_back(st[top]); -- top;
if (lca != st[top]) st[++ top] = lca;
}
if (st[top] != u_) st[++ top] = u_;
}
void Build(int siz_) {
for (int i = 1; i <= siz_; ++ i) {
node[i] = read();
tag[node[i]] = 1;
}
std::sort(node + 1, node + siz_ + 1, CMP);
st[top = 0] = 1;
for (int i = 1; i <= siz_; ++ i) Push(node[i]);
for (; top; -- top) newv[st[top - 1]].push_back(st[top]);
}
}
网络流
网络最大流
EK
从源点开始 bfs 实现 FF,到达汇点后停止 bfs 并增广一次。增广时记录转移前驱并更新路径上边的残余容量,答案加上最小流量。
//知识点:网络最大流,EK
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kN = 210 + 10;
const int kM = 1e4 + 10;
const LL kInf = 9e18 + 2077;
//=============================================================
int n, m, s, t;
int e_num = 1, head[kN], v[kM], ne[kM], from[kN];
LL f[kN], w[kM];
bool vis[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_, int w_) {
v[++ e_num] = v_;
w[e_num] = 1ll * w_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
bool Bfs() { //找到一条增广路
memset(vis, false, sizeof (vis));
std::queue <int> q;
vis[s] = false;
f[s] = kInf;
q.push(s);
while (!q.empty()) {
int u_ = q.front(); q.pop();
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
LL w_ = w[i];
if (!w_ || vis[v_]) continue;
f[v_] = std::min(f[u_], w_);
vis[v_] = true;
from[v_] = i;
q.push(v_);
if (v_ == t) return true;
}
}
return false;
}
LL EK() {
LL ret = 0;
while (Bfs()) { //更改路径的残余容量
for (int u_ = t; u_ != s; u_ = v[from[u_] ^ 1]) {
w[from[u_]] -= f[t];
w[from[u_] ^ 1] += f[t];
}
ret += f[t];
}
return ret;
}
//=============================================================
int main() {
n = read(), m = read(), s = read(), t = read();
for (int i = 1; i <= m; ++ i) {
int u_ = read(), v_ = read(), w_ = read();
AddEdge(u_, v_, w_);
AddEdge(v_, u_, 0);
}
printf("%lld\n", EK());
return 0;
}
Dinic
使用 dfs 找增广路,不过在每轮增广前使用 bfs 将残余网络分层。一个点的层数为残余网络上它与源点的最小边数。分层时若汇点的层数不存在,说明不存在增广路,即可停止增广。
之后 dfs 增广时,每次转移仅向比当前点层数多 1 的点转移。这可以保证每次找到的增广路是最短的。
多路增广 + 当前弧优化。
//知识点:网络最大流,Dinic
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kN = 210 + 10;
const int kM = 1e4 + 10;
const LL kInf = 9e18 + 2077;
//=============================================================
int n, m, s, t;
int e_num = 1, head[kN], v[kM], ne[kM];
int cur[kN], dep[kN];
LL w[kM];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_, int w_) {
v[++ e_num] = v_;
w[e_num] = 1ll * w_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
bool Bfs() {
std::queue <int> q;
memset(dep, 0, sizeof (dep));
dep[s] = 1; //注意初始化
q.push(s);
while (!q.empty()) {
int u_ = q.front(); q.pop();
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
LL w_ = w[i];
if (w_ > 0 && !dep[v_]) {
dep[v_] = dep[u_] + 1;
q.push(v_);
}
}
}
return dep[t];
}
LL Dfs(int u_, LL into_) {
if (u_ == t || !into_) return into_;
LL out = 0;
for (int& i = cur[u_]; i && into_; i = ne[i]) { //当前弧优化
int v_ = v[i];
LL w_ = w[i];
if (dep[v_] == dep[u_] + 1 && w_ > 0) {
LL ret = Dfs(v_, std::min(into_, w_));
if (!ret) dep[v_] = kN; //没有贡献,之后不会再访问
w[i] -= ret, w[i ^ 1] += ret;
out += ret, into_ -= ret;
}
}
return out;
}
LL Dinic() {
LL ret = 0;
while (Bfs()) {
for (int i = 1; i <= n; ++ i) cur[i] = head[i]; //初始化当前弧
ret += Dfs(s, kInf);
}
return ret;
}
//=============================================================
int main() {
n = read(), m = read(), s = read(), t = read();
for (int i = 1; i <= m; ++ i) {
int u_ = read(), v_ = read(), w_ = read();
AddEdge(u_, v_, w_);
AddEdge(v_, u_, 0);
}
printf("%lld\n", Dinic());
return 0;
}
二分图匹配
匈牙利
枚举一边的点,找增广路。
复杂度 \(O(n^2 m)\)。
//知识点:二分图最大匹配
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 500 + 10;
const int kMaxe = 5e4 + 10;
//=============================================================
int n, m, e, ans;
int e_num, head[kMaxn], v[kMaxe], ne[kMaxe];
int match[kMaxn];
bool vis[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
v[++ e_num] = v_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
bool Dfs(int u_) {
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (vis[v_]) continue ;
vis[v_] = true;
if (! match[v_] or Dfs(match[v_])) {
match[v_] = u_;
return true;
}
}
return false;
}
//=============================================================
int main() {
n = read(), m = read(), e = read();
for (int i = 1; i <= e; ++ i) {
int u_ = read(), v_ = read();
if (u_ > n || v_ > m) continue ;
AddEdge(u_, v_);
}
for (int i = 1; i <= n; ++ i) {
memset(vis, 0, sizeof (vis));
if (Dfs(i)) ++ ans;
}
printf("%d\n", ans);
return 0;
}
网络流
建模,在一个匹配中, 任意两条边都没有共同的端点,每一个端点 最多只有 1 的贡献。 在网络流中,最多只有 1 的贡献可转化为 边的容量为 1。由此可对原二分图进行转化:
新建源点 与 汇点。
- 源点向每个左侧点 各连一条容量为 1 的有向边。
- 原图中的边改为 自左向右的有向边,容量为 1。
- 每个右侧点向汇点 各连一条容量为 1 的有向边。
求最大流即为最大匹配。
线段树优化建图
用于解决区间连边的一个建图小技巧。
对于区间连边问题,其解决方案是建立一系列虚点,虚点到实点的权值为 0,一个虚点与某一段连续区间内的实点相连。此时若想对该区间进行区间连边,直接连向此虚点即可。
为解决区间的拆分问题,可以将虚点建成一个如下图所示的类似线段树的结构,使得区间的拆分可以放到线段树上进行。
//知识点:线段树优化建图
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kN = 4e5 + 10;
const LL kInf = 0x3f3f3f3f3f3f3f3f;
//=============================================================
int n, q, start, node_num;
int e_num, head[kN], v[kN << 4], w[kN << 4], ne[kN << 4];
bool vis[kN];
LL dis[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) {
w = (w << 3) + (w << 1) + (ch ^ '0');
}
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void Add(int u_, int v_, int w_) {
v[++ e_num] = v_, w[e_num] = w_;
ne[e_num] = head[u_], head[u_] = e_num;
}
#define ls (lson[now_])
#define rs (rson[now_])
#define mid ((L_+R_)>>1)
struct SegmentTree {
int root, lson[kN], rson[kN];
void Build(int &now_ ,int L_, int R_, bool type_) {
if (L_ == R_) {
now_ = L_;
return ;
}
now_ = ++ node_num;
Build(ls, L_, mid, type_);
Build(rs, mid + 1, R_, type_);
if (!type_) Add(now_, ls, 0), Add(now_, rs, 0);
if (type_) Add(ls, now_, 0), Add(rs, now_, 0);
}
void Modify(int now_, int L_, int R_, int l_, int r_, int u_, int w_,
bool type_) {
if (l_ <= L_ && R_ <= r_) {
if (!type_) Add(u_, now_, w_);
if (type_) Add(now_, u_, w_);
return ;
}
if (l_ <= mid) Modify(ls, L_, mid, l_, r_, u_, w_, type_);
if (r_ > mid) Modify(rs, mid + 1, R_, l_, r_, u_, w_, type_);
}
} Seg[2];
#undef ls
#undef rs
#undef mid
void Init() {
node_num = n;
Seg[0].Build(Seg[0].root, 1, n, 0);
Seg[1].Build(Seg[1].root, 1, n, 1);
}
void Spfa(int s_) { //他死了
std::queue <int> q;
memset(dis, 0x3f, sizeof(dis));
dis[s_] = 0;
q.push(s_);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = head[u]; i; i = ne[i]) {
int v_ = v[i], w_ = w[i];
if (dis[u] + w_ < dis[v_]) {
dis[v_] = dis[u] + w_;
q.push(v_);
}
}
}
}
#define pr std::pair
#define mp std::make_pair
void Dijkstra(int s_) {
std::priority_queue <pr <LL, int> > q;
memset(dis, 63, sizeof (dis));
dis[s_] = 0;
q.push(mp(0, s_));
while (! q.empty()) {
int u_ = q.top().second; q.pop();
if (vis[u_]) continue;
vis[u_] = true;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i], w_ = w[i];
if (dis[u_] + w_ < dis[v_]) {
dis[v_] = dis[u_] + w_;
q.push(mp(-dis[v_], v_));
}
}
}
}
#undef pr
#undef mp
//=============================================================
int main() {
n = read(), q = read(), start = read();
Init();
while (q --) {
int opt = read();
if (opt == 1) {
int u_ = read(), v_ = read(), w_ = read();
Add(u_, v_, w_);
} else {
int u_ = read(), l_ = read(), r_ = read(), w_ = read();
int type = (opt == 3);
Seg[type].Modify(Seg[type].root, 1, n, l_, r_, u_, w_, type);
}
}
Dijkstra(start);
for (int i = 1; i <= n; ++ i) {
printf("%lld ", dis[i] < kInf ? dis[i] : -1);
}
return 0;
}
字符串
Trie
字典树。
namespace Trie {
const int kMaxNode = kN << 5;
int node_num, tr[kMaxNode][2];
LL val[kMaxNode];
void Insert(LL val_) {
int now_ = 0;
for (LL i = 33; i >= 0; -- i) {
int ch = val_ >> i & 1ll;
if (!tr[now_][ch]) tr[now_][ch] = ++ node_num;
now_ = tr[now_][ch];
}
val[now_] = val_;
}
}
可持久化 Trie
Q:什么时候需要可持久化?
A:想建一车 Trie 但是建不下的时候。
其实我很疑惑该把这个加到数据结构还是字符串里的= =
可持久化 Trie 可以将全局的二进制运算问题转化到区间上进行。
//知识点:
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <stack>
#define LL long long
const int kN = 6e6 + 10;
//=============================================================
int n, m, a[kN], sum[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
namespace Trie {
#define ls (tr[now_][0])
#define rs (tr[now_][1])
const int kMaxRoot = kN;
const int kMaxNode = kN << 2;
int node_num, root[kMaxRoot], tr[kMaxNode][2], last[kMaxNode];
void Insert(int &now_, int pre_, int val_, int lth_, int id_) {
now_ = ++ node_num;
ls = tr[pre_][0], rs = tr[pre_][1], last[now_] = id_;
if (lth_ < 0) return ;
int ch = val_ >> lth_ & 1;
Insert(tr[now_][ch], tr[pre_][ch], val_, lth_ - 1, id_);
}
int Query(int now_, int val_, int lth_, int l_) {
if (lth_ < 0) return sum[last[now_]];
int ch = val_ >> lth_ & 1;
if (last[tr[now_][ch ^ 1]] >= l_) {
return Query(tr[now_][ch ^ 1], val_, lth_ - 1, l_);
}
return Query(tr[now_][ch], val_, lth_ - 1, l_);
}
}
void Init() {
n = read(), m = read();
Trie::last[0] = -1;
Trie::Insert(Trie::root[0], 0, 0, 25, 0);
for (int i = 1; i <= n; ++ i) {
sum[i] = sum[i - 1] ^ read();
Trie::Insert(Trie::root[i], Trie::root[i - 1], sum[i], 25, i);
}
}
//=============================================================
int main() {
Init();
while (m --) {
char opt[5]; scanf("%s", opt + 1);
if (opt[1] == 'A') {
++ n;
sum[n] = sum[n - 1] ^ read();
Trie::Insert(Trie::root[n], Trie::root[n - 1], sum[n], 25, n);
} else {
int l = read(), r = read(), val = sum[n] ^ read();
printf("%d\n", Trie::Query(Trie::root[r - 1], val, 25, l - 1) ^ val);
}
}
return 0;
}
KMP
子串匹配问题。在朴素算法中,如果在某一位上失配,则会抛弃当前已匹配的部分,跳到下一个位置再从开头进行匹配。
而 KMP 利用了当前已匹配的部分,使得在下一个位置时不必从开头进行匹配,从而对朴素算法进行了加速。
//知识点:KMP
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e6 + 10;
//=============================================================
char s1[kN], s2[kN];
int n1, n2;
int fail[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
//=============================================================
int main() {
scanf("%s", s1 + 1);
scanf("%s", s2 + 1);
n1 = strlen(s1 + 1), n2 = strlen(s2 + 1);
fail[1] = 0;
for (int i = 2, j = 0; i <= n2; ++ i) {
while (j > 0 && s2[i] != s2[j + 1]) j = fail[j];
if (s2[i] == s2[j + 1]) ++ j;
fail[i] = j;
}
for (int i = 1, j = 0; i <= n1; ++ i) {
while (j > 0 && (j == n2 || s1[i] != s2[j + 1])) j = fail[j];
if (s1[i] == s2[j + 1]) ++ j;
if (j == n2) printf("%d\n", i - n2 + 1);
}
for (int i = 1; i <= n2; ++ i) printf("%d ", fail[i]);
return 0;
}
AC 自动机
可以认为是 KMP 算法在 Trie 树上的应用,与 KMP 算法在失配时应用已匹配部分的 border 进行跳转类似,AC 自动机在失配时会根据失配指针跳转到 Trie 树上代表已匹配部分的 border 的节点,从而加速匹配。
//知识点:ACAM
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#define LL long long
const int kT = 2e6 + 10;
const int kN = 2e5 + 10;
//=============================================================
int n;
char s[kT];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
namespace ACAM {
std::vector <int> id[kN];
int node_num, tr[kN][26], sum[kN], fail[kN];
int e_num, size[kN], head[kN], v[kN], ne[kN];
void Add(int u_, int v_) {
v[++ e_num] = v_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
void Dfs(int u_) {
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
Dfs(v_);
size[u_] += size[v_];
}
for (int i = 0, lim = id[u_].size(); i < lim; ++ i) {
sum[id[u_][i]] = size[u_];
}
}
void Insert(int id_, char *s_) {
int now = 0, lth = strlen(s_ + 1);
for (int i = 1; i <= lth; ++ i) {
if (! tr[now][s_[i] - 'a']) tr[now][s_[i] - 'a'] = ++ node_num;
now = tr[now][s_[i] - 'a'];
}
id[now].push_back(id_);
}
void Build() {
std::queue <int> q;
for (int i = 0; i < 26; ++ i) {
if (tr[0][i]) q.push(tr[0][i]);
}
while (! q.empty()) {
int u_ = q.front(); q.pop();
for (int i = 0; i < 26; ++ i) {
if (tr[u_][i]) {
fail[tr[u_][i]] = tr[fail[u_]][i];
q.push(tr[u_][i]);
} else {
tr[u_][i] = tr[fail[u_]][i];
}
}
}
}
void Query(char *t_) {
int now = 0, lth = strlen(t_ + 1);
for (int i = 1; i <= lth; ++ i) {
now = tr[now][t_[i] - 'a'];
++ size[now];
}
for (int i = 1; i <= node_num; ++ i) Add(fail[i], i);
Dfs(0);
for (int i = 1; i <= n; ++ i) printf("%d\n", sum[i]);
}
}
//=============================================================
int main() {
n = read();
for (int i = 1; i <= n; ++ i) {
scanf("%s", s + 1);
ACAM::Insert(i, s);
}
ACAM::Build();
scanf("%s", s + 1);
ACAM::Query(s);
return 0;
}
最小表示法
当字符串 \(S\) 中可以选定一个位置 \(i\) 满足:
则称 \(S\) 与 \(T\) 循环同构。
字符串 \(S\) 的 最小表示 为与 \(S\) 循环同构的所有字符串中字典序最小的字符串。
最小表示法思想与 KMP 类似,都是重复利用之前比较过的部分,减少时间复杂度。
学习笔记:「最小表示法」P1368 工艺 /【模板】最小表示法
//知识点:最小表示法
/*
By:Luckyblock
*/
#include <cstdio>
#include <ctype.h>
#include <cstring>
#include <algorithm>
#define ll long long
const int kMaxn = 3e5 + 10;
//=============================================================
int n, a[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void GetMax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void GetMin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
int MinRep() {
int i = 0, j = 1, k = 0;
while (i < n && j < n && k < n) {
int delta = a[(i + k) % n] - a[(j + k) % n];
if (delta == 0) {
++ k;
continue ;
}
if (delta > 0) i += k + 1;
else j += k + 1;
if (i == j) j ++;
k = 0;
}
return std :: min(i, j);
}
//=============================================================
int main() {
n = read();
for (int i = 0; i < n; ++ i) a[i] = read();
for (int i = MinRep(), j = 0; j < n; ++ j) {
printf("%d ", a[(i + j) % n]);
}
return 0;
}
后缀数组
字符串 \(S\) 的后缀数组定义为两个数组 \(sa,rk\)。
\(sa\) 储存 \(S\) 的所有后缀 按字典序排序后的起始下标,满足 \(S[sa_{i-1}:n]<S[sa_i:n]\)。
\(rk\) 储存 \(S\) 的所有后缀的排名。
结合后缀的最长公共前缀,后缀数组可以用来处理许多复杂度字符串问题。
//知识点:SA
/*
By:Luckyblock
I love Marisa;
But Marisa has died;
*/
#include <cstdio>
#include <ctype.h>
#include <cstring>
#include <algorithm>
#define ll long long
const int kMaxn = 1e6 + 10;
//=============================================================
char S[kMaxn];
//rk[i]: 倍增过程中子串[i:i+2^k-1]的排名,
//sa[i] 排名为i的子串 [i:i+2^k-1],
//它们互为反函数。
//存在越界风险,如果不写特判,rk 和 oldrk 要开 2 倍空间。
int n, m, sa[kMaxn], rk[kMaxn << 1], oldrk[kMaxn << 1];
int id[kMaxn], cnt[kMaxn]; //用于计数排序的两个 temp 数组
int height[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void GetHeight() {
for (int i = 1, k = 0; i <= n; ++ i) {
if (rk[i] == 1) {
k = 0;
} else {
if (k) -- k;
int j = sa[rk[i] - 1];
while (i + k <= n && j + k <= n &&
S[i + k] == S[j + k]) {
++ k;
}
}
height[rk[i]] = k;
}
}
//=============================================================
int main() {
scanf("%s", S + 1);
n = strlen(S + 1);
m = std :: max(n, 300); //值域大小
//初始化 rk 和 sa
for (int i = 1; i <= n; ++ i) ++ cnt[rk[i] = S[i]];
for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; -- i) sa[cnt[rk[i]] --] = i;
//倍增过程。w 是已经推出的子串长度,数值上等于上述分析中的 2^{k-1}。
//注意此处的 sa 数组存的并不是后缀的排名,存的是倍增过程中指定长度子串的排名。
for (int w = 1; w < n; w <<= 1) {
//按照后半截 rk[i+w] 作为第二关键字排序。
memset(cnt, 0, sizeof (cnt));
for (int i = 1; i <= n; ++ i) id[i] = i;
for (int i = 1; i <= n; ++ i) ++ cnt[rk[id[i] + w]]; //越界风险,2倍空间
for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; -- i) sa[cnt[rk[id[i] + w]] --] = id[i];
//按照前半截 rk[i] 作为第一关键字排序。
memset(cnt, 0, sizeof (cnt));
for (int i = 1; i <= n; ++ i) id[i] = sa[i];
for (int i = 1; i <= n; ++ i) ++ cnt[rk[id[i]]];
for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; -- i) sa[cnt[rk[id[i]]] --] = id[i];
//更新 rk 数组,可以滚动数组一下,但是可读性会比较差(
for (int i = 1; i <= n; ++ i) oldrk[i] = rk[i];
for (int p = 0, i = 1; i <= n; ++ i) {
if (oldrk[sa[i]] == oldrk[sa[i - 1]] && //判断两个子串是否相等。
oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]) { //越界风险,2倍空间
rk[sa[i]] = p;
} else {
rk[sa[i]] = ++ p;
}
}
}
for (int i = 1; i <= n; ++ i) printf("%d ", sa[i]);
printf("\n");
GetHeight();
for (int i = 2; i <= n; ++ i) printf("%d ", height[i]);
return 0;
}
后缀自动机
字符串 \(S\) 的后缀自动机 (suffix automaton, SAM) 是一个可以且尽可以接受 \(S\) 所有后缀的 最小的 DFA。
更形式化的定义:
- 字符串 \(S\) 的 SAM 是一张 DAWG(有向单词无环图)。节点被称作 状态,边被称作状态间的 转移。
- 存在一个起始节点,称作 起始状态。其它节点均可从起始节点出发到达。
- 每个 转移 都标有一些字母。从一个节点出发的所有转移均 不同。
- 存在数个 终止状态。若从 \(t_0\) 出发,最终转移到一个终止状态,路径上所有转移连接起来一定是 \(S\) 的一个后缀。\(S\) 的每个后缀均可用一条 从起始节点到某个终止状态 的路径构成。
- 在所有满足上述条件的 DFA 中,SAM 的节点数是最少的。
SAM 并不是一个典型的 DFA,在 DAWG 基础上,除 \(t_0\) 外的每个状态都被添加了一条 后缀链接。所有后缀链接组成了树状结构,这棵树被称为 parent 树。
字符串 \(S\) 的 SAM 能包含 \(S\) 所有子串的信息。
SAM 将这些信息以高度压缩的形式储存,对于一个长度为 \(n\) 的字符串,它的 SAM 空间复杂度仅为 \(O(n)\),构建 SAM 的时间复杂度也仅为 \(O(n)\)。
//知识点:SAM
/*
By:Luckyblock
试了试变量写法,挺清爽的。
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e6 + 10;
const int kMaxm = 26;
//=============================================================
int n, last = 1, node_num = 1; //注意 1 为起始状态,其他状态的标号从 1 开始。
int ch[kMaxn << 1][kMaxm], len[kMaxn <<1], link[kMaxn << 1]; //SAM
int edge_num, v[kMaxn << 1], ne[kMaxn << 1], head[kMaxn << 1]; //Graph
ll ans, size[kMaxn << 1];
char S[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void GetMax(ll &fir, ll sec) {
if (sec > fir) fir = sec;
}
void AddEdge(int u_, int v_) {
v[++ edge_num] = v_, ne[edge_num] = head[u_], head[u_] = edge_num;
}
void Insert(int c_) { //原串 S -> 新串 S + c
int p = last, now = last = ++ node_num;
size[now] = 1, len[now] = len[p] + 1; //仅为终止状态 size 赋值。
for (; p && ! ch[p][c_]; p = link[p]) ch[p][c_] = now;
//case 1:
if (! p) {link[now] = 1; return ;}
//case 2:
int q = ch[p][c_];
if (len[q] == len[p] + 1) {link[now] = q; return ;}
//case 3:
int newq = ++ node_num;
len[newq] = len[p] + 1;
memcpy(ch[newq], ch[q], sizeof(ch[q]));
link[newq] = link[q];
link[q] = link[now] = newq;
for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
}
//parent 树上 DP,用孩子更新父亲。
//孩子的长度比父亲长,则孩子出现的地方,父亲一定出现,size 累加。
//父亲的 size 也可能不为 0(为终止状态)。
void Dfs(int u) {
for (int i = head[u]; i; i = ne[i]) {
Dfs(v[i]);
size[u] += size[v[i]];
}
if (size[u] > 1) GetMax(ans, 1ll * size[u] * len[u]);
}
//=============================================================
int main() {
scanf("%s", S + 1);
int n = strlen(S + 1);
for (int i = 1; i <= n; ++ i) Insert(S[i] - 'a');
for (int i = 2; i <= node_num; ++ i) AddEdge(link[i], i);
Dfs(1);
printf("%lld\n", ans);
return 0;
}
广义后缀自动机
广义 SAM 是一种用于维护 Trie 的子串信息的 SAM 的简单变体。
将多个模式串插入到 Trie 后,即可使用广义 SAM 维护多模式串的信息。这是广义 SAM 最广泛的应用之一,本文的重点也在于此。其基本思想是将多串的信息进行压缩,使得 SAM 在仍满足节点数最少的同时 包含所有子串的信息。此时 SAM 中的一个状态可能同时代表多个串中相应的子串。
//知识点:SAM
/*
By:Luckyblock
试了试变量写法,挺清爽的。
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e6 + 10;
const int kMaxm = 26;
//=============================================================
ll ans;
char S[kMaxn];
int node_num = 1, ch[kMaxn << 1][kMaxm], len[kMaxn <<1], link[kMaxn << 1];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
int Insert(int c_, int last_) {
if (ch[last_][c_]) {
int p = last_, q = ch[p][c_];
if (len[p] + 1 == len[q]) return q;
int newq = ++ node_num;
memcpy(ch[newq], ch[q], sizeof(ch[q]));
len[newq] = len[p] + 1;
link[newq] = link[q];
link[q] = newq;
for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
return newq;
}
int p = last_, now = ++ node_num;
len[now] = len[p] + 1;
for (; p && ! ch[p][c_]; p = link[p]) ch[p][c_] = now;
if (! p) {link[now] = 1; return now;}
int q = ch[p][c_];
if (len[q] == len[p] + 1) {link[now] = q; return now;}
int newq = ++ node_num;
memcpy(ch[newq], ch[q], sizeof(ch[q]));
link[newq] = link[q], len[newq] = len[p] + 1;
link[q] = link[now] = newq;
for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
return now;
}
//=============================================================
int main() {
int T = read();
while (T --) {
scanf("%s", S + 1);
int n = strlen(S + 1), last = 1;
for (int i = 1; i <= n; ++ i) {
last = Insert(S[i] - 'a', last);
}
}
for (int i = 2; i <= node_num; ++ i) {
ans += len[i] - len[link[i]];
}
printf("%lld\n", ans);
return 0;
}
数学
快速幂
转化为 \(\prod a^{2^?}\)。
//知识点:快速幂
/*
By:Luckyblock
注意 p = 0
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
//=============================================================
LL a, b, p;
//=============================================================
inline LL read() {
LL f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3ll) + (w << 1ll) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
LL QPow(LL x_, LL y_, LL mod_) {
LL ret = 1;
x_ %= mod_;
for (; y_; y_ >>= 1ll) {
if (y_ & 1) ret = ret * x_ % mod_;
x_ = x_ * x_ % mod_;
}
return ret % mod_;
}
//=============================================================
int main() {
LL a = read(), b = read(), p = read();
printf("%lld^%lld mod %lld=%lld", a, b, p, QPow(a, b, p));
return 0;
}
线性筛
每个数只被它的最小质因子筛掉。
//知识点:先行筛
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e8 + 10;
//=============================================================
int n, q;
int p_num, p[kMaxn];
bool vis[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void GetPrime(int n_) {
for (int i = 2; i <= n_; ++ i) {
if (! vis[i]) {
p[++ p_num] = i;
}
for (int j = 1; j <= p_num; ++ j) {
if (i * p[j] > n_) break;
vis[i * p[j]] = true;
if (i % p[j] == 0) break;
}
}
}
//=============================================================
int main() {
n = read(), q = read();
GetPrime(n);
while (q --) {
int k = read();
printf("%d\n", p[k]);
}
return 0;
}
线性求逆元
若 \(p\in \mathbb{P}\),则有:
//知识点:线性求逆元
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 3e6 + 10;
//=============================================================
int n, p, inv[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
//=============================================================
int main() {
n = read(), p = read();
inv[1] = 1;
for (int i = 2; i <= n; ++ i) {
inv[i] = 1ll * (p - p / i) * inv[p % i] % p;
}
for (int i = 1; i <= n; ++ i) {
printf("%d\n", inv[i]);
}
return 0;
}
矩阵快速幂
矩阵乘法:
矩阵满足结合律,故可拆分为 \(\prod A^{2^?}\),使用快速幂求解。
//知识点:矩阵乘法,矩阵快速幂
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxm = 100 + 1;
const LL kMod = 1e9 + 7;
//=============================================================
int n;
LL b;
struct Matrix {
LL a[kMaxm][kMaxm];
void build() {
for (int i = 1; i <= n; ++ i) {
a[i][i] = 1;
}
}
Matrix operator * (const Matrix &b_) const {
Matrix ret;
memset(ret.a, 0, sizeof (ret.a));
for (int k = 1; k <= n; ++ k) {
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
ret.a[i][j] += a[i][k] * b_.a[k][j] % kMod;
ret.a[i][j] %= kMod;
}
}
}
return ret;
}
} a;
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
Matrix QMatrixPow(Matrix a_, LL b_) {
Matrix ret;
ret.build();
for (; b_; b_ >>= 1) {
if (b_ & 1) ret = ret * a_;
a_ = a_ * a_;
}
return ret;
}
//=============================================================
int main() {
n = read(); scanf("%lld", &b);
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
a.a[i][j] = read();
}
}
Matrix ans = QMatrixPow(a, b);
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
printf("%d ", ans.a[i][j]);
}
printf("\n");
}
return 0;
}
三分法
之前讲课的课件:「二分与三分」课件配套资料。
//知识点:三分法求函数极值
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 13 + 5;
const double eps = 1e-7;
//=============================================================
int n;
double l, r, a[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
double f(double x_) {
double ret = 0;
for (int i = 1; i <= n + 1; ++ i) {
ret = ret * x_ + a[i];
}
return ret;
}
//=============================================================
int main() {
n = read();
scanf("%lf %lf", &l, &r);
for (int i = 1; i <= n + 1; ++ i) scanf("%lf", &a[i]);
while (r - l > eps) {
double lx = (2.0 * l + r) / 3, rx = (l + 2.0 * r) / 3;
if (f(lx) < f(rx)) {
l = lx;
} else {
r = rx;
}
}
printf("%.5lf", l);
return 0;
}
高斯消元
小学解方程。
//知识点:高斯消元
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cmath>
#include <cstring>
#define LL long long
const double eps = 1e-7;
const int kMaxn = 100 + 10;
//=============================================================
int n;
double a[kMaxn][kMaxn], ans[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
//=============================================================
int main() {
n = read();
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n + 1; ++ j) {
scanf("%lf", &a[i][j]);
}
}
for (int i = 1; i <= n; ++ i) { //每次将第 i 行第 i 个元素消为 1。
int pos = i; //每次找到最大系数消除,减小误差
for (int j = i + 1; j <= n; ++ j) {
if (fabs(a[j][i]) > fabs(a[pos][i])) {
pos = j;
}
}
if (fabs(a[pos][i]) < eps) { //系数为 0,无解
printf("No Solution");
return 0;
}
for (int j = 1; j <= n + 1; ++ j) { //交换到第 i 行
std::swap(a[pos][j], a[i][j]);
}
double tmp = a[i][i]; //将第 i 行第 i 个消成 1,同时处理该行其他系数
for (int j = i; j <= n + 1; ++ j) {
a[i][j] /= tmp;
}
for (int j = i + 1; j <= n; ++ j) { //枚举其他行
tmp = a[j][i]; //该行对应系数
for (int k = i; k <= n + 1; ++ k) {
a[j][k] -= a[i][k] * tmp; //注意第 i 行的系数均已被处理过
}
}
}
ans[n] = a[n][n + 1];
for (int i = n - 1; i >= 1; -- i) {
ans[i] = a[i][n + 1];
for (int j = i + 1; j <= n; ++ j) { //回带
ans[i] -= a[i][j] * ans[j];
}
}
for (int i = 1; i <= n; ++ i) {
printf("%.2lf\n", ans[i]);
}
return 0;
}
卢卡斯定理
//知识点:Lucas
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxp = 1e5 + 10;
//=============================================================
LL p, fac[kMaxp];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
LL QPow(LL x_, LL y_) {
x_ %= p;
LL ret = 1ll;
for (; y_; y_ >>= 1ll) {
if (y_ & 1) ret = ret * x_ % p;
x_ = x_ * x_ % p;
}
return ret;
}
LL C(LL n_, LL m_) {
if (m_ > n_) return 0ll;
return fac[n_] * QPow(fac[m_], p - 2ll) * QPow(fac[n_ - m_], p - 2ll);
}
LL Lucas(LL n_, LL m_) {
if (! m_) return 1;
return Lucas(n_ / p, m_ / p) * C(n_ % p, m_ % p) % p;
}
//=============================================================
int main() {
int T = read();
while (T --) {
int n = read(), m = read();
p = read();
fac[0] = 1;
for (int i = 1; i <= p; ++ i) {
fac[i] = 1ll * fac[i - 1] * i % p;
}
printf("%lld\n", Lucas(n + m, m));
}
return 0;
}
数学乱记
懒得往这搬了= =
杂项
CDQ 分治
Cdq 分治是一种牛逼思想,一般用于解决区间点对问题。设当前处理的区间为 \([l, r]\),Cdq 分治的一般过程:
- 若 \(l = r\),返回。
- 设区间中点为 \(mid\),递归处理 \([l,mid]\) 和 \([mid + 1, r]\)。
- 不横跨 \(mid\) 的点对都会在递归中被解决,仅考虑横跨 \(mid\) 的点对的贡献。
个人理解:
将 \(O(n^2)\) 级别的点对统计,利用单调性确定合法性,变为 \(O(n\log^k n)\) 级别。
太神了!
//知识点:CDQ分治
/*
By:Luckyblock
*/
#include <algorithm>
#include <cstdio>
#include <ctype.h>
#include <cstring>
#define ll long long
#define lowbit(x) (x&-x)
#define mid ((l_+r_)>>1)
const int kMaxn = 1e5 + 10;
//=============================================================
struct Data {
int x, y, z, cnt, ans;
} a[kMaxn];
int n, k, ans[kMaxn], t[kMaxn << 1];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void GetMax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void GetMin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
bool Compare1(Data fir_, Data sec_) {
if (fir_.x != sec_.x) return fir_.x < sec_.x;
if (fir_.y != sec_.y) return fir_.y < sec_.y;
return fir_.z < sec_.z;
}
bool Compare2(Data fir_, Data sec_) {
if (fir_.y != sec_.y) return fir_.y < sec_.y;
return fir_.z < sec_.z;
}
void Add(int pos_, int val_) {
for (; pos_ <= k; pos_ += lowbit(pos_)) {
t[pos_] += val_;
}
}
int Sum(int pos_) {
int ret = 0;
for (; pos_; pos_ -= lowbit(pos_)) ret += t[pos_];
return ret;
}
void Cdq(int l_, int r_) {
if (l_ == r_) return ;
Cdq(l_, mid), Cdq(mid + 1, r_);
std :: sort(a + l_, a + mid + 1, Compare2);
std :: sort(a + mid + 1, a + r_ + 1, Compare2);
int i = l_, j = mid + 1;
for (; j <= r_; ++ j) {
for (; a[i].y <= a[j].y && i <= mid; ++ i) {
Add(a[i].z, a[i].cnt);
}
a[j].ans += Sum(a[j].z);
}
for (j = l_; j < i; ++ j) Add(a[j].z, - a[j].cnt);
}
bool JudgeEqual(Data fir_, Data sec_) {
if (fir_.x != sec_.x) return false;
if (fir_.y != sec_.y) return false;
return fir_.z == sec_.z;
}
//=============================================================
int main() {
int tmpn = n = read();
k = read();
for (int i = 1; i <= n; ++ i) {
a[i] = (Data) {read(), read(), read()};
}
std :: sort(a + 1, a + n + 1, Compare1);
n = 0;
for (int i = 1, cnt = 0; i <= tmpn; ++ i) {
cnt ++;
if (! JudgeEqual(a[i], a[i + 1])) {
a[++ n] = a[i],
a[n].cnt = cnt,
cnt = 0;
}
}
Cdq(1, n);
for (int i = 1; i <= n; ++ i) {
ans[a[i].ans + a[i].cnt - 1] += a[i].cnt;
}
for (int i = 0; i < tmpn; ++ i) {
printf("%d\n", ans[i]);
}
return 0;
}