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

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⁵的数据范围
关键优化点
  1. 状态压缩:将连续未选中节点数作为状态,最多3种状态,保证算法高效
  2. 矩阵倍增:将路径上的状态转移用矩阵乘法(min-plus半环)表示,通过倍增快速合并
  3. 预处理邻居最小权值:用于在需要额外选点时快速获取最小代价
注意事项
  1. 根节点的处理:根节点没有父节点,其转移矩阵在dfs中会特殊处理
  2. 初始状态:查询开始时,起点必须被选中,初始状态为0,代价为起点的权值
  3. 合并时的特殊情况:当两边在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;
}
相关推荐
hetao17338372 小时前
2026-01-29~02-03 hetao1733837 的刷题记录
c++·笔记·算法
晚风吹长发2 小时前
初步了解Linux中的POSIX信号量及环形队列的CP模型
linux·运维·服务器·数据结构·c++·算法
近津薪荼2 小时前
优选算法——前缀和(1):一维前缀和
c++·学习·算法
草莓熊Lotso4 小时前
Linux 基础 IO 初步解析:从 C 库函数到系统调用,理解文件操作本质
linux·运维·服务器·c语言·数据库·c++·人工智能
闻缺陷则喜何志丹4 小时前
P8699 [蓝桥杯 2019 国 B] 排列数|普及+
c++·数学·蓝桥杯·数论·洛谷·数列
D_evil__10 小时前
【Effective Modern C++】第三章 转向现代C++:16. 让const成员函数线程安全
c++
Queenie_Charlie11 小时前
前缀和的前缀和
数据结构·c++·树状数组
kokunka12 小时前
【源码+注释】纯C++小游戏开发之射击小球游戏
开发语言·c++·游戏
John_ToDebug14 小时前
浏览器内核崩溃深度分析:从 MiniDump 堆栈到 BindOnce UAF 机制(未完待续...)
c++·chrome·windows