CCF-CSP第35次认证第四题——通讯延迟(满分题解)

题目描述

给定二维平面上 n 个节点,以及 m 个通讯基站。第 ii 个基站可以覆盖以坐标 (xi​,yi​) 为中心、2ri​ 为边长的正方形区域,并使正方形区域内(包含边界)所有节点以 ti​ 单位时间的延迟进行相互通讯。

求节点 1 到 n 的最短通讯延迟。

输入格式

从标准输入读入数据。

第一行包含空格分隔的两个正整数 n、m;

接下来 n 行,每行两个整数 xi​,yi​,代表第 i 个节点的坐标;

接下来 m 行,每行四个整数 xj​,yj​,rj​,tj​,代表第 j 个通讯基站的坐标,通讯半径与通讯延迟。

输出格式

输出到标准输出。

输出一行,即节点 1 到 n 的最短通讯延迟;如果无法通讯,则输出 Nan

样例输入

5 5
0 0
2 4
4 0
5 3
5 5
1 2 2 5
3 5 2 6
2 0 2 1
4 2 2 3
5 4 1 2

样例输出

6

样例解释

1 号通讯基站延迟为 5,覆盖节点 1、2;

2 号通讯基站延迟为 6,覆盖节点 2、4、5;

3 号通讯基站延迟为 1,覆盖节点 1、3;

4 号通讯基站延迟为 3,覆盖节点 2、3、4;

5 号通讯基站延迟为 2,覆盖节点 4、5。

最短延迟方案为:

  1. 节点 1 通过 3 号基站传讯至节点 3,延迟 1;

  2. 节点 3 通过 4 号基站传讯至节点 4,延迟 3;

  3. 节点 4 通过 5 号基站传讯至节点 5,延迟 2;

总计延迟为 6。

子任务

30 的测试数据满足 n,m≤100;

对于额外 30 的测试数据,每个通讯基站至多覆盖 20 个节点;

全部的测试数据满足 n,m≤5000 且 0≤xi​,yi​,ri​≤109、1≤ti​≤105。

题解

这道题目是一个典型的单源最短路问题,即求解从起点1到终点n的最短路径。这类问题通常采用Dijkstra算法或Bellman-Ford算法等进行求解。在实际应用中,最关键的是如何将题目给定的条件和约束抽象为一张合适的有向图,并设计合理的图结构,使得图中的顶点数和边数尽可能少。

我最开始的想法很简单:(60分思路)

建立一个map<ll,pair<ll,ll>>数据结构,用于将二维平面上的点映射为唯一的整数标号。由于标准库中没有为pair类型提供默认的哈希函数,我们需要自定义哈希函数。可以采用以下方式实现:

struct pair_hash {
    template <class T1, class T2>
    std::size_t operator() (const std::pair<T1, T2> &p) const {
        auto h1 = std::hash<T1>{}(p.first);
        auto h2 = std::hash<T2>{}(p.second);
        return h1 ^ (h2 << 1);
    }
};

std::unordered_map<std::pair<ll, ll>, ll, pair_hash> point_to_id;

首先将所有基站的位置信息存储在pair中,其中ll表示坐标类型(如long long)。然后使用sort函数对这些点进行排序,排序规则是:

  1. 优先比较x坐标
  2. 当x坐标相同时,比较y坐标

由于基站覆盖范围是正方形区域,我们需要高效地找到位于同一个块内的所有点。具体实现步骤如下:

  1. 使用lower_bound二分查找定位到x坐标大于等于x0-r的第一个点
  2. 从这个位置开始顺序遍历,检查每个点是否满足:
    • x坐标在[x0-r, x0+r]范围内
    • y坐标在[y0-r, y0+r]范围内
  3. 符合条件的点存入结果集合ans

对于同一个块内的节点,我们需要建立两两之间的连接关系。实现方式是取出该块内所有点,然后进行双重循环遍历:

