当前位置:首页 > 技术 > 正文内容

使用线段树优化图构建中的区间连边

访客 技术 2026年5月26日 3

引言

在处理图论问题时,当涉及大量区间到点或点到区间的连边操作,直接建图会导致边数爆炸。此时,可以借助线段树结构对建图过程进行优化,将原本的高复杂度操作降至对数级别。本文介绍一种基于两棵辅助线段树的建图优化技术,并结合最短路算法解决典型问题。

核心思想

该方法的核心在于引入两棵虚拟的线段树——"出边树"和"入边树",分别用于高效实现以下三类操作:

  • 从单个点向一个区间内所有点连边(点 → 区间)
  • 从一个区间内所有点向单个点连边(区间 → 点)
  • 两个区间之间的连边(区间 → 区间),可拆解为上述两种操作组合

若采用朴素方式,一次区间连边可能需要添加 \(O(n^2)\) 条边,而通过线段树优化后,每次操作仅需 \(O(\log n)\) 条边即可完成。

结构设计

出边树(向外扩散)

出边树中,每个非叶子节点代表其对应区间的集合。父节点向子节点连权值为0的边,表示信息可以从父节点传递至子节点。当我们需要从某个点 \(v\) 向区间 \([l, r]\) 连一条权值为 \(w\) 的边时,只需将 \(v\) 连接到出边树中覆盖 \([l, r]\) 的若干线段树节点上,利用树内零权边自动扩展到具体点。

入边树(向内汇聚)

入边树则相反,子节点向父节点连零权边。这样,任意属于某个区间的点都可以通过树内路径上升到对应的区间节点。当要求区间 \([l, r]\) 内所有点向某点 \(u\) 连边时,我们先让这些点通过入边树汇聚到对应节点,再由该节点向 \(u\) 连边。

两棵树的关系

原始图中的每个点 \(i\) 在两棵树中均作为叶子节点存在。即:出边树的叶节点 \(i\) 和入边树的叶节点 \(i\) 都对应原图中的第 \(i\) 个顶点。整个图的顶点集包括原始点和两棵树新增的内部节点。

实现细节

数据结构定义

#include <vector>
#include <queue>
using namespace std;

const int MAX = 400010; // 足够容纳原始点 + 线段树节点

struct Edge {
    int to;
    long long weight;
};

vector<Edge> graph[MAX];
int nodeCount; // 当前总节点数量,初始为n

构建线段树

分别建立出边树与入边树,树中内部节点编号从 \(n+1\) 开始递增分配。

int leftChild[MAX * 4], rightChild[MAX * 4];
int rootOut, rootIn; // 出边树和入边树的根

// 构建出边树:父节点指向子节点
void buildOut(int& node, int l, int r) {
    if (l == r) {
        node = l;
        return;
    }
    node = ++nodeCount;
    int mid = (l + r) / 2;
    buildOut(leftChild[node], l, mid);
    buildOut(rightChild[node], mid + 1, r);
    // 父节点向左右子节点连零权边
    graph[node].push_back({leftChild[node], 0});
    graph[node].push_back({rightChild[node], 0});
}

// 构建入边树:子节点指向父节点
void buildIn(int& node, int l, int r) {
    if (l == r) {
        node = l;
        return;
    }
    node = ++nodeCount;
    int mid = (l + r) / 2;
    buildIn(leftChild[node], l, mid);
    buildIn(rightChild[node], mid + 1, r);
    // 子节点向父节点连零权边
    graph[leftChild[node]].push_back({node, 0});
    graph[rightChild[node]].push_back({node, 0});
}

执行区间连边

利用线段树的区间查询机制,在树上定位对应节点并连接。

// 从点 u 向区间 [L,R] 连权值为 w 的边(使用出边树)
void connectPointToRange(int node, int segL, int segR, int L, int R, int u, long long w) {
    if (L <= segL && segR <= R) {
        graph[u].push_back({node, w});
        return;
    }
    int mid = (segL + segR) / 2;
    if (L <= mid)
        connectPointToRange(leftChild[node], segL, mid, L, R, u, w);
    if (R > mid)
        connectPointToRange(rightChild[node], mid + 1, segR, L, R, u, w);
}

// 从区间 [L,R] 向点 u 连权值为 w 的边(使用入边树)
void connectRangeToPoint(int node, int segL, int segR, int L, int R, int u, long long w) {
    if (L <= segL && segR <= R) {
        graph[node].push_back({u, w});
        return;
    }
    int mid = (segL + segR) / 2;
    if (L <= mid)
        connectRangeToPoint(leftChild[node], segL, mid, L, R, u, w);
    if (R > mid)
        connectRangeToPoint(rightChild[node], mid + 1, segR, L, R, u, w);
}

应用实例:CF786B Legacy

题目要求支持三种操作:

  1. 单点到单点连边
  2. 单点到区间连边
  3. 区间到单点连边

解决方案如下:

  • 初始化时,令原始节点数为 \(n\),然后建立两棵范围为 \([1, n]\) 的线段树
  • 对于操作2,调用 connectPointToRange(rootOut, 1, n, l, r, v, w)
  • 对于操作3,调用 connectRangeToPoint(rootIn, 1, n, l, r, v, w)
  • 最终在扩展后的图上运行 Dijkstra 算法求最短路

Dijkstra 实现

long long dist[MAX];
bool visited[MAX];

void dijkstra(int start) {
    priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<>> pq;
    fill(dist, dist + MAX, LLONG_MAX);
    dist[start] = 0;
    pq.push({0, start});

    while (!pq.empty()) {
        auto [d, u] = pq.top(); pq.pop();
        if (visited[u]) continue;
        visited[u] = true;
        for (auto &edge : graph[u]) {
            int v = edge.to;
            long long nd = d + edge.weight;
            if (nd < dist[v]) {
                dist[v] = nd;
                pq.push({nd, v});
            }
        }
    }
}

主流程

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);

    int q, s;
    cin >> n >> q >> s;
    nodeCount = n;

    buildOut(rootOut, 1, n);
    buildIn(rootIn, 1, n);

    while (q--) {
        int op; cin >> op;
        if (op == 1) {
            int v, u, w; cin >> v >> u >> w;
            graph[v].push_back({u, w});
        } else if (op == 2) {
            int v, l, r, w; cin >> v >> l >> r >> w;
            connectPointToRange(rootOut, 1, n, l, r, v, w);
        } else if (op == 3) {
            int v, l, r, w; cin >> v >> l >> r >> w;
            connectRangeToPoint(rootIn, 1, n, l, r, v, w);
        }
    }

    dijkstra(s);

    for (int i = 1; i <= n; ++i) {
        cout << (dist[i] == LLONG_MAX ? -1 : dist[i]) << ' ';
    }
    cout << '\n';

    return 0;
}
标签: 线段树图论

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。