长链剖分和重链剖分类差不多,都是解决树上问题的一些trick。
下面定义(dep[u])表示(u)到根节点路径上有多少条边((u)的深度),(mdep[u])表示(u)子树内的所有点的深度的最大值。
对比重链剖分选择(size)最大的儿子作为重儿子,长链剖分选择(mdep)最大的儿子作为“重”儿子(这样方便优化一些树上以深度为下标的DP)。
还是直接来看题吧QAQ
LuoguP5903 【模板】树上 k 级祖先
https://www.luogu.com.cn/problem/P5903
这题搞法挺多的...而且实际跑下来长链剖分也不是很快...但还是讲一下吧
不难想到一些单次询问在(O(log n))时间内回答的做法,如果重链剖分后二分重链的话可以做到(O(log log n)),但这些都不是重点,长链剖分可以做到(O(n log n))预处理,单次(O(1))回答 别问我为什么这样还跑的那么慢...
首先长链有一个性质,对于点(u),它的(k)级祖先所在的长链的长度一定大于或等于(k)。
这很显然,因为如果那条长链的长度小于(k)的话直接用(u)的(k)级祖先到(u)这一段就可以得到一条更长的链,与长链的定义矛盾。
然后有啥子用呢?我们先预处理出(1...n)这(n)个数二进制下的最高位(high-bit),然后第一遍(Dfs)时算一个数组(p[x][i])表示(x)向上跳(2^i)步所到达的节点。
然后对于一个询问((u,k)),令(h=2^{lfloorlog_2k floor}),先把(u)调到(f[u][h])上,此时(u)还要向上跳(k-h)层,此时(u)所在重链的长度一定(ge h>k-h),所以直接将(u)跳至链顶,判断向上或者向下跳即可。
也就是说对于一跳重链的链顶(u),维护两个数组(up[u][0...len],down[u][0...len])((len)表示长链长度)。
void dfs1(int u,int f,int deep) {
dep[u]=deep,mdep[u]=dep[u];
p[u][0]=f;
rep(i,1,LOGN) p[u][i]=p[p[u][i-1]][i-1];
for (auto v:e[u]) if (v!=f) {
dfs1(v,u,deep+1);
if (mdep[v]>mdep[u])
mdep[u]=mdep[v],son[u]=v;
}
}
void dfs2(int u,int topf) {
top[u]=topf;
if (u==top[u]) {
for (int x=u,i=0;i<=mdep[u]-dep[u];i++,x=son[x]) dn[u].pb(x);
for (int x=u,i=0;i<=mdep[u]-dep[u];i++,x=p[x][0]) up[u].pb(x);
}
if (!son[u]) return;
dfs2(son[u],topf);
for (auto v:e[u]) if (v!=p[u][0]&&v!=son[u]) dfs2(v,v);
}
// lg[n]=floor(log2(n))
int query(int u,int k) {
if (k==0) return u;
u=p[u][lg[k]],k-=(1<<lg[k])+dep[u]-dep[top[u]],u=top[u];
return k>=0?up[u][k]:dn[u][-k];
}
(但实际上这个题并没有什么用,只是熟悉一下长链剖分QAQ)
CF1009F Dominant Indices
定义(d(u,k))表示(u)子树中到(u)的距离为(k)的点的个数,对于每个(u),你需要找到最小的(k),使得(d(u,k))最大。
考虑把(d)全算出来,不难发现有如下DP
特别的(d(u,0)=1)。
这是一个(n^2)的做法,考虑用长链剖分优化。
令(len[u])表示(u)到叶子节点的最大距离。
那么只用考虑(d(u,0)...d(u,len[u]))。
对于点(u),我们直接让他的重儿子(v),与(d(u,1...len[u]))共用同一空间,然后对于(u)向下链的一些重链,的儿子为链首的一些重链,直接将信息合并到(d(u,0...len[u]))上即可。
这样对每条重链只会合并一次,单次合并的复杂度是(O(1))的,所以最后复杂度为(O(n)),也只需要开(O(n))的空间(下面代码内存池开的naive了...)
可能有点抽象...看代码吧QAQ
#include <bits/stdc++.h>
using namespace std;
#define rep(i,a,n) for (int i=a;i<n;++i)
#define per(i,a,n) for (int i=n-1;i>=a;--i)
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define all(x) (x).begin(),(x).end()
#define SZ(x) ((int)(x).size())
typedef double db;
typedef long long ll;
typedef pair<int,int> PII;
typedef vector<int> VI;
const int N=1e6+10;
namespace Pool {
int buf[N*23],*p=buf;
inline int* New(int len) {int *ret=p; p+=len; return ret;}
}
using Pool::New;
int n,fa[N],dep[N],mdep[N],son[N];
VI e[N];
void dfs(int u,int f,int deep) {
dep[u]=mdep[u]=deep;
fa[u]=f;
for (auto v:e[u]) if (v!=f) {
dfs(v,u,deep+1);
if (mdep[v]>mdep[u])
mdep[u]=mdep[v],son[u]=v;
}
}
int *f[N],ans[N];
void dp(int u) {
if (son[u]) {
int v=son[u];
f[v]=f[u]+1,dp(v);
ans[u]=ans[v]+1;
}
f[u][0]=1;
if (f[u][0]>=f[u][ans[u]]) ans[u]=0;
int mx=f[u][ans[u]];
for (auto v:e[u]) if (v!=fa[u]&&v!=son[u]) {
f[v]=New(mdep[v]-dep[v]+1);
dp(v);
for (int i=1;i<=mdep[v]-dep[v]+1;i++) {
f[u][i]+=f[v][i-1];
if (f[u][i]>mx||(f[u][i]==mx&&i<ans[u])) ans[u]=i,mx=f[u][i];
}
}
}
int main() {
#ifdef LOCAL
freopen("a.in","r",stdin);
#endif
scanf("%d",&n);
rep(i,0,n-1) {
int u,v; scanf("%d%d",&u,&v);
e[u].pb(v),e[v].pb(u);
}
dfs(1,0,1);
f[1]=New(mdep[1]-dep[1]+1);
dp(1);
rep(i,1,n+1) printf("%d
",ans[i]);
return 0;
}
LuoguP3899 [湖南集训]谈笑风生
https://www.luogu.com.cn/problem/P3899
这题有多种数据结构做法......但这里讲的是长链剖分...
首先一种情况是(b)是(a)的祖先,这个很显然,我们不考虑。
对于(b)在(a)的子树内,我们考虑DP
令(f[u][i])表示(u)子树内到(u)距离为(i)的所有(v)的(siz[v]-1)的和((siz[u])表示(u)的子树大小)。
有转移(f[u][i]=sumlimits_{v in son(u)} f[v][i-1])
那么这一部分的贡献就是(sumlimits_{i le k} f[a][i])
这样是一个n^2的DP。
考虑优化,发现这里优化不太好做,将状态的定义改成前缀和又不太好转移,那没事,改后缀和就好了。
转移的柿子不变,只是对于(f[u][0])要将他加上所有(f[v][0])的和以及(siz[u]-1)。
长链剖分优化这个DP即可,还有注意这是一个离线的做法(因为内存会被覆盖),所以要算完就回答和(u)有关的问题。
复杂度(O(n+q))
#include <bits/stdc++.h>
using namespace std;
#define rep(i,a,n) for (int i=a;i<n;++i)
#define per(i,a,n) for (int i=n-1;i>=a;--i)
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define all(x) (x).begin(),(x).end()
#define SZ(x) ((int)(x).size())
typedef double db;
typedef long long ll;
typedef pair<int,int> PII;
typedef vector<int> VI;
const int N=3e5+10;
struct Pool {
ll buf[N<<3],*pt;
Pool() {pt=buf;}
inline ll *New(int sz) {ll *ret=pt; pt+=sz; return ret;}
}p;
VI e[N];
int n,Q,mdep[N],dep[N],son[N];
int fa[N],len[N],siz[N];
ll *f[N];
void dfs(int u,int ff) {
fa[u]=ff,siz[u]=1;
mdep[u]=dep[u]=dep[ff]+1;
for (auto v:e[u]) if (v!=ff) {
dfs(v,u),siz[u]+=siz[v];
if (mdep[v]>mdep[u]) {
mdep[u]=mdep[v];
son[u]=v;
}
}
len[u]=mdep[u]-dep[u]+1;
}
vector<PII> qs[N];
ll ans[N];
void dp(int u) {
if (son[u]) {
f[son[u]]=f[u]+1;
dp(son[u]);
f[u][0]+=f[son[u]][0];
}
f[u][0]+=siz[u]-1;
for (auto v:e[u]) if (v!=fa[u]&&v!=son[u]) {
f[v]=p.New(len[v]<<1);
dp(v);
f[u][0]+=f[v][0];
rep(i,0,len[v]) f[u][i+1]+=f[v][i];
}
for (auto q:qs[u]) {
int k=q.fi,id=q.se;
ans[id]=1ll*(siz[u]-1)*min(k,dep[u]-1);
if (k+1<len[u]) ans[id]+=f[u][0]-f[u][k+1]-siz[u]+1;
else ans[id]+=f[u][0]-siz[u]+1;
}
}
int main() {
#ifdef LOCAL
freopen("a.in","r",stdin);
#endif
scanf("%d%d",&n,&Q);
rep(i,0,n-1) {
int u,v; scanf("%d%d",&u,&v);
e[u].pb(v),e[v].pb(u);
}
dfs(1,0);
f[1]=p.New(len[1]<<1);
rep(i,1,Q+1) {
int u,k; scanf("%d%d",&u,&k);
qs[u].pb(mp(k,i));
}
dp(1);
rep(i,1,Q+1) printf("%lld
",ans[i]);
return 0;
}
P5904 [POI2014]HOT-Hotels
https://www.luogu.com.cn/problem/P3565
https://www.luogu.com.cn/problem/P5904(加强版 需要长链剖分优化)
这题更侧重思维...并不是后来的优化......
对于三元组((a,b,c)),我们考虑枚举(LCA(a,b,c)),换句话说在(LCA)处算它的贡献。
也就是说Dfs的时候对于点u,枚举儿子v,然后考虑一或两个点在v子树内,其他点在u别的儿子内时的合法三元组个数(或者别的点就是u)。
首先不难想到维护一个东西(f[u][i])表示(u)子树内到(u)的距离为(i)的点的个数。
然后再维护一个(g[u][i])表示(u)子树内有多少个二元组((v,w)),满足(d(v,lca_{v,w})=d(w,lca_{v,w})=d(lca_{v,w},u)+i)((d(i,j))表示树上点(i)到点(j)的距离,(lca)就字面意思啦)。
那么再枚举u的儿子v时,先不要更新u的f和g,枚举(i),用(f[v][i]*g[u][i+1]+g[v][i]*f[u][i-1]),更新答案(建议画图理解QAQ)。
然后考虑如何更新(f)和(g)。还是枚举(i),用(f[v][i])更新(f[u][i+1]),(g[v][i])更新(g[u][i-1]),对于(g),还有一种情况是一个点在(v)的子树内,另一个点在前面遍历的子树内或另一点是(u),对于这种情况用(f[v][i]*f[u][i+1])更新(g[u][i+1])。
用长链剖分优化上述DP即可,注意对(g)要开两倍的空间(因为要吧g[u]-1给g[v])。
#include <bits/stdc++.h>
using namespace std;
#define rep(i,a,n) for (int i=a;i<n;++i)
#define per(i,a,n) for (int i=n-1;i>=a;--i)
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define all(x) (x).begin(),(x).end()
#define SZ(x) ((int)(x).size())
typedef double db;
typedef long long ll;
typedef pair<int,int> PII;
typedef vector<int> VI;
const int N=1e5+10;
namespace Pool {
ll buf[N<<3],*p=buf;
inline ll *New(int len) {ll *ret=p; p+=len; return ret;}
}
using Pool::New;
VI e[N];
int n,len[N],fa[N],son[N];
ll *f[N],*g[N],ans,fs[N],gs[N];
void dfs(int u,int ff) {
fa[u]=ff;
int mx=0;
for (auto v:e[u]) if (v!=ff) {
dfs(v,u);
if (len[v]>mx) mx=len[v],son[u]=v;
}
len[u]=mx+1;
}
void dp(int u) {
if (son[u]) {
int v=son[u];
f[v]=f[u]+1,g[v]=g[u]-1;
dp(v);
}
ans+=g[u][0],f[u][0]=1;
for (auto v:e[u]) if (v!=fa[u]&&v!=son[u]) {
f[v]=New(len[v]<<1),g[v]=New(len[v]<<1);
dp(v);
rep(i,0,len[v]) {
if (i>0) ans+=g[v][i]*f[u][i-1];
ans+=f[v][i]*g[u][i+1];
}
rep(i,0,len[v]) {
g[u][i+1]+=f[v][i]*f[u][i+1];
if (i>0) g[u][i-1]+=g[v][i];
}
rep(i,0,len[v]) f[u][i+1]+=f[v][i];
}
}
int main() {
#ifdef LOCAL
freopen("a.in","r",stdin);
#endif
scanf("%d",&n);
rep(i,0,n-1) {
int u,v; scanf("%d%d",&u,&v);
e[u].pb(v),e[v].pb(u);
}
dfs(1,0);
f[1]=New(len[1]<<1),g[1]=New(len[1]<<1);
dp(1);
printf("%lld
",ans);
return 0;
}
LuoguP4292 [WC2010]重建计划
总的来讲一些逼格很高的题目注重的并不是什么奇怪的优化...珂能还是考察思维能力QAQ