为了能到远方,脚下的每一步都不能|

Aurora-JC

园龄:3年粉丝:3关注:4

2023-06-04 15:44阅读: 263评论: 0推荐: 1

*【学习笔记】(18) 长链剖分

长链剖分

1.算法简介与性质

长链剖分本质上就是另外一种链剖分方式。

长链剖分与重链剖分有相通之处,后者是将 子树大小 最大的儿子作为重儿子,前者则是将 子树深度 最大的儿子作为重儿子。可见两者只是换了一个剖分形式。

长链剖分有如下性质:

  • 性质 1:每个节点所在长链末端为其子树内最深节点。根据定义可知。
  • 性质 2: 一个节点的 k 级祖先所在长链长度一定不小于 k
    • xk 级祖先为 yk 级祖先,向上跳 k 到达的祖先),如果 y 所在的长链小于 k, 那么它所在的链一定不是长链,显然 yx 这条链更优。那么 y 所在的重链长度至少为 k,性质成立。
  • 性质 3:从根节点到任意叶子节点经过的轻边条数不超过 n,这比重链剖分 log n 稍劣一些。
    • 如果一个点 x 从一条重链跳到了另外一条重链上,那么跳跃到的这条重链的长度不会小于之前的重链长度。
      那么在最坏的情况下,重链长度分别为 1,2,3,...,n,也就是最多跳跃 n次。

2.应用:树上 k 级祖先

n log n 倍增预处理求出每个节点 u2k 级祖先,以及对于每条长链,从长链顶端向上 / 向下 i 步分别能走到哪个节点,其中 i 不大于长链深度。此外,预处理每个数在二进制下的最高位,记为 hi

查询 (u,k) (uk 级祖先)
首先跳到 u2hk 级祖先 v。由于我们预处理了从 v 所在长链顶端 t 向上 / 下走不超过链长步分别到哪个节点,故不难直接查询。综上,时间复杂度为 O(n log n)O(1)

P5903 【模板】树上 k 级祖先

#include<bits/stdc++.h>
#define ui unsigned int
#define ll long long
using namespace std;
const int N = 5e5 + 67;
ui s;
inline ui get(ui x) {
x ^= x << 13, x ^= x >> 17, x ^= x << 5;
return s = x;
}
int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, q, rt, lg;
ll ans;
int f[N][20], d[N], son[N], top[N], mxd[N], h[N];
int tot, Head[N], to[N], Next[N];
vector<int> up[N], down[N];
void add(int u, int v){
to[++tot] = v, Next[tot] = Head[u], Head[u] = tot;
}
void dfs1(int x){
d[x] = d[f[x][0]] + 1, mxd[x] = 1;
for(int i = 1; i <= lg; ++i) f[x][i] = f[f[x][i - 1]][i - 1];
for(int i = Head[x]; i; i = Next[i]){
int y = to[i]; dfs1(y);
mxd[x] = max(mxd[x], mxd[y] + 1);
if(mxd[son[x]] < mxd[y]) son[x] = y;
}
}
void dfs2(int x, int topf){
if(x == topf) up[x].resize(mxd[x]), down[x].resize(mxd[x]);
down[topf][d[x] - d[topf]] = x, top[x] = topf;
if(son[x]) dfs2(son[x], topf);
for(int i = Head[x]; i; i = Next[i]){
int y = to[i]; if(y == son[x]) continue;
dfs2(y, y);
}
}
int main(){
n = read(), q = read(), s = read(), lg = log2(n);
for(int i = 1; i <= n; ++i) !(f[i][0] = read()) ? (rt = i, void()) : add(f[i][0], i);
dfs1(rt), dfs2(rt, rt);
for(int i = 1; i <= n; ++i){
if(top[i] != i) continue;
for(int j = 0, cur = i; j < mxd[i] && cur; ++j, cur = f[cur][0]) up[i][j] = cur;
}
for(int i = 1; i <= n; ++i) for(int j = 0; j <= lg; ++j) if(i >> j & 1) h[i] = j;
for(int i = 1, las = 0; i <= q; ++i){
int x = (get(s) ^ las) % n + 1, k = (get(s) ^ las) % d[x];
x = !k ? x : f[x][h[k]], k -= (k == 0 ? 0 : (1 << h[k]));
int t = top[x];
if(d[x] - k >= d[t]) las = down[t][d[x] - k - d[t]];
else las = up[t][d[t] - (d[x] - k)];
ans ^= 1ll * i * las;
}
printf("%lld\n", ans);
return 0;
}

3. 应用:优化深度相关的 DP

3.1 一般形式

长链剖分的价值主要体现在能优化树上 与深度有关 的 DP。如果子树内 每个深度仅有一个信息,就可以使用长链剖分优化。一般形式如:设 fi,j 表示以 i 为根的子树内,深度为 j 的节点的贡献。

3.2 例题 CF1009F Dominant Indices

我们只关心每个节点子树内深度为 j 的节点个数而非具体是哪些节点,很容易写出转移方程。设 fi,j 表示子树 i 深度为 j 的节点个数,则

fi,j=kson(i) fk,j1

fi,0=1

直接暴力显然会 T 飞。

这时候就要请出我们的 长链剖分优化DP 了。

我们可以采用一个优化策略:对于一个节点 x,我们先对它的长儿子做 DP,但这里可以使用一些技巧,让长儿子把 dp 出来的东西直接存到 fx​ 里面去(当然观察 dp 式可以发现这边需要错一位, 即 fx,i=fson[x],i1),然后再把其他轻儿子 dp 出来的东西与 fu​ 暴力合并。

