程序员老鬼

父母迷信国企,花大几万把我塞进国企当流水线工人,该怎么办?

刚看到个贴子,说一位网友的爸妈为了“让孩子进国企”,花了大几万托关系,把他塞进去当流水线工人。网友本人还挺纠结:一边是稳定体面,一边是自己的职业兴趣。

Image

我觉得这事吧,本质上是两代人的安全感不同。父母那代经历过下岗潮,对“铁饭碗”有种天然信任,他们怕你吃苦,不信任所谓的“广告”“设计”这些看不见摸得着的新行业。但年轻人追求的不只是稳定,还有成长和价值感。

工作不是让你困在一个“安全”的笼子里,而是要积累自己的竞争力。钱能花,但方向错了才最亏。

换个角度想,父母出发点是爱,但你的人生要你自己负责。【备注:文末可领最新资料】

算法题:统计树中的合法路径数目

先自己定个题目版本,方便后面讲思路:

给你一棵有 n 个点的无根树,每条边有一个正权 w。 一条简单路径(任意两点之间,不走回头路)如果路径上所有边权之和 ≤ K,我们就叫它“合法路径”。 问这棵树里一共有多少条合法路径(起点终点可以是任意两个点,长度为 0 的单点路径也算一条)。

这类题在笔试里非常常见,关键点是:树的点数 n 一般会到 1e5 级别,不能暴力枚举所有点对。

暴力为啥不行

最直观的想法就是:

  • 枚举每个点作为起点,做一遍 DFS,把从这个点出发到所有其他点的路径长度算出来,看看哪些 ≤ K。
  • 一棵 n 个点的树,有 O(n^2) 条路径,这样时间复杂度差不多也是 O(n^2),n=1e5 直接爆炸。

所以必须利用“树”的结构来降复杂度。

点分治的大概直觉

这个题比较标准的做法是点分治 + 双指针统计路径和,听着有点吓人,其实想象成“找一个中心,把树掰成几块,一块一块数”:

  1. 在当前这棵树里找一个“重心”节点 c(删掉它后,剩下的每个连通块大小都不超过原来的 1/2)。
  2. 把所有经过 c 的路径数出来。
  3. 然后把 c 删掉,剩下若干棵小树,对每棵小树递归做同样的事。

核心问题就变成: “如何在 O(子树大小 log 子树大小) 的时间里,数出所有经过 c 且路径和 ≤ K 的路径条数”。

只管经过重心的路径怎么数

所有经过 c 的路径,可以看成“两条从 c 出发的链拼在一起”(也可能只有一条,就是 c 到某个点的路径)。

