题目链接:http://uoj.ac/problem/33

题解链接:http://vfleaking.blog.uoj.ac/blog/38

现在感觉到了做OI的层层递进的思路的伟大之处,作为一个大学才开始接触C的人只能orz了

算法一:

傻逼暴力+lca,所以树O(n*n*logn)

所以10分

算法二:(orz我竟然看了半天)

对于随机生成的树,那么树的高度都是log层的,所以省略去算法一中的傻逼暴力。因为每一层的树高都是log,所以我们只需要暴力树根u,然后以u为根,dfs(u)的所有子节点,并且用cnt[h]记录高度为h的子树的个数即可。最后我们就是暴力h1和h2,然后用cnt[h1]*cnt[h2],然后加入到ans[gcd(h1, h2)]里面即可。因为h1和h2的高度是log,所以我们最终复杂度为O(N * logn * logn)。并且dfs也都是dfs子树,子树的平均分摊也肯定是logn次

所以30分

算法三:(早上看的时候完全没有反应过来,上完一下午的课回来再看发现,还是很难= =)

通过算法二的提示我们发现,我们只需要知道ans[gcd(h1, h2)]而已,所以,我们假定d=gcd(h1, h2),那么对于每一条链,设他的高度为hi,那么他为d倍数的个数为hi/d个,

策爷的代码,怪我水平不够,代码看不懂

