复赛二:天天爱跑步(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节点的列表。
玩家相关变量:tot1、tot2:分别用于统计玩家加入终点节点和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,在进入时保存b1和b2在特定索引处的值(t1和t2)。 - 遍历所有子节点,并递归调用
dfs2(y)进行深度优先搜索。 - 更新
b1和b2: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上。
- 恢复
b1和b2的状态:- 遍历当前节点的LCA玩家列表
h2[x],对于每个玩家y,减少相应的b1和b2计数,以避免影响其他分支。
- 遍历当前节点的LCA玩家列表
- 对于每个节点
4. 答案输出
- 最后,通过遍历所有节点,输出每个节点被观察到的玩家数量
ans[i]。
算法原理与逻辑
1. 二进制提升法用于LCA查询
- 二进制提升表:
- 通过预处理二进制提升表
fa[x][i],可以在 O(logN) 时间内计算任意两个节点的LCA。 - 该方法通过逐步提升节点的祖先来快速对齐深度并找到公共祖先。
- 通过预处理二进制提升表
2. 玩家位置的统计与观察
- 玩家的移动路径:
- 玩家从起点
S_i出发,按照树的边逐步移动,每秒移动一条边,最终到达终点T_i。 - 玩家的位置在每一秒钟的变化可以通过玩家的路径和时间
Wj[x]来确定。
- 玩家从起点
- 观察时间
Wj[x]:- 每个节点
x在时间Wj[x]需要统计有多少玩家恰好位于该节点上。
- 每个节点
- 玩家分组与计数:
- 起点分组:通过
js[x]统计每个节点x作为起点的玩家数量。 - 终点分组:通过
e1数组将玩家按终点节点分组,便于在第二次DFS中快速访问。 - LCA分组:通过
e2数组将玩家按LCA节点分组,便于在第二次DFS中进行特殊处理。
- 起点分组:通过
3. 第二次DFS的作用
- 辅助计数数组
b1和b2:b1[d]:当前子树中,起点深度为d的玩家数量。b2[d]:当前子树中,路径长度为d的玩家数量。
- 答案计算:
- 对于每个节点
x,在时间Wj[x]时刻,位于节点x的玩家数量可以通过以下两部分统计:- 起点深度匹配:玩家起点深度为
w[x] + deep[x],即玩家在w[x]秒后到达节点x。 - 路径长度匹配:玩家路径长度为
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玩家列表,恢复
b1和b2的状态,确保其他分支的统计不受影响。
- 在处理完当前节点后,通过遍历LCA玩家列表,恢复
算法复杂度分析
- 预处理阶段(第一次DFS):
- 时间复杂度:O(NlogN),其中 N是节点数。
- 主要消耗在二进制提升表的构建上,每个节点最多计算 logN个祖先。
- 玩家信息处理:
- 时间复杂度:O(MlogN),其中 M 是玩家数。
- 每个玩家需要进行一次LCA查询,时间复杂度为 O(logN)。
- 答案计算阶段(第二次DFS):
- 时间复杂度:O(N+M)。
- 遍历每个节点一次,并处理与节点相关的玩家列表。
- 总体时间复杂度:O((N+M)logN),适用于大规模的数据范围。
总结
这段代码通过两次深度优先搜索(DFS)和二进制提升法(Binary Lifting)有效地计算了每个节点在特定时间 Wj 时刻被观察到的玩家数量。主要步骤包括:
- 树的构建与预处理:
- 使用邻接表表示树结构。
- 通过DFS计算每个节点的深度和二进制提升表,支持高效的LCA查询。
- 玩家信息处理:
- 计算每个玩家的LCA和路径长度。
- 将玩家按终点节点和LCA节点进行分组,便于在第二次DFS中快速访问和处理。
- 答案计算:
- 通过第二次DFS,维护辅助计数数组
b1和b2,统计满足观察条件的玩家数量。 - 结合起点深度和路径长度的匹配,准确计算每个节点被观察到的玩家数量。
- 通过第二次DFS,维护辅助计数数组
- 输出结果:
- 按节点编号顺序输出每个节点的观察结果。
代码实现:
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; } |