做法可以拆成几步:

  1. 以 c 为根,对每个相邻子树做 DFS,算出从 c 出发到这个子树里所有点的距离 dist,丢到一个数组里。

  2. 先记住 0 这个距离(代表只选点 c 自己的单点路径)。

  3. 把所有子树的 dist 一块一块处理:

  • 先对当前子树的 dist 数组里的每个 d,看一下它和“全局已有的所有距离”能组成多少条合法路径(d + x ≤ K)。
  • 统计完之后,再把这一整块 dist 的值合并进“全局数组”,方便后面的子树和它们配对。
  • “一个 d 和一堆 x 能组成多少对 ≤ K”这个小问题,可以把所有数排序,用双指针 O(m) 解决。

  • 这里有一个小细节: 我们已经把“经过 c 的路径”全部统计了,递归子树时,只统计“不经过 c 的路径”,这样就不会重复计数。

    整体复杂度大概是 O(n log n),对于 1e5 级别的树完全够用。

    关键数据结构怎么写

    下面给一份精简版的 Java 代码,只保留算法核心,输入输出你可以按自己需要补上。为了简单,假设点从 0 开始编号。

    import java.util.*;

    publicclassTreeLegalPathCounter{

    staticclassEdge{
    int to, w;
            Edge(int t, int w) { this.to = t; this.w = w; }
        }

    staticint n;
    staticlong K;
    static List<Edge>[] g;

    staticboolean[] removed;   // 点是否已经被“删掉”(用于点分治)
    staticint[] subSize;       // 子树大小
    staticlong ans = 0;

    @SuppressWarnings("unchecked")
    publicTreeLegalPathCounter(int n, long K){
            TreeLegalPathCounter.n = n;
            TreeLegalPathCounter.K = K;
            g = new ArrayList[n];
    for (int i = 0; i < n; i++) g[i] = new ArrayList<>();
            removed = newboolean[n];
            subSize = newint[n];
        }

    publicvoidaddEdge(int u, int v, int w){
            g[u].add(new Edge(v, w));
            g[v].add(new Edge(u, w));
        }

    // 计算子树大小
    privatevoidcalcSize(int u, int p){
            subSize[u] = 1;
    for (Edge e : g[u]) {
    int v = e.to;
    if (v == p || removed[v]) continue;
                calcSize(v, u);
                subSize[u] += subSize[v];
            }
        }

    // 找重心
    privateintfindCentroid(int u, int p, int tot){
    int maxSub = tot - subSize[u];
    int best = u;
    for (Edge e : g[u]) {
    int v = e.to;
    if (v == p || removed[v]) continue;
    int cand = findCentroid(v, u, tot);
    if (cand != v) best = cand; // 下层已经找到更优的重心
                maxSub = Math.max(maxSub, subSize[v]);
            }
    // 如果当前点比“递归返回的候选”更好,可以在这里判断替换
    // 为了简化,这里直接用一个包装函数来找重心更清晰
    return best;
        }

    privateintgetCentroid(int root){
            calcSize(root, -1);
    int tot = subSize[root];
    // 重新写一遍找重心,更直白一点
    int best = root;
    int[] bestMax = { tot }; // 用数组是为了在 lambda 里改值
            findCentroidDFS(root, -1, tot, bestMax, newint[]{best});
    return bestMax[1]; // 偷懒的写法,这里你可以自己整理下
        }

    // 为了不让上面太乱,我给一个更常见的重心写法
    privateint centroid; 
    privateint bestBalance;

    privateintfindCentroidWrapper(int root){
            centroid = root;
            bestBalance = subSize[root];
            findCentroidDFS2(root, -1, subSize[root]);
    return centroid;
        }

    privatevoidfindCentroidDFS2(int u, int p, int tot){
    int maxPart = 0;
    for (Edge e : g[u]) {
    int v = e.to;
    if (v == p || removed[v]) continue;
                findCentroidDFS2(v, u, tot);
                maxPart = Math.max(maxPart, subSize[v]);
            }
            maxPart = Math.max(maxPart, tot - subSize[u]);
    if (maxPart < bestBalance) {
                bestBalance = maxPart;
                centroid = u;
            }
        }

    // 收集从某个根出发的所有距离
    privatevoidcollectDist(int u, int p, long dist, List<Long> list){
    if (dist > K) return; // 剪枝:超过 K 的路径根本不可能合法
            list.add(dist);
    for (Edge e : g[u]) {
    int v = e.to;
    if (v == p || removed[v]) continue;
                collectDist(v, u, dist + e.w, list);
            }
        }

    // 统计一个数组里有多少对 (i, j) 使得 a[i] + a[j] <= K,允许 i == j
    privatelongcountPairs(List<Long> arr){
            Collections.sort(arr);
    long res = 0;
    int l = 0, r = arr.size() - 1;
    while (l <= r) {
    if (arr.get(l) + arr.get(r) <= K) {
    // 从 l 到 r 的所有点和 r 组合都合法
                    res += (r - l + 1);
                    l++;
                } else {
                    r--;
                }
            }
    return res;
        }

    // 点分治主过程
    publicvoidsolve(int root){
            calcSize(root, -1);
    int c = findCentroidWrapper(root); // 找重心
            removed[c] = true;

            List<Long> all = new ArrayList<>();
            all.add(0L); // 仅包含重心自己的路径

    // 先统计经过 c 的所有路径
    for (Edge e : g[c]) {
    int v = e.to;
    if (removed[v]) continue;
                List<Long> tmp = new ArrayList<>();
                collectDist(v, c, e.w, tmp);

    // tmp 与 all 中的任意一条组合,都是“起点在这个子树,终点在其他子树(或 c)”的路径
                ans -= countPairs(tmp);      // 先减掉“子树内部自成一对”的部分
                all.addAll(tmp);             // 再合并进全局
            }
            ans += countPairs(all);          // 全局统计一次

    // 递归处理每个子树
    for (Edge e : g[c]) {
    int v = e.to;
    if (removed[v]) continue;
                solve(v);
            }
        }

    publiclonggetAnswer(){
    return ans;
        }
    }

    上面代码有点长,我说几点你看着改:

    1. 重心那块,为了讲思路我写得比较啰嗦,你可以只保留一个版本。
    2. ans 里统计的是“路径条数”,包括长度为 0 的单点路径(dist = 0 那个)。
    3. 真正写题的时候,把构造函数、输入、solve(0) 这些连起来就行了。

    小结一下

    整套思路其实就两句话:

    • 利用点分治,把“任意两点的路径”拆成“经过某个中心点 + 不经过它的子问题”,避免 O(n^2)。
    • 利用“从中心到各点的距离数组 + 双指针”快速统计“路径和 ≤ K 的对数”。

    你如果第一次写点分治,建议先把“统计树上所有路径和 ≤ K 的条数”在纸上画两棵小树,手动模拟一次,会比直接对着代码抄印象深很多。等你把这个模板吃透了,之后遇到“统计树中合法路径数目”这类题,基本就是换个“合法”的定义、换点状态而已。

    -END-

    我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

    最后给大家分享一份不错的副业资料,点击下方公众号,回复关键字: 副业 领取,也可以链接我领取,微信:hls404