2022信奥赛C++提高组csp-s复赛真题及题解:数据传输

题目描述
小 C 正在设计计算机网络中的路由系统。
测试用的网络总共有 n n n 台主机,依次编号为 1 ∼ n 1 \sim n 1∼n。这 n n n 台主机之间由 n − 1 n - 1 n−1 根网线连接,第 i i i 条网线连接两个主机 a i a_i ai 和 b i b_i bi。保证任意两台主机可以通过有限根网线直接或者间接地相连。受制于信息发送的功率,主机 a a a 能够直接将信息传输给主机 b b b 当且仅当两个主机在可以通过不超过 k k k 根网线直接或者间接的相连。
在计算机网络中,数据的传输往往需要通过若干次转发。假定小 C 需要将数据从主机 a a a 传输到主机 b b b( a ≠ b a \neq b a=b),则其会选择出若干台用于传输的主机 c 1 = a , c 2 , ... , c m − 1 , c m = b c_1 = a, c_2, \ldots, c_{m - 1}, c_m = b c1=a,c2,...,cm−1,cm=b,并按照如下规则转发:对于所有的 1 ≤ i < m 1 \le i < m 1≤i<m,主机 c i c_i ci 将信息直接发送给 c i + 1 c_{i + 1} ci+1。
每台主机处理信息都需要一定的时间,第 i i i 台主机处理信息需要 v i v_i vi 单位的时间。数据在网络中的传输非常迅速,因此传输的时间可以忽略不计。据此,上述传输过程花费的时间为 ∑ i = 1 m v c i \sum_{i = 1}^{m} v_{c_i} ∑i=1mvci。
现在总共有 q q q 次数据发送请求,第 i i i 次请求会从主机 s i s_i si 发送数据到主机 t i t_i ti。小 C 想要知道,对于每一次请求至少需要花费多少单位时间才能完成传输。
输入格式
输入的第一行包含三个正整数 n , Q , k n, Q, k n,Q,k,分别表示网络主机个数,请求个数,传输参数。数据保证 1 ≤ n ≤ 2 × 10 5 1 \le n \le 2 \times {10}^5 1≤n≤2×105, 1 ≤ Q ≤ 2 × 10 5 1 \le Q \le 2 \times {10}^5 1≤Q≤2×105, 1 ≤ k ≤ 3 1 \le k \le 3 1≤k≤3。
输入的第二行包含 n n n 个正整数,第 i i i 个正整数表示 v i v_i vi,保证 1 ≤ v i ≤ 10 9 1 \le v_i \le {10}^9 1≤vi≤109。
接下来 n − 1 n - 1 n−1 行,第 i i i 行包含两个正整数 a i , b i a_i, b_i ai,bi,表示一条连接主机 a i , b i a_i, b_i ai,bi 的网线。保证 1 ≤ a i , b i ≤ n 1 \le a_i, b_i \le n 1≤ai,bi≤n。
接下来 Q Q Q 行,第 i i i 行包含两个正整数 s i , t i s_i, t_i si,ti,表示一次从主机 s i s_i si 发送数据到主机 t i t_i ti 的请求。保证 1 ≤ s i , t i ≤ n 1 \le s_i, t_i \le n 1≤si,ti≤n, s i ≠ t i s_i \ne t_i si=ti。
输出格式
Q Q Q 行,每行一个正整数,表示第 i i i 次请求在传输的时候至少需要花费多少单位的时间。
输入输出样例 1
输入 1
7 3 3
1 2 3 4 5 6 7
1 2
1 3
2 4
2 5
3 6
3 7
4 7
5 6
1 2
输出 1
12
12
3
说明/提示
【样例解释 #1】
对于第一组请求,由于主机 4 , 7 4, 7 4,7 之间需要至少 4 4 4 根网线才能连接,因此数据无法在两台主机之间直接传输,其至少需要一次转发;我们让其在主机 1 1 1 进行一次转发,不难发现主机 1 1 1 和主机 4 , 7 4, 7 4,7 之间都只需要两根网线即可连接,且主机 1 1 1 的数据处理时间仅为 1 1 1,为所有主机中最小,因此最少传输的时间为 4 + 1 + 7 = 12 4 + 1 + 7 = 12 4+1+7=12。
对于第三组请求,由于主机 1 , 2 1, 2 1,2 之间只需要 1 1 1 根网线就能连接,因此数据直接传输就是最优解,最少传输的时间为 1 + 2 = 3 1 + 2 = 3 1+2=3。
【数据范围】
对于所有的测试数据,满足 1 ≤ n ≤ 2 × 10 5 1 \le n \le 2 \times {10}^5 1≤n≤2×105, 1 ≤ Q ≤ 2 × 10 5 1 \le Q \le 2 \times {10}^5 1≤Q≤2×105, 1 ≤ k ≤ 3 1 \le k \le 3 1≤k≤3, 1 ≤ a i , b i ≤ n 1 \le a_i, b_i \le n 1≤ai,bi≤n, 1 ≤ s i , t i ≤ n 1 \le s_i, t_i \le n 1≤si,ti≤n, s i ≠ t i s_i \ne t_i si=ti。
| 测试点 | n ≤ n \le n≤ | Q ≤ Q \le Q≤ | k = k = k= | 特殊性质 |
|---|---|---|---|---|
| 1 1 1 | 10 10 10 | 10 10 10 | 2 2 2 | 是 |
| 2 2 2 | 10 10 10 | 10 10 10 | 3 3 3 | 是 |
| 3 3 3 | 200 200 200 | 200 200 200 | 2 2 2 | 是 |
| 4 ∼ 5 4 \sim 5 4∼5 | 200 200 200 | 200 200 200 | 3 3 3 | 是 |
| 6 ∼ 7 6 \sim 7 6∼7 | 2000 2000 2000 | 2000 2000 2000 | 1 1 1 | 否 |
| 8 ∼ 9 8 \sim 9 8∼9 | 2000 2000 2000 | 2000 2000 2000 | 2 2 2 | 否 |
| 10 ∼ 11 10 \sim 11 10∼11 | 2000 2000 2000 | 2000 2000 2000 | 3 3 3 | 否 |
| 12 ∼ 13 12 \sim 13 12∼13 | 2 × 10 5 2 \times {10}^5 2×105 | 2 × 10 5 2 \times {10}^5 2×105 | 1 1 1 | 否 |
| 14 14 14 | 5 × 10 4 5 \times {10}^4 5×104 | 5 × 10 4 5 \times {10}^4 5×104 | 2 2 2 | 是 |
| 15 ∼ 16 15 \sim 16 15∼16 | 10 5 {10}^5 105 | 10 5 {10}^5 105 | 2 2 2 | 是 |
| 17 ∼ 19 17 \sim 19 17∼19 | 2 × 10 5 2 \times {10}^5 2×105 | 2 × 10 5 2 \times {10}^5 2×105 | 2 2 2 | 否 |
| 20 20 20 | 5 × 10 4 5 \times {10}^4 5×104 | 5 × 10 4 5 \times {10}^4 5×104 | 3 3 3 | 是 |
| 21 ∼ 22 21 \sim 22 21∼22 | 10 5 {10}^5 105 | 10 5 {10}^5 105 | 3 3 3 | 是 |
| 23 ∼ 25 23 \sim 25 23∼25 | 2 × 10 5 2 \times {10}^5 2×105 | 2 × 10 5 2 \times {10}^5 2×105 | 3 3 3 | 否 |
特殊性质:保证 a i = i + 1 a_i = i + 1 ai=i+1,而 b i b_i bi 则从 1 , 2 , ... , i 1, 2, \ldots, i 1,2,...,i 中等概率选取。
思路分析
问题理解
将问题转化为基于状态机的动态规划问题。定义状态x表示从上一个被选中的节点到当前节点已经连续跳过了x个节点(即连续未选中节点数)。由于k≤3,状态数只有0、1、2三种可能。
数据结构设计
a[]:存储每个节点的权值m[]:存储每个节点及其邻居中的最小权值,用于在需要"中转"时快速获取最小代价g[]:邻接表存储树结构fa[][]:倍增祖先数组d[]:节点深度w[][][][]:四维数组,存储倍增转移矩阵
核心函数功能
1. dfs(int u, int p):深度优先搜索预处理
- 计算节点深度和倍增祖先
- 初始化转移矩阵
w[0][u],根据k的不同设置不同的转移规则 - 通过倍增合并计算高维转移矩阵
2. lca(int x, int y):求最近公共祖先
- 使用标准倍增LCA算法
- 先调整到同一深度,然后同时向上跳
3. get_states(int u, int l):获取从u到l的状态数组
- 从节点u向上跳到节点l
- 使用倍增合并转移矩阵,得到从起点u到终点l的各种状态的最小代价
4. solve(int s, int t):处理查询
- 求s和t的LCA
- 分别计算s到LCA和t到LCA的状态数组
- 合并两个状态数组,考虑LCA是否被选中,以及是否需要额外选点中转
状态转移规则
k=1(T=1):
- 状态0:当前节点被选中
- 只能从状态0转移到状态0,代价为父节点的权值
k=2(T=2):
- 状态0:当前节点被选中
- 状态1:连续1个节点未选中
- 转移规则:
- 从状态0可以选父节点(到状态0)或不选父节点(到状态1)
- 从状态1必须选父节点(否则会连续2个未选中)
k=3(T=3):
- 状态0:当前节点被选中
- 状态1:连续1个节点未选中
- 状态2:连续2个节点未选中
- 转移规则更复杂,包含特殊转移:从未选2个状态可以通过选择邻居来重置状态
时间复杂度分析
- 预处理:O(n log n * k³) ≈ O(n log n)
- 单次查询:O(log n * k²) ≈ O(log n)
- 总体复杂度:O((n+q) log n),可以高效处理n,q≤2×10⁵的数据范围
关键优化点
- 状态压缩:将连续未选中节点数作为状态,最多3种状态,保证算法高效
- 矩阵倍增:将路径上的状态转移用矩阵乘法(min-plus半环)表示,通过倍增快速合并
- 预处理邻居最小权值:用于在需要额外选点时快速获取最小代价
注意事项
- 根节点的处理:根节点没有父节点,其转移矩阵在dfs中会特殊处理
- 初始状态:查询开始时,起点必须被选中,初始状态为0,代价为起点的权值
- 合并时的特殊情况:当两边在LCA处的状态和超过k时,需要在LCA的邻居中额外选一个点作为"中转"
代码实现
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e5 + 10, B = 20, K = 3; // B: 倍增深度, K: 最大状态数
const ll INF = 1e18;
int n, q, T; // T: 传输参数k
int a[N], m[N]; // a: 点权, m: 邻居最小点权(包括自己)
vector<int> g[N]; // 邻接表
int fa[B][N], d[N]; // fa: 倍增祖先, d: 深度
ll w[B][N][K][K]; // w[i][u][x][y]: 从u向上跳2^i步, 状态x到y的最小花费
// 更新最小值
void upd(ll& x, ll y) { x = min(x, y); }
void dfs(int u, int p) {
d[u] = d[p] + 1;
// 初始化倍增祖先数组
fa[0][u] = p;
for (int i = 1; i < B; i++) fa[i][u] = fa[i-1][fa[i-1][u]];
auto D = w[0][u]; // D: w[0][u]的别名, 便于操作
// 根据T(即k)的不同, 初始化转移矩阵
if (T == 1) {
// 状态0表示当前点已选, 只能转移到状态0, 花费为父节点权值
D[0][0] = a[p];
} else if (T == 2) {
// 状态0: 已选; 状态1: 未选1个
// 从状态0: 可以选父节点(到0)或跳过父节点(到1)
D[0][0] = a[p]; // 选父节点
D[0][1] = 0; // 不选父节点
D[1][0] = a[p]; // 从未选1个状态, 必须选父节点(否则会连续2个未选)
} else { // T == 3
// 状态0: 已选; 状态1: 未选1个; 状态2: 未选2个
// 从状态0: 可以选父节点(到0)或跳过父节点(到1)
D[0][0] = a[p]; // 选父节点
D[0][1] = 0; // 不选父节点
// 从状态1: 必须选父节点(到0), 否则会连续3个未选
D[1][0] = a[p];
// 从状态2: 必须选父节点(到0), 否则会连续3个未选
D[2][0] = a[p];
// 特殊转移: 从未选2个状态, 可以通过选择u的邻居(花费m[u])来重置
D[2][2] = m[u];
// 注意: 从状态1还可以选择不选父节点, 但会变成连续2个未选
D[1][2] = 0;
}
// 倍增合并转移矩阵
for (int i = 1; i < B; i++) {
for (int x = 0; x < K; x++) {
for (int y = 0; y < K; y++) {
w[i][u][x][y] = INF; // 初始化为无穷大
}
}
int t = fa[i-1][u]; // 中间祖先
for (int x = 0; x < K; x++) {
for (int y = 0; y < K; y++) {
if (w[i-1][u][x][y] >= INF) continue;
for (int z = 0; z < K; z++) {
upd(w[i][u][x][z], w[i-1][u][x][y] + w[i-1][t][y][z]);
}
}
}
}
// 递归处理子节点
for (int v : g[u]) {
if (v != p) dfs(v, u);
}
}
// 求LCA
int lca(int x, int y) {
if (d[x] < d[y]) swap(x, y);
for (int i = B-1; i >= 0; i--) {
if (d[fa[i][x]] >= d[y]) x = fa[i][x];
}
if (x == y) return x;
for (int i = B-1; i >= 0; i--) {
if (fa[i][x] != fa[i][y]) x = fa[i][x], y = fa[i][y];
}
return fa[0][x];
}
// 计算从u到l的状态数组
vector<ll> get_states(int u, int l) {
vector<ll> res(K, INF);
res[0] = a[u]; // 初始状态: 在u点必须选u
for (int i = B-1; i >= 0; i--) {
if (d[fa[i][u]] < d[l]) continue; // 不能跳过l
vector<ll> nxt(K, INF);
// 合并当前状态和转移矩阵
for (int x = 0; x < K; x++) {
if (res[x] >= INF) continue;
for (int y = 0; y < K; y++) {
upd(nxt[y], res[x] + w[i][u][x][y]);
}
}
res = nxt;
u = fa[i][u];
}
return res;
}
// 计算s到t的最小花费
ll solve(int s, int t) {
int l = lca(s, t);
auto F = get_states(s, l); // s到l的状态数组
auto G = get_states(t, l); // t到l的状态数组
ll ans = INF;
// 情况1: lca被两边都选中了
upd(ans, F[0] + G[0] - a[l]); // 减去重复计算的lca点权
// 情况2: 考虑lca可能没有被选中, 需要额外处理
for (int i = 0; i < T; i++) {
for (int j = 0; j < T; j++) {
// 如果两边连续未选的点数之和超过k, 需要在lca的邻居中选一个点来"中转"
if (i + j > T) {
upd(ans, F[i] + G[j] + m[l]);
} else {
upd(ans, F[i] + G[j]);
}
}
}
return ans;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> q >> T;
for (int i = 1; i <= n; i++) {
cin >> a[i];
m[i] = a[i]; // 初始化m[i]为自己的权值
}
// 读入边, 同时更新m数组
for (int i = 1; i < n; i++) {
int x, y;
cin >> x >> y;
g[x].push_back(y);
g[y].push_back(x);
m[x] = min(m[x], a[y]);
m[y] = min(m[y], a[x]);
}
// 初始化w数组为INF
memset(w, 0x3f, sizeof(w));
dfs(1, 0);
while (q--) {
int s, t;
cin >> s >> t;
cout << solve(s, t) << '\n';
}
return 0;
}
专栏推荐:信奥赛C++提高组csp-s初赛&复赛真题题解(持续更新)
https://blog.csdn.net/weixin_66461496/category_13125089.html
各种学习资料,助力大家一站式学习和提升!!!
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"########## 一站式掌握信奥赛知识! ##########";
cout<<"############# 冲刺信奥赛拿奖! #############";
cout<<"###### 课程购买后永久学习,不受限制! ######";
return 0;
}
1、csp信奥赛高频考点知识详解及案例实践:
CSP信奥赛C++动态规划:
https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转
CSP信奥赛C++标准模板库STL:
https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转
信奥赛C++提高组csp-s知识详解及案例实践:
https://blog.csdn.net/weixin_66461496/category_13113932.html
2、csp信奥赛冲刺一等奖有效刷题题解:
CSP信奥赛C++初赛及复赛高频考点真题解析(持续更新):https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转
CSP信奥赛C++一等奖通关刷题题单及题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12673810.html 点击跳转
3、GESP C++考级真题题解:

GESP(C++ 一级+二级+三级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转

GESP(C++ 四级+五级+六级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转

GESP(C++ 七级+八级)真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13117178.html
4、CSP信奥赛C++竞赛拿奖视频课:
https://edu.csdn.net/course/detail/40437 点击跳转

· 文末祝福 ·
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"跟着王老师一起学习信奥赛C++";
cout<<" 成就更好的自己! ";
cout<<" csp信奥赛一等奖属于你! ";
return 0;
}