算法竞赛常见模板
分层图最短路
板子是:m条边,可以任选 k 条路免费走,求最短路。
和普通的最短路区别不大,但是要想到分层建图。
例子:
小薇要开始一场星际旅行!小薇有一艘飞船,要驾驶这艘飞船穿越一个星系。
这个星系可以简化为一个N个点,M条有向边的地图,其中小薇会确定一个起点S和终点T。一共存在三种星球,3类星球可以任意航行不受限制,而小薇已经知道,要想顺利到达终点,飞船必须经过正好一颗1类星球和一颗2类星球,并且1类星球需要先于2类星球经过,否则,小薇将会到达一个邪恶的地方,这对小薇的身体不好。
已知每穿过一条航线i,会消耗飞船 wi的能量,请你帮助小薇计算出在正确从起点到达终点的前提下,消耗的最小能量是多少。
参考:
struct node {
int ennd, wi;
};
int d[N], u[N];
vector<node> v[N];
bool ve[N];
void dij() {
priority_queue<par, vector<par>, greater<par>> q;
q.push({0, stt});
memset(d, 0x3f, sizeof d);
d[stt] = 0;
while (!q.empty()) {
int x = q.top().second;
q.pop();
if (ve[x]) continue;
ve[x] = 1;
for (auto [ed, z] : v[x]) {
if (d[ed] > d[x] + z) {
d[ed] = d[x] + z;
q.push({d[ed], ed});
}
}
}
}
void add(int a, int b, int c) {
v[a].push_back({b, c});
}
signed main() {
n = read();
m = read();
stt = read();
enn = read();
rep(i, 1, n) u[i] = read();
rep(i, 1, m) {
a = read();
b = read();
t = read();
// 建图
if (u[a] == 3) {
add(a, b, t);
add(a + n, b + n, t);
add(a + 2 * n, b + 2 * n, t);
} else if (u[a] == 2) {
add(a + n, b + 2 * n, t);
} else if (u[a] == 1) {
add(a, b + n, t);
}
}
dij();
if (d[enn + 2 * n] == INF)
cout << -1;
else
cout << d[enn + 2 * n];
return 0;
}
分层图最长路
约翰有 n 块草场,编号 1 到 n,这些草场由若干条单行道相连。奶牛贝西是美味牧草的鉴赏家,她想到达尽可能多的草场去品尝牧草。
贝西总是从 1 号草场出发,最后回到 1 号草场。她想经过尽可能多的草场,贝西在通一个草场只吃一次草,所以一个草场可以经过多次。因为草场是单行道连接,这给贝西的品鉴工作带来了很大的不便,贝西想偷偷逆向行走一次,但最多只能有一次逆行。问,贝西最多能吃到多少个草场的牧草。
题目链接:www.luogu.com.cn/problem/P31…
这题需要缩点
void tarjan(int x) {
dfn[x] = low[x] = ++num;
stk[++top] = x;
ins[x] = 1;
for (auto y : v[x]) {
if (!dfn[y]) {
tarjan(y);
low[x] = min(low[x], low[y]);
} else if (ins[y])
low[x] = min(low[x], dfn[y]);
}
if (dfn[x] == low[x]) {
sccnum++;
int z;
do {
z = stk[top--];
ins[z] = 0;
c[z] = sccnum;
wi[sccnum]++;
} while (x != z);
}
}
void dij() {
queue<int> q;
d[stt] = 0;
q.push(stt);
while (!q.empty()) {
int x = q.front();
// cout << x << "\n";
q.pop();
ve[x] = 0;
for (auto y : vc[x]) {
int z = wi[y];
if (d[y] < d[x] + z) {
d[y] = d[x] + z;
if (!ve[y]) q.push(y), ve[y] = 1;
}
}
}
}
signed main() {
n = read();
m = read();
rep(i, 1, m) {
a = read();
b = read();
add(a, b);
}
rep(i, 1, n) if (!dfn[i]) tarjan(i);
rep(x, 1, n) {
for (auto y : v[x]) {
if (c[y] == c[x]) continue;
addc(c[x], c[y]);
addc(c[y], c[x] + sccnum);
addc(c[x] + sccnum, c[y] + sccnum);
}
}
rep(i, 1, sccnum) wi[i + sccnum] = wi[i];
stt = c[1];
dij();
cout << max(d[stt], d[c[1] + sccnum]);
return 0;
}
DFS序
树上多个点的LCA,就是DFS序最小的和DFS序最大的这两个点的LCA。
这可以用来判断,一个点是否在另外一个点的子树里面。
int indfn[N], outdfn[N];
// tot=0;
void dfs(int x, int fa) {//求dfn序
indfn[x] = ++tot;
for (auto y : v[x]) {
if (y == fa) continue;
dfs(y, x);
}
outdfn[x] = tot;
}
bool isin(int u, int v) {//判断
return indfn[u] <= indfn[v] && outdfn[v] <= outdfn[u];
}
bool is_link(vector<int> p) {
sort(p.begin(), p.end(), cmp);
for (int i = 1; i < p.size(); i++) {
if (!isin(p[i - 1], p[i])) return 0;
}
return 1;
}
最近公共祖先:LCA(nlogn)
void bfs() {
queue<int> q;
q.push(1);
dpth[1] = 1;
while (!q.empty()) {
int x = q.front();
q.pop();
for (auto y : v[x]) {
if (dpth[y]) continue;
dpth[y] = dpth[x] + 1;
//此处可以顺便求从原点出发的距离/异或路径等
q.push(y);
f[y][0] = x;
rep(j, 1, t) {
//此处可以通过dp求别的东西 用类似的思想
f[y][j] = f[f[y][j - 1]][j - 1];
}
}
}
}
int lca(int x, int y) {
if (dpth[x] > dpth[y]) swap(x, y);
per(i, t, 0) {
if (dpth[x] <= dpth[y] - (1 << i)) y = f[y][i];
}
if (x == y) return x;
per(i, t, 0) {
if (f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
}
return f[x][0];
}
t = log(n) / log(2) + 1;
bfs();
//将树上一个点往上移动depth长度所到达的点
int lca(int u, int depth) {
for (int i = 20; i >= 0; i--) {
if (depth <= dpth[u] - (1 << i)) { u = f[u][i]; }
}
return u;
}
轻/重链剖分
- 重儿子:对于每一个非叶子节点,它的儿子中 以那个儿子为根的子树节点数最大的儿子 为该节点的重儿子。
- 轻儿子:对于每一个非叶子节点,它的儿子中 非重儿子 的剩下所有儿子即为轻儿子。
- 重边:一个父亲连接他的重儿子的边称为重边。
- 轻边:剩下的即为轻边。
- 重链:相邻重边连起来的 连接一条重儿子 的链叫重链。每一条重链以轻儿子为起点。
对于叶子节点,若其为轻儿子,则有一条以自己为起点的长度为1的链。
int fa[N]; //父节点
int hson[N]; //重儿子
int dpth[N]; //深度
int siz[N]; //子树大小
vector<int> v[N];
// dpth[root]=1
void dfs1(int x, int f) {
//处理以上四个信息
fa[x] = f;
dpth[x] = dpth[f] + 1;
siz[x] = 1;
int now = -1; //重儿子的儿子个数
for (auto y : v[x]) {
if (y == f) continue;
dfs1(y, x);
siz[x] += siz[y];
if (siz[y] > now) {
now = siz[y];
hson[x] = y;
}
}
}
int u[N];
int id[N]; //新编号
int wid[N]; //新编号的权值
int ntop[N]; //点所在的链的顶端
//处理(确定)新编号
//处理链的顶端
void dfs2(int x, int tp) {
id[x] = ++tot;
wid[tot] = u[x];
ntop[x] = tp;
if (!hson[x]) return; //无子节点 也就是x是叶子节点
dfs2(hson[x], tp); //先处理重儿子
for (auto y : v[x]) {
if (y == hson[x] || y == fa[x]) continue;
dfs2(y, y); //重链的起点都是儿子
}
}
// 1.查询一条链上的区间和
int askL(int a, int b) {
int ans = 0;
while (ntop[a] != ntop[b]) {
if (dpth[ntop[a]] < dpth[ntop[b]]) swap(a, b); //始终让a链的顶端更深
ans += query(1, id[ntop[a]], id[a]); //线段树上的区间查询
ans %= mod;
a = fa[ntop[a]];
}
if (dpth[a] < dpth[b]) swap(a, b); //让a更深
ans += query(1, id[b], id[a]); //因为一条链上新编号连续 且上边的新编号更小 所以id[a]不会小于id[b]
ans %= mod;
return ans;
}
// 2.区间修改一条链上的值
void changeL(int a, int b, int z) {
while (ntop[a] != ntop[b]) {
if (dpth[ntop[a]] < dpth[ntop[b]]) swap(a, b); //始终让a链的顶端更深
change(1, id[ntop[a]], id[a], z); //线段树上的区间查询
a = fa[ntop[a]];
}
if (dpth[a] < dpth[b]) swap(a, b); //让a更深
change(1, id[b], id[a], z); //因为一条链上新编号连续 且上边的新编号更小 所以id[a]不会小于id[b]
return;
}
//查询和修改大致相同 区别只在于调用线段树的操作
// 3.查询子树和
int askT(int x) {
return query(1, id[x], id[x] + siz[x] - 1); //每一个子树的新编号也是连续的,所以可以直接求
}
// 4.子树修改
void changeT(int x, int z) {
change(1, id[x], id[x] + siz[x] - 1, z);
}
启发式合并
启发式合并就是在两个集合合并的时候,将size小的那个集合合并到size大的那个集合里面。
NlogN的复杂度,因为每个元素至多被pushback logn次
void merge(vector<int>&a, vector<int>&b) {
if (a.size() > b.size()) {
for (int x : b)a.push_back(x);
}
else {
for (int x : a)b.push_back(x);
}
}
线段树求和
struct sgt {
int l, r, sum, add;
} tree[4 * N];
void build(int p, int l, int r) {
tree[p].l = l;
tree[p].r = r;
if (l == r) {
tree[p].sum = wid[l];
tree[p].sum %= mod;
return;
}
int mid = (l + r) >> 1;
build(2 * p, l, mid);
build(2 * p + 1, mid + 1, r);
tree[p].sum = (tree[2 * p].sum + tree[2 * p + 1].sum) % mod;
}
void spread(int p) {
if (tree[p].add != 0) {
tree[2 * p].add += tree[p].add;
tree[2 * p + 1].add += tree[p].add;
tree[2 * p].sum += tree[p].add * (tree[2 * p].r - tree[2 * p].l + 1);
tree[2 * p + 1].sum += tree[p].add * (tree[2 * p + 1].r - tree[2 * p + 1].l + 1);
tree[2 * p].sum %= mod;
tree[2 * p + 1].sum %= mod;
tree[2 * p].add %= mod;
tree[2 * p + 1].add %= mod;
tree[p].add = 0;
}
}
void change(int p, int l, int r, int d) {
if (l <= tree[p].l && r >= tree[p].r) {
tree[p].sum += d * (tree[p].r - tree[p].l + 1);
tree[p].add += d;
tree[p].sum %= mod;
tree[p].add %= mod;
return;
}
spread(p);
int mid = (tree[p].l + tree[p].r) >> 1;
if (l <= mid) change(2 * p, l, r, d);
if (r > mid) change(2 * p + 1, l, r, d);
tree[p].sum = tree[2 * p].sum + tree[2 * p + 1].sum;
tree[p].sum %= mod;
return;
}
int query(int p, int l, int r) {
if (l <= tree[p].l && r >= tree[p].r) return tree[p].sum;
spread(p);
int mid = (tree[p].l + tree[p].r) >> 1;
int val = 0;
if (l <= mid) val += query(2 * p, l, r);
if (r > mid) val += query(2 * p + 1, l, r);
val %= mod;
return val;
}
线段树区间赋值
struct sgt {
int l, r, sum, add;
} tree[4 * N];
void build(int p, int l, int r) {
tree[p].l = l;
tree[p].r = r;
tree[p].add = -1;
if (l == r) return;
int mid = (l + r) >> 1;
build((p << 1), l, mid);
build((p << 1) | 1, mid + 1, r);
tree[p].sum = (tree[(p << 1)].sum + tree[(p << 1) | 1].sum);
}
void spread(int p) {
if (tree[p].add != -1) {
tree[(p << 1)].add = tree[p].add;
tree[(p << 1) | 1].add = tree[p].add;
tree[(p << 1)].sum = tree[p].add * (tree[(p << 1)].r - tree[(p << 1)].l + 1);
tree[(p << 1) | 1].sum = tree[p].add * (tree[(p << 1) | 1].r - tree[(p << 1) | 1].l + 1);
tree[p].add = -1;
}
}
void change(int p, int l, int r, int d) {
if (l <= tree[p].l && r >= tree[p].r) {
tree[p].sum = d * (tree[p].r - tree[p].l + 1);
tree[p].add = d;
return;
}
spread(p);
int mid = (tree[p].l + tree[p].r) >> 1;
if (l <= mid) change((p << 1), l, r, d);
if (r > mid) change((p << 1) | 1, l, r, d);
tree[p].sum = tree[(p << 1)].sum + tree[(p << 1) | 1].sum;
return;
}
int query(int p, int l, int r) {
if (l <= tree[p].l && r >= tree[p].r) return tree[p].sum;
spread(p);
int mid = (tree[p].l + tree[p].r) >> 1;
int val = 0;
if (l <= mid) val += query((p << 1), l, r);
if (r > mid) val += query((p << 1) | 1, l, r);
return val;
}
李超树
//李超线段树
//添加一条线段(给出起点终点)
//询问x=k处 所有线段的max
//每个节点存的是该区间优势最大的线段
//依次枚举包含 x=k 的线段树区间,取每个区间的最大优势线段的对应纵坐标的最大值即可
//对于垂直于x的线段 存储为y=0x+y(l=r=x) (y是上顶点) 因为只关心最大值 同时要能够表达出这条线段
struct line {
dd k, b;
} u[N];
int tree[4 * N];
dd Calc(line a, int x) {
return a.k * x + a.b;
}
int query(int p, int l, int r, int x) { // 单点查询
if (l == r) return tree[p];
int mid = (l + r) >> 1;
if (x <= mid) {
int tmp = query(p << 1, l, mid, x);
return Calc(u[tmp], x) - Calc(u[tree[p]], x) > EPS ? tmp : tree[p];
} else {
int tmp = query(p << 1 | 1, mid + 1, r, x);
return Calc(u[tmp], x) - Calc(u[tree[p]], x) > EPS ? tmp : tree[p];
}
}
// Modify(1, 1, N, aaa, ccc, cnt);
// 1, 左区间 ,右区间,修改的区域l,r, 和线段的k,b
inline void Modify(int p, int l, int r, int s, int t, int x) { // 插入一条线段
int mid = (l + r) >> 1;
if (s <= l && r <= t) { //完全包含
if (Calc(u[x], mid) - Calc(u[tree[p]], mid) > EPS) swap(tree[p], x);
if (Calc(u[x], l) - Calc(u[tree[p]], l) > EPS) Modify(p << 1, l, mid, s, t, x);
if (Calc(u[x], r) - Calc(u[tree[p]], r) > EPS) Modify(p << 1 | 1, mid + 1, r, s, t, x);
return;
}
if (s <= mid) Modify(p << 1, l, mid, s, t, x);
if (t > mid) Modify(p << 1 | 1, mid + 1, r, s, t, x);
}
signed main() {
cf {
Q = read();
if (Q == 1) {
cin >> a >> b >> c >> d;
aaa = (a + mas - 1) % 39989 + 1;
bbb = (b + mas - 1) % 1000000000 + 1;
ccc = (c + mas - 1) % 39989 + 1;
ddd = (d + mas - 1) % 1000000000 + 1;
if (aaa > ccc) swap(aaa, ccc), swap(bbb, ddd);
if (aaa == ccc)
u[++cnt] = {0, (dd)max(bbb, ddd)};
else
u[++cnt] = {1.0 * (ddd - bbb) / (ccc - aaa), bbb - 1.0 * (ddd - bbb) / (ccc - aaa) * aaa};
Modify(1, 1, N, aaa, ccc, cnt);
} else {
k = read();
kkk = (k + mas - 1) % 39989 + 1;
// ask()
mas = query(1, 1, N, kkk);
cout << mas << "\n";
}
}
return 0;
}
主席树
应用:求静态区间第k小的数
// 利用了前缀和 线段树维护的是权值的分布数量
// 作差 即可得到区间范围的值的个数
int u[N], vv[N];
int rot[N];
struct node {
int l, r, val;
} tree[N << 5];
int clone(int p) {
tot++;
tree[tot] = tree[p];
return tot;
}
int build(int p, int l, int r) {
p = ++tot;
if (l == r) return p;
int mid = (l + r) >> 1;
tree[p].l = build(tree[p].l, l, mid);
tree[p].r = build(tree[p].r, mid + 1, r);
return p;
}
int add(int p, int l, int r, int val) {
p = clone(p);
tree[p].val++;
if (l == r) { return p; }
int mid = (l + r) >> 1;
if (val <= mid) {
tree[p].l = add(tree[p].l, l, mid, val);
} else {
tree[p].r = add(tree[p].r, mid + 1, r, val);
}
// tree[p].val = tree[tree[p].l].val + tree[tree[p].r].val;
return p;
}
int ask(int p1, int p2, int l, int r, int x) {
if (l == r) { return l; }
int mid = (l + r) >> 1;
int y = tree[tree[p2].l].val - tree[tree[p1].l].val; //左子的个数
if (x <= y) {
return ask(tree[p1].l, tree[p2].l, l, mid, x);
} else {
return ask(tree[p1].r, tree[p2].r, mid + 1, r, x - y);
}
}
signed main() {
n = read();
m = read();
rep(i, 1, n) u[i] = read(), vv[i] = u[i];
sort(vv + 1, vv + 1 + n);
int x = unique(vv + 1, vv + 1 + n) - vv - 1;
rot[0] = build(0, 1, x);
rep(i, 1, n) {
int now = lower_bound(vv + 1, vv + 1 + x, u[i]) - vv;
rot[i] = add(rot[i - 1], 1, x, now);
}
rep(i, 1, m) {
l = read();
r = read();
k = read();
cout << vv[ask(rot[l - 1], rot[r], 1, x, k)] << "\n";
}
return 0;
}
可持久化数组
用主席树来实现。操作:单点修改 单点查询
注意数组要开大
struct kkk {
int l, r, val;
} tree[25 * N];//m*logn+4*N
int u[N];
int rot[N]; //每个版本号所对应的树根的编号
int clone(int p) {
top++;
tree[top] = tree[p]; //全部信息都传到新节点
return top;
}
int build(int p, int l, int r) {
//与普通线段树的区别:节点编号不再是2*p+1 而是用结构体存储左右子节点
//单点修改 每次只复制一份被修改的数据 所以每次空间只多了logn 动态开点 省空间
//另外用数组存每个历史版本的树根
//修改树边 只要修改子节点即可
//返回的是 树的根的编号
p = ++top;
if (l == r) {
tree[p].val = u[l];
return p;
}
int mid = (l + r) >> 1;
tree[p].l = build(tree[p].l, l, mid);
tree[p].r = build(tree[p].r, mid + 1, r);
return p;
}
int update(int p, int l, int r, int x, int val) { // p是版本号所对应的根
p = clone(p); //更新就要新建根节点(new版本)
if (l == r) {
tree[p].val = val;
} else {
int mid = (l + r) >> 1;
if (x <= mid)
tree[p].l = update(tree[p].l, l, mid, x, val); //访问左子树
else
tree[p].r = update(tree[p].r, mid + 1, r, x, val); //访问右子树
}
return p;
}
int ask(int p, int l, int r, int x) {
if (l == r) {
return tree[p].val;
} else {
int mid = (l + r) >> 1;
if (x <= mid)
return ask(tree[p].l, l, mid, x); //访问左子树
else
return ask(tree[p].r, mid + 1, r, x); //访问右子树
}
}
ST求最值
//t=log(n)/log2)+1;
void st() {
rep(i, 1, n) {
rep(j, 1, 32) {
g[i][j] = 0;
}
}
rep(i, 1, n) g[i][0] = as[i];
rep(j, 1, t) {
rep(i, 1, n - (1 << j) + 1) {
g[i][j] = max(g[i][j - 1], g[i + (1 << (j - 1))][j - 1]);
}
}
}
int query(int l, int r) {
if (l > r) return 0;
int k = log(r - l + 1) / log(2);
return max(g[l][k], g[r - (1 << k) + 1][k]);
}
KRUSKAL重构树
int find(int x) {
if (x == bcj[x]) return x;
return bcj[x] = find(bcj[x]);
}
struct node {
int u, v, w;
} u[N];
bool cmp(node a, node b) {
return a.w < b.w;
}
void kruskal() {
// sort(u + 1, u + 1 + m, cmp);
int lim = (n << 1) - 1;
rep(i, 1, lim) v[i].clear();
tot = n;
// cout << tot << "\n";
rep(i, 1, lim) bcj[i] = i;
rep(i, 1, m) {
int x = find(u[i].u);
int y = find(u[i].v);
if (x == y) continue;
tot++;
bcj[x] = tot;
bcj[y] = tot;
bcj[tot] = tot;
val[tot] = u[i].w;
v[tot].push_back(x);
v[tot].push_back(y);
if (tot == lim) {
break;
}
}
}
Spfa判正环
vector<par> v[N];
bool is[N];
dd d[N];
int ct[N];
bool spfa() {
queue<int> q;
rep(i, 1, n) d[i] = 0, ct[i] = 0, is[i] = 1, q.push(i);
while (!q.empty()) {
int x = q.front();
q.pop();
is[x] = 0;
for (auto [y, w] : v[x]) {
w = log(w);//边权太大 取对数
if (d[y] < d[x] + w) {
d[y] = d[x] + w;
ct[y] = ct[x] + 1;
if (ct[y] >= n) return 0;//有
if (!is[y]) q.push(y), is[y] = 1;
}
}
}
return 1;//无
}
求二元二次函数最值
三分化为一元,然后公式求出极值点,On判三分,需要注意精度问题,三分终止条件可以写成次数而非r-l的差值
dd ask(dd fir) {
return 1.0 * sum / n - (n - 1) / 2.0 * fir;
}
dd f(dd fir) {
dd k = ask(fir);
dd now = k;
dd ans = 0;
ans += (u[1] - now) * (u[1] - now);
rep(i, 2, n) {
now += fir;
ans += (u[i] - now) * (u[i] - now);
}
return ans;
}
signed main() {
cf {
n = read();
sum = 0;
rep(i, 1, n) u[i] = read(), sum += u[i];
dd l = -1e9, r = 1e9;
cnt = 0;
while (cnt++ <= 120) {
dd m1 = l + (r - l) / 3;
dd m2 = r - (r - l) / 3;
if (f(m2) > f(m1)) {
r = m2;
} else
l = m1;
}
printf("%.9f\n", (double)f(l));
}
return 0;
}