复赛二:天天爱跑步(running)

洛谷:P1600
OJ : i1600

第一步:LCA+倍增
第二步:不枚举每个运动员,而是枚举每个观察员i,看看哪些结点会为这个观察员i做贡献(刚好在wi​秒跑到他这儿)。
第三步:对于一个结点P来说,以P为根递归整颗子树过程中在桶内产生的差值才是有效的

变量含义:

数据变量:
SIZE:定义了最大节点数和玩家数,设为300,000。
n:节点数。
m:玩家数。
tot:边的计数器,用于构建邻接表。
h[SIZE]:邻接表的头指针数组,
h[x] 表示节点 x 的第一条边在数组 E 中的索引。
deep[SIZE]:记录每个节点的深度。
fa[SIZE][20]:倍增表,fa[x][i] 表示节点 x 的 2i级祖先。
w[SIZE]:每个节点的观察时间 Wj

边的结构体变量:
edge:结构体,用于表示树的边,包括目标节点 to 和下一条边的索引 next
E[SIZE*2]:主树的边数组。
e1[SIZE*2]:用于存储玩家到终点节点的列表。
e2[SIZE*2]:用于存储玩家到LCA节点的列表。

玩家相关变量:
tot1tot2:分别用于统计玩家加入终点节点和LCA节点列表的边数。
h1[SIZE]h2[SIZE]:分别表示终点节点和LCA节点的玩家列表头指针。
b1[SIZE*2]b2[SIZE*2]:辅助计数数组,用于在第二次DFS中统计玩家位置。
js[SIZE]:记录每个节点作为玩家起点的玩家数量。
dist[SIZE]:记录每个玩家的路径长度。
s[SIZE]t_nodes[SIZE]:分别记录每个玩家的起点和终点节点。
l[SIZE]:暂时未使用的数组,可能是冗余的。
ans[SIZE]:记录每个节点被观察到的玩家数量。

算法流程详细分析

1. 树的构建与预处理
  • 邻接表构建
    • 使用邻接表表示树结构,通过 add(x, y) 函数将边 (x, y) 添加到邻接表中。由于树是无向的,需双向添加边。
  • DFS预处理 (dfs1)
    • 通过深度优先搜索,计算每个节点的深度 deep[x]
    • 同时,构建倍增表 fa[x][i],用于快速计算LCA。
    • 倍增表 fa[x][i] 表示节点 x 的 2i级祖先。

2. 玩家信息处理

  • 读取玩家信息
    • 对于每个玩家,读取起点 s[i] 和终点 t_nodes[i]
    • 计算玩家的最近公共祖先 lca = get_lca(s[i], t_nodes[i])
    • 计算玩家的路径长度 dist[i] = deep[s[i]] + deep[t_nodes[i]] - 2 * deep[lca]
  • 玩家分组
    • 起点统计:通过 js[s[i]]++ 统计每个节点作为玩家起点的玩家数量。
    • 终点玩家列表:通过 add1(t_nodes[i], i) 将玩家编号 i 添加到终点节点 t_nodes[i] 的玩家列表中。
    • LCA玩家列表:通过 add2(lca, i) 将玩家编号 i 添加到LCA节点 lca 的玩家列表中。
  • 特殊条件处理
    • 如果 deep[lca] + Wj[lca] == deep[s[i]],则 ans[lca]--。这意味着在节点 lca 处,有一个玩家在观察时间正好位于该节点,需调整计数。

答案计算(第二次DFS)

  • 辅助计数数组
    • b1[d]:记录当前子树中起点深度为 d 的玩家数量。
    • b2[d]:记录当前子树中路径长度为 d 的玩家数量。
  • 第二次DFS (dfs2)
    • 对于每个节点 x,在进入时保存 b1b2 在特定索引处的值(t1t2)。
    • 遍历所有子节点,并递归调用 dfs2(y) 进行深度优先搜索。
    • 更新 b1b2
      • b1[deep[x]] += js[x]:增加当前节点深度处的玩家数量。
      • 遍历当前节点的终点玩家列表 h1[x],对于每个玩家 y,更新 b2
    • 计算当前节点的答案 ans[x]
      • ans[x] += b1[w[x] + deep[x]] - t1 + b2[w[x] - deep[x] + SIZE] - t2
      • 这表示在时间 Wj[x] 时刻,有多少玩家位于节点 x 上。
    • 恢复 b1b2 的状态:
      • 遍历当前节点的LCA玩家列表 h2[x],对于每个玩家 y,减少相应的 b1b2 计数,以避免影响其他分支。

4. 答案输出

  • 最后,通过遍历所有节点,输出每个节点被观察到的玩家数量 ans[i]

算法原理与逻辑

