luogu Journeys (P6348)

\(\textsf{P6348 Journeys}\)

有 \(n\) 个点,\(m\) 次连边。每次连边给定 \(a,b,c,d\),将 \([a,b]\) 内所有的点向 \([c,d]\) 内所有的点连边,边权为 \(1\)。

求主城市 \(P\) 到每一点的最短路径长度,保证图连通。

\(m \le 10^5, n \le 5 \times 10^5\)。

因为暴力维护每条边,复杂度为 \(O(n^2 m)\),空间也会炸。

所以这题使用线段树优化建图。

线段树优化建图

维护两棵线段树。分别占用 \([1,4n]\) 与 \([4n+1,8n]\) 两个区间。

  • 一棵从父节点向子节点连边权为 \(0\) 的边(\(\texttt{p}\) 向 \(\texttt{ls}\) 与 \(\texttt{rs}\) 分别连边)
  • 一棵从子节点向父节点连边权为 \(0\) 的边(\(\texttt{ls+4n}\) 与 \(\texttt{rs+4n}\) 向 \(\texttt{p+4n}\) 连边)

叶子节点分别对应图上的每个点。所以对于每一个叶子节点 \(\texttt{p}\),都要与 \(\texttt{p+4n}\) 连边,边权为 \(0\)。

接下来考虑连边。

方法 $1$

首先我们想到,在第一棵线段树上跑 update 时,对于每一个最终得到的区间,都向第二棵连边权为 \(1\) 边。因为区间数最多差不多 \(\log n\),所以 \(m\) 次连边的时间复杂度为 \(O(m \log^2 n)\)。

我让 deepseek 帮我算了一下:

\[10^5 \times \log^2 (5 \times 10^5) \approx 3.584 \times 10^7 \]

即 \(3.584 \times 10^7\) 条边。

然后你求 Dijkstra 的时候你就炸了。显然需要优化。

于是我们牺牲一点空间。

方法 $2$

建立一个新的虚拟节点,将两棵线段树的区间均连过去。

其中,第一棵线段树连向虚拟节点的边权为 \(1\),第二棵线段树为 \(0\)。这样明显是正确的。

所以即可在 \(O(m \log n)\) 的时间内进行 \(m\) 次连边,空间也是 \(O(m \log n)\) 的。

于是我又借助了 deepseek 帮我算了一下 Dijkstra 的时间。发现是 \(O((n+m) \log^2 n)\) 的。在本题 \(\textsf{2.00 s}\) 的时限下,显然可以过。

最大的点在 \(\textsf{1.18 s}\)。

于是用 update1 与 update2 分别把两个线段树在访问区间时连边。

这时我们发现:建图后的线段树+虚拟节点是可以直接跑 Dijkstra 的。

然后就没了。

\(\textsf{code}\) 代码实现

想看就看

用 T2 表示第二棵线段树的偏移量,conpl 表示虚拟点的偏移量。

untree[i] 表示原图上的点 \(i\) 在第一棵线段树上对应的点。

建线段树(图):

#define T2 (n*4)
#define conpl (n*8)

build(1, 1, n);
for(int i = 1; i <= n; i++){
    edge[untree[i]+(T2)].emplace_back(untree[i], 0);
    edge[untree[i]].emplace_back(untree[i]+(T2), 0);
}
void build(int p, int l, int r){
    if(l == r){
        untree[l] = p;
        return;
    }
    edge[p].emplace_back(lp(p), 0);
    edge[p].emplace_back(rp(p), 0);
    edge[lp(p)+(T2)].emplace_back(p+(T2), 0);
    edge[rp(p)+(T2)].emplace_back(p+(T2), 0);
    
    int mid = (l + r) >> 1;
    build( lp(p), l, mid );
    build( rp(p), mid+1, r );
}

每一个虚拟节点编号为 conpl+tot。tot 表示第几个虚拟节点。