#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<iostream>
#include<cstdio>
//#include<ctime>
#define MAX 200005
using namespace std;
int n;
struct edge{int v,next;}e[MAX];int etot=;int g[MAX];
void addedge(int u,int v){///建立有根树
e[etot].v=v;e[etot].next=g[u];g[u]=etot++;
}
typedef long long ll;
ll ans[MAX]={};///ans(i)表示两棵子树间深度为i的倍数的值的个数
ll ans2[MAX]={};
int h[MAX],pre[MAX];///h表示目前树的高度,pre(x)表示节点x的fa
int off[MAX]={};///off(i)表示目前是否对i节点打标记,就表示是否成为过重心 /**orz 这重心找的太强了,比dfs不知道快多少*/
int qu[MAX],sz[MAX],bobo[MAX];//这个bobo应该是bool的意思,表示是否能选择
int findc(int rt){
int p=,q=;
qu[q++]=rt;///就是queue
while(p!=q){///bfs对所有子节点进行初始化(当然,也可以在这里进行pre节点的修改)
int u=qu[p++];
bobo[u]=sz[u]=;
for (int i=g[u];~i;i=e[i].next)if(!off[e[i].v])///如果没有访问过,就刚入bfs的节点
qu[q++]=e[i].v;
}
for (int i=q-;i>=;i--){///迭代使用
if(bobo[qu[i]] && sz[qu[i]]*>=q)return qu[i];///重心肯定是树size的一半,所以sz肯定也是重心的一半左右
sz[pre[qu[i]]]+=sz[qu[i]];///对他的父亲节点的sz进行更新
if(sz[qu[i]]*>q)bobo[pre[qu[i]]]=;///如果目前节点的sz*2比q大了,那么父亲节点也肯定比他大了,所以对他的父亲节点都是false
}
} int nub[MAX];///统计这一棵子树,深度为h的有几个
/**统计深度为h的有几个,并且返回最大深度*/
int bfs(int rt,int *nub,int hh){///注意,这里的hh可能会发生改变的
int p=,q=;qu[q++]=rt;
while(p!=q){///bfs出所有的节点
int u=qu[p++];
for (int i=g[u];~i;i=e[i].next)if(!off[e[i].v])qu[q++]=e[i].v;
}
///h在main函数的输入中就已经得到了
int hmax=h[qu[q-]]-h[rt]+hh;///得到最大深度,因为bfs到的最大深度就是
for (int i=;i<=hmax;i++)nub[i]=;///对目前深度一下的进行初始化
for (int i=;i<q;i++){
nub[h[qu[i]]-h[rt]+hh]++;///统计目前深度的cnt的个数
}
/*if (hh == 0){
for (int i = 0; i < 10; i++){
printf("%d ", nub[i]);
}
cout << endl;
}
if (hh == 0) printf("hmax = %d\n", hmax);*/
return hmax;
} int tmp[MAX];///tmp[j]就是保存所有子树中被j整除的个数
ll tmpsq[MAX];///tmpsq[j]是保存了所有子树中自身两两搭配的个数
int nsu[MAX];///nsu表示所有子树的高度为j的数目
int tmpnu[MAX],*start[MAX],tmph[MAX];
int ord[MAX];
int cmp(int i,int j){return tmph[i]<tmph[j];}
int ind=;
int check[MAX]={},val[MAX]; void work(int rt){
int c=findc(rt);///找到重心
off[c]=;///对重心进行标记
/*u和v是在同一棵子树里面*/
int hmax=;
///暴力所有的重心的子节点
//printf("cetroid = %d\n", c);
//这里从来不计算高度=0的时候的东西
for (int i=g[c];~i;i=e[i].next)if(!off[e[i].v]){
int h=bfs(e[i].v,nub,);///这里之所以是1,因为他是重心的子节点
if(h>hmax){
///表示高度为j的是1,并且在这里初始化
for (int j=hmax+;j<=h;j++)
nsu[j]=,tmp[j]=tmpsq[j]=;
hmax=h;
}
for (int j=;j<=h;j++){
nsu[j]+=nub[j];///nsu表示所有子树的高度为j的数目
int sum=;
///k是j的倍数,sum保存能被j整除的数的个数
for (int k=j;k<=h;k+=j) sum+=nub[k];
///tmp[j]就是保存所有子树中被j整除的个数
///tmpsq[j]是保存了所有子树中自身两两搭配的个数
tmp[j]+=sum; tmpsq[j]+=1ll*sum*sum;
}
}
/*但是存在一个疑问点就是,如果两个都是2*d怎么解决呢?*/
///tmp[i]*tmp[i]表示所有子树的两两搭配,但是不包括自身的两两搭配
for (int i=;i<=hmax;i++)
ans[i]+=(1ll*tmp[i]*tmp[i]-tmpsq[i])>>; int u,num=;///num表示往上爬几次
int *st=tmpnu;///st为tmpnu数组
///往重心上面的结点爬 for (u=pre[c]; u && !off[u]; u=pre[u]){
off[u]=;///对父亲结点打标记
start[++num]=st;///让start[++num]等于st数组,并且复制给他
ord[num]=num;///记录编号
tmph[num]=bfs(u,st,);///这里之所以为0因为st是根
///因为u是之前的fa,所以这里要+1,表示的就是往右边移动一步 //表明st的移动并不对start存在影响
/*
printf("num = %d\n", num);
cout << "st1" << endl;
for (int j = 0; j < 10; j++){
printf("%d ", st[j]);
}
cout << endl; printf("start1\n");
for (int i = 0; i < 10; i++)
printf("%d ", start[num][i]);
cout << endl;
*/
st+=tmph[num]+;///然后让st指针移动maxn deep+1步,把所有的东西给清除掉?
//for (int j = 0; j <= tmph[num] + 1; j++) st[j] = 0;
/*
printf("tmph[%d] = %d\n", num, tmph[num]);
cout << "st2" << endl;
for (int j = 0; j < 10; j++){
printf("%d ", st[j]);
}
cout << endl; cout << "start2" << endl;
for (int i = 0; i < 10; i++)
printf("%d ", start[num][i]);
cout << endl;
*/
}
///对所有的东西标记去除,因为这里表示的就是不断地往上面走
for (int v=pre[c]; v!=u; v=pre[v]) off[v]=;
/*
后面这里完全看不懂啊TAT
*/
for (int i=;i<=hmax;i++){
ans2[i+]+=nsu[i];
ans2[i+num+]-=nsu[i];
}
///根据树的高度进行排序
sort(ord+,ord++num,cmp);
int tms=;
for (int h=,cur=;cur<=num;h++){
ind++;
///找到高度>=h的,因为ord是排序好了的
while(cur<=num && tmph[ord[cur]]<h)cur++;
for (int i=cur;i<=num;i++){
int s=,t;
///加上高度是j的倍数的东西
for (int j=h;j<=tmph[ord[i]];j+=h)s+=start[ord[i]][j];
if(check[ord[i]%h]==ind)t=val[ord[i]%h],tms++;
else{
int ss=h-(ord[i]-)%h-;t=;
for (int j=ss;j<=hmax;j+=h)t+=nsu[j],tms++;
check[ord[i]%h]=ind;
val[ord[i]%h]=t;
}
ans[h]+=1ll*s*t;
}
}
///也就是说,重心必须是目前的rt
if(c!=rt)work(rt);
///继续dfs
for (int i=g[c];~i;i=e[i].next)if(!off[e[i].v])work(e[i].v);
} int main(){
nsu[]=;///自己到自己的树高就是0
memset(g,-,sizeof(g));
scanf("%d",&n);
for (int i=;i<=n;i++){///输入
int x;scanf("%d",&x);
h[i]=h[x]+;pre[i]=x;///因为x是h[i]的fa,所以h[i]的深度比h[x]要大1.然后在此时记录i的父亲是x
addedge(x,i);///建立单向边,即有根树
if(x>=i)return ;///因为题目保证了i>x,所以这个for循环里面可以这么写
}
work();
for (int i=n-;i>=;i--){
for (int j=*i;j<=n-;j+=i) ans[i]-=ans[j];
}
for (int i=;i<=n-;i++) ans2[i]+=ans2[i-];
for (int i=;i<=n-;i++) printf("%lld\n",ans[i]+ans2[i]);
return ;
}
/*
7
1
2
3
3
1
2
*/

