程序员老鬼

真的被大厂人的存款搞破防了!一个36岁手握230万,问够不够躺平养老;另一个存够250万,说完全不想上班了。

有大厂网友说,36岁手里攒了230万,问这钱够不够躺平养老。还有一个更狠,存到250万以后,直接说一想到上班就难受,想彻底歇了。

我看完第一反应是:哥们你这是烦恼吗?这明明是我的人生幻想。

Image

同样是打工人,人家纠结的是“200多万够不够不上班”,我这边纠结的是“这个月花呗怎么撑到发工资”。人家打开账户是在算养老,我打开账户是在算还能不能点个外卖。

最扎心的不是他们有钱,是他们已经有了选择权。想干就干,不想干还能退一步。

而我们这种普通牛马,别说躺平了,周一早上能准时爬起来都算意志力惊人。差距这东西,平时没感觉,一看存款,直接给人干沉默了。

面试题:树上的操作

这题一眼看上去像树形 DP,我第一反应反而不是 DP。

因为它的操作很硬:lock、unlock、upgrade。 真正麻烦的是 upgrade,它要求当前节点没被锁、祖先不能被锁、子树里至少有一个被锁节点,然后把子树所有锁清掉,再锁当前节点。

这地方最容易写歪。很多人上来就想维护一堆状态,什么子树锁数量、祖先标记,最后代码越写越像线上补丁。其实数据范围不大的情况下,老老实实 DFS,反而最稳。

我会把每个节点存两样东西:父节点是谁,孩子有哪些。再用一个数组 lockedBy 记录节点被谁锁了,0 表示没锁。

关键代码大概长这样:

classLockingTree{
privatefinalint[] parent;
privatefinal List<Integer>[] children;
privatefinalint[] lockedBy;

publicLockingTree(int[] parent){
this.parent = parent;
int n = parent.length;
this.children = new ArrayList[n];
this.lockedBy = newint[n];

for (int i = 0; i < n; i++) {
            children[i] = new ArrayList<>();
        }

for (int i = 1; i < n; i++) {
            children[parent[i]].add(i);
        }
    }

publicbooleanlock(int num, int user){
if (lockedBy[num] != 0) {
returnfalse;
        }
        lockedBy[num] = user;
returntrue;
    }

publicbooleanunlock(int num, int user){
if (lockedBy[num] != user) {
returnfalse;
        }
        lockedBy[num] = 0;
returntrue;
    }

publicbooleanupgrade(int num, int user){
if (lockedBy[num] != 0) {
returnfalse;
        }

if (hasLockedParent(num)) {
returnfalse;
        }

if (!clearLockedChildren(num)) {
returnfalse;
        }

        lockedBy[num] = user;
returntrue;
    }

privatebooleanhasLockedParent(int num){
int cur = parent[num];
while (cur != -1) {
if (lockedBy[cur] != 0) {
returntrue;
            }
            cur = parent[cur];
        }
returnfalse;
    }

privatebooleanclearLockedChildren(int num){
boolean found = false;

for (int child : children[num]) {
if (lockedBy[child] != 0) {
                lockedBy[child] = 0;
                found = true;
            }

if (clearLockedChildren(child)) {
                found = true;
            }
        }

return found;
    }
}

这里有个细节我一般会盯一下:upgrade 里清子树锁,不能只判断直接孩子。题目说的是后代节点,孙子、重孙都算。

所以 clearLockedChildren 不只是判断,还顺手把锁释放掉。它返回 true,表示这棵子树里确实出现过锁。这个返回值很关键,不然你可能会把一个完全没有锁后代的节点也 upgrade 成功,样例可能过,隐藏用例直接炸。

这题复杂度也不用装得太高级。

lock 和 unlock 都是 O(1)。upgrade 最坏要往上扫祖先,再往下扫整棵子树,所以是 O(n)。

要不要优化?看数据范围。

如果操作次数特别大,可以给每个节点维护“子树锁数量”,这样判断子树有没有锁能变快。但释放所有后代锁这一步,本来就得找到那些节点,绕不开。为了一个面试题把代码写成状态同步系统,没必要。

这题真正考的不是树有多难,而是条件有没有按顺序落准。

我的习惯是:

当前节点自己先判。 再判祖先。 最后处理子树。

顺序反过来也能写,但可读性差一点。尤其是 upgrade 这种带副作用的操作,最好先把所有失败条件挡在前面,确认能做了,再真正改状态。线上代码也是这个味道,先验票,再开闸。