题目描述
给定二维平面上 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 通过 3 号基站传讯至节点 3,延迟 1;
-
节点 3 通过 4 号基站传讯至节点 4,延迟 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
函数对这些点进行排序,排序规则是:
- 优先比较x坐标
- 当x坐标相同时,比较y坐标
由于基站覆盖范围是正方形区域,我们需要高效地找到位于同一个块内的所有点。具体实现步骤如下:
- 使用
lower_bound
二分查找定位到x坐标大于等于x0-r的第一个点 - 从这个位置开始顺序遍历,检查每个点是否满足:
- x坐标在[x0-r, x0+r]范围内
- y坐标在[y0-r, y0+r]范围内
- 符合条件的点存入结果集合
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];
}
为了优化算法的时间复杂度,我们需要减少图中的边数,通过引入中转点来重构图的连接方式。具体实现步骤如下:
-
中转点设计:
- 为每个块(block)创建一个专门的中转点(relay point)
- 该中转点将与块内的所有节点建立双向连接
- 中转点到块内各节点的边权设置为该节点的延迟时间
-
边重构:
- 将原有的直接边(如节点A→节点B)替换为:
- 节点A→块内中转点(边权=节点A延迟)
- 中转点→节点B(边权=节点B延迟)
- 这样原来的1条直接边就被拆分为2条通过中转点的边
- 将原有的直接边(如节点A→节点B)替换为:
-
节点编号方案:
- 原始节点保持原有编号(1到n)
- 中转点使用偏移后的编号(n+1到n+m,其中m为块数)
- 这样可以清晰地区分原始节点和中转点
-
最短路径计算:
- 在重构后的图上运行单源最短路算法(如Dijkstra)
- 从起点1到终点n的路径距离实际上是原路径的两倍
- 因此最终结果需要将计算得到的距离除以2
-
复杂度分析:
- 原图中每个块内最多有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;
}