长链剖分

规定若\(x\)为叶结点,则\(len[x]=1\)。

否则定义\(preferredchild[x]\)(以下简称\(pc[x]\),称\(pc[x]\)为\(x\)的长儿子)为\(x\)的所有子结点\(ver\)中,\(len[ver]\)最大的一个。\(len[x]=len[pc[x]]+1\)。

这里的\(pc[x]\)相当于树链剖分中的\(heavychild[x]\),类似地,我们可以认为整棵树被划分为了若干条互不相交的长链。

有什么用?

求LCA。

到底有什么用?

优化树形DP。

通过长链剖分,我们可以将一些状态下标为深度的树形DP优化至线性复杂度。

一道例题

给你一棵\(n\)个结点的树,根结点编号为\(1\)。对于每个结点\(x\),求出以\(x\)为根的子树中到\(x\)路径的长度(边数)为多少的结点数最多,在结点数最多的条件下最小化路径的长度。

\(n \leq 1000000\)。

不准Dsu on Tree!

题解

先考虑普通的树形DP,设状态\(f[x][j]\)表示以\(x\)为根的子树中,到\(x\)距离为\(j\)的结点数。

转移方程显然:

\[f[x][0]=1
\]

\[f[x][j]=\sum_{ver是x的子结点}{f[ver][j-1]}
\]

时间复杂度\(O(n^2)\)。

但是对于本题,这样的算法无论时间还是空间都是无法接受的。

考虑优化这个树形DP。

定义\(f[x]\)的有效长度为\(len[x]-1\),因为对于\(j>len[x]-1\)时,\(f[x][j]\)显然为\(0\)。

如果\(x\)只有一个子结点\(ver\)的话,转移显然为:

\[f[x][0]=1
\]

\[f[x][j]=f[ver][j-1]
\]

这样的转移可以使用指针\(O(1)\)地完成。

\[f[x]=f[ver]-1
\]

\[f[x][0]=1
\]

可以想到,如果\(x\)的子结点不止一个的话,我们也可以采取相同的方式,即先从\(x\)的所有子结点中选择一个子结点让\(x\) \(O(1)\)“继承”它的状态,其他子结点仍采用DP方法暴力合并。

可以发现,我们在将\(f[ver]\)合并到\(f[x]\)时,时间复杂度是\(O(len[ver])\)的。

为了使时间复杂度达到最优,我们选择继承的那个子结点必然是\(x\)的长儿子\(pc[x]\),因为根据长儿子的定义,\(f[pc[x]]\)在所有的\(f[ver]\)中有效长度最长,这样继承可以最大程度地减少合并时遍历所带来的程序运行时间。

并且结合上面关于有效长度的叙述,我们还可以使用指针动态分配每个\(f[x]\)的内存。

考虑进行了以上优化后,时间复杂度是多少?

\(O(n)\)。顺便一提,空间复杂度也是\(O(n)\)。

Why?

考虑到每个结点只属于一条长链,且每一条长链只会在链顶处被\(O(len)\)暴力合并一次,所以时间复杂度为\(O(n)\)。

空间复杂度的话,我们发现任意一条长链链顶\(top\)处的\(f[top]\)被分配到的内存必然包含了这条长链上所有的结点被分配到的内存(换句话说,对于同一条长链上的两个结点\(x,y\),如果\(y\)是\(x\)的祖先,那么\(y\)被分配到的内存必然包含\(x\)被分配到的内存),且每一个\(f[top]\)的有效长度均为\(len[top]-1\),结合之前所述\(n\)个结点的树被划分为了若干条互不相交的长链,空间复杂度\(O(n)\)得证。

