初见安~这里讲述的树上操作真的都是非常非常基础的哈:)都是要掌握的。
树的重心是指树上一点,去掉后最大子树可以取得最小值的点。
这样定义可能比较抽象,我们来看一个例子——
【无根树】
去掉1后,子树最大为4;去掉2后,子树最大为5;去掉3后,没影响;去掉4后,子树最大为7;去掉5后,没影响;去掉6后,子树最大为5;去掉7后,子树最大为6;去掉8、9后,均无影响。所以这棵树的重心为点1。
由此也可以得出重心的另一个定义——去掉该点后最大子树大小不超过n/2。这一定义在应用方面比较多。
看看代码就可以大概理解了——
#include
#define maxn 20005
using namespace std;
int n;
struct edge
{
int to, nxt;
edge(){}
edge(int tt, int nn)
{
to = tt, nxt = nn;
}
}e[maxn << 1];
int head[maxn], k = 0;
void add(int u, int v)
{
e[k] = edge(v, head[u]);
head[u] = k++;
}
bool vis[maxn];
int size[maxn], ans = 0x3f3f3f3f;
int maxpart[maxn];
void dfs(int x)
{
vis[x] = 1, size[x] = 1;
maxpart[x] = 0;
for(int i = head[x]; ~i; i = e[i].nxt)
{
int y = e[i].to;
if(vis[y]) continue;
dfs(y);
size[x] += size[y];
maxpart[x] = max(maxpart[x], size[y]);//这是x的子树
}
maxpart[x] = max(maxpart[x], n - size[x]);//这是x的另一颗子树(无根树,换根法
if(maxpart[x] < ans)
{
ans = maxpart[x];
}
}
int main()
{
memset(head, -1, sizeof head);
scanf("%d", &n);
int u, v;
for(int i = 1; i < n; i++)
{
scanf("%d%d", &u, &v);
add(u, v);
add(v, u);
}
dfs(1);
for(int i = 1; i <= n; i++)
{
if(maxpart[i] == ans) printf("%d\n", i);
}
return 0;
}
树的直径的定义相对而言要简单一些——就是树上最远两点之间的距离。
比方说还是方才那个图——
最远的两点5、8或者5、9之间的距离为6。6就是这棵树的直径。
那么我们怎么求树的直径呢——因为我们不知道直径的两端分别是哪两个点。所以我们可以先随便找个点,找到离他最远的点,再在那个最远的点上找一次最远的点,这两个点之间的距离就是直径。比如上图,我们从1开始,最远的是5(或者8、9),从5开始找,最远的点就是8or9了。
证明如下——我们可以发现,最远的两点一定不在根节点的同一子树上,在此基础上各自深度为这棵树的最深。也就是说——既然是最深的点,那么对于其他普通点来说,最远的点也一定是到达了叶子节点的,所以一定会找到一个非同一子树的深度最深的叶子节点。而这个点就一定是我们直径两端的其中一点。再从这个点出发找到最远,就一定是树的直径了。
所以我们两次bfs找最远就可以了。
代码实现如下——
#include
#define maxn 100005
using namespace std;
int n, m;
int dep[maxn], dis[maxn], l = 1, r = 0;
int res, ans;//res为找到的最远点,ans为直径
bool vis[maxn];
struct edge
{
int to, w, nxt;
edge(){};
edge(int tt, int ww, int nn)
{
to = tt, w = ww, nxt = nn;
}
}e[maxn << 1];
int k = 0, head[maxn];
void add(int u, int v, int w)
{
e[k] = edge(v, w, head[u]);
head[u] = k++;
}
void bfs(int x)
{
memset(dis, 0, sizeof dis);//调用两次,必须清空
memset(vis, 0, sizeof vis);
queue q;
q.push(x);
vis[x] = 1;
while(q.size())
{
int p = q.front();
q.pop();
if(dis[p] > ans)//找到了距离更远的
ans = dis[p], res = p;//可以直接更新
for(int i = head[p]; ~i; i = e[i].nxt)
{
int y = e[i].to, w = e[i].w;
if(!vis[y])
{
vis[y] = 1;
dis[y] = dis[p] + w;//计算dis,在下一次取出来时再和ans作比较
q.push(y);
}
}
}
}
int main()
{
memset(head, -1, sizeof head);
scanf("%d%d", &n, &m);
int u, v, w;
char op;
for(int i = 1; i <= m; i++)
{
scanf("%d%d%d %c", &u, &v, &w, &op);
add(u, v, w);
add(v, u, w);
}
bfs(1);//随便一个点
ans = 0;
bfs(res);//第二次
printf("%d\n", ans);
return 0;
}
大雪履盖了整个城市,市政府要求冬季服务部门尽快将一些街道(列在一份清单中)的积雪清除掉以恢复交通,整个城市由许多交叉路口和街道构成,当然任意两个交叉路口都是直接或间接连通的,清单给出了最少的街道,使得这些街道的积雪清除后任意两个交叉路口之间有且仅有一条通路,冬季服务部门只有一辆铲雪车及一名司机,这辆铲雪车的出发点位于某个交叉路口。
无论街道上有没有积雪,铲雪车每前进一米都要消耗一升燃料,冬季服务部门要求司机在铲除清单上的所有街道的积雪的前提下,要求消耗燃料最少,积雪铲完后车可以停在任意的交叉路口。
输入文件的第一行包含两个整数N和S,1≤N≤100000,1≤S≤N。N为交叉路口的总数;S为铲雪车出发的路口序号。路口的标号为1••N。
接下来的N-1行为清单上的街道,每一行包含三个用空格隔开的整数A、B、C,表示一条从交叉路口A到交叉路口B的街道,C为该街道的长度,单位为米,1≤C≤1000。
输出文件仅一行包含一个整数表示清除所有积雪所需的最少的燃料数量。
5 1
1 2 1
2 3 1
3 5 1
3 4 1
5
很明显——如果这一辆车要遍历树上所有点并且返回节点1的话那么距离一定是两倍的所以边权和。但是车子可以停在任意节点,也就是说最后不用返回。那么我们就可以让返回的路尽量的少,而在保证遍历所有点的前提下要做到这一点就只有考虑在必须返回的时候让最后返回节点1的路程尽量大,这样我们在不用返回的时候节省的路就尽量大了。而这条路——就是从根节点出发距离根节点最远的路。bfs走一遍就可以了。【bfs部分和求直径时的代码一样
【就是在扫雪系列1的基础上把一辆车变成两辆,然而样例的输入输出还能惊人的相似】
两辆车了。也就是说两辆车返回的路径都可以省了。那就直接是树的直径了。没了。【??!
至于样例为什么一模一样——因为扫雪2中的直径并没有经过根节点。画一画就知道了。
所以代码也就和求树的直径几乎一样——只是说最后的ans是两倍边权和减去直径。
就这一步不一样。