然后就是这位大牛的代码,终于看懂了,是我太菜了,晚上我再重敲一遍

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
using namespace std; int read()
{
int x=,f=; char ch=getchar();
while (ch<'' || ch>'') {if (ch=='-') f=-; ch=getchar();}
while (ch>='' && ch<='') {x=x*+ch-''; ch=getchar();}
return x*f;
}
#define maxn 200010 int n,m,fa[maxn],root,block,siz;
struct EdgeNode{int next,to;}edge[maxn<<];
int head[maxn],cnt;
///插入双向边
void add(int u,int v) {cnt++; edge[cnt].next=head[u]; head[u]=cnt; edge[cnt].to=v;}
void insert(int u,int v) {add(u,v); add(v,u);}
/**/
int size[maxn],maxx[maxn]; bool visit[maxn]; ///得到重心,root就是重心
void Getroot(int now,int fa){
size[now]=; maxx[now]=;
for (int i=head[now]; i; i=edge[i].next)
if (edge[i].to!=fa && !visit[edge[i].to])
{
Getroot(edge[i].to,now);
size[now]+=size[edge[i].to];
maxx[now]=max(maxx[now],size[edge[i].to]);
}
maxx[now]=max(maxx[now],siz-size[now]);
if (maxx[now]<maxx[root]) root=now;
} int num[maxn],deep[maxn];///deep表示目前节点的深度,num[i]表示深度为i的节点有几个
int tmp;///表示最大深度
///得到最大深度
void DFSdeep(int now,int fa){
num[deep[now]]++;
for (int i=head[now]; i; i=edge[i].next)
if (edge[i].to!=fa && !visit[edge[i].to]){
deep[edge[i].to]=deep[now]+;
DFSdeep(edge[i].to,now);
}
if (deep[now]>tmp) tmp=deep[now];
} long long S[maxn],Ans[maxn],ans[maxn];
long long tim[maxn],Num[maxn],Tim[maxn],f[][];
/*
tim(i)表示当前子树里面的距离为i的倍数的数目
Tim(i)表示除了当前子树之外的其他子树的距离是i的倍数的数目
S(i)=tim[i]*Tim[i]表示两个子树之间相乘,就是ans(i)
num(i)就记录当前子树内距离根为i的有几个点
Num(i)就表示除了当前子树外,距离根为i的有几个点 f(i,j)表示目前深度为tmp,走j的倍数步的节点的个数是多少? 然后Ans[i]只记录(u, v)在同一条链上的,即LCA(u,v) = u 或者 LCA(u, v) = v
*/
void GetAns(int now)
{
visit[now]=;
int maxd=,Maxd=;///maxd表示当前子树的最大的deep,Maxd表示其他子树目前最大的Maxdeep
for (int i=head[now]; i; i=edge[i].next)///暴力子节点,且不暴力fa
if (!visit[edge[i].to] && edge[i].to!=fa[now])
{
deep[edge[i].to]=;///对刚开始的位置定义深度为1
tmp=;///tmp表示最深的深度
DFSdeep(edge[i].to,now);///寻找最深的深度
if (tmp>maxd) maxd=tmp;///修改最大深度
for (int j=; j<=tmp; j++)
for (int k=j; k<=tmp; k+=j)
tim[j]+=num[k];
for (int j=; j<=tmp; j++)///Ans[j]在这个时候加入,因为高度为j的一定是自己的gcd
Num[j]+=num[j],Ans[j]+=num[j],S[j]+=tim[j]*Tim[j],
Tim[j]+=tim[j],tim[j]=,num[j]=;///对tim和num初始化
}
Num[]=;///高度为0的在这个时候放入
int zz=,ss=now;
for (int i=fa[now]; !visit[i]&&i; ss=i,i=fa[i])
{
tmp=; zz++;///zz表示往上面走几步
for (int j=head[i]; j; j=edge[j].next)///得到目前的最大深度
if (edge[j].to!=fa[i] && !visit[edge[j].to] && edge[j].to!=ss)
deep[edge[j].to]=,DFSdeep(edge[j].to,i);
if (tmp>Maxd) Maxd=tmp;
for (int j=; j<=tmp; j++)///计算为k的倍数的东西
for (int k=j; k<=tmp; k+=j)
tim[j]+=num[k];
///tt就是小的那个,tmp表示最大深度,block表示块的大小
int tt = min(tmp, block);
for (int j=; j<=tt; j++)
{
if (f[j][zz%j]==-)///小于sqrt(n)的我们提前储存起来分治,这样就防止复杂度过高
{
f[j][zz%j]=;
for (int k=(j-zz%j)%j; k<=maxd; k+=j){///请注意(j-zz%j)和(j-zz%j)%j之间的区别
f[j][zz%j]+=Num[k];
}
}
///zz%j其实是一样的
S[j]+=f[j][zz%j]*tim[j];
}
for (int j=block+; j<=tmp; j++)///如果>j的话tim[j]
for (int k=(j-zz%j)%j; k<=maxd; k+=j)
S[j]+=Num[k]*tim[j];
///不放入Num和Tim之中
for (int j=; j<=tmp; j++) tim[j]=,num[j]=;
/*这个Ans是做啥的?*/
Ans[zz]++;///因为每次往上面走一步,所以要加上从重心开始到这个点的cnt
}
int l=,r=;///这个的作用是,莫队?,表示区间[i-zz, i-1]
/*这一步也看不懂*/
long long tmpans=;
/*
5
1
2
3
4
*/
/*
printf("cetroid = %d\n", now);
for (int i = 0; i <= maxd + zz; i++)
printf("%d ", Num[i]);
cout << endl; printf("zz = %d maxd = %d\n", zz, maxd);
*/
/**
终于弄明白了这里是什么意思了,Ans(i)的定义是不变的,所以i就表示距离是i的有几个,然后之前不是有一个向上
爬的过程嘛,另LCA为这个向上爬的每一个点,那么要满足在zz个之中深度在i的只有区间[i-zz,i-1],因为还有一个
节点并没有算上去!
*/
for (int i=; i<=zz+maxd; i++){///往上走了zz步,然后又+上原来的最大深度,所以i表示最大深度
tmpans += r+<i ? Num[++r]:;
tmpans -= l+zz<i ? Num[l++]:;
Ans[i]+=tmpans;
printf("i = %d l = %d r = %d Ans[%d] = %I64d tmpans = %I64d\n", i, l, r, i, Ans[i], tmpans);
}
int tt = min(Maxd, block);
///清空
for (int i=; i<=tt; i++)
for (int j=; j<=i-; j++)
f[i][j]=-;
for (int i=; i<=maxd; i++) Num[i]=,Tim[i]=;
///这里是重新开始分治
for (int i=head[now]; i; i=edge[i].next)
if (!visit[edge[i].to]){
root=;
siz=size[edge[i].to];
Getroot(edge[i].to,now);
GetAns(root);
}
} int main(){
n=read(); block=(int)sqrt(n);
for (int i=; i<=n; i++)
fa[i]=read(),insert(fa[i],i);
maxx[root]=0x7fffffff;
siz=n;
memset(f,-,sizeof(f));
Getroot(,);///得到重心
GetAns(root);///dfs从重心开始
for (int i=n-; i; i--){
ans[i]=S[i];
for (int j=i+i; j<=n-; j+=i)///为因为S(i)表示能被i整除的,但是可能存在两个都是大于i的东西的,所以我们这里要容斥一下
ans[i]-=ans[j];
}
for (int i=; i<=n-; i++) printf("%lld\n",ans[i]+Ans[i]);
return ;
}

