[蓝桥杯 2013 省 A] 大臣的旅费
题目描述
很久以前,T 王国空前繁荣。为了更好地管理国家,王国修建了大量的快速路,用于连接首都和王国内的各大城市。
为节省经费,T 国的大臣们经过思考,制定了一套优秀的修建方案,使得任何一个大城市都能从首都直接或者通过其他大城市间接到达。同时,如果不重复经过大城市,从首都到达每个大城市的方案都是唯一的。
J 是 T 国重要大臣,他巡查于各大城市之间,体察民情。所以,从一个城市马不停蹄地到另一个城市成了 J 最常做的事情。他有一个钱袋,用于存放往来城市间的路费。
聪明的 J 发现,如果不在某个城市停下来修整,在连续行进过程中,他所花的路费与他已走过的距离有关,在走第 x−1 千米到第 x 千米这一千米中(x 是整数),他花费的路费是 x+10 这么多。也就是说走 1 千米花费 11,走 2 千米要花费 23。
J 大臣想知道:他从某一个城市出发,中间不休息,到达另一个城市,所有可能花费的路费中最多是多少呢?
输入格式
输入的第一行包含一个整数 n(n≤10^5),表示包括首都在内的 T 王国的城市数。
城市从 1 开始依次编号,1 号城市为首都。
接下来 n−1 行,描述 T 国的高速路(TT 国的高速路一定是 n−1 条)。
每行三个整数 Pi,Q,Di,表示城市 Pi 和城市 Qi 之间有一条高速路,长度为 Di(Di≤1000) 千米。
输出格式
输出一个整数,表示大臣J最多花费的路费是多少。
输入输出样例
输入 #1复制
5 1 2 2 1 3 1 2 4 5 2 5 4
输出 #1复制
135
说明/提示
样例解释:大臣 J 从城市 4 到城市 5 要花费 135 的路费。
时限 5 秒, 64M。蓝桥杯 2013 年第四届省赛
附:树的直径的定义
在一棵树中,每一条边都有权值,树中的两个点之间的距离,定义为连接两点的路径上边权之和,那么树上最远的两个点,他们之间的距离,就被称之为,树的直径。
树的直径的别称,树的最长链。
请注意:树的直径,还可以认为是一条路径,不一定是只是一个数值。
考察算法:图论---无向无环带权图,树的直径,dfs,bfs,邻接表,树形dp
思路:题目中说,如果不重复经过大城市,从首都到达每个大城市的方案都是唯一的,说明是无环图,城市与城市直接有距离,说明是带权的,又任一两个城市都可以到达,所以是无向的,所以可以将这个图转化为树,题目让我们求花费路费最多是多少,而路费又与城市间距离有关,所以就转化为了求任意两点间距离的最大值,也就是求树的直径.先想一种暴力做法,枚举任意一个起点,再枚举除起点以外的任意一个点作为终点,求两点间的距离,取最大值.在一棵树中或一个图中求任意两点的距离,可以用dfs.处理输入时,可以采用邻接表(方便查找任意一个点的邻居)存储.优化:以首都为根,且树的直径一定经过根,将根结点一分为二,比如从根结点走到右边某个叶子结点一定是半径最长的,再以该叶子结点为根,走到左边某个叶子结点一定是直径最长的,所以该题可以用两次dfs,第一次dfs以编号为1的城市(首都)为起点,且起点固定的情况下,执行一次dfs可以遍历所有的点,以1为起点找到路径最长的终点,再以该终点为根结点,再次dfs.
两次dfs/bfs:
优点 : 可以通过一个新的数组记录路径信息(例如父节点与子节点之间的关系) 缺点 : 无法处理 负边权 .
实现过程 : 1、从任意一个节点出发,通过 BFS 或 DFS 对树进行一次遍历,求出与出发点距离最远的节点,记为 p。 2、从节点 p 出发,通过 BFS 或 DFS 再进行一次遍历,求出与 p 距离最远的节点,记为 q。 (p 是一个节点的最远的一个端点,那么从 p 出发的最远的端点就是直径的另一个端点) 为什么无法处理负边权?
看上面这个图: 如果按照 DFS 或者 BFS 我们第一次 找到的最远距离的节点是 2 , 然后从 2 出发
到达的最远距离的节点是 1 ,所以得到的树的直径长度是 1 ,但我们从图中很容易看出来树的直径最长应该是 2.(用树形 DP 的话从下向上就可以得到最长的树的直径的长度)
#include<bits/stdc++.h>
#define x first
#define y second
#define endl '\n'
using namespace std;
const int N=1e5+5,INF=0x3f3f3f3f,mod=1e9+7,M=2*N;
typedef pair<int,int>PII;
typedef long long LL;
using namespace std;
int head[N * 2],edge[N * 2],Next[N * 2],ver[N * 2];
int vis[N],dist[N];
int n,p,q,d;
int tot = 0;
int maxd = 0;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int BFS(int u);
void add(int u,int v,int w);
scanf("%d",&n);
for(int i = 1; i < n; i ++) {
scanf("%d%d%d",&p,&q,&d);
add(p,q,d); //建立无向图
add(q,p,d);
}
int u = BFS(1);
int s = BFS(u);
printf("第一次遍历得到的节点 : %d\n",u);
printf("第二次遍历得到的节点 : %d\n",s);
return 0;
}
void add(int u,int v,int w) {
ver[ ++ tot] = v,edge[tot] = w;
Next[tot] = head[u],head[u] = tot;
return ;
}
int BFS(int u) {
queue<int>Q;
while(!Q.empty()) Q.pop();
memset(vis,0,sizeof(vis)); //每次遍历的时候记得对数组进行Clear
memset(dist,0,sizeof(dist));
Q.push(u);
int x,max_num = 0;
while(!Q.empty()) {
x = Q.front();
Q.pop();
vis[x] = 1;
for(int i = head[x]; i ; i = Next[i]) {
int y = ver[i];
if(vis[y]) continue;
vis[y] = 1;
dist[y] = dist[x] + edge[i];//从上向下走,所以需要进行累加(这是与树形DP最大的不同)
if(dist[y] > maxd ) { //更新值和节点编号
maxd = dist[y];
max_num = y;
}
Q.push(y); //每个新的节点都要加入到队列中,有可能与该节点相连的路径是比较长的
}
}
return max_num;
}
#include<bits/stdc++.h>
#define x first
#define y second
#define endl '\n'
using namespace std;
const int N=1e5+5,INF=0x3f3f3f3f,mod=1e9+7,M=2*N;
typedef pair<int,int>PII;
typedef long long LL;
using namespace std;
vector<PII> G[N]; // 用 pair<int,int>来保存部分信息,相对于 结构体来说更加方便一点
int dist[N];
int n,p,q,d;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
void solve();
scanf("%d",&n);
for(int i = 1; i < n; i ++) {
scanf("%d%d%d",&p,&q,&d);
G[p].push_back(make_pair(q,d));
G[q].push_back(make_pair(p,d));
}
solve();
return 0;
}
void DFS(int u,int father,int value) {
dist[u] = value; // 这种方式就不用进行对数组Clear了
for(int i = 0; i < G[u].size(); i ++) {
if(G[u][i].x != father) {
DFS(G[u][i].x,u,value + G[u][i].y);
}
}
return ;
}
void solve() {
DFS(1,-1,0);
int u = 1;
for(int i = 1; i <= n; i ++) { // 遍历寻找最大值
if(dist[i] > dist[u]) {
u = i;
}
}
int x = u;
DFS(u,-1,0); // 第二次进行找另一个端点
for(int i = 1; i <= n; i ++) {
if(dist[i] > dist[u]) {
u = i;
}
}
int s = u;
printf("第一次遍历得到的最远的节点编号 : %d\n",x);
printf("第二次遍历得到的最远的节点编号 : %d\n",s);
return ;
}
第二种方法可以用树形dp.
优点 : 可以有效处理 负边权 缺点 : 对于记录路径的信息效率较低
简单分析 : 先通过递归的方式到叶子底部,然后通过自底向上的方式进行更新距离,找到最长路径。 (看下图,可以得到这棵树的直径是经过根节点 1 的 路径最长的链 5 -> 2 -> 1 和 经过根节点 1 的路径 次长链 3 -> 6 -> 1 两者之和 由此可得:树的直径 = (经过某个节点的) 最长链 + 次长链) -- 是路径长度
实现过程:设 D[x] 表示从节点 x 出发走向以 x 为根的子树,能够到达的最远距离。 设 x 的子节点为 y1,y2....yt,edge(x,y)表示边权,显然有: D[x] = max(D[yi] + edge(x,yi))(i 的范围是 1 - t) 也就是说,从 根节点出发,找到自己的最小辈,然后从最小辈向根节点更新,找一个最长的路径链。 只要子节点是最长的,那么我们更新到根节点时,这条链毫无疑问也就是最长的。 我们在找某个节点的最长链会发现一个问题,就是当前节点有有几个子节点,有一个我们没得选,当有多个时我们就需要选择最长的。 拿上面的图来说, 2 号节点有 两个子节点,我们就需要进行比较一下,100 > 2 + 30 ,所以我们应该选择 5 -> 2 这条链。
具体代码:
void dp(int x) {
vis[x] = 1;
for(int i = head[x]; i ; i = Next[i]) {
int y = ver[i];
if(vis[y]) continue; // 判断是否已经经过该节点
dp(y); // 继续向下寻找子节点
ans = max(ans,dist[x] + dist[y] + edge[i]); // 枚举从 x 节点出发的所有边,找一个最远的路径(看上面的 2 号节点)(dist[x] 是当前目前已知的最长的,
// 但是 x 可能有多个分支,所以需要枚举找最长的或者次长的,形成经过该节点的直径)
dist[x] = max(dist[x],dist[y] + edge[i]); // 经过枚举后 dist[x] 就不一定是当前最长的的,所以需要更新一下。
}
本题代码:
#include<bits/stdc++.h>
#define x first
#define y second
#define endl '\n'
using namespace std;
const int N=1e5+5,INF=0x3f3f3f3f,mod=1e9+7,M=2*N;
typedef pair<int,int>PII;
typedef long long LL;
struct node{
int num,cost;//点的编号和到这个点的距离
};
int ans;
void dfs(vector<node>g[],int vis[],int i,int j,int dist){
//查看如果是直接邻居
for(int k=0;k<g[i].size();k++){//访问i所有的邻居
if(g[i][k].num==j){//i的直接邻居中有j
ans=max(ans,dist+g[i][k].cost);
return;
}
}
vis[i]=0;
//如果不是直接邻居,就从现有的直接邻居去连接
for(int k=0;k<g[i].size();k++){
if(vis[g[i][k].num]==-1)
dfs(g,vis,g[i][k].num,j,dist+g[i][k].cost);
}
vis[i]=-1;
}
int check(int dist){
/* 第一项是11,第二项是12,第三项是13....
第n项是11+(dist-1),所以总和就是(11+11+(dist-1))*dist/2 */
return (21+dist)*dist/2;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int n;
cin>>n;
vector<node>g[n+1];
int vis[n+1];
memset(vis,-1,sizeof vis);//初始化为-1
for(int i=1;i<n;i++){
int a,b,c;
cin>>a>>b>>c;
node p1={b,c};
node p2={a,c};
g[a].push_back(p1);
g[b].push_back(p2);
}
//暴力求任意两点间距离,并维护最长距离
for(int i=1;i<=n-1;i++){
for(int j=i+1;j<=n;j++){
dfs(g,vis,i,j,0);//i,j表示从i到j的距离
}
}
cout<<check(ans)<<endl;
return 0;
}
//暴力解法
#include<bits/stdc++.h>
#define x first
#define y second
#define endl '\n'
using namespace std;
const int N=1e5+5,INF=0x3f3f3f3f,mod=1e9+7,M=2*N;
typedef pair<int,int>PII;
typedef long long LL;
struct node{
int num,cost;//点的编号和到这个点的距离
};
int ans;
int point=-1;
void dfs(vector<node>g[],int vis[],int start,int dist){
vector<node>nei_start=g[start];//表示start的所有邻居的集合
vis[start]=0;//表示被访问
bool isLeaf=true;
for(int k=0;k<nei_start.size();k++){
int num=nei_start[k].num;//邻居点的标号
if(vis[num]==-1){
isLeaf=false;
dfs(g,vis,num,dist+nei_start[k].cost);
}
}
vis[start]=-1;
if(isLeaf){
if(dist>ans){//更新答案
ans=dist;
point=start;
}
}
}
int check(int dist){
/* 第一项是11,第二项是12,第三项是13....
第n项是11+(dist-1),所以总和就是(11+11+(dist-1))*dist/2 */
return (21+dist)*dist/2;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int n;
cin>>n;
vector<node>g[n+1];
int vis[n+1];
memset(vis,-1,sizeof vis);//初始化为-1
for(int i=1;i<n;i++){
int a,b,c;
cin>>a>>b>>c;
node p1={b,c};
node p2={a,c};
g[a].push_back(p1);
g[b].push_back(p2);
}
dfs(g,vis,1,0);
ans=0;//重置答案
dfs(g,vis,point,0);
cout<<check(ans)<<endl;
return 0;
}
//两次dfs优化做法
#include<bits/stdc++.h>
#define x first
#define y second
#define endl '\n'
using namespace std;
const int N=1e5+5,INF=0x3f3f3f3f,mod=1e9+7,M=2*N;
typedef pair<int,int>PII;
typedef long long LL;
int e[M],w[M],ne[M],h[M],idx;
int vis[M],far;//far为最远点
int ans;
int n;
void add(int a,int b,int c){
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
}
void dfs(int a,int val){
vis[a]=1;
if(val>ans){
ans=val;
far=a;
}
for(int i=h[a];i!=-1;i=ne[i]){
if(!vis[e[i]])
dfs(e[i],val+w[i]);
}
}
int check(int dist){
return (21+dist)*dist/2;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
memset(h,-1,sizeof h);
for(int i=1;i<n;i++){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
}
dfs(1,0);
memset(vis,0,sizeof vis);
dfs(far,0);
cout<<check(ans)<<endl;
return 0;
}
//两次dfs另一种做法
#include<bits/stdc++.h>
#define x first
#define y second
#define endl '\n'
using namespace std;
const int N=1e5+5,INF=0x3f3f3f3f,mod=1e9+7,M=2*N;
typedef pair<int,int>PII;
typedef long long LL;
int h[N*2],e[N*2],ne[N*2],ver[N*2];
int dist[N],vis[N];
int tot,ans;
int n;
void add(int u,int v,int w){
ver[++tot]=v;
e[tot]=w;
ne[tot]=h[u];
h[u]=tot;
}
void dp(int x){
vis[x]=1;
for(int i=h[x];i;i=ne[i]){
int y=ver[i];
if(vis[y]) continue;
dp(y);
ans=max(ans,dist[x]+dist[y]+e[i]);
dist[x]=max(dist[x],dist[y]+e[i]);
}
}
int check(int dist){
return (21+dist)*dist/2;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<n;i++){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
}
ans=0;
dp(1);
cout<<check(ans)<<endl;
return 0;
}
//树形dp做法