for(int i = 1, l1, r1, l2, r2; i <= m; i++){
    cin >> l1 >> r1 >> l2 >> r2;
    tot++;
    update1(1, l1, r1, 1, n);
    update2(1, l2, r2, 1, n);
    tot++;
    update2(1, l1, r1, 1, n);
    update1(1, l2, r2, 1, n);
}

update 就不放了,就是在返回时与虚拟节点连边就行了。

注意连边的方向。

  • update1:
edge[p+(T2)].emplace_back((conpl)+tot, 1);
  • update2:
edge[(conpl)+tot].emplace_back(p, 0);
$\green{\textsf{AC}}$ 代码
#include<bits/stdc++.h>

using namespace std;

const int N = 5e5+10;
struct Edge {
    int to, w;
    Edge(int to, int w): to(to), w(w) {}
};
struct pq {
    int dis, u;
    bool operator>(const pq &A) const {
        return dis > A.dis;
    }
};
int n, m, root, tot;
int untree[N], dis[12*N];
bool vis[12*N];
vector <Edge> edge[12*N];
priority_queue <pq, vector<pq>, greater<pq> > p;

#define T2 (n*4)
#define conpl (n*8)

int lp(int x){
    return x << 1;
}
int rp(int x){
    return x << 1 | 1;
}

void build(int p, int l, int r){
    if(l == r){
        untree[l] = p;
        return;
    }
    edge[p].emplace_back(lp(p), 0);
    edge[p].emplace_back(rp(p), 0);
    edge[lp(p)+(T2)].emplace_back(p+(T2), 0);
    edge[rp(p)+(T2)].emplace_back(p+(T2), 0);
    
    int mid = (l + r) >> 1;
    build( lp(p), l, mid );
    build( rp(p), mid+1, r );
}

void update1(int p, int x, int y, int l, int r){
    if(x <= l && r <= y){
        edge[p+(T2)].emplace_back((conpl)+tot, 1);
        return;
    }
    int mid = (l + r) >> 1;
    if(x <= mid)
        update1( lp(p), x, y, l, mid );
    if(y > mid)
        update1( rp(p), x, y, mid+1, r );
}
void update2(int p, int x, int y, int l, int r){
    if(x <= l && r <= y){
        edge[(conpl)+tot].emplace_back(p, 0);
        return;
    }
    int mid = (l + r) >> 1;
    if(x <= mid)
        update2( lp(p), x, y, l, mid );
    if(y > mid)
        update2( rp(p), x, y, mid+1, r );
}

void Dij(){
    p.push((pq){0, untree[root]});
    memset(dis, 0x3f3f3f3f, sizeof dis);
    dis[untree[root]] = 0;
    while(!p.empty()){
        int u = p.top().u;
        p.pop();
        if(vis[u])
            continue;
        vis[u] = 1;
        for(auto i : edge[u]){
            if(dis[i.to] > dis[u] + i.w){
                dis[i.to] = dis[u] + i.w;
                p.push((pq){dis[i.to], i.to});
            }
        }
    }
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    cin >> n >> m >> root;
    build(1, 1, n);
    for(int i = 1; i <= n; i++){
        edge[untree[i]+(T2)].emplace_back(untree[i], 0);
        edge[untree[i]].emplace_back(untree[i]+(T2), 0);
    }

    for(int i = 1, l1, r1, l2, r2; i <= m; i++){
        cin >> l1 >> r1 >> l2 >> r2;
        tot++;
        update1(1, l1, r1, 1, n);
        update2(1, l2, r2, 1, n);
        tot++;
        update2(1, l1, r1, 1, n);
        update1(1, l2, r2, 1, n);
    }

    Dij();

    for(int i = 1; i <= n; i++)
        cout << dis[untree[i]] << '\n';
    return 0;
}

#undef T2
#undef conpl

这么简单的东西,再不会看代码吧。

posted @ 2026-10-05 20:03  qjy123  阅读(6)  评论(0)    收藏  举报