1. 二进制提升法用于LCA查询

  • 二进制提升表
    • 通过预处理二进制提升表 fa[x][i],可以在 O(log⁡N) 时间内计算任意两个节点的LCA。
    • 该方法通过逐步提升节点的祖先来快速对齐深度并找到公共祖先。

2. 玩家位置的统计与观察

  • 玩家的移动路径
    • 玩家从起点 S_i 出发,按照树的边逐步移动,每秒移动一条边,最终到达终点 T_i
    • 玩家的位置在每一秒钟的变化可以通过玩家的路径和时间 Wj[x] 来确定。
  • 观察时间 Wj[x]
    • 每个节点 x 在时间 Wj[x] 需要统计有多少玩家恰好位于该节点上。
  • 玩家分组与计数
    • 起点分组:通过 js[x] 统计每个节点 x 作为起点的玩家数量。
    • 终点分组:通过 e1 数组将玩家按终点节点分组,便于在第二次DFS中快速访问。
    • LCA分组:通过 e2 数组将玩家按LCA节点分组,便于在第二次DFS中进行特殊处理。

3. 第二次DFS的作用

  • 辅助计数数组 b1b2
    • b1[d]:当前子树中,起点深度为 d 的玩家数量。
    • b2[d]:当前子树中,路径长度为 d 的玩家数量。
  • 答案计算
    • 对于每个节点 x,在时间 Wj[x] 时刻,位于节点 x 的玩家数量可以通过以下两部分统计:
      1. 起点深度匹配:玩家起点深度为 w[x] + deep[x],即玩家在 w[x] 秒后到达节点 x
      2. 路径长度匹配:玩家路径长度为 w[x] - deep[x],即玩家在移动 w[x] - deep[x] 秒后到达节点 x
    • 通过 ans[x] += b1[w[x] + deep[x]] - t1 + b2[w[x] - deep[x] + SIZE] - t2,统计满足上述条件的玩家数量。
  • 状态恢复
    • 在处理完当前节点后,通过遍历LCA玩家列表,恢复 b1b2 的状态,确保其他分支的统计不受影响。

算法复杂度分析

  • 预处理阶段(第一次DFS)
    • 时间复杂度:O(Nlog⁡N),其中 N是节点数。
    • 主要消耗在二进制提升表的构建上,每个节点最多计算 log⁡N个祖先。
  • 玩家信息处理
    • 时间复杂度:O(Mlog⁡N),其中 M 是玩家数。
    • 每个玩家需要进行一次LCA查询,时间复杂度为 O(log⁡N)。
  • 答案计算阶段(第二次DFS)
    • 时间复杂度:O(N+M)。
    • 遍历每个节点一次,并处理与节点相关的玩家列表。
  • 总体时间复杂度:O((N+M)log⁡N),适用于大规模的数据范围。

总结

这段代码通过两次深度优先搜索(DFS)和二进制提升法(Binary Lifting)有效地计算了每个节点在特定时间 Wj​ 时刻被观察到的玩家数量。主要步骤包括:

  1. 树的构建与预处理
    • 使用邻接表表示树结构。
    • 通过DFS计算每个节点的深度和二进制提升表,支持高效的LCA查询。
  2. 玩家信息处理
    • 计算每个玩家的LCA和路径长度。
    • 将玩家按终点节点和LCA节点进行分组,便于在第二次DFS中快速访问和处理。
  3. 答案计算
    • 通过第二次DFS,维护辅助计数数组 b1b2,统计满足观察条件的玩家数量。
    • 结合起点深度和路径长度的匹配,准确计算每个节点被观察到的玩家数量。
  4. 输出结果
    • 按节点编号顺序输出每个节点的观察结果。

代码实现:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/**************************************************************** 
 * Description: 2016年提高组第二题  天天爱跑步
 * Author: Alex Li
 * Date: 2024-09-30 23:27:41
 * LastEditTime: 2024-10-01 10:44:15
****************************************************************/
#include<iostream>
#include<cmath>
using namespace std;

const int SIZE=300000; // 定义常量,假设节点数和玩家数不超过300000

int n, m, tot, h[SIZE], deep[SIZE], fa[SIZE][20], w[SIZE];

// 定义边的结构体,包含目标节点和下一条边的索引
struct edge{
    int to, next;
}E[SIZE*2], e1[SIZE*2], e2[SIZE*2];

// 函数:添加边到邻接表
void add(int x, int y){
    E[++tot].to = y;        // 设置目标节点
    E[tot].next = h[x];     // 指向当前节点x的第一条边
    h[x] = tot;             // 更新节点x的第一条边索引
}

int tot1, tot2, h1[SIZE], h2[SIZE];

// 函数:添加玩家到t节点的列表
void add1(int x, int y)
{
    e1[++tot1].to = y;      // y表示玩家编号
    e1[tot1].next = h1[x];  // 链接当前节点x的玩家列表
    h1[x] = tot1;           // 更新节点x的玩家列表头
}