for(int i = 0; i < block_points.size(); ++i) {
    for(int j = i+1; j < block_points.size(); ++j) {
        add_edge(block_points[i], block_points[j]);
    }
}

这种方法的时间复杂度为O(n²),其中n是块内点的数量。当块内点较多时(如n=1e5),会产生约1e10条边,显然会超出时间限制,因此这种实现方式只能获得60%的分数。更高效的算法需要考虑使用空间分割或其他优化策略来减少边的数量。

代码如下(60分):

#include<bits/stdc++.h>

using namespace std;

typedef long long ll;
typedef pair<ll,ll> PII;

const int N=5010,M=5e7;
ll n,m,cnt;
ll idx,h[N],e[M],ne[M];
ll w[M],d[N];
bool st[N];
vector<pair<ll,ll>>pos;

struct hash_pair{
    template<class T1,class T2>
    size_t operator()(const pair<T1,T2>&p)const{
        return hash<T1>()(p.first)^(hash<T2>()(p.second)<<1);
    }
};
unordered_map<pair<ll,ll>,ll,hash_pair>mp;

void add(ll x,ll y,ll z){
    e[idx]=y,w[idx]=z,ne[idx]=h[x],h[x]=idx++;
}

void dijkstra(ll x){
    priority_queue<PII,vector<PII>,greater<PII>>q;
    memset(d,0x3f,sizeof d);
    q.push({0,x});
    d[x]=0;
    while(!q.empty()){
        auto t=q.top();
        q.pop();
        ll ver=t.second;
        //cout<<ver<<' ';
        if(st[ver])continue;
        st[ver]=true;
        for(ll i=h[ver];~i;i=ne[i]){
            ll j=e[i];
            if(d[j]>d[ver]+w[i]){
                d[j]=d[ver]+w[i];
                q.push({d[j],j});
            }
        }
    }
}

int main(){
    cin>>n>>m;
    memset(h,-1,sizeof h);
    for(ll i=1;i<=n;i++){
        ll x,y;
        cin>>x>>y;
        pos.push_back({x,y});
        mp[{x,y}]=i;
    }
    sort(pos.begin(),pos.end());
    for(ll i=1;i<=m;i++){
        ll x,y,r,t;
        cin>>x>>y>>r>>t;
        vector<ll>temp;
        auto it=lower_bound(pos.begin(),pos.end(),make_pair(x-r,y-r));
        while(it!=pos.end()&&it->first<=x+r){
            if(it->second<=y+r&&it->second>=y-r)temp.push_back(mp[{it->first,it->second}]);
            //cout<<it->first<<' '<<it->second<<endl;
            it++;
        }
        for(ll j=0;j<temp.size();j++)
            for(ll k=0;k<j;k++){
                add(temp[j],temp[k],t),add(temp[k],temp[j],t);
                //cout<<temp[i]<<' '<<temp[j]<<endl;
            }
    }
    dijkstra(1);
    if(d[n]>=0x3f3f3f3f)cout<<"Nan";
    else cout<<d[n];
}

为了优化算法的时间复杂度,我们需要减少图中的边数,通过引入中转点来重构图的连接方式。具体实现步骤如下:

  1. 中转点设计

    • 为每个块(block)创建一个专门的中转点(relay point)
    • 该中转点将与块内的所有节点建立双向连接
    • 中转点到块内各节点的边权设置为该节点的延迟时间
  2. 边重构

    • 将原有的直接边(如节点A→节点B)替换为:
      1. 节点A→块内中转点(边权=节点A延迟)
      2. 中转点→节点B(边权=节点B延迟)
    • 这样原来的1条直接边就被拆分为2条通过中转点的边
  3. 节点编号方案

    • 原始节点保持原有编号(1到n)
    • 中转点使用偏移后的编号(n+1到n+m,其中m为块数)
    • 这样可以清晰地区分原始节点和中转点
  4. 最短路径计算

    • 在重构后的图上运行单源最短路算法(如Dijkstra)
    • 从起点1到终点n的路径距离实际上是原路径的两倍
    • 因此最终结果需要将计算得到的距离除以2
  5. 复杂度分析

    • 原图中每个块内最多有k个节点
    • 通过这种方式,将一个块内潜在的O(k²)条边减少为O(k)条边
    • 有效降低了图的整体边数