这里我们详细说一下如何实现这些优化(好多博客都没有讲的很清楚,导致我看了很久,也可能是我太菜了)。

这里我们使用指针为 f 动态申请内存来减少空间消耗。显然空间变成 O(n) 的了。

我们只需要对每一个长链的顶端节点申请内存,具体的,对 u 节点申请内存后,设 v 为它的长儿子,那么 fv=fu+1,这样写还有一个好处,就是我们可以直接实现 fx,i=fson[x],i1 这一步操作,因为 v 的下标相当于 u 的往前移了一位。

这样时间也能变成 O(n) 了。因为每个节点都只会在它所在的长链顶端被暴力合并一次。

答案的统计就很显然了。注意一点,由于枚举距离的时候没有枚举 0 ,所以 0 的情况要特判一下,即fx,ansu=1 的时候(子树为一条链)。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 67;
int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, cur;
int buf[N], ans[N], *f[N], mx[N];
int fa[N], d[N], mxd[N], son[N];
int tot, Head[N << 1], Next[N << 1], to[N << 1];
void add(int u, int v){
to[++tot] = v, Next[tot] = Head[u], Head[u] = tot;
}
void dfs1(int x, int f){
d[x] = d[f] + 1, fa[x] = f;
for(int i = Head[x]; i; i = Next[i]){
int y = to[i]; if(y == f) continue;
dfs1(y, x);
if(mxd[y] > mxd[son[x]]) son[x] = y;
}
mxd[x] = mxd[son[x]] + 1;
}
void dfs2(int x){
f[x][0] = 1;
if(son[x]){
f[son[x]] = f[x] + 1; //共享内存
dfs2(son[x]), ans[x] = ans[son[x]] + 1; //从长儿子继承答案
}
for(int i = Head[x]; i; i = Next[i]){
int y = to[i]; if(y == fa[x] || y == son[x]) continue;
f[y] = buf + cur, cur += mxd[y], dfs2(y); //分配内存
for(int j = 1; j <= mxd[y]; ++j){
f[x][j] += f[y][j - 1]; //暴力合并
if(f[x][j] > f[x][ans[x]] || (f[x][j] == f[x][ans[x]] && j < ans[x])) ans[x] = j; //更新答案
}
}
if(f[x][ans[x]] == 1) ans[x] = 0;
}
int main(){
n = read();
for(int i = 1; i < n; ++i){
int u = read(), v = read();
add(u, v), add(v, u);
}
dfs1(1, 0), f[1] = buf, cur += mxd[1], dfs2(1);
for(int i = 1; i <= n; ++i) printf("%d\n", ans[i]);
return 0;
}

3.3 经典结论

这个结论实在是太经典了,以至于它经常出现:选一个节点能覆盖它到根的所有节点。那么选 k 个节点,覆盖的最多节点数就是前 k 条长链长度之和,选择的节点即 k 条长链末端。结果显然。

4.例题

Ⅰ. P4292 [WC2010]重建计划

Ⅱ. P5904 [POI2014]HOT-Hotels 加强版

题解

Ⅲ.CF526G Spiders Evil Plan

Ⅳ. P3441 [POI2006]MET-Subway

找到直径一端作为根,长链剖分取 2l1 个叶子即可。

画一下图发现显然,这题居然卡空间,可以参考洛谷题解的另一种做法,我下面展示的代码会 MLE

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 67;
int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
//bool x1;
int n, k, rt, maxn;
int tot, Head[N], to[N << 1], Next[N << 1];
int son[N], mxd[N];
vector<int> E[N];
bool vis[N];
void add(int u, int v){
to[++tot] = v, Next[tot] = Head[u], Head[u] = tot;
}
void find_root(int x){
queue<int> q;
q.push(x); mxd[x] = 1;
while(!q.empty()){
int x = q.front(); q.pop();
for(int i = Head[x]; i; i = Next[i]){
int y = to[i]; if(mxd[y]) continue;
mxd[y] = mxd[x] + 1;
if(mxd[y] > maxn) maxn = mxd[y], rt = y;
q.push(y);
}
}
}
void dfs(int x, int fa){
for(int i = Head[x]; i; i = Next[i]){
int y = to[i]; if(y == fa) continue;
dfs(y, x); if(mxd[y] > mxd[son[x]]) son[x] = y;
}
mxd[x] = mxd[son[x]] + 1;
}
int main(){
n = read(), k = read() * 2 - 1;
for(int i = 1; i < n; ++i){
int u = read(), v = read();
add(u, v), add(v, u);
}
find_root(1);
memset(mxd, 0, sizeof(mxd));
dfs(rt, 0);
int ans = 0;
for(int i = 1; i <= n; ++i) E[mxd[i]].push_back(i);
for(int i = n; i; --i){
for(int x : E[i]){
if(vis[x]) continue;
int cur = x; --k;
while(cur) ++ans, vis[cur] = 1, cur = son[cur];
if(!k) break;
}
if(!k) break;
}
printf("%d\n", ans);
// bool x2;
// fprintf(stderr, "%.3lf\n", abs(&x1 - &x2) / 1048576.0);
return 0;
}

参考:https://www.cnblogs.com/alex-wei/p/Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree.html

本文作者:南风未起

本文链接:https://www.cnblogs.com/jiangchen4122/p/17455781.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Aurora-JC  阅读(263)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起