【前言】最近一直在忙着学算法,但是效果似乎不是很好。前段时间的树剖也快忘了= =。树套树没熟练,就开始写主席树了= =。更别说本身就不是很懂的莫比乌斯反演了。~~决定好好复习一下。
【点分治的作用】套用SYC大神的话说是:用来解决树上路径点权统计问题。
【大致流程】
①找出这颗树的重心。
②统计经过这个重心的答案
③用重心把树割开
④对每个“小树”做同样的事
【Q1——重心】其实找重心再进行计算只是为了不被卡链。什么是重心?就是当前树中的一个点K,使得MAX(SON[K])最小。SON[K]指的是以K为根的情况下某个孩子的点数。感性的想,重心在树的中间位置。
【Q2——统计答案】假设我们已经确定了一个重心K。我们先计算和K有关的(即经过K的路径或是以K为起点/终点的路径)答案个数,然后再递归每一棵小树,同样进行找重心、计算的过程。而且有些时候,我们计算出有关K的答案是有重复的,因此我们在递归的时候还要减去重复的(容斥原理)。
【Q3——边界】怎么使得某棵小树和其他树分开呢?其实很简单,我们可以开个1..n的布尔数组,表示x是否成为过重心。如果成为过,我在搜索的时候就可以直接退出了。
【T1——BZOJ】
【分析】真是一道经典题,POJ,USACO上都有。我们每次找到重心K后,把每个点到K的相对距离都算出来。然后把他们存到一个数组里去,用O(NLOGN)的快排和O(N)的扫描算出点对数。但是这样是有重复的,如图。
这样的话,设K=1,那么3和4这一条(假设3-2-1-2-4是符合要求的)就重复算了。于是我们在2--3--4这棵树中,把3-2-4这条路给删掉。怎么实现呢?我们调用左下角的小树,给d[3]一个初始值path[1][2]。这样我们可以得到d[3]=path[1][2]+path[2][3],d[4]=path[1][2]+path[2][4]。如果3-2-1-2-4符合要求,这当然也符合。这样就可以成功地删掉了。
【代码】
#include<cstdio> #include<algorithm> #define N 40005 using namespace std; struct arr{int s,go,next;}a[N*2]; int end[N],son[N],f[N],d[N],data[N]; int cnt,L,All,ans,i,x,y,z,n,root,K; bool Can[N]; void add(int u,int v,int s) { a[++cnt].go=v;a[cnt].next=end[u];a[cnt].s=s;end[u]=cnt; } void Get_root(int k,int fa) { son[k]=1;f[k]=0; for (int i=end[k];i;i=a[i].next) { int go=a[i].go; if (go==fa||Can[go]) continue; Get_root(go,k);son[k]+=son[go]; if (son[go]>f[k]) f[k]=son[go]; } if (All-son[k]>f[k]) f[k]=All-son[k]; if (f[k]<f[root]) root=k; } void Get_array(int k,int fa) { data[++L]=d[k]; for (int i=end[k];i;i=a[i].next) { int go=a[i].go; if (go!=fa&&!Can[go]) d[go]=d[k]+a[i].s,Get_array(go,k); } } int calc(int k,int now) { d[k]=now;L=0;Get_array(k,-1); int A=0,l,r; sort(data+1,data+L+1); for (l=1,r=L;l<r;) if (data[r]+data[l]<=K) A+=(r-l),l++;else r--; return A; } void work(int k) { ans+=calc(k,0);Can[k]=1; for (int i=end[k];i;i=a[i].next) { int go=a[i].go; if (Can[go]) continue; ans-=calc(go,a[i].s);f[root=0]=n+1; All=son[go];Get_root(go,-1); work(root); } } int main() { scanf("%d",&n); for (i=1;i<n;i++) scanf("%d%d%d",&x,&y,&z),add(x,y,z),add(y,x,z); scanf("%d",&K);All=n; f[root=0]=n+1;Get_root(1,-1); work(root); printf("%d",ans); return 0; }
聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃、两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏。他们的爸爸快被他们的争吵烦死了,所以他发明了一个新游戏:由爸爸在纸上画n个“点”,并用n-1条“边”把这n个“点”恰好连通(其实这就是一棵树)。并且每条“边”上都有一个数。接下来由聪聪和可可分别随即选一个点(当然他们选点时是看不到这棵树的),如果两个点之间所有边上数的和加起来恰好是3的倍数,则判聪聪赢,否则可可赢。聪聪非常爱思考问题,在每次游戏后都会仔细研究这棵树,希望知道对于这张图自己的获胜概率是多少。现请你帮忙求出这个值以验证聪聪的答案是否正确。
输入的第1行包含1个正整数n。后面n-1行,每行3个整数x、y、w,表示x号点和y号点之间有一条边,上面的数是w。
以即约分数形式输出这个概率(即“a/b”的形式,其中a和b必须互质。如果概率为1,输出“1/1”)。
【分析】可能这道更简单吧,不用快排,也不用去重。我们对于当前的树G,若找到的重心是K,我们就从K出发,搜索与K相邻的点,寻找边长的余数分别是是0,1,2的情况数,然后分情况讨论。
【代码】
#include<cstdio> #define N 50005 using namespace std; struct arr{int s,go,next;}a[N*2]; int son[N],f[N],end[N],F[N][4];bool Can[N]; int root,n,i,All,ans,Mon,Son,c,cnt,x,y,z; void add(int u,int v,int s) { a[++cnt].go=v;a[cnt].next=end[u];a[cnt].s=s;end[u]=cnt; } void Get(int k,int fa) { son[k]=1;f[k]=0; for (int i=end[k];i;i=a[i].next) { int go=a[i].go; if (fa==go||Can[go]) continue; Get(go,k);son[k]+=son[go];if (son[go]>f[k]) f[k]=son[go]; } if (All-son[k]>f[k]) f[k]=All-son[k]; if (f[k]<f[root]) root=k; } void dfs(int k,int fa,int P,int now) { F[P][now]++; for (int i=end[k];i;i=a[i].next) { int go=a[i].go; if (go==fa||Can[go]) continue; dfs(go,k,P,(now+a[i].s)%3); } } void work(int k) { Can[k]=1; for (int i=end[k];i;i=a[i].next) { int go=a[i].go;if (Can[go]) continue; int N0=F[k][0],N1=F[k][1],N2=F[k][2]; dfs(go,k,k,a[i].s); ans+=(N0+1)*(F[k][0]-N0)+N2*(F[k][1]-N1)+N1*(F[k][2]-N2); f[root=0]=n+1;All=son[go]; Get(go,-1);work(go); } } int gcd(int a,int b){return (a%b)?gcd(b,a%b):b;} int main() { scanf("%d",&n); if (n==0) {printf("0/1");return 0;} for (i=1;i<n;i++) scanf("%d%d%d",&x,&y,&z),add(x,y,z%3),add(y,x,z%3); f[root=0]=n+1;All=n; Get(1,-1);work(root); Mon=n*n;Son=ans*2+n;c=gcd(Mon,Son);Mon/=c;Son/=c; printf("%d/%d\n",Son,Mon); return 0; }