示例: 假设有一个块包含节点A、B、C,延迟分别为1、2、3

  • 原连接方式:A-B,A-C,B-C(3条边)
  • 新连接方式:
    • 创建中转点R
    • A-R(1),B-R(2),C-R(3)
    • 实际路径比如A→B变为A→R→B,距离=1+2=3(原直接距离需要×2)

这种优化方法特别适用于块内连接密集的图结构,可以显著减少边数,同时保持最短路径计算的准确性。

代码

有注释代码:

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef pair<ll, ll> PII;

// 一些全局常量
const int N = 20010;   // 最大节点数(n 个城市 + m 个信号塔)
const int M = 5e7;     // 最大边数(邻接表存图用)

// ===================== 图的存储 =====================
ll idx;                // 当前使用的边下标
ll h[N];               // 邻接表头
ll e[M], ne[M], w[M];  // e: 终点;ne: 下一条边;w: 边权

// ===================== Dijkstra 用 =====================
ll d[N];               // 单源最短路距离
bool st[N];            // 标记是否已确定最短路

// ===================== 题意相关 =====================
ll n, m;               // n 个城市,m 个信号塔
vector<PII> pos;       // 每个城市的坐标 (x, y)

// 向邻接表加一条有向边 (x -> y,权值 z)
void add(ll x, ll y, ll z) {
    e[idx] = y;
    w[idx] = z;
    ne[idx] = h[x];
    h[x] = idx++;
}

// 朴素的 Dijkstra,起点为 x
void dijkstra(ll x) {
    priority_queue<PII, vector<PII>, greater<PII>> q;
    memset(d, 0x3f, sizeof d);

    d[x] = 0;
    q.push({0, x});

    while (!q.empty()) {
        auto t = q.top();
        q.pop();
        ll ver = t.second;

        if (st[ver]) continue;
        st[ver] = true;

        for (ll i = h[ver]; ~i; i = ne[i]) {
            ll j = e[i];
            if (d[j] > d[ver] + w[i]) {
                d[j] = d[ver] + w[i];
                q.push({d[j], j});
            }
        }
    }
}

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

    cin >> n >> m;

    // 初始化邻接表头为 -1(空)
    memset(h, -1, sizeof h);

    // 读入 n 个城市的坐标
    for (ll i = 1; i <= n; i++) {
        ll x, y;
        cin >> x >> y;
        pos.push_back({x, y});   // 下标从 0 开始存
    }

    // 读入 m 个信号塔
    for (ll i = 1; i <= m; i++) {
        ll x, y, r, t;
        cin >> x >> y >> r >> t; // (x, y) 为中心,r 为边长,t 为权值

        // 枚举所有城市,判断是否在信号塔覆盖的「曼哈顿距离」范围内
        // 注意:这里用的是 abs(dx) <= r && abs(dy) <= r,等价于
        // 城市坐标 (cx, cy) 满足 x-r <= cx <= x+r 且 y-r <= cy <= y+r
        for (ll j = 0; j < n; j++) {
            if (abs(pos[j].first - x) <= r && abs(pos[j].second - y) <= r) {
                // 城市 j+1 与 信号塔 i+n 之间建双向边,权值为 t
                add(j + 1, i + n, t);
                add(i + n, j + 1, t);
            }
        }
    }

    // 从 1 号城市出发跑最短路
    dijkstra(1);

    // 如果到 n 号城市不可达输出 "Nan";否则输出 d[n]/2
    // 注意:这里除以 2 是因为建图时把双向边都存了一次,
    // 但每对双向边其实是同一条物理链路,权值被重复计算了一次。
    if (d[n] >= 0x3f3f3f3f3f3f3f3fLL)   // 注意 long long 的 0x3f3f3f3f3f3f3f3f
        cout << "Nan";
    else
        cout << d[n] / 2;
    return 0;
}

