父母迷信国企,花大几万把我塞进国企当流水线工人,该怎么办?
刚看到个贴子,说一位网友的爸妈为了“让孩子进国企”,花了大几万托关系,把他塞进去当流水线工人。网友本人还挺纠结:一边是稳定体面,一边是自己的职业兴趣。
我觉得这事吧,本质上是两代人的安全感不同。父母那代经历过下岗潮,对“铁饭碗”有种天然信任,他们怕你吃苦,不信任所谓的“广告”“设计”这些看不见摸得着的新行业。但年轻人追求的不只是稳定,还有成长和价值感。
工作不是让你困在一个“安全”的笼子里,而是要积累自己的竞争力。钱能花,但方向错了才最亏。
换个角度想,父母出发点是爱,但你的人生要你自己负责。【备注:文末可领最新资料】
算法题:统计树中的合法路径数目
先自己定个题目版本,方便后面讲思路:
给你一棵有 n 个点的无根树,每条边有一个正权 w。 一条简单路径(任意两点之间,不走回头路)如果路径上所有边权之和 ≤ K,我们就叫它“合法路径”。 问这棵树里一共有多少条合法路径(起点终点可以是任意两个点,长度为 0 的单点路径也算一条)。
这类题在笔试里非常常见,关键点是:树的点数 n 一般会到 1e5 级别,不能暴力枚举所有点对。
暴力为啥不行
最直观的想法就是:
枚举每个点作为起点,做一遍 DFS,把从这个点出发到所有其他点的路径长度算出来,看看哪些 ≤ K。 一棵 n 个点的树,有 O(n^2) 条路径,这样时间复杂度差不多也是 O(n^2),n=1e5 直接爆炸。
所以必须利用“树”的结构来降复杂度。
点分治的大概直觉
这个题比较标准的做法是点分治 + 双指针统计路径和,听着有点吓人,其实想象成“找一个中心,把树掰成几块,一块一块数”:
在当前这棵树里找一个“重心”节点 c(删掉它后,剩下的每个连通块大小都不超过原来的 1/2)。 把所有经过 c 的路径数出来。 然后把 c 删掉,剩下若干棵小树,对每棵小树递归做同样的事。
核心问题就变成: “如何在 O(子树大小 log 子树大小) 的时间里,数出所有经过 c 且路径和 ≤ K 的路径条数”。
只管经过重心的路径怎么数
所有经过 c 的路径,可以看成“两条从 c 出发的链拼在一起”(也可能只有一条,就是 c 到某个点的路径)。
做法可以拆成几步:
以 c 为根,对每个相邻子树做 DFS,算出从 c 出发到这个子树里所有点的距离 dist,丢到一个数组里。
先记住 0 这个距离(代表只选点 c 自己的单点路径)。
把所有子树的 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;
}
}
上面代码有点长,我说几点你看着改:
重心那块,为了讲思路我写得比较啰嗦,你可以只保留一个版本。 ans里统计的是“路径条数”,包括长度为 0 的单点路径(dist = 0 那个)。真正写题的时候,把构造函数、输入、 solve(0)这些连起来就行了。
小结一下
整套思路其实就两句话:
利用点分治,把“任意两点的路径”拆成“经过某个中心点 + 不经过它的子问题”,避免 O(n^2)。 利用“从中心到各点的距离数组 + 双指针”快速统计“路径和 ≤ K 的对数”。
你如果第一次写点分治,建议先把“统计树上所有路径和 ≤ K 的条数”在纸上画两棵小树,手动模拟一次,会比直接对着代码抄印象深很多。等你把这个模板吃透了,之后遇到“统计树中合法路径数目”这类题,基本就是换个“合法”的定义、换点状态而已。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html