训练军队题解
题目描述
韩信所在的军队共nnn个基地(编号1−n1-n1−n)每个基地恰好由n−1n−1n−1条道路连接,每条道路iii都有花费的时间timeitime_itimei,如果一个基地只有一条道路与其他基地相连,则该基地会用来囤积粮食。
训练军队时,军队前进的速度恒定,韩信总是选择两个相距最远的粮食基地,并从这两个基地中的一个基地到达另外一个粮食基地,对于走过的每一条道路,军队都会修整和修复道路,这使得下一次途径该道路时花费的时间减半。
每一天韩信都会举行两次训练,请你计算两次训练军队花费的总时间为多少?
输入描述
第一行包含整数 nnn。接下来 n−1n−1n−1 行,每行包含三个整数 ai,bi,cia_i,b_i,c_iai,bi,ci表示点 aia_iai和 bib_ibi之间存在一条道路,此条道路花费的时间为cic_ici。
输出描述
输出一个小数,表示训练军队花费的总时间,结果保留两位小数。
样例输入
6
5 1 6
1 4 5
6 3 9
2 6 8
6 1 7
样例输出
38.50
数据范围
1≤n≤100001≤n≤100001≤n≤10000
1≤ai,bi≤n1≤a_i,b_i≤n1≤ai,bi≤n
0≤ci≤1000000≤c_i≤1000000≤ci≤100000
时间限制:1秒 内存限制:128M
样例解释
第一次:训练军队路径为5−1−6−35-1-6-35−1−6−3 ,道路上花费的时间为22。
5−1、1−6、6−35-1、1-6、6-35−1、1−6、6−3这三条路径时间减半。
第二次:训练军队路径为4−1−6−24-1-6-24−1−6−2 ,道路上花费的时间为16.5。
答案就是两次训练军队时间之和,22+16.5=38.5,保留小数为38.50。
解题思路
因为此题要求改变边权,所以使用bfs解题。
大致思路:
1.使用链式前向星存储。
2. bfs求直径。
3. 权值减半。
4. 使用修改过的权值,再bfs求一次直径。
5. 将两次直径加和,保留小数输出。
具体思路:
1.链式前向星存储。
- 一定要注意把www(权值数组)设为doubledoubledouble类型,因为权值减半后有小数。
void add(int a,int b,int c)
{
ver[idx]=b,w[idx]=c,nxt[idx]=head[a],head[a]=idx++;
}
//...
memset(head,-1,sizeof(head));
int a,b,c;
cin>>n;
for(int i=1;i<n;i++)
{
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
}`
2.使用广度优先搜索求树的直径
- 设visvisvis为判重数组,disdisdis为距离数组,preprepre为节点的前驱。
- 建队列,将visvisvis设为000,将disdisdis设为0x3f0x3f0x3f。
- rootrootroot节点入队,将vis[root]vis[root]vis[root]设为已访问,将dis[root]dis[root]dis[root]设为000。
- 如果队列不空,取出队头元素,名为tmptmptmp,并进行一次出队。
- 遍历边集,设yyy为边的终点,zzz为边权。
- 如果yyy没有被遍历过,就更新visyvis_yvisy为111,将yyy点入队,将disydis_ydisy设为dis[tmp]+zdis[tmp]+zdis[tmp]+z。
- 将preypre_yprey设为当前遍历到的iii。
- 设maxmaxmax为−1-1−1,设nodenodenode为000。
- 循环遍历disdisdis数组,若disidis_idisi大于maxmaxmax,则让maxmaxmax等于disidis_idisi,并让nodenodenode等于iii。
- bfsbfsbfs函数返回nodenodenode。
int bfs(int root)
{
queue<int>q;
memset(vis,0,sizeof(vis));
memset(dis,0x3f,sizeof(dis));
q.push(root);
vis[root]=1;
dis[root]=0;
while(!q.empty())
{
int tmp=q.front();
q.pop();
for(int i=head[i];~i;i=nxt[i])
{
int y=ver[i];
double z=w[i];
if(!vis[y])
{
vis[y]=1;
q.push(y);
dis[y]=dis[tmp]+z;
pre[y]=i;
}
}
}
int max=-1;
int node=0;
for(int i=1;i<=n;i++)
{
if(dis[i]>max)
{
max=dis[i];
node=i;
}
}
return node;
}
3.将直径经过的边权值减半
- 设sum1sum1sum1,sum2sum2sum2为两次bfsbfsbfs(即第一次求直径)的结果。
- 设ansansans为dis[sum2]dis[sum2]dis[sum2]的值。
- 设变量iii为sum2sum2sum2的前驱,变量wwwwww为wiw_iwi的值。
- 使用wi=wi异或1w_i=w_i异或1wi=wi异或1的方法,找到反向边(树是一个无向连通图,必有反向边)。
- 将这两条边权值除以2.02.02.0
int sum1=bfs(1);
int sum2=bfs(sum1);
double ans=dis[sum2];
while(sum2!=sum1)
{
int i=pre[sum2];
double ww=w[i];
w[i]=w[i^1]=ww/2.0;
sum2=ver[i^1];
}
4.再次求直径。
sum1=bfs(1);
sum2=bfs(sum1);
5.将ansansans进行加和,并保留小数输出。
ans+=dis[sum2];
printf("%.2lf",ans);
AC代码
#include<bits/stdc++.h>
using namespace std;
const int N=100005;
int head[N];
int ver[2*N];
int nxt[2*N];
int idx=0;
double w[2*N];
bool vis[N];
double dis[N];
int pre[N];
int n;
void add(int a,int b,int c)
{
ver[idx]=b,w[idx]=c,nxt[idx]=head[a],head[a]=idx++;
}
int bfs(int root)
{
queue<int>q;
memset(vis,0,sizeof(vis));
memset(dis,0x3f,sizeof(dis));
q.push(root);
vis[root]=1;
dis[root]=0;
while(!q.empty())
{
int tmp=q.front();
q.pop();
for(int i=head[i];~i;i=nxt[i])
{
int y=ver[i];
double www=w[i];
if(!vis[y])
{
vis[y]=1;
q.push(y);
dis[y]=dis[tmp]+www;
pre[y]=i;
}
}
}
int max=-1;
int node=0;
for(int i=1;i<=n;i++)
{
if(dis[i]>max)
{
max=dis[i];
node=i;
}
}
return node;
}
int main()
{
memset(head,-1,sizeof(head));
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
add(b,a,c);
}
int sum1=bfs(1);
int sum2=bfs(sum1);
double ans=dis[sum2];
while(sum2!=sum1)
{
int i=pre[sum2];
double ww=w[i];
w[i]=w[i^1]=ww/2.0;
sum2=ver[i^1];
}
sum1=bfs(1);
sum2=bfs(sum1);
ans+=dis[sum2];
printf("%.2lf",ans);
return 0;
}