刚看到个贴子,说微软确认Windows有个bug——“更新并关机”其实是“更新并重启”,整整两年没人发现。哈哈,这事吧,又离谱又合理。想想确实,大多数人电脑重启后都怀疑是自己点错了,或者系统抽风,根本不会想到是微软的问题。
网友们调侃说,这说明微软人浮于事,我倒觉得更像是“人性共谋”——大家都太习惯忍了。遇到点小毛病就想着凑合,能用就行。微软能混两年没被发现,也算得益于我们这种“算了”心态。
不过话说回来,大厂也该警醒了。信任不是天生的,是靠一次次“正常”积累出来的。一个小bug暴露的,不只是技术问题,还有企业的责任心。希望以后别再让用户替你“体谅”了。【备注:文末可领最新资料】
算法题:石子游戏
一排石堆,每堆有若干个石子,两个人轮流拿,每次只能拿最左或最右一堆,石子总数更多的人赢。这个题最容易掉坑的地方,是被“我拿这堆更大”这种局部直觉带偏了;真正要赢,得算清楚“我比对手多拿多少”,也就是分差思维。
问题建模与直觉
把 piles[i] 当成第 i 堆的石子数。设区间 [l, r] 还没拿。轮到我时,有两种选法:拿左边 l,或者拿右边 r。拿完之后,对手会在剩余区间里尽力让我的总分差变小。所以当前最优分差=我拿到的石子 − “对手在剩余区间里能拿到的分差”。这就把博弈递归成了区间 DP。
核心转移(区间 DP)
定义 dp[l][r]:在只剩 piles[l..r] 时,当前玩家相对对手的最大分差。
拿左边:piles[l] − dp[l+1][r]
拿右边:piles[r] − dp[l][r-1] 于是:dp[l][r] = max(上述两者)。 边界:dp[i][i] = piles[i](只剩一堆,当前玩家全拿)。
最后答案看 dp[0][n-1] 是否 > 0,大于零说明先手能赢(先手比分差正)。
Java 实现
public class StoneGame {
public boolean stoneGame(int[] piles) {
int n = piles.length;
int[][] dp = newint[n][n];
// 长度为1的区间
for (int i = 0; i < n; i++) dp[i][i] = piles[i];
// 按区间长度扩展
for (int len = 2; len <= n; len++) {
for (int l = 0; l + len - 1 < n; l++) {
int r = l + len - 1;
int leftPick = piles[l] - dp[l + 1][r];
int rightPick = piles[r] - dp[l][r - 1];
dp[l][r] = Math.max(leftPick, rightPick);
}
}
return dp[0][n - 1] > 0;
}
// 小优化:O(n)空间的写法
public boolean stoneGameSpaceOptimized(int[] piles) {
int n = piles.length;
int[] dp = newint[n]; // 表示上一层(r 维度)的分差
for (int i = 0; i < n; i++) dp[i] = piles[i];
for (int l = n - 2; l >= 0; l--) {
for (int r = l + 1; r < n; r++) {
int takeL = piles[l] - dp[r];
int takeR = piles[r] - dp[r - 1];
dp[r] = Math.max(takeL, takeR);
}
}
return dp[n - 1] > 0;
}
}
复杂度与几个顺嘴提醒
时间 O(n²),空间 O(n²),用一维滚动数组可降到 O(n)。数据范围大时要注意整型溢出(用 long)。如果题目改成“分差相等算平局”,就判断 dp[0][n-1] ≥ 0。
为啥常见版本“先手必胜”?
经典版(LeetCode 877)里堆数是偶数且每堆石子数为正,先手总能预定奇偶位(全拿奇数位或全拿偶数位),先看哪一侧总和更大,就通过第一步把对手“锁”到另一侧,后续无论对手怎么选,先手都能把剩下的目标位拿完。这是一个构造性证明;但在代码实现里,用上面的 DP 更通用,改规则也能顶得住。
就这样,别被每步“看起来更大的那堆”骗了,算清分差,你基本就稳了。