当前位置:首页 > 随笔 > 正文内容

树结构算法核心技巧与典型题目解析

访客 随笔 2026年9月1日 1

树的直径

树中任意两点间最长路径称为直径。求解策略:对每个节点维护其子树中到叶子的最长路径和次长路径,最终答案为所有节点的两路径之和的最大值。

int max_len[N], second_max_len[N];
int dfs(int u, int parent) {
    for (auto v : graph[u]) {
        if (v == parent) continue;
        int len = dfs(v, u);
        if (len > max_len[u]) {
            second_max_len[u] = max_len[u];
            max_len[u] = len;
        } else if (len > second_max_len[u]) {
            second_max_len[u] = len;
        }
    }
    ans = std::max(ans, max_len[u] + second_max_len[u]);
    return max_len[u] + edge_weight; // 边权
}

树的中心

到所有节点最远距离最小的点即为中心。关键性质:中心必位于从根出发的重链上。通过两次DFS预处理子树最大深度,并在第二次遍历中更新最优解。

struct MaxNode {
    int child;
    int dist;
};
MaxNode ml[N], sl[N];

void dfs1(int u, int p) {
    for (auto v : graph[u]) {
        if (v == p) continue;
        dfs1(v, u);
        MaxNode temp{v, ml[v].dist + w};
        if (temp.dist > ml[u].dist) {
            std::swap(ml[u], temp);
        }
        if (temp.dist > sl[u].dist) {
            std::swap(sl[u], temp);
        }
    }
}

void dfs2(int u, int p, int up_dist) {
    int current_max = std::max(ml[u].dist, up_dist);
    if (current_max < min_max_dist) {
        min_max_dist = current_max;
        center_node = u;
    }
    if (up_dist > ml[u].dist) return;
    for (auto v : graph[u]) {
        if (v == ml[u].child) {
            dfs2(v, u, std::max(sl[u].dist, up_dist) + w);
        }
    }
}

树的重心

使最大子树大小最小的点。可通过一次后序遍历统计子树大小并计算各节点对应的最大子树规模。

int size[N];
int best_center, min_max_subtree;

void dfs(int u, int p) {
    size[u] = 1;
    int max_child_size = 0;
    for (auto v : graph[u]) {
        if (v == p) continue;
        dfs(v, u);
        size[u] += size[v];
        max_child_size = std::max(max_child_size, size[v]);
    }
    max_child_size = std::max(max_child_size, n - size[u]);
    if (max_child_size < min_max_subtree) {
        min_max_subtree = max_child_size;
        best_center = u;
    }
}

洛谷P3398:路径交集判断

给定树上两条路径,判断是否有公共点。核心结论:两路径相交当且仅当一条路径的LCA位于另一条路径上。

// LCA查询
int lca(int a, int b) {
    if (deep[a] < deep[b]) std::swap(a, b);
    for (int i = 20; i >= 0; --i)
        if (deep[fa[a][i]] >= deep[b]) a = fa[a][i];
    if (a == b) return a;
    for (int i = 20; i >= 0; --i)
        if (fa[a][i] != fa[b][i])
            a = fa[a][i], b = fa[b][i];
    return fa[a][0];
}

// 判断点是否在路径上
bool in_path(int x, int a, int b, int lca_ab) {
    if (x == a || x == b || x == lca_ab) return true;
    int l1 = lca(x, a), l2 = lca(x, b);
    return (l1 == x && l2 == lca_ab) || (l1 == lca_ab && l2 == x);
}

// 主逻辑
if (in_path(lca_a_b, c, d, lca_c_d) || in_path(lca_c_d, a, b, lca_a_b))
    puts("Y");
else
    puts("N");

洛谷P4281:三点汇合点求解

求三节点到某点的总距离最小的位置。通过分析发现,答案要么是三个两两LCA中的某个,要么是其中一个唯一不同的LCA。

int la1 = lca(a, b), la2 = lca(b, c), la3 = lca(a, c);
if (la1 == la2 && la2 == la3) {
    printf("%d %d\n", la1, depth[a] + depth[b] + depth[c] - 3 * depth[la1]);
} else if (la1 == la2) {
    printf("%d %d\n", la3, abs(depth[la3] - depth[a]) + abs(depth[la3] - depth[c]) + 
                 abs(depth[la1] - depth[la3]) + abs(depth[la1] - depth[b]));
} else if (la2 == la3) {
    // 同理处理
} else {
    // 处理其他情况
}

洛谷P5588:同色路径计数

统计每种颜色下长度大于1且包含该颜色所有出现点的路径数量。关键思想:识别"极低点"(子树中无同色点的最低点)。

  • 若只有一个极低点:所有同色点构成链,路径数由最高点决定。
  • 若有且仅有两个极低点:若最高点不在两点路径上,则路径数为两子树大小乘积。
  • 超过两个极低点:无合法路径。
void dfs2(int u, int p) {
    ll old_cnt = cnt[color[u]];
    for (auto v : graph[u]) {
        if (v == p) continue;
        dfs2(v, u);
    }
    if (cnt[color[u]] == old_cnt) {
        low_points[color[u]].push_back(u);
    }
    ++cnt[color[u]];
}

洛谷P5536:核心城市选址

选k个点作为核心,使得非核心点到最近核心点的最大距离最小。贪心策略:先选直径中点,再依次选择能覆盖最远区域的点。

