题面

思路

首先,可以有一个$dp$的思路

不难发现本题中,三个点如果互相距离相同,那么一定有一个“中心点”到三个点的距离都相同

那么我们可以把本题转化计算以每个点为根的情况下,从三个不同的子树中选择深度相同的三个点的方案数

进一步,我们选定1号点为根,这样定义我们的$dp$方程:

$f[u][dis]$表示以$u$为根的子树中,和$u$的距离为$dis$的点的数量

$g[u][dis]$表示以$u$为根的子树中已经选择了不属于同一个$u$的儿子子树的两个点,并且距离为dis的方案数

那么转移方程如下:

$f[u][i]=\sum_{v=son(u)}f[v][i-1]$

$g[u][i]=\sum_{v=son(u)}g[v][i+1]+\sum_{p=son(u)}\sum_{q=son(u),q\neq p} f[p][i-1]*f[q][i-1]$

这个过程非常方便$dp$,但是时间复杂度和空间复杂度都是$O(n^2)$的

然后我们惊喜地发现一件事情:

这里的$f,g$都可以先继承一个儿子的内容,然后再和别的儿子合并!

考虑我们继承的这个过程,也就是$u$从确定的继承者$v$那里获得信息的过程:

$f[u][i]=f[v][i-1]$

$g[u][i]=g[v][i+1]$

我们又惊喜地发现,如果我们一整条链都用这样的继承方式的话,那么长度为$n$的链只需要$O(n)$的空间!

具体而言,我们对这$n$个点开两个数组,$F,G$

此时,最下层的叶子最深,它的$f,g$数组的第二位只有0

那么$f[leaf][0]=F[1],g[leaf][0]=G[n]$

然后$f[fa[leaf]][0]=F[2],f[fa[leaf]][1]=F[1]$,$g$也类似

就这样一直下去

可以看到实际上$F[leaf]$保存了$n$个$dp$值,因为这$n$个是相等的

让后我们又又惊喜地发现,现在我们的$dp$,可以逐个子树做合并,每一次合并的复杂度是$maxdep(v)$

这样我们就得到了一个时间复杂度$O(n)$,空间复杂度$O(n)$的算法

PS.本题中其实什么剖分方法都是一样的qwq,都是$O(n)$

Summary

个人认为这道题是一道好题

其实,原本的$dp$思路并不难想,同时后面的做优化的原因其实都已经很好地植入在上一层的方法里面了

也就是说,这道题如果你的思维开阔,是很容易顺着思路一路就从$O(n2)+O(n2)$优化到$O(n)+O(n)$的

本题中体现的主要思想,在于观察已有方法中的一些性质,并代入一些非常规手法(启发式合并和一个位置保存多个$dp$值)

这和之前我曾经遇到的一些题目不同

大部分题目是在旧方法里面找时间复杂度高的部分,然后以这些部分为主要攻关点来进行突破、优化

然而当我们遇到影响复杂度的地方并不集中的算法时,我们就需要打开思路,从算法本身性质入手,套用更优秀的方法来解题

这道题并不是一道“套路题”,不是只要做过类似的东西就能一下子秒掉的

现在的OI考场上,我们遇到的这类题目也一定会越来越多——这实际上意味着OI比赛题目质量的整体上升

因此我们一定要锻炼自己打开思路、广撒网的能力,不要老是一条路走到黑(就像我以前想题一样)