还不会做! 树上的gcd 树分治 UOJ33的更多相关文章

  1. 树上的构造 树分治+树重心的性质 Codeforces Round #190 (Div. 2) E

    http://codeforces.com/contest/322/problem/E E. Ciel the Commander time limit per test 1 second memor ...

  2. 【BZOJ-1468】Tree 树分治

    1468: Tree Time Limit: 10 Sec  Memory Limit: 64 MBSubmit: 1025  Solved: 534[Submit][Status][Discuss] ...

  3. 【线段树分治 01背包】loj#6515. 「雅礼集训 2018 Day10」贪玩蓝月

    考试时候怎么就是没想到线段树分治呢? 题目描述 <贪玩蓝月>是目前最火爆的网页游戏.在游戏中每个角色都有若干装备,每件装备有一个特征值 $w$ 和一个战斗力 $v$ .在每种特定的情况下, ...

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

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

  5. 5.15 牛客挑战赛40 E 小V和gcd树 树链剖分 主席树 树状数组 根号分治

    LINK:小V和gcd树 时限是8s 所以当时好多nq的暴力都能跑过. 考虑每次询问暴力 跳父亲 这样是nq的 4e8左右 随便过. 不过每次跳到某个点的时候需要得到边权 如果直接暴力gcd的话 nq ...

  6. 【BZOJ3460】Jc的宿舍(树上莫队+树状数组)

    点此看题面 大致题意: 一棵树,每个节点有一个人,他打水需要\(T_i\)的时间,每次询问两点之间所有人去打水的最小等待时间. 伪·强制在线 这题看似强制在线,但实际上,\(pre\ mod\ 2\) ...

  7. BZOJ 2588: Spoj 10628. Count on a tree 树上跑主席树

    2588: Spoj 10628. Count on a tree Time Limit: 1 Sec Memory Limit: 256 MB 题目连接 http://www.lydsy.com/J ...

  8. [POJ1741]树上的点对 树分治

    Description 给一棵有n个节点的树,每条边都有一个长度(小于1001的正整数). 定义dist(u,v)=节点u到节点v的最短路距离. 给出一个整数k,我们称顶点对(u,v)是合法的当且仅当 ...

  9. BZOJ_4238_电压_树上差分+dfs树

    BZOJ_4238_电压_树上差分+dfs树 Description 你知道Just Odd Inventions社吗?这个公司的业务是“只不过是奇妙的发明(Just Odd Inventions)” ...

