团体天梯练习 L2-043 龙龙送外卖

发布时间 2023-04-20 23:35:50作者: Amαdeus

L2-043 龙龙送外卖

龙龙是“饱了呀”外卖软件的注册骑手,负责送帕特小区的外卖。帕特小区的构造非常特别,都是双向道路且没有构成环 —— 你可以简单地认为小区的路构成了一棵树,根结点是外卖站,树上的结点就是要送餐的地址。

每到中午 12 点,帕特小区就进入了点餐高峰。一开始,只有一两个地方点外卖,龙龙简单就送好了;但随着大数据的分析,龙龙被派了更多的单子,也就送得越来越累……

看着一大堆订单,龙龙想知道,从外卖站出发,访问所有点了外卖的地方至少一次(这样才能把外卖送到)所需的最短路程的距离到底是多少?每次新增一个点外卖的地址,他就想估算一遍整体工作量,这样他就可以搞明白新增一个地址给他带来了多少负担。

输入格式:

输入第一行是两个数 \(N\)\(M\) ( \(2≤N≤10^{5}\), \(1≤M≤10^{5}\) ),分别对应树上节点的个数(包括外卖站),以及新增的送餐地址的个数。

接下来首先是一行 \(N\) 个数,第 \(i\) 个数表示第 \(i\) 个点的双亲节点的编号。节点编号从 \(1\)\(N\) ,外卖站的双亲编号定义为 \(−1\)

接下来有 \(M\) 行,每行给出一个新增的送餐地点的编号 \(X_{i}\) 。保证送餐地点中不会有外卖站,但地点有可能会重复。

为了方便计算,我们可以假设龙龙一开始一个地址的外卖都不用送,两个相邻的地点之间的路径长度统一设为 \(1\) ,且从外卖站出发可以访问到所有地点。

注意:所有送餐地址可以按任意顺序访问,且完成送餐后无需返回外卖站。

输出格式:

对于每个新增的地点,在一行内输出题目需要求的最短路程的距离。

输入样例:

7 4
-1 1 1 1 2 2 3
5
6
2
4

输出样例:

2
4
4
6


解题思路

这道题还是比较难的,关键在于如何确定这个最短路径距离。

首先这是一个树的结构,题目中也说了。每多出现一个点,需要重新规划从根节点开始到所有需要到达的点的最短距离,当然每个点的访问顺序可以是乱序的,只要保证都到达了即可。对于所有需要到达的点,其连到父节点的边一定是要经历的,也就是说假如有一条边一个端点为 \(v\) ,其父节点也就是这条边的另一条端点 \(u\),那么如果 \(v\) 是需要到达的节点之一,这条 \(u->v\) 的边是一定需要经过的,其原因是这是一个树的结构,到达 \(v\) 的路径一定只能经过其父节点到达。

根据这一点,而且从根节点到达子节点路径是唯一的,所以所有从根节点到某一子节点上路径涉及到的边一定都需要经历,且由于只能从根节点出发,所以有很多边需要经历两次。但问题在于,最短路径距离如何确定,有哪些边只需要经历一次,有哪些边需要经历两次。

不妨逆向思维考虑这个问题,题中说从根节点出发但是不必要回到根节点。我们可以先将所有涉及到的边都经历两次,算出此时的路径距离。由于我们可以不用返回根节点,而所有涉及的边都经历两次,必定有一条从根节点到某一子节点的路径是多余的。此时,我们可以采取贪心的策略,因为要求最短路径距离,所以应当选取最长的一条从根节点到当前需要到达的子节点的距离,将这个距离减去,那么最终得到的就是当前的最短路径距离。

所以最后,我们的问题转换为,累加所有涉及的边两倍的边权(边权为1,所以每次累加2),然后减去一条最长的从根节点到需要到达的子节点的距离,这个最长距离随着需要到达的点的增多而更新。

分析完了这个关键问题,剩下的主要就是如何求解这个问题。

首先要建树,并且确定根节点,我自己使用的是链式前向星,当然也可以用\(vector\)邻接表。然后第一要事是求出根节点到其它各点的路径长度,可以用 \(dfs\) 自顶向下求这个距离,应该也可以用 \(bfs\) 求点的层次一样求这个距离,两者应该都行,而且我们不需要记录其父节点或者判断某个点是否被遍历过,因为这是一个树的结构,每个点只会被遍历一次

建图的同时,还需要记录每个节点的父节点,这主要是为了后期计算答案需要用到。在计算答案时,每多出现一个需要到达的节点,都需要更新此时的最长路径距离,然后累加所有 未被累加过 的边两倍的边权,注意是 未被累加过的, 不然会造成重复累加导致错误。如果在累加过程中某个节点 \(v\) 的父节点 \(u\) 作为下端点的边发现已经被累加过了,那么也可以直接退出循环,这是因为如果 \(v\) 的父节点 \(u\) 已经被访问过,那么从根节点到达这个 \(u\) 的路径一定已经被累加到路径距离中了,所以此时可以在累加了 \(u->v\) 后,直接退出循环;如果根节点到 \(v\) 路径上所有边没被累加过,需要从 \(v\) 开始一直向上迭代,直到根节点,累加所有边两倍的边权。

/*   一切都是命运石之门的选择  El Psy Kongroo  */
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<vector>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#include<bitset>
#include<cmath>
#include<functional>
using namespace std;

typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
typedef pair<int, pii> piii;
typedef pair<double, double> pdd;
typedef pair<string, int> psi;
//typedef __int128 int128;
#define PI acos(-1.0)
#define x first
#define y second
//int dx[4] = {1, -1, 0, 0};
//int dy[4] = {0, 0, 1, -1};
const int inf = 0x3f3f3f3f, mod = 1e9 + 7;


const int N = 1e5 + 10;
int h[N], e[N], ne[N], idx;  //链式前向星
int fa[N];       //fa[v]表示v的父节点
int dist[N];     //dist[v]表示根节点到v的距离
bool st[N];      //记录当前点是否已经到达过
int n, m, root;

void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

//深度优先搜索根节点到各个节点路径距离
void dfs(int u){
    for(int i = h[u]; ~i; i = ne[i]){
        int v = e[i];
        dist[v] = dist[u] + 1;  //自顶向下
        dfs(v);
    }
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    memset(h, -1, sizeof(h));
    cin >> n >> m;
    for(int v = 1; v <= n; v ++ ){
        cin >> fa[v];
        if(fa[v] == -1){
            root = v;
            continue;
        }
        int u = fa[v];
        add(u, v);
    }

    dfs(root);    //dfs求根节点到各点距离 (应该也可用BFS或其它)

    int ans = 0, maxd = 0;
    while(m -- ){
        int v; cin >> v;
        maxd = max(maxd, dist[v]);   //更新最长路径
        while(!st[v] && v != root){
            st[v] = true;
            ans += 2;   //每个没经过的点,其父节点到这个点这条边经过两次
            v = fa[v];
        }
        cout << ans - maxd << endl;  //因为可以不回去 所以减去最长的路径
    }

    return 0;
}