// 函数:添加玩家到LCA节点的列表
void add2(int x, int y){
    e2[++tot2].to = y;      // y表示玩家编号
    e2[tot2].next = h2[x];  // 链接当前节点x的玩家列表
    h2[x] = tot2;           // 更新节点x的玩家列表头
}

// 深度优先搜索,预处理深度和祖先节点
void dfs1(int x){
    // 预处理二进制提升表
    for(int i=1; (1<<i)<=deep[x]; i++)
        fa[x][i] = fa[fa[x][i-1]][i-1];
    
    // 遍历所有邻接节点
    for(int i=h[x]; i; i=E[i].next){
        int y = E[i].to;
        if(y == fa[x][0]) continue; // 避免回到父节点
        fa[y][0] = x;               // 设置y的直接父节点为x
        deep[y] = deep[x] + 1;      // 设置y的深度
        dfs1(y);                    // 递归DFS
    }
}

// 计算两个节点的最近公共祖先(LCA)
int get_lca(int x, int y){
    if(x == y) return x; // 如果两个节点相同,直接返回
    if(deep[x] < deep[y]) swap(x, y); // 确保x的深度不小于y
    
    // 将x提升到与y相同的深度
    int t = log(deep[x] - deep[y]) / log(2);
    for(int i=t; i>=0; i--){
        if(deep[fa[x][i]] >= deep[y])
            x = fa[x][i];
        if(x == y)
            return x;
    }
    
    // 同时提升x和y,找到LCA
    t = log(deep[x]) / log(2);
    for(int i=t; i>=0; i--){
        if(fa[x][i] != fa[y][i])
            x = fa[x][i], y = fa[y][i];
    }
    return fa[x][0];
}

int b1[SIZE*2], b2[SIZE*2], js[SIZE], dist[SIZE], s[SIZE], t_nodes[SIZE], l[SIZE], ans[SIZE];

// 第二次DFS,用于计算每个节点被观察到的玩家数量
void dfs2(int x){
    // 保存当前b1和b2的状态
    int t1 = b1[w[x] + deep[x]];
    int t2 = b2[w[x] - deep[x] + SIZE];
    
    // 遍历所有子节点
    for(int i=h[x]; i; i=E[i].next){
        int y = E[i].to;
        if(y == fa[x][0]) continue; // 避免回到父节点
        dfs2(y); // 递归DFS
    }
    
    // 更新b1和b2
    b1[deep[x]] += js[x];
    for(int i=h1[x]; i; i=e1[i].next){
        int y = e1[i].to;
        b2[dist[y] - deep[t_nodes[y]] + SIZE]++;
    }
    
    // 计算当前节点的答案
    ans[x] += b1[w[x] + deep[x]] - t1 + b2[w[x] - deep[x] + SIZE] - t2;
    
    // 恢复b1和b2的状态,避免影响其他分支
    for(int i=h2[x]; i; i=e2[i].next){
        int y = e2[i].to;
        b1[deep[s[y]]]--;
        b2[dist[y] - deep[t_nodes[y]] + SIZE]--;
    }
}

int main(){
    freopen("running.in","r",stdin);
    freopen("running.out","w",stdout);
    // 读取节点数n和玩家数m
    cin>>n>>m;
    
    // 读取n-1条边,构建树的邻接表
    for(int i=1; i<n; i++){
        int u, v;
        cin>>u>>v;
        add(u, v);
        add(v, u);
    }
    
    // 初始化根节点的深度和父节点
    deep[1] = 1;
    fa[1][0] = 1;
    dfs1(1); // 预处理深度和二进制提升表
    
    // 读取每个节点的观察时间Wj
    for(int i=1; i<=n; i++) cin>>w[i];
    
    // 读取m个玩家的信息
    for(int i=1; i<=m; i++){
        cin>>s[i]>>t_nodes[i];
        int lca = get_lca(s[i], t_nodes[i]); // 计算玩家的LCA
        dist[i] = deep[s[i]] + deep[t_nodes[i]] - 2 * deep[lca]; // 计算玩家的路径长度
        
        js[s[i]]++; // 统计每个起点有多少玩家
        add1(t_nodes[i], i); // 将玩家加入终点节点的列表
        add2(lca, i); // 将玩家加入LCA节点的列表
        
        // 如果LCA节点的深度加上Wj恰好等于起点的深度,减去该节点的答案
        if(deep[lca] + w[lca] == deep[s[i]]) ans[lca]--;
    }
    
    dfs2(1); // 计算每个节点被观察到的玩家数量
    
    // 输出每个节点的答案
    for(int i=1; i<=n; i++) 
    cout<<ans[i]<<' ';
    //printf("%d ", ans[i]);
    return 0;
}
Scroll to Top