随机推荐

  1. Python_1

    转载来源:http://www.cnblogs.com/wupeiqi/articles/4906230.html python内部执行过程如下: python解释器在加载 .py 文件中的代码时,会 ...

  2. C#中委托的理解

    请注意,这只是个人关于C#中委托的一点点理解,参考了一些博客,如有不周之处,请指出,谢谢! 委托是一种函数指针,委托是方法的抽象,方法是委托的实例.委托是C#语言的一道坎,明白了委托才能算是C#真正入 ...

  3. DB2 日志

    跟Oracle类似DB2也分为两个模式,日志循环vs归档日志,也就是非归档和归档模式,下面对这两种模式做简单的介绍. 日志循环 日志循环是默认方式,也就是非归档模式,这种模式只支持backup off ...

  4. Week2-作业1 -阅读《构建之法》

    第一章 在阅读第1.2.2节时,感受最深,记得开学初有老师就给我们分析过计算机专业和我们专业的区别,当时是给我们讲的是计算机科学注重的是理论,偏向于硬件方面,而软件工程则注重实践,偏向于软件方面.然很 ...

  5. PECE

     CE客户端边界路由器.与PE设备直连,主要功能是将VPN客户的路由通告给PE,以及从PE学习同一个VPN下其他站点的路由.PE和CE直连的运营商设备(运营商边界路由器). #PE和CE也可以是用一台 ...

  6. django的第一个问题

    /usr/local/lib/python2.7/dist-packages/allauth/account/utils.py in setup_user_email, line 258 /usr/l ...

  7. element-ui中单独引入Message组件的问题

    import Message from './src/main.js'; export default Message; 由于Message组件并没有install 方法供Vue来操作的,是直接返回的 ...

  8. C++解析(31):自定义内存管理(完)

    0.目录 1.遗失的关键字mutable 2.new / delete 3.new[] / delete[] 4.小结 5.C++语言学习总结 1.遗失的关键字mutable 笔试题: 统计对象中某个 ...

  9. [CTSC2012]熟悉的文章 后缀自动机

    题面:洛谷 题解: 观察到L是可二分的,因此我们二分L,然后就只需要想办法判断这个L是否可行即可. 因为要尽量使L可行,因此我们需要求出对于给定L,这个串最多能匹配上多少字符. 如果我们可以对每个位置 ...

  10. 【移动支付】.NET微信扫码支付接入(模式二-NATIVE)

    一.前言       经过两三天的琢磨总算完成了微信扫码支付功能,不得不感叹几句: 微信提供的DEMO不错,直接复制粘贴就可以跑起来了: 微信的配置平台我真是服了.公众平台.商户平台.开放平台,一个平 ...