Code

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
#define fpos DEEP_DARK_FANTASY_1
#define gpos DEEP_DARK_FANTASY_2
using namespace std;
inline int read(){
    int re=0,flag=1;char ch=getchar();
    while(ch>'9'||ch<'0'){
        if(ch=='-') flag=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9') re=(re<<1)+(re<<3)+ch-'0',ch=getchar();
    return re*flag;
}
int n,first[50010],cnte;
struct edge{
    int to,next;
}a[100010];
inline void add(int u,int v){
    a[++cnte]=(edge){v,first[u]};first[u]=cnte;
    a[++cnte]=(edge){u,first[v]};first[v]=cnte;
}
int dep[50010],maxdep[50010],fpos[50010],gpos[50010],cntf,cntg,son[50010],siz[50010],top[50010];
ll f[200010],g[200010];
int getf(int u,int i){
    return fpos[top[u]]+dep[u]-dep[top[u]]+i;
}
int getg(int u,int i){
    return gpos[top[u]]-dep[u]+dep[top[u]]+i;
}
int getlen(int u){
    return maxdep[u]-dep[u]+1;
}
int fa[50010];
void dfs1(int u,int f){
    int i,v;fa[u]=f;
    dep[u]=dep[f]+1;maxdep[u]=dep[u];
    siz[u]=1;son[u]=0;
    for(i=first[u];~i;i=a[i].next){
        v=a[i].to;if(v==f) continue;
        dfs1(v,u);
        siz[u]+=siz[v];
        if(siz[son[u]]<siz[v]) son[u]=v;
        maxdep[u]=max(maxdep[u],maxdep[v]+1);
    }
}
void dfs2(int u,int t){
    int i,v;
    top[u]=t;
    if(u==t){
        int len=getlen(u);
        fpos[u]=cntf;//这里记录的是链头,每个链头都分配一段数组,长度等于这条链的长度
        gpos[u]=cntg+len;//后来发现本题中什么剖法都是O(n),所以我写了重链剖分
        cntf+=len;
        cntg+=len<<1;
    }
    if(!son[u]) return;
    dfs2(son[u],t);
    for(i=first[u];~i;i=a[i].next){
        v=a[i].to;if(v==son[u]||v==fa[u]) continue;
        dfs2(v,v);
    }
}
ll ans=0;
void dfs(int u){
    f[getf(u,0)]=1;
    if(son[u]){
        dfs(son[u]);
        ans+=g[getg(u,0)];
    }
    int i,v,j,lim;
    for(i=first[u];~i;i=a[i].next){
        v=a[i].to;if(v==fa[u]||v==son[u]) continue;
        dfs(v);lim=getlen(v);
        for(j=1;j<=lim;j++) ans+=g[getg(u,j)]*f[getf(v,j-1)];
        for(j=1;j<=lim-1;j++) ans+=g[getg(v,j)]*f[getf(u,j-1)];
        for(j=1;j<=lim;j++) g[getg(u,j)]+=f[getf(u,j)]*f[getf(v,j-1)];
        for(j=0;j<=lim-2;j++) g[getg(u,j)]+=g[getg(v,j+1)];
        for(j=1;j<=lim;j++) f[getf(u,j)]+=f[getf(v,j-1)];
    }
}
void init(){
    memset(first,-1,sizeof(first));
    cnte=ans=0;cntf=cntg=1;
    memset(f,0,sizeof(f));memset(g,0,sizeof(g));
    memset(dep,0,sizeof(dep));memset(maxdep,0,sizeof(maxdep));
}
int main(){
    int i,t1,t2;
    while((n=read())){
        init();
        for(i=1;i<n;i++){
            t1=read();t2=read();
            add(t1,t2);
        }
        dfs1(1,0);dfs2(1,1);
        dfs(1);
        printf("%lld\n",ans);
    }
}

thr [树链剖分+dp]的更多相关文章

  1. bzoj2325 [ZJOI2011]道馆之战 树链剖分+DP+类线段树最大字段和

    题目传送门 https://lydsy.com/JudgeOnline/problem.php?id=2325 题解 可以参考线段树动态维护最大子段和的做法. 对于线段树上每个节点 \(o\),维护 ...

  2. Tsinsen A1219. 采矿(陈许旻) (树链剖分,线段树 + DP)

    [题目链接] http://www.tsinsen.com/A1219 [题意] 给定一棵树,a[u][i]代表u结点分配i人的收益,可以随时改变a[u],查询(u,v)代表在u子树的所有节点,在u- ...

  3. (中等) HDU 5293 Tree chain problem,树链剖分+树形DP。

    Problem Description   Coco has a tree, whose vertices are conveniently labeled by 1,2,…,n.There are ...

  4. BZOJ4712洪水——动态DP+树链剖分+线段树

    题目描述 小A走到一个山脚下,准备给自己造一个小屋.这时候,小A的朋友(op,又叫管理员)打开了创造模式,然后飞到 山顶放了格水.于是小A面前出现了一个瀑布.作为平民的小A只好老实巴交地爬山堵水.那么 ...

  5. LOJ2269 [SDOI2017] 切树游戏 【FWT】【动态DP】【树链剖分】【线段树】

    题目分析: 好题.本来是一道好的非套路题,但是不凑巧的是当年有一位国家集训队员正好介绍了这个算法. 首先考虑静态的情况.这个的DP方程非常容易写出来. 接着可以注意到对于异或结果的计数可以看成一个FW ...

  6. Luogu P4643 【模板】动态dp(矩阵乘法,线段树,树链剖分)

    题面 给定一棵 \(n\) 个点的树,点带点权. 有 \(m\) 次操作,每次操作给定 \(x,y\) ,表示修改点 \(x\) 的权值为 \(y\) . 你需要在每次操作之后求出这棵树的最大权独立集 ...

  7. 算法笔记--树的直径 && 树形dp && 虚树 && 树分治 && 树上差分 && 树链剖分

    树的直径: 利用了树的直径的一个性质:距某个点最远的叶子节点一定是树的某一条直径的端点. 先从任意一顶点a出发,bfs找到离它最远的一个叶子顶点b,然后再从b出发bfs找到离b最远的顶点c,那么b和c ...

  8. 【bzoj5210】最大连通子块和 树链剖分+线段树+可删除堆维护树形动态dp

    题目描述 给出一棵n个点.以1为根的有根树,点有点权.要求支持如下两种操作: M x y:将点x的点权改为y: Q x:求以x为根的子树的最大连通子块和. 其中,一棵子树的最大连通子块和指的是:该子树 ...

  9. 【bzoj4712】洪水 树链剖分+线段树维护树形动态dp

    题目描述 给出一棵树,点有点权.多次增加某个点的点权,并在某一棵子树中询问:选出若干个节点,使得每个叶子节点到根节点的路径上至少有一个节点被选择,求选出的点的点权和的最小值. 输入 输入文件第一行包含 ...

随机推荐

  1. django+xadmin在线教育平台(十二)

    6-4 用form实现登录-1 上面我们的用户登录的方法是基于函数来做的.本节我们做一个基于类方法的版本. 要求对类的继承有了解. 基础教程中基本上都是基于函数来做的,其实更推荐基于类来做.基于类可以 ...

  2. PHP实现的一个时间帮助类

    背景 工作的过程中经常会遇到各种时间类的操作,因此封装了一个帮助工具类,提高代码的复用率 主要功能 根据相差的天数获取连续的时间段 /** * 根据相差的天数获取所有连续的时间段 * @param $ ...

  3. 【Python 2 到 3 系列】 此整型非彼整型

    v2.2 (2.x)以后,python支持不会溢出的 long 型. v3.0后,确切的讲, int 型(依赖运行环境C编译器中long型的精度)消失了,long型替代 int 型,成为新的.不依赖运 ...

  4. Lucene简单总结

    Lucene API Document Document:文档对象,是一条原始数据 文档编号 文档内容 1 谷歌地图之父跳槽FaceBook 2 谷歌地图之父加盟FaceBook 3 谷歌地图创始人拉 ...

  5. python爬虫:利用BeautifulSoup爬取链家深圳二手房首页的详细信息

    1.问题描述: 爬取链家深圳二手房的详细信息,并将爬取的数据存储到Excel表 2.思路分析: 发送请求--获取数据--解析数据--存储数据 1.目标网址:https://sz.lianjia.com ...

  6. exec族函数

    作用 在进程的创建上Unix采用了一个独特的方法,它将进程创建与加载一个新进程映象分离.这样的好处是有更多的余地对两种操作进行管理. 当我们创建了一个进程之后,通常将子进程替换成新的进程映象,这可以用 ...

  7. 笔记-reactor pattern

    笔记-reactor pattern 1.      reactor模式 1.1.    什么是reactor模式 The reactor design pattern is an event han ...

  8. Servlet过滤器---简介

    过滤器的基本概念 Servlet过滤器从字面上的字意理解为经过一层次的过滤处理才达到使用的要求,而其实Servlet过滤器就是服务器与客户端请求与响应的中间层组件,在实际项目开发中Servlet过滤器 ...

  9. [转] PHP在不同页面之间传值的三种常见方式

    转自: http://my.oschina.net/jiec/blog/196153 一. POST传值 post传值是用于html的<form>表单跳转的方法,很方便使用.例如: < ...

  10. python - web自动化测试 - 元素操作 - 定位

    # -*- coding:utf-8 -*- ''' @project: web学习 @author: Jimmy @file: find_ele.py @ide: PyCharm Community ...