void dfs1(int u, int p) {
    mv[u] = 1;
    for (auto v : graph[u]) {
        if (v == p) continue;
        dfs1(v, u);
        mv[u] = std::max(mv[u], mv[v] + 1);
        update_max(u, v, mv[v]);
    }
}

void dfs2(int u, int up) {
    int max_depth = ml[u].dist > up ? ml[u].dist : up;
    if (max_depth < best_dist) {
        best_dist = max_depth;
        center = u;
    }
    if (ml[u].dist <= up) return;
    dfs2(ml[u].child, std::max(up + 1, sl[u].dist + 1));
}

// 使用优先队列扩展覆盖范围
priority_queue<Node> pq;
pq.push({center, depth_from_center});
while (k--) {
    auto top = pq.top(); pq.pop();
    for (auto neighbor : graph[top.node]) {
        if (!visited[neighbor]) pq.push({neighbor, new_depth});
    }
}

洛谷P1273:有线电视网收益最大化

树形分组背包问题:选取若干叶节点,目标为(收益和)减去(路径代价和)≥0 的前提下最大化数量。

const int INF = -1e9;
for (int i = 1; i <= n; ++i)
    for (int j = 1; j <= n; ++j)
        dp[i][j] = INF;

// 叶子节点初始化
for (int i = n - m + 1; i <= n; ++i)
    dp[i][1] = value[i], siz[i] = 1;

// 后序遍历更新
void dfs(int u, int p) {
    for (auto [v, w] : graph[u]) {
        if (v == p) continue;
        dfs(v, u);
        for (int i = siz[u]; i >= 0; --i)
            for (int k = 0; k <= siz[v] && k <= i; ++k)
                dp[u][i] = std::max(dp[u][i], dp[u][i - k] + dp[v][k] - w);
    }
}

HDU6035:彩色路径颜色种类总和

求所有路径上不同颜色数量之和。转换思路:对每种颜色,计算不经过它的路径数,再用总数减去。

ll total_pairs = n * (n - 1) / 2;
ll ans = 0;

void dfs(ll u, ll p) {
    ll old_sum = sum[col[u]];
    for (auto v : graph[u]) {
        if (v == p) continue;
        dfs(v, u);
        ll new_sum = sum[col[u]];
        ll delta = new_sum - old_sum;
        ll remaining = siz[v] - delta;
        ans += remaining * (remaining - 1) / 2;
        sum[col[u]] += remaining;
    }
    sum[col[u]]++;
}

// 最终结果
ans = total_pairs * n - ans;

相关文章

可以按小时收费的VPS

很多 VPS 提供商都支持 按小时计费(hourly billing),想短期试用 / 临时搭建节点、测试网络、短期项目等场景非常合适。下面是当前最主流且靠谱的按小时 VPS 选项,分别按不同需求场景整理: 1. Vultr(全球节点,包括日本) 按小时计费 可选机房:东京 / 大阪 / 洛杉矶 / 法兰克福 / 伦敦 … 支持 PayPal(部分情况),但更常用信用卡/PayPal+卡价格参考$...

在 iPhone 上下载国外App

地区/国家限制App Store 会根据 Apple ID 的国家或地区限制应用下载。如果你的 Apple ID 绑定的是中国大陆,就可能无法下载 OpenAI 官方的 ChatGPT 应用,因为它在大陆 App Store 不上架。解决办法:换成美国、加拿大、香港等地区的 Apple ID。或者在现有 Apple ID 上更改地区。注册一个国外 Apple ID(推荐)比如注册 美国区 Appl...

Node.js 中的异步编程:回调与 Promise

Node.js 是一个基于 JavaScript 构建的单线程、非阻塞运行环境,它通过异步编程机制来高效处理多个操作。在执行如文件读取、API 请求或数据库查询等任务时,Node.js 不会等待这些操作完成,而是使用回调函数和 Promise 来避免阻塞主线程。 回调方式实现异步 那么当异步操作完成后,Node.js 如何知道接下来要做什么呢?这就要用到 回调函数(callback)。 回调本质上...

Selenium自动化测试入门指南

Selenium自动化测试入门指南

什么是自动化测试? 自动化测试是指利用软件工具自动执行测试用例,模拟用户操作,如打开网页、点击链接、输入文本等,并验证结果是否符合预期。 其主要优点包括: 大幅减少人工成本 测试速度快 可以在非工作时间运行 支持持续集成和交付 然而,它也存在一些局限性,例如开发成本较高、不适合快速变化的项目、依赖稳定的UI界面等。 自动化测试的应用条件 适合引入自动化测试的情况包括: 手动测试耗时且需要大量...

MariaDB Galera集群故障快速恢复指南

OpenStack控制节点采用三节点MariaDB Galera集群架构。当数据库集群因故障重启时,有时会出现Galera集群无法正常启动的问题。虽然有多种方法可以恢复数据库服务,但如何实现快速启动同时确保数据完整性呢? 通过分析日志发现,MariaDB Galera集群节点宕机时会在日志中输出以下信息: [Note] WSREP: 新集群视图:全局状态: 874d8e7e-5980-11e8-8...

二叉树基础操作实现(C语言)

二叉树基础操作实现(C语言)

二叉树遍历方法 以下为二叉树结构示例 前序访问实现: void traversePreOrder(TreeNode* node) { if (node == NULL) { printf("N "); return; } printf("%d ", node->value); trave...

发表评论

访客

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