题意:给定一棵n个结点的树和边上的权值,树上两点之间的距离为经过边的权值之和,若两点之间距离<=k,则认为是一个合法点对,问有多少个合法点对
(据说是楼教主男人八题之一...做了很久,又参考了网上各种大牛们的代码,终于A掉了,自己真是太弱了。。。)
分析:
由于点有10000个,所以直接枚举两个点是TLE的,本题要用到树的分治(可以参看漆子超大牛09年的论文)。首先我们可以将所有的点对分为两种:
1.经过根节点
2.不经过根节点
对于经过根节点的点对我们可以这样算,先O(n)的计算出每个点到根的距离,存到dep[]数组中,若dep[i]+dep[j] <=k 就认为这是一个合法的点对,总数记为ans
但是,这样会把不经过根的点对算进去,所以,对于根的每个孩子结点,都要若如此处理算出ans1,ans2,ans3...... 然后ans-ans1-ans2-ans3-.........这样横跨根的合法点对就算出来了。
第二种情况怎么办呢?这就是分治了,删去跟结点会形成一个森林,我们可以把森林的每棵树都如上述的进行处理。
大概的思路就是如此,还要两个细节处理要注意:
1.在算dep[i]+dep[j] <=k时,单纯的两两枚举是会TLE的,所以可以先将dep排序 一个左指针L,一个右指针R,R指向的是最后一个满足条件dep[L]+dep[R] <=k的点,这样合法点对增加(R-L+1)个,由于L只增不减,R只减不增,可以在O(n)时间内算出.
2.树的分治,选择合适的根是关键,如果根选得不好,可能分的森林中一棵树结点特别的多,而其他的特别的少,复杂度容易将退化,每次我们要选择树的"重心"作为根来分治,什么是重心呢?简单来说是一棵树中,删去一个点x后,可以变成几棵树,使得这些树中最大的结点数尽量小,这样的一个x就是原树的重心,可以先刷poj1655 这道求重心的题
ayecsz | 1741 | Accepted | 768K | 188MS | C++ | 2624B |
c++ 代码:
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAX = 10010;
int list[MAX],next[2*MAX],p[MAX*2],c[MAX*2],a[MAX],f[MAX],s[MAX],flag[MAX],dep[MAX];
int n,kkk,size,temproot,ans;
void init()
{
int x,y,z,i,tot=0;
ans = 0;
for (i = 1; i <= n; i++)
{
list[i] = 0;
flag[i] = true;
}
for (i = 1; i < n; i++)
{
scanf("%d%d%d",&x,&y,&z);
tot++;
next[tot] = list[x];
list[x] = tot;
p[tot] = y;
c[tot] = z;
tot++;
next[tot] = list[y];
list[y] = tot;
p[tot] = x;
c[tot] = z;
}
}
int getmax(int x,int y)
{
return x>y?x:y;
}
void findroot(int x,int pre)
{
int sum = 0,k;
k = list[x];
s[x] = 1;
f[x] = 0;
while (k > 0)
{
if (p[k] != pre && flag[p[k]])
{
findroot(p[k],x);
f[x] = getmax(f[x],s[p[k]]);
s[x] += s[p[k]];
}
k = next[k];
}
f[x] = getmax(f[x],size-s[x]);
if (f[x]<f[temproot]) temproot = x;
}
void dfs(int x,int pre)
{
int k;
k = list[x];
a[++a[0]] = dep[x];
while (k > 0)
{
if (p[k] != pre && flag[p[k]])
{
dep[p[k]] = dep[x]+c[k];
dfs(p[k],x);
}
k = next[k];
}
}
int calc(int root,int dist)
{
int l,r,ret;
a[0] = 0;
dep[root] = dist;
dfs(root,0);
sort(a+1,a+a[0]+1);
l = 1; r = a[0];
ret = 0;
while (l < r)
{
if (a[l]+a[r] <= kkk)
{
ret = ret + r-l;
l++;
}
else r--;
}
return ret;
}
void getsize(int x,int pre)
{
int k;
k = list[x];
size++;
while (k > 0)
{
if (p[k] != pre && flag[p[k]])
{
getsize(p[k],x);
}
k = next[k];
}
}
void work(int root)
{
int k;
ans = ans + calc(root,0);
flag[root] = false;
k = list[root];
while (k > 0)
{
if (flag[p[k]])
{
ans = ans - calc(p[k],c[k]);
size = 0;
getsize(p[k],0);
temproot = 0;
f[0] = size;
findroot(p[k],0);
work(temproot);
}
k = next[k];
}
}
int main()
{
while (scanf("%d%d",&n,&kkk)== 2)
{
if (n == 0) break;
init();
temproot = 0;
size = f[0] = n;
findroot(1,0);
work(temproot);
printf("%d\n",ans);
}
return 0;
}
PS:
最近还看到一道codechef上的题:
给定一棵n个结点的树,树上两点之间的距离为经过的边数,若两点之间距离为质数,则认为是一个合法点对,问有多少个合法点对?
其实,这题的思路也是树的分治,只是将合法条件换了,我们可以同样处理,将dep[i]保存到根距离为i的结点有多少个
x^dep[1]+x^dep[2]+x^dep[3]+...+x^dep[n]
将其和自身做一遍乘法,就可以得到合法点对的数量,乘法要用FFT来实现