外包离职后,把核心代码上传到GitHub,项目组已退场,大概率找不到人了
刚看到个贴子,说有外包离职后,直接把营销系统的核心代码和全年营收数据一股脑扔上 GitHub,项目组也早就散了,人基本找不回来了。嗯…这事吧,确实离谱,但我倒是不意外。
把核心业务交给流动性极高的外包团队,却没有权限隔离、没有审计记录、没有交接机制,这就像把家门钥匙随手扔路边还怪路人捡走。
当然,员工泄密肯定不对,这是底线问题,怎么都洗不白。但我也认同部分网友提到的:外包人力被压得太狠、项目赶工混乱,其实埋下隐患的不只是个人情绪,而是整个体系的疲于奔命。
不过话说回来,企业做系统像修水坝,不能等漏了再补。权限、流程、交付规范这些“看起来麻烦”的东西,其实都是救命绳。【备注:文末可领最新资料】
面试题:我能赢吗
我昨天晚上十一点多在公司楼下抽烟,手机上随手点开这题,第一眼很多人都会这么想: “那不就是轮流抢数字嘛,我挑大的就好了?”。
结果一写就发现不对劲:
数字是一次性用掉的,你拿了 10 就没 10 了 你现在贪大,可能给对面留下一个刚好能凑到目标的局面 而且题目数据范围:maxChoosableInteger ≤ 20,这就有点“暗示你用状态压缩”的味道了
暴力想法是有的:把所有没拿过的数字枚举一遍,每种拿法都递归下去,看是不是能赢。问题是,如果不记忆化,这棵搜索树会爆炸。
几个关键小结论(真的很重要)
这题有两个非常实用的剪枝,面试的时候顺着这个路子说,面试官一般就知道你会做了:
总和都不够,直接 false所有数加起来小于 desiredTotal,那不管谁怎么玩,都到不了目标,肯定没人能赢:
int sum = (1 + maxChoosableInteger) * maxChoosableInteger / 2;
if (sum < desiredTotal) returnfalse;当前一手就能赢,直接 true在某个局面下,如果还有一个没用过的数字 i,满足
i >= remain(remain 是离目标还差多少),那当前这个人直接拿 i 就结束游戏了,这个状态就是必胜状态。
接下来就是:怎么表示“当前已经用了哪些数字”,以及怎么记忆化。
状态怎么存?位掩码这玩意就派上用场了
因为 maxChoosableInteger ≤ 20,我们可以用一个 int 的二进制位来表示每个数字用没用:
0 号 bit 表示 1 用没用 1 号 bit 表示 2 用没用 … 第 (i-1) 位表示数字 i 是否已经被选过
某一位是 1,表示这个数字已经拿过了;是 0,表示还可以选。 这样整个“局面”就可以用一个整数 state 表示。
为啥要这样干? 因为我们递归的时候,每次面对的,其实就是“当前剩哪些数字 + 还差多少总和”,而剩哪些数字完全可以用 state 表示,然后我们就可以用一个 Map<Integer, Boolean> 或 Boolean[] memo 来记忆化:
memo[state] == true表示:在这个状态下,当前轮到的人是必胜的memo[state] == false表示:当前轮到的人怎么下都赢不了(对手太聪明)
核心递归逻辑其实就一句话
从当前状态出发,只要你能找到一个数字 i:
i 还没被用过
如果你拿了 i,要么:
直接赢( i >= remain)要么拿完后,对手的局面是必败的
那你就是必胜的。
翻成伪代码就是:
对所有还能选的 i: 如果 i >= remain,当前玩家赢 否则,看下一步状态
nextState对手是不是必败 如果有一个 i 能让对手必败,那当前就是必胜
如果所有 i 都试完了:要么拿了也赢不了,要么拿了之后对手都是必胜,那你就完蛋了,这个状态就是失败状态。
上点 Java 代码,别太花里胡哨的那种
publicclassSolution{
publicbooleancanIWin(int maxChoosableInteger, int desiredTotal){
// 剪枝一:总和都不够,谁也赢不了
int sum = (1 + maxChoosableInteger) * maxChoosableInteger / 2;
if (sum < desiredTotal) {
returnfalse;
}
// 剪枝二:目标 <= 最大数,你先手直接拿最大那个就赢
if (desiredTotal <= maxChoosableInteger) {
returntrue;
}
// 记忆化:state -> 当前玩家在这个状态下是否必胜
Boolean[] memo = new Boolean[1 << maxChoosableInteger];
return dfs(maxChoosableInteger, desiredTotal, 0, memo);
}
/**
* @param max 最大可选数字
* @param remain 距离目标还差多少
* @param state 已选择数字的状态,二进制位表示
* @param memo 记忆化数组
*/
privatebooleandfs(int max, int remain, int state, Boolean[] memo){
if (memo[state] != null) {
return memo[state];
}
// 从 1 到 max 尝试每一个没用过的数字
for (int i = 1; i <= max; i++) {
int bit = 1 << (i - 1);
// 已经用过就跳过
if ((state & bit) != 0) {
continue;
}
// 1. 直接拿这个数就能达到或超过目标,当前玩家立刻获胜
if (i >= remain) {
memo[state] = true;
returntrue;
}
// 2. 否则,假装我拿了 i,轮到对手
int nextState = state | bit;
boolean opponentWin = dfs(max, remain - i, nextState, memo);
// 只要有一种选择能让对手在之后的状态中“赢不了”,
// 那当前就是必胜状态
if (!opponentWin) {
memo[state] = true;
returntrue;
}
}
// 所有选择尝试完,都不能让对手陷入必败,那我就是必败
memo[state] = false;
returnfalse;
}
}
这个写法有几个点面试时可以顺带嘴上提一嘴:
用 Boolean[]而不是boolean[],因为我们需要三态:null / true / false,null表示没算过状态数量最多 2^20 ≈ 100w,dfs + 记忆化在这个规模是完全扛得住的搜索顺序无所谓,只要逻辑是“有没有一种选择能让对手必败”,就行
最后随口唠一句
这题本质上就是一个“带记忆化的博弈搜索 + bitmask 压缩状态”,思路一旦通了,你会发现很多类似的“我能赢吗”“先手后手谁有优势”的题,套路都差不多。
行了,不唠了,我去给我们组那个小李解释为啥他的 bit 掩码总写反了…
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html