无注释代码

#include<bits/stdc++.h>

using namespace std;

typedef long long ll;
typedef pair<ll,ll> PII;

const int N=20010,M=5e7;
ll n,m,cnt;
ll idx,h[N],e[M],ne[M];
ll w[M],d[N];
bool st[N];
vector<pair<ll,ll>>pos;

void add(ll x,ll y,ll z){
    e[idx]=y,w[idx]=z,ne[idx]=h[x],h[x]=idx++;
}

void dijkstra(ll x){
    priority_queue<PII,vector<PII>,greater<PII>>q;
    memset(d,0x3f,sizeof d);
    q.push({0,x});
    d[x]=0;
    while(!q.empty()){
        auto t=q.top();
        q.pop();
        ll ver=t.second;
        if(st[ver])continue;
        st[ver]=true;
        for(ll i=h[ver];~i;i=ne[i]){
            ll j=e[i];
            if(d[j]>d[ver]+w[i]){
                d[j]=d[ver]+w[i];
                q.push({d[j],j});
            }
        }
    }
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>m;
    memset(h,-1,sizeof h);
    for(ll i=1;i<=n;i++){
        ll x,y;
        cin>>x>>y;
        pos.push_back({x,y});
    }
    for(ll i=1;i<=m;i++){
        ll x,y,r,t;
        cin>>x>>y>>r>>t;
        for(int j=0;j<n;j++){
            if(abs(pos[j].first-x)<=r&&abs(pos[j].second-y)<=r){
                add(j+1,i+n,t);
                add(i+n,j+1,t);
            }
        }
    }
    dijkstra(1);
    if(d[n]>=0x3f3f3f3f)cout<<"Nan";
    else cout<<d[n]/2;
}
目《202305-5 闪耀巡航》是CCF CSP(中国大学生程序设计竞赛)的一个挑战目,它通常涉及算法数据结构和数学思维。不过,由于你提到的是Python和C++满分题解,这说明我们需要考虑如何高效地使用这两种语言解决此问。 闪耀巡航的问描述一般涉及到路径优化、动态规划或者是图论中的最短路径问。你需要帮助一个机器人在网格上找到从起点到终点的最优路线,可能会有光照限制或其他规则影响行进路径。 在Python中,可以利用Pandas处理二维数组,然后使用深度优先搜索(DFS)、广度优先搜索(BFS),或者更复杂一些,如Bellman-Ford算法来求解最短路径。记得检查边界条件和可能存在的循环引用问C++方面,可以使用STL中的vector和list等容器,结合dfs函数或者Dijkstra算法(如果允许的话)。C++的迭代器和模板元编程能提高代码效率,但需要对数据结构算法有深入理解。 以下是简化的步骤: 1. **分析问**:明确输入格式,理解光照、障碍物等因素的影响。 2. **数据结构**:用二维数组表示地图,定义状态转移方程。 3. **选择策略**:如果是简单的遍历,用DFS;复杂一点的情况,考虑动态规划或者图搜索算法。 4. **编写代码**:递归或迭代实现算法,注意优化边界处理和空间复杂度。 5. **测试**:用样例数据验证算法是否正确,并尝试多种情况验证其鲁棒性。 至于具体的代码,这里给出一个大致框架,但因为竞赛目细节未提供,无法给出完整的解答: ```cpp #include <iostream> using namespace std; // 用二维数组表示地图,值代表路径长度 int map[grid_size][grid_size]; // 动态规划或搜索函数 int shortestPath(int startRow, int startCol) { // 代码实现... } int main() { // 输入读取、初始化地图 // 调用shortestPath函数并打印结果 cout << shortestPath(0, 0) << endl; return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值