算法补全——倍增、树上倍增与LCA
算法补全——倍增、树上倍增与LCA
倍增
倍增法可以将区间转换成\(2\)的幂次方长度,在查询中进行快速的跳转,将线性的处理转化为对数级的处理。
2022牛客寒假算法基础集训营1 B-炸鸡块君与FIFA22
热爱足球(仅限游戏)的炸鸡块君最近购买了FIFA22,并且沉迷于FIFA22的Rivals排位上分。
在该排位系统中,每局游戏可能有胜利(用W表示)、失败(用L表示)、平局(用D表示)三种结果,胜利将使得排位分加一、失败使排位分减一、平局使排位分不变。特别地,该排位系统有着存档点机制,其可以简化的描述为:若你当前的排位分是333的整倍数(包括0倍),则若下一局游戏失败,你的排位分将不变(而不是减一)。
现在,给定一个游戏结果字符串和若干次询问,你需要回答这些询问。
每次询问格式为\((l,r,s)\),询问若你初始有\(s\)分,按从左到右的顺序经历了\([l,r]\)这一子串的游戏结果后,最终分数是多少。
每次给出的分数会影响在失败时的得分情况,暴力求解需要线性的时间,显然不现实。因为不需要进行区间修改,线段树会显得有点杀鸡用牛刀。
这里可以利用ST表来解决。\(st[i][j][k]\)表示起始分数模数为\(i\),起始位置为\(j\),区间长度为\(2^k\)的得分情况,状态转移类似于区间dp。对于每次查询,只需要对数级的时间即可。
#include <bits/stdc++.h>
#define ll long long
#define pb push_back
#define fast ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
using namespace std;
const int maxn = 2e5 + 10;
int n, q;
int st[3][maxn][21];
char s[maxn];
int mod3(int x)
{
return (x % 3 + 3) % 3;
}
int main()
{
fast;
cin >> n >> q;
cin >> s + 1;
for (int j = 0; j <= 20; j++)
{
for (int i = 1; i <= n; i++)
{
if (j == 0)
{
if (s[i] == 'W')
{
st[0][i][j] = st[1][i][j] = st[2][i][j] = 1;
}
if (s[i] == 'L')
{
st[1][i][j] = st[2][i][j] = -1;
st[0][i][j] = 0;
}
if (s[i] == 'D')
{
st[0][i][j] = st[1][i][j] = st[2][i][j] = 0;
}
}
else
{
int p1 = i, p2 = i + (1 << (j - 1));
if (p2 > n)
{
st[0][p1][j] = st[0][p1][j - 1];
st[1][p1][j] = st[1][p1][j - 1];
st[2][p1][j] = st[2][p1][j - 1];
}
else
{
st[0][p1][j] = st[0][p1][j - 1] + st[mod3(0 + st[0][p1][j - 1])][p2][j - 1];
st[1][p1][j] = st[1][p1][j - 1] + st[mod3(1 + st[1][p1][j - 1])][p2][j - 1];
st[2][p1][j] = st[2][p1][j - 1] + st[mod3(2 + st[2][p1][j - 1])][p2][j - 1];
}
}
}
}
int l, r, ans;
while (q--)
{
cin >> l >> r >> ans;
int pos = l;
while (pos <= r)
{
int j = 0;
while (pos + (1 << j) - 1 <= r)
j++;
j--;
ans += st[ans % 3][pos][j];
pos += (1 << j);
}
cout << ans << '\n';
}
return 0;
}
树上倍增
树上倍增更多的作用是为了快速找到第\(k\)个父亲。
用\(fa[i][j]\)表示节点\(i\)的第\(2^j\)个父亲是谁,可以很容易的得到:\(fa[i][j]=fa[fa[i][j-1]][j-1]\)。这样我们可以进行如下操作:
在dfs遍历树的同时记录\(fa[i][0]\),之后利用\(fa[i][j]=fa[fa[i][j-1]][j-1]\)得到整个\(fa\)数组。
LCA
LCA,最近公共祖先。对于\(u,v\)两个节点,首先先将两个节点跳转到同一深度,之后根据预处理的\(fa\)数组一起向上跳转寻找公共祖先。
LCA部分代码如下,其中预处理\(lg[i]\)表示\(i\)的对数:
int lca(int x, int y)
{
if (deep[x] < deep[y])
swap(x, y);
for (int i = lg[n - 1]; i >= 0; i--)
{
if (deep[f[x][i]] >= deep[y])
x = f[x][i];
}
if (x == y)
return x;
for (int i = lg[n - 1]; i >= 0; i--)
{
if (f[x][i] != f[y][i])
x = f[x][i], y = f[y][i];
}
return f[x][0];
}
2022牛客寒假算法基础集训营2 G-小沙的身法
小沙发现他打球打不赢别人,打架也打不赢别人,所以他去了少林寺学习身法,以求打球的时候可以跑的更快,打架的时候也可以跑的更快。
少林寺的方丈告诉小沙,他这种情况可以练习跳木桩,少林寺的一共有\(n\)个木桩,为了锻炼打球时的步伐,方丈严格限制的小沙的可移动路线,他规定小沙一定要按照他制定的方案来训练,否则小沙就会被方丈制裁。
小沙发现 方丈制定的路线是一个以一个点扩散开的树形结构,而小沙就需要在这一个树形结构上跳来跳去。由于两个木桩之间的间隔特别特别远,所以每次跳跃都只能在相邻的木桩之间移动。
以地面高度为例,如果地面的高度都视为\(0\)(平地),木桩的高度不一,我们视每个木桩高度为\(a[i]\),小沙每次从低处跳向高处都会消耗两地高度之差的体力,但是从高的跳向低的则不消耗体力。
小沙练习了许久,终于熟能生巧了。
于是他叫来了师傅,想要给师傅秀一吧。
师傅便给了他\(m\)个考验 每个考验师傅都要他从地面上跳上指定的\(u\)节点,然后最快速的速度跳向\(v\)节点,然后回到地面,小沙心中也没底所以想问你他至少需要多少体力才能把这个考验完成。
每个考验均相互独立,小沙的体力不会受到上一次考验的影响。
由于是最快到达,对于\(u->v\)的最快方法,路径为\(u->lca(u,v)->v\),当\(u,v\)在同一条链上时,\(lca(u,v)=u或v\)。在处理时,若当前节点为\(x\),其父节点为\(fa\),需要同时记录\(x->fa\)和\(fa->x\)的体力消耗。可以利用二维数组记录,在内存吃紧的情况下也可以利用前缀和记录。若利用前缀和记录,最终的结果为\(up[u]-up[lca(u,v)]+down[v]-down[lca(u,v)]+a[i]\)。
#include <bits/stdc++.h>
#define pb push_back
#define ppb pop_back
#define rep(i, a, b) for (int i = a; i <= b; i++)
#define per(i, a, b) for (int i = b; i >= a; i--)
#define fast ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
typedef long long ll;
using namespace std;
const int maxn = 1e6 + 10;
vector<int> G[maxn];
int deep[maxn];
ll up[maxn];
ll down[maxn];
int f[maxn][21];
ll h[maxn];
int lg[maxn];
int n, m;
void dfs(int x, int fa)
{
deep[x] = deep[f[x][0] = fa] + 1;
up[x] = up[fa] + max(0ll, h[fa] - h[x]);
down[x] = down[fa] + max(0ll, h[x] - h[fa]);
for (auto v : G[x])
{
if (v == fa)
continue;
dfs(v, x);
}
}
int lca(int x, int y)
{
if (deep[x] < deep[y])
swap(x, y);
for (int i = lg[n - 1]; i >= 0; i--)
{
if (deep[f[x][i]] >= deep[y])
x = f[x][i];
}
if (x == y)
return x;
for (int i = lg[n - 1]; i >= 0; i--)
{
if (f[x][i] != f[y][i])
x = f[x][i], y = f[y][i];
}
return f[x][0];
}
int main()
{
fast;
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> h[i];
}
for (int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
G[u].pb(v);
G[v].pb(u);
}
dfs(1, 0);
lg[0] = -1;
for (int i = 1; i <= n; i++)
{
lg[i] = lg[i >> 1] + 1;
}
for (int j = 1; j <= lg[n - 1]; j++)
{
for (int i = 1; i <= n; i++)
{
f[i][j] = f[f[i][j - 1]][j - 1];
}
}
while (m--)
{
int u, v;
cin >> u >> v;
int l = lca(u, v);
cout << up[u] - up[l] + down[v] - down[l] + h[u] << '\n';
}
return 0;
}