代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <cctype>
#include <algorithm>
#include <vector>
#define rin(i,a,b) for(int i=(a);i<=(b);i++)
#define rec(i,a,b) for(int i=(a);i>=(b);i--)
#define trav(i,a) for(int i=head[(a)];i;i=e[i].nxt)
using std::cin;
using std::cout;
using std::endl;
typedef long long LL; inline int read(){
int x=0;char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
return x;
} const int MAXN=1000005;
int n;
int ecnt,head[MAXN];
int fa[MAXN],len[MAXN],pc[MAXN];
int ans[MAXN];
int Memory[MAXN],*f[MAXN],*ptr;
struct Edge{
int to,nxt;
}e[MAXN<<1]; inline void add_edge(int bg,int ed){
ecnt++;
e[ecnt].to=ed;
e[ecnt].nxt=head[bg];
head[bg]=ecnt;
} void dfs1(int x,int pre){
fa[x]=pre;
int maxlen=-1;
trav(i,x){
int ver=e[i].to;
if(ver==pre) continue;
dfs1(ver,x);
if(len[ver]>maxlen){
maxlen=len[ver];
pc[x]=ver;
}
}
len[x]=len[pc[x]]+1;
} void dfs2(int x,int *ff){
f[x]=ff;
if(pc[x]){
dfs2(pc[x],ff+1);
ans[x]=ans[pc[x]]+1;
if(ans[x]==1&&f[x][ans[x]]==1) ans[x]--;
}
f[x][0]=1;
trav(i,x){
int ver=e[i].to;
if(ver==fa[x]||ver==pc[x]) continue;
int *temp=ptr;
ptr+=len[ver];
dfs2(ver,temp);
rin(j,0,len[ver]-1){
f[x][j+1]+=temp[j];
if(j+1<ans[x]&&f[x][j+1]>=f[x][ans[x]]) ans[x]=j+1;
if(j+1>ans[x]&&f[x][j+1]>f[x][ans[x]]) ans[x]=j+1;
}
}
} int main(){
n=read();
rin(i,2,n){
int u=read(),v=read();
add_edge(u,v);
add_edge(v,u);
}
dfs1(1,0);
ptr=Memory+len[1];
dfs2(1,Memory);
rin(i,1,n) printf("%d\n",ans[i]);
return 0;
}

还有一种使用\(std::vector\)实现的方法,为了快速继承同样使用了指针。

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <cctype>
#include <algorithm>
#include <vector>
#define rin(i,a,b) for(int i=(a);i<=(b);i++)
#define rec(i,a,b) for(int i=(a);i>=(b);i--)
#define trav(i,a) for(int i=head[(a)];i;i=e[i].nxt)
using std::cin;
using std::cout;
using std::endl;
typedef long long LL; inline int read(){
int x=0;char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
return x;
} const int MAXN=1000005;
int n;
int ecnt,head[MAXN];
int fa[MAXN],len[MAXN],pc[MAXN];
int ans[MAXN],realans[MAXN];
std::vector<int> *f[MAXN];
struct Edge{
int to,nxt;
}e[MAXN<<1]; inline void add_edge(int bg,int ed){
ecnt++;
e[ecnt].to=ed;
e[ecnt].nxt=head[bg];
head[bg]=ecnt;
} void dfs1(int x,int pre){
fa[x]=pre;
int maxlen=-1;
trav(i,x){
int ver=e[i].to;
if(ver==pre) continue;
dfs1(ver,x);
if(len[ver]>maxlen){
maxlen=len[ver];
pc[x]=ver;
}
}
len[x]=len[pc[x]]+1;
} void dfs2(int x){
if(pc[x]){
dfs2(pc[x]);
f[x]=f[pc[x]];
ans[x]=ans[pc[x]];
if(ans[x]==(int)(*f[x]).size()-1&&(*f[x])[(int)f[x]->size()-1]==1) ans[x]++;
}
else{
f[x]=new std::vector<int>();
}
f[x]->push_back(1);
trav(i,x){
int ver=e[i].to;
if(ver==fa[x]||ver==pc[x]) continue;
dfs2(ver);
rin(j,0,(int)f[ver]->size()-1){
int jj=(int)f[x]->size()-((int)f[ver]->size()+1-j);
(*f[x])[jj]+=(*f[ver])[j];
if(jj>ans[x]&&(*f[x])[jj]>=(*f[x])[ans[x]]) ans[x]=jj;
if(jj<ans[x]&&(*f[x])[jj]>(*f[x])[ans[x]]) ans[x]=jj;
}
}
realans[x]=(int)f[x]->size()-ans[x]-1;
} int main(){
n=read();
rin(i,2,n){
int u=read(),v=read();
add_edge(u,v);
add_edge(v,u);
}
dfs1(1,0);
dfs2(1);
rin(i,1,n) printf("%d\n",realans[i]);
return 0;
}

长链剖分优化树形DP总结的更多相关文章

  1. 【BZOJ3522&BZOJ4543】Hotel加强版(长链剖分,树形DP)

    题意:求一颗树上三点距离两两相等的三元组对数 n<=1e5 思路:From https://blog.bill.moe/bzoj4543-hotel/ f[i][j]表示以i为根的子树中距离i为 ...

  2. 【CF1009F】Dominant Indices(长链剖分优化DP)

    点此看题面 大致题意: 设\(d(x,y)\)表示\(x\)子树内到\(x\)距离为\(y\)的点的个数,对于每个\(x\),求满足\(d(x,y)\)最大的最小的\(y\). 暴力\(DP\) 首先 ...

  3. CF1009F Dominant Indices——长链剖分优化DP

    原题链接 \(EDU\)出一道长链剖分优化\(dp\)裸题? 简化版题意 问你每个点的子树中与它距离为多少的点的数量最多,如果有多解,最小化距离 思路 方法1. 用\(dsu\ on\ tree\)做 ...

  4. 2019.01.19 bzoj3653: 谈笑风生(长链剖分优化dp)

    传送门 长链剖分优化dpdpdp水题. 题意简述:给一棵树,mmm次询问,每次给一个点aaa和一个值kkk,询问满足如下条件的三元组(a,b,c)(a,b,c)(a,b,c)的个数. a,b是c的祖先 ...

  5. 长链剖分优化dp三例题

    首先,重链剖分我们有所认识,在dsu on tree和数据结构维护链时我们都用过他的性质. 在这里,我们要介绍一种新的剖分方式,我们求出这个点到子树中的最长链长,这个链长最终从哪个儿子更新而来,那个儿 ...

  6. 2018.11.03 NOIP模拟 树(长链剖分优化dp)

    传送门 考虑直接推式子不用优化怎么做. 显然每一个二进制位分开计算贡献就行. 即记录fi,jf_{i,j}fi,j​表示距离iii这个点不超过jjj的点的每个二进制位的0/10/10/1个数. 但直接 ...

  7. cogs 2652. 秘术「天文密葬法」(0/1分数规划 长链剖分 二分答案 dp

    http://cogs.pro:8080/cogs/problem/problem.php?pid=vSXNiVegV 题意:给个树,第i个点有两个权值ai和bi,现在求一条长度为m的路径,使得Σai ...

  8. BZOJ4543 POI2014 Hotel加强版 【长链剖分】【DP】*

    BZOJ4543 POI2014 Hotel加强版 Description 同OJ3522 数据范围:n<=100000 Sample Input 7 1 2 5 7 2 5 2 3 5 6 4 ...

  9. LOJ3053 十二省联考2019 希望 容斥、树形DP、长链剖分

    传送门 官方题解其实讲的挺清楚了,就是锅有点多-- 一些有启发性的部分分 L=N 一个经典(反正我是不会)的容斥:最后的答案=对于每个点能够以它作为集合点的方案数-对于每条边能够以其两个端点作为集合点 ...

随机推荐

  1. 20191127 Spring Boot官方文档学习(4.12)

    4.12.缓存(Caching) Spring框架提供了对应用程序透明添加缓存的支持.从本质上讲,抽象将缓存应用于方法,从而根据缓存中可用的信息减少执行次数.缓存逻辑是透明应用的,不会对调用者造成任何 ...

  2. 傻傻分不清?Integer、new Integer() 和 int 的面试题

    这篇有意思: 基本概念的区分: 1.Integer 是 int 的包装类,int 则是 java 的一种基本数据类型 2.Integer 变量必须实例化后才能使用,而int变量不需要 3.Intege ...

  3. mock.js的运用

    一:概念 Mock.js是一款模拟数据生成器,旨在帮助前端攻城师独立于后端进行开发,帮助编写单元测试.提供了以下模拟功能: 根据数据模板生成模拟数据 模拟 Ajax 请求,生成并返回模拟数据 基于 H ...

  4. spring boot 整合activemq

    1 Spring Boot与ActiveMQ整合 1.1使用内嵌服务 (1)在pom.xml中引入ActiveMQ起步依赖 <properties> <spring.version& ...

  5. CF573E Bear and Bowling

    题目 我们设\(f_{i,j}\)表示前\(i\)个数中选\(j\)个的最大值. 那么显然有\(f_{i,j}=max(f_{i-1,j},f_{i-1,j-1}+j*a_i)\). 这个东西我们首先 ...

  6. kotlin学习(6)运算符重载和其他约定

    约定 在Kotlin中,可以调用自己代码中定义的函数,来实现语言结构.这戏功能与特定的函数命名相关,例如,在你的类中定义了一个名为plus的特殊方法,那么按照约定,就可以在该类的实例上使用 + 运算符 ...

  7. Ubantu创建热点并共享——2019年5月10日更新

    只需要两步,参考以下两篇文章: ubuntu16.04上安装配置DHCP服务的详细过程 Ubuntu18.04 创建与编辑热点的方法

  8. 模板 - SG函数

    https://scut.online/p/93 每次取走的石子是b的幂次.打表暴力发现规律. #include <bits/stdc++.h> using namespace std; ...

  9. linux基本命令之磁盘管理命令(ls,cd,pwd,mkdir,rmdir,clear, touch)

    linux磁盘管理命令 1.ls(list)命令:列出目录内容. 格式:ls [参数][文件或目录] ls -a或-all表示列出所有文件和目录,以点开始的是影藏文件,例如,.bash_history ...

  10. vue.js(20)--vue路由

    后端路由 对于普通的网站,所有的超链接都是url地址,所有的url地址都对应着服务器上的资源 前端路由 对于单页面应用程序来说,主要通过单页面中的hash(#)来进行页面的切换.hash的特点是htt ...