#035. 编程五:二分、开方与蒙特卡洛
#学习目标:精度、代价与口径
前三章的编程题有一个共同点:答案是精确的,算法只关心快慢。本章加入另一个维度——精度。开方要输出到指定小数位,蒙特卡洛要回答「多少样本才够」,区间计数要处理端点开闭。一旦精度进入题面,三个新习惯必须养成:第一,先问「精度怎么定义」(第 \(p\) 位是截断还是四舍五入);第二,把「误差减半/缩小十倍」翻译成迭代的次数;第三,浮点类型的有效数字本身只有约 15–16 位十进制,\(p\) 更大时要换整数定点或高精度运算——这三句话正是面试官想听的。
本章的另一半是「看似 DP、实则各有专属模板」的一族题:素数步长走数组的路径 DP、花束收益 DP、青蛙过河的等待最小化、拆分元素使数组非降、按类别加权的产品利润,以及两个方向的区间计数。它们共同示范了量化面试编程题的典型难度:建模不超过动态规划入门,但边界与口径占了一半分数。
单调即可二分;可导且收敛好可用牛顿迭代。开方题把两者的迭代次数差(\(O(\log n + p)\) 对 \(O(\log p)\) 量级)摆在同一道题里。
蒙特卡洛估 π 是「均匀采样 + 拒绝判定」的最小样例;误差按 \(1/\sqrt n\) 衰减,每多一位十进制精度要一百倍样本。
「含 k 个不同值的最短窗口」与 033 章的最长窗口方向相反,收缩条件要反着写;区间计数用端点差分把逐点查询变成前缀和。
#知识点一:开方的三种迭代
题库里同一道「sqrt(number, precision)」出现了三次(源题 3、52、63),例如此题约定 \(\text{sqrt}(3,0)=1\)、\(\text{sqrt}(3,1)=1.7\)、\(\text{sqrt}(3,2)=1.73\)——注意 \(1.732\ldots\) 给出 \(1.73\),这是截断而非四舍五入。三种实现都要会,因为面试官会逐个追问复杂度。
实现一:二分区间
平方函数在正半轴单调,所以「找 \(x\) 使 \(x^2 = n\)」可以二分:维护不变量 \(\text{lo}^2 \le n \le \text{hi}^2\),中点偏小抬 lo、偏大压 hi,直到区间宽度小于 \(\varepsilon\)。要把 \(p\) 位小数做对,取 \(\varepsilon = 0.5\times 10^{-p}\)(半个最小刻度)。注意 \(n \lt 1\) 时根落在 \((n, 1)\),上界必须取 \(\max(1, n)\),否则区间一开始就错了。
实现二:牛顿迭代
解 \(f(x) = x^2 - n = 0\) 的牛顿法:在当前点作切线,取切线与 \(x\) 轴的交点为下一个近似:
怎么读:每一步把当前猜测换成「它与其倒数对称点 \(\frac{n}{x_t}\) 的平均」。它的收敛是二阶的:记 \(e_t = x_t - \sqrt n\),则
即误差被平方——粗略地说,正确的小数位数每步翻倍。二分要 \(p\) 位精度得跑 \(O(\log n + p)\) 步,牛顿在进入邻域后只要 \(O(\log p)\) 步量级。代价是对初值与函数性质有要求(本题任意正初值都收敛,是最friendly的场景)。
实现三:逐位生成
先求整数部分(在整数上二分),再从十分位起逐位贪心:对每一位从 9 往 1 试,取最大的使「已确定前缀 + 该位数字」的平方仍不超过 \(n\)。每位至多 9 次试探,总代价 \(O(\log n + 10p)\),而且它天然产出截断值——正对上 \(\text{sqrt}(3,2)=1.73\) 的口径。全整数实现可以做到任意精度而不受 double 限制,这是它存在的第二个理由。
| 方法 | 每步做什么 | 到 \(p\) 位小数的迭代次数 | 备注 |
|---|---|---|---|
| 二分区间 | 区间减半 | \(O(\log n + p)\) | 只要单调就成立,最稳 |
| 牛顿迭代 | 切线取交点 | 约 \(O(\log p)\)(有效数字翻倍) | 需可导;本题任意正初值收敛 |
| 逐位生成 | 每位试 9…1 | \(O(\log n + 10p)\) | 天然截断口径;整数版可任意精度 |
#知识点二:蒙特卡洛与 1/√n 律
向单位正方形 \([0,1]^2\) 均匀撒 \(n\) 个点,落在四分之一圆 \(x^2+y^2 \le 1\) 内的概率是面积比 \(p = \pi/4\)。统计命中率 \(\hat p = \text{hit}/n\),就得到 \(\hat\pi = 4\hat p\)。它是一个无偏估计,标准差由二项分布直接给出:
怎么读:\(n = 10^6\) 时标准差约 \(1.6\times 10^{-3}\)——想要第三位小数可信,样本要乘一百倍到 \(10^8\)。这就是蒙特卡洛的 \(1/\sqrt n\) 律:精度线性烧钱,指数烧样本。面试里这行推导比代码更值钱。
一、不要用 rand():它的周期短、低位质量差,考官听到会皱眉;标准答案是 mt19937 配 uniform_real_distribution。二、固定种子保证可复现——把种子作为参数传入,测试与调参才能重现同一条随机序列。若被追问「怎么降低方差」,答对偶变体(antithetic variates,把每个随机数 \(u\) 与 \(1-u\) 成对使用)或分层采样,各能给出常数因子的改进,但 \(1/\sqrt n\) 的阶不变。
#例题一:sqrt(number, precision) 三合一(源题 3、52、63)
例题 1(源题 3、52、63,同一题的三次收录):实现 sqrt(number, precision)——给定数与精度(小数位数),返回开方结果并分析时间复杂度。
思路。三种实现共用知识点一的推导:二分靠单调不变量,牛顿靠切线迭代,逐位靠每位贪心。以下给出全部三个函数。
// 实现一:二分区间。不变量:lo^2 <= n <= hi^2,宽度收敛到 eps
double mySqrtBisect(double n, double eps) {
double lo = 0, hi = max(1.0, n); // n < 1 时根在 (n,1),上界取 1
while (hi - lo > eps) {
double mid = (lo + hi) / 2;
if (mid * mid < n) lo = mid; // mid 偏小,答案在右半
else hi = mid; // mid 偏大,答案在左半
}
return (lo + hi) / 2;
}
// 实现二:牛顿迭代,切线与 x 轴的交点即下一个近似
double mySqrtNewton(double n, int digits) {
if (n == 0) return 0; // 0 直接返回,避免除零
double eps = pow(10.0, -digits) * 0.5; // 半个最小刻度
double x = max(1.0, n / 2); // 初值不必讲究,收敛极快
while (fabs(x * x - n) > eps)
x = (x + n / x) / 2; // x_{t+1} = (x_t + n/x_t)/2
return x;
}
// 实现三:逐位生成,整数部分二分求出,小数位从高到低贪心确定
double mySqrtDigits(double n, int digits) {
long long lo = 0, hi = 1, r = (long long)n; // 整数部分:倍增上界后二分
while (hi * hi <= r) hi <<= 1;
while (lo < hi) {
long long mid = lo + (hi - lo + 1) / 2; // 取上中位防死循环
if (mid * mid <= r) lo = mid; else hi = mid - 1;
}
double ans = lo, step = 0.1;
for (int k = 0; k < digits; ++k, step /= 10)
for (int d = 9; d >= 1; --d) // 从大到小试:天然截断口径
if ((ans + d * step) * (ans + d * step) <= n) { ans += d * step; break; }
return ans;
}
复杂度。二分 \(O(\log(n/\varepsilon)) = O(\log n + p)\) 次乘法;牛顿约 \(O(\log p)\) 次量级(二阶收敛);逐位 \(O(\log n + 10p)\) 次试探。三者空间都是 \(O(1)\)。
数值与边界。\(\text{sqrt}(3, 2)\):二分/牛顿给 \(1.7320\ldots\),按题意应输出截断值 \(1.73\)(逐位版直接给出)。\(n = 0\) 直接返回 0;\(n \lt 0\) 非法应断言;\(n \lt 1\) 时上界取 1;\(p\) 超过 double 的约 15 位有效数字时要转整数定点(把 \(n\) 放大 \(10^{2p}\) 倍后在整数上开方)或高精度库——能主动说出这句就是满分答案。
面试怎么讲。先写二分(最稳),再主动升级牛顿并给出误差平方公式,最后用逐位版解释截断与任意精度。复杂度对比表直接背:\(O(\log n + p)\)、\(O(\log p)\)、\(O(10p)\)。
#例题二:蒙特卡洛估 π(源题 44、58)
例题 2(源题 44 与 58 为同一题的两次收录):用蒙特卡洛方法估计 π。
建模。单位正方形内均匀采样,点落在四分之一圆内的概率为 \(\pi/4\);命中率乘 4 即估计值。
double monteCarloPi(long long n, unsigned seed = 42) {
mt19937 rng(seed); // 梅森旋转引擎:质量好且可复现
uniform_real_distribution<double> U(0.0, 1.0); // [0,1) 均匀分布
long long hit = 0;
for (long long i = 0; i < n; ++i) {
double x = U(rng), y = U(rng);
if (x * x + y * y <= 1.0) ++hit; // 落入四分之一圆
}
return 4.0 * hit / n; // 4 × 命中率即 π 的估计
}
误差与样本量。由知识点二,\(\operatorname{sd}(\hat\pi)\approx 1.64/\sqrt n\):\(n=10^4\) 约 \(\pm 0.016\),\(n = 10^6\) 约 \(\pm 0.0016\),\(n=10^8\) 约 \(\pm 0.00016\)。每多一位十进制精度,样本乘一百——这就是回答「多少样本才够」的模板。
复杂度与边界。时间 \(O(n)\)、空间 \(O(1)\)。判定用 \(x^2 + y^2 \le 1\) 避免开方;边界上的点计入圆内(测度为零,不影响统计);若考官要求置信区间,用正态近似报 \(\hat\pi \pm 1.96\,\operatorname{sd}\)。
面试怎么讲。三句话:几何概率是面积比;误差 \(1/\sqrt n\);要降方差可用对偶变体。若被问「为什么不用级数 \(\pi = 4(1-\frac13+\frac15-\cdots)\)」,回答:莱布尼茨级数收敛是 \(O(1/n)\) 且振荡,蒙特卡洛的 \(1/\sqrt n\) 配合独立性更易做置信区间——两句话就能显出知识面。
#例题三:素数家族(源题 39、46)
例题 3(源题 39):给定 \(n\),生成前 \(n\) 个非素数(合数)。
思路。埃拉托斯特尼筛(Sieve of Eratosthenes)先标出区间内全部素数,再依序收集非素数。上界先猜 \(2n\):合数密度随 \(n\) 增大趋于 1,\(2n\) 几乎总是够;不够就加倍重筛(均摊只多一个常数因子)。
vector<int> firstNComposites(int n) {
if (n <= 0) return {};
int B = max(10, 2 * n); // 先猜上界:合数约占大半
while (true) {
vector<char> isPrime(B + 1, 1);
isPrime[0] = isPrime[1] = 0; // 0、1 既非素数也通常不计入
for (int i = 2; (long long)i * i <= B; ++i) // 埃氏筛
if (isPrime[i])
for (int j = i * i; j <= B; j += i) isPrime[j] = 0;
vector<int> res;
for (int v = 4; v <= B && (int)res.size() < n; ++v)
if (!isPrime[v]) res.push_back(v); // 依序收集非素数
if ((int)res.size() == n) return res;
B *= 2; // 不够就加倍重来
}
}
数值结果。\(n = 6\) 得 \([4, 6, 8, 9, 10, 12]\)。
复杂度与边界。筛为 \(O(B \log\log B)\)、收集 \(O(B)\)。口径要问清:0 与 1 既非素数也非合数,默认从 4 开始;若考官把「非素数」理解为「含 1 的非素数」,首元素改成 1 即可。
例题 4(源题 46):数组每个格子有分值(正负皆可),你从数组外出发,每步可移动 1 格或移动「以 3 结尾的素数」格,落点分值累加,必须到达最后一格。求最大总得分(例 \([20,10,-30,40]\) 得 60)。
建模。把合法步长集合 \(S = \{1\} \cup \{3, 13, 23, 43, \ldots\}\)(以 3 结尾的素数)用筛子预处理到 \(n\),然后做路径 DP:\(dp[i]\) 为恰好落在 \(i\) 的最大得分,\(dp[i] = a[i] + \max_{s \in S,\ i - s \ge -1} dp[i - s]\),其中 \(i - s = -1\) 表示从数组外一步直达,取值 0。
long long maxScoreWalk(const vector<int>& a) {
int n = a.size();
if (n == 0) return 0;
vector<int> steps{1}; // 步长 1 恒合法
vector<char> comp(n + 1, 0); // 埃氏筛:comp[i]=1 为合数
for (int i = 2; i <= n; ++i) {
if (!comp[i]) {
if (i % 10 == 3) steps.push_back(i); // 以 3 结尾的素数是合法步长
for (long long j = 1LL * i * i; j <= n; j += i) comp[j] = 1;
}
}
const long long NEG = LLONG_MIN / 4;
vector<long long> dp(n, NEG); // dp[i]:恰好落在 i 的最大得分
for (int i = 0; i < n; ++i)
for (int s : steps) {
int from = i - s; // 上一个落点,-1 表示数组外
if (from == -1) dp[i] = max(dp[i], (long long)a[i]);
else if (from >= 0 && dp[from] > NEG)
dp[i] = max(dp[i], dp[from] + a[i]);
}
return dp[n - 1]; // 必须停在最后一格
}
数值结果。\([20,10,-30,40]\):\(dp[0]=20\)(步长 1 进场),\(dp[3] = 40 + \max(dp[2], dp[0]) = 40+20 = 60\)——先走 1 步吃 20、再走 3 步吃 40,跳过负分格,与题给答案一致。
复杂度与边界。筛 \(O(n \log\log n)\);DP 是 \(O(n \cdot |S|)\),而 \(|S|\)(不超过 \(n\) 的「尾 3 素数」个数)约为 \(\frac{n}{4\ln n}\),故总量 \(O(n^2/\log n)\),面试规模完全够用。负分全数组时答案可以是负数(必须到终点,无权放弃);分数和用 long long。若考官问「步长还能优化吗」,可答:对每个 \(i\) 取 \(\max\) 可用单调队列把 \(|S|\) 摊薄,但实现复杂度不值当——主动画界也是加分。
#例题四:最短窗口与网格三题(源题 47、48、60)
例题 5(源题 47):给定整数数组与整数 \(k\),返回包含 \(k\) 个不同值的最短子数组长度;不存在返回 -1,并给出复杂度。
思路。与 033 章的「最长含 k 种水果窗口」同骨架、方向相反:右端扩张,一旦窗口内不同值数 \(\ge k\) 就边记录边收缩左端。注意一个关键事实:最短的「至少含 \(k\) 个不同值」窗口必然恰好含 \(k\) 个——若含 \(k+1\) 个,删去一个边界值可得更短且仍达 \(k\) 个,矛盾。所以「至少」与「恰好」在本题答案一致,收缩条件写 \(\text{distinct} \ge k\) 即可。
int shortestWindowKDistinct(const vector<int>& a, int k) {
if (k <= 0) return 0; // 空窗口即含 0 个不同值
unordered_map<int, int> cnt;
int distinct = 0, best = INT_MAX;
for (int l = 0, r = 0; r < (int)a.size(); ++r) {
if (cnt[a[r]]++ == 0) ++distinct; // 右端进入:新值则计数
while (distinct >= k) { // 已达 k 个:尽量收缩
best = min(best, r - l + 1);
if (--cnt[a[l]] == 0) --distinct; // 左端移出
++l;
}
}
return best == INT_MAX ? -1 : best;
}
数值结果。\([1,1,2,2,3,4]\)、\(k=3\):最短窗口 \([2,3,4]\),长度 3。
复杂度与边界。左右指针各至多走 \(n\) 步,时间均摊 \(O(n)\)、空间 \(O(k)\)。不同值总数不足 \(k\) 返回 -1;\(k \le 0\) 返回 0。最大误区是把 033 章最长窗口的收缩条件(distinct 超过 \(k\) 才收)照搬过来——那是「最长」的逻辑,本题要「最短」,必须达标后立刻收缩。
例题 6(源题 48):一个门建模为 \(n \times m\) 的单位格网格,删去若干竖线(存于 v)与横线(存于 h)形成洞。求最大洞的面积。
建模。先与考官对齐口径:洞是被「仍然存在的线」围出的空腔。竖线位于 \(0..m\)、横线位于 \(0..n\),边框不可删。删去一段连续的线后,相邻两条剩余线之间的间隔就是洞的一维宽度;最大洞面积 = 竖向最大间隔 × 横向最大间隔。
long long largestHole(int n, int m, vector<int> v, vector<int> h) {
// 洞被「剩余的线」围出:一维最大间隔 = 相邻两条剩余线的最大距离
auto maxGap = [](int limit, vector<int>& removed) {
vector<char> gone(limit + 1, 0);
for (int x : removed)
if (x > 0 && x < limit) gone[x] = 1; // 边框 0 与 limit 不可删
long long best = 1, prev = 0; // 全未删时间隔为 1 格
for (int i = 1; i <= limit; ++i)
if (!gone[i]) { best = max(best, (long long)i - prev); prev = i; }
return best;
};
return maxGap(m, v) * maxGap(n, h); // 最大洞 = 最大宽 × 最大高
}
数值结果。\(5\times 5\) 网格删竖线 2、横线 2:剩余线 \(\{0,1,3,4,5\}\),最大间隔 2,洞面积 \(2\times 2 = 4\)(即原本 \(1,2\) 两行与 \(1,2\) 两列并成的大洞)。
复杂度与边界。时间 \(O(n + m + |v| + |h|)\)、空间 \(O(n + m)\)。另一常见口径是「把被删线本身当作洞的边界」(对 v 排序取相邻差乘积),答案会偏大——物理上那对应「被删线的位置仍能挡住洞」,一般不是本意。面试务必先复述模型再写码;一条线都没删时最大洞就是 \(1\times 1\)。
例题 7(源题 60):0/1 矩阵每行可循环左移或右移一格(一次操作)。求让某一列全为 1 的最少总操作数;不可能返回 -1。
思路。枚举目标列 \(c\):每行独立贡献代价——该行任选一个 1,把它移到第 \(c\) 列,循环距离为 \(\min(d, m-d)\),\(d = (c - p + m) \bmod m\)。对每行取最小代价求和,再对 \(c\) 取最小。全 0 行直接 -1。
int minShiftsToFullColumn(vector<vector<int>>& g) {
int n = g.size(), m = g[0].size();
vector<vector<int>> pos(n); // pos[i]:第 i 行 1 的列号
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
if (g[i][j] == 1) pos[i].push_back(j);
for (auto& p : pos) if (p.empty()) return -1; // 有全 0 行必无解
int best = INT_MAX;
for (int c = 0; c < m; ++c) { // 枚举目标列
int total = 0;
for (int i = 0; i < n; ++i) {
int cost = INT_MAX;
for (int p : pos[i]) { // 行内任选一个 1 移到第 c 列
int d = (c - p + m) % m; // 左移 d 步(右移 m-d 步)
cost = min(cost, min(d, m - d));
}
total += cost;
}
best = min(best, total);
}
return best;
}
数值结果。行 \(\langle 101, 010 \rangle\):目标第 0 列,第一行 1 已就位代价 0,第二行唯一的 1 在第 1 列、循环距离 \(\min(2,1)=1\),总计 1。
复杂度与边界。时间 \(O(m \cdot \#\text{1}) \le O(n m^2)\)、空间存 1 的位置 \(O(nm)\)。注意一行整体移动,所有 1 一起动,但只有落点列的 1 有用,故逐 1 取最小循环距离正确。矩阵已含全 1 列答案为 0;单列矩阵(\(m=1\))若有 0 同样 -1。
#例题五:DP 一族(源题 57、67、64、59、72、74)
例题 8(源题 57 与 67 为同一模型的花束两变体):花圃是 0/1 串(0 玫瑰、1 另一花种),只能用相邻花做花束。变体一(源题 57):「一红一白」相邻两朵卖 \(p\),「三朵玫瑰 000」卖 \(q\),求最大利润;变体二(源题 67):只有「000」卖 \(P\) 与「111」卖 \(Q\) 两种花束,求最大收益。
建模。区间型 DP:\(dp[i]\) 为前 \(i\) 朵花的最大利润,转移是「跳过第 \(i\) 朵」或「从第 \(i\) 朵起做一个合法花束跳 \(2\) 或 \(3\) 格」。这与 032 章的花束 DP 基型(AAA 与 AB 两种花束)完全同族,差别只在合法花束清单。
// 变体一(源题 57):相邻「01」卖 p,连续「000」卖 q
int maxBouquetProfit(const string& s, int p, int q) {
int n = s.size();
vector<int> dp(n + 1, 0); // dp[i]:前 i 朵的最大利润
for (int i = 0; i < n; ++i) {
dp[i + 1] = max(dp[i + 1], dp[i]); // 第 i 朵不用
if (i + 1 < n && s[i] != s[i + 1]) // 一红一白相邻
dp[i + 2] = max(dp[i + 2], dp[i] + p);
if (i + 2 < n && s[i] == '0' && s[i+1] == '0' && s[i+2] == '0')
dp[i + 3] = max(dp[i + 3], dp[i] + q); // 三朵玫瑰
}
return dp[n];
}
变体差异(源题 67)。把转移清单换成「\(\texttt{000} \to P\)、\(\texttt{111} \to Q\)」即可:\(dp[i+3] = \max(dp[i+3],\ dp[i] + P)\) 当 \(s[i..i+2]\) 全 0,全 1 时同理加 \(Q\);跳过转移保留。其余骨架、复杂度、边界与变体一完全一致——面试中先写一版,再说「换花束清单即可」,是最经济的表达。
复杂度与边界。时间空间均 \(O(n)\)。空串利润 0;\(p, q\)(或 \(P, Q\))可为 0,DP 自动处理「不值得做花束」的情形;两变体都不要求用完所有花,\(dp[n]\) 自然涵盖。
例题 9(源题 64):青蛙踩卵石过河,每跳至多跳过一块石头(跳 1 或 2 格),每块石头有等待时间 \(w_i\),青蛙必须经过第一块与最后一块石头。求最小总等待。
建模。爬楼梯型路径 DP:\(dp[i] = w_i + \min(dp[i-1], dp[i-2])\),起点 \(dp[0] = w_0\)(必须踩第一块),答案 \(dp[n-1]\)(必须停在最后一块)。总等待即所有落石的等待之和。
long long minTotalWait(const vector<int>& w) {
int n = w.size();
if (n == 0) return 0;
vector<long long> dp(n, LLONG_MAX / 4);
dp[0] = w[0]; // 必须从第一块出发
for (int i = 1; i < n; ++i) {
long long best = dp[i - 1]; // 跳 1 步
if (i >= 2) best = min(best, dp[i - 2]); // 至多隔一块:跳 2 步
dp[i] = w[i] + best; // 落石即等待
}
return dp[n - 1]; // 必须停在最后一块
}
数值结果。\(w = [5, 10, 3, 8]\):\(dp = [5, 15, 8, 16]\),路径 \(5 \to 3 \to 8\),答案 16。
复杂度与边界。时间 \(O(n)\)、空间可压到 \(O(1)\)。单块石头答案 \(w_0\);若「至多隔一块」改为「至多隔 \(k\) 块」,转移改为往前看 \(k+1\) 项取最小(可用单调队列优化)。等待时间累加用 long long。
例题 10(源题 59 与 72 同题两次收录):一次操作把数组某项拆成两个正整数之和(数组长度加一,可对同一项反复拆)。求使数组非降的最少操作数。
建模。关键洞察:把 \(x\) 拆成 \(k\) 段非降正整数、每段不小于前一项末段 \(L\),可行的最小末段是 \(\max(\lceil x/k \rceil, \max(L, 1))\),可行条件是 \(k \cdot \max(L,1) \le x\);代价为 \(k - 1\)。段数越多末段越小但代价越高,存在「末段小 vs 操作少」的权衡,简单贪心会翻车(如 \([1,6,4]\):整体保留 6 会在 4 处卡死,把 6 拆成 \(3+3\) 只要 1 次操作)。
算法(Pareto 前沿 DP)。状态是二元组(末段 \(L\),已用操作数 \(s\))。对每个 \(x\),从每个状态出发枚举 \(k\),只保留「末段严格变小」的候选(同末段更大代价被支配);转移后按 \(L\) 升序剪枝:代价不严格更小的状态全部丢弃。
int minSplitsNonDecreasing(const vector<int>& a) {
map<long long, int> cur; // Pareto 前沿:末段 -> 最少操作
cur[0] = 0; // 初始无元素,末段视为 0
for (int x : a) {
map<long long, int> nxt;
for (auto& [L, s] : cur) {
long long m = max(L, 1LL); // 每段至少为 1 且不低于前段
long long lastY = LLONG_MAX;
for (long long k = 1; k * m <= x; ++k) {
long long y = max((x + k - 1) / k, m); // 拆 k 段的最小末段
if (y >= lastY) continue; // 末段未变小:只贵不优
lastY = y;
int cost = s + (int)k - 1;
if (!nxt.count(y) || nxt[y] > cost) nxt[y] = cost;
if (y == m) break; // 已到下界,再拆无意义
}
}
cur = move(nxt);
if (cur.empty()) return -1; // 无法变成非降
int best = INT_MAX; // 支配剪枝:保留代价严格递减者
map<long long, int> keep;
for (auto& [L, s] : cur)
if (s < best) { keep[L] = s; best = s; }
cur = move(keep);
}
int ans = INT_MAX;
for (auto& [L, s] : cur) ans = min(ans, s);
return ans;
}
数值结果。\([1,6,4]\):处理 6 后前沿为 \(\{1{:}5,\ 2{:}2,\ 3{:}1,\ 6{:}0\}\)(末段:操作数);处理 4 时从 \((3,1)\) 整体保留得到末段 4、操作 1——即 \(1,3,3,4\),答案 1。
复杂度与边界。每元素候选末段只有 \(O(\sqrt x)\) 个(\(\lceil x/k\rceil\) 的不同取值),前沿长度被剪枝压到很小,总量约 \(O(n \sqrt{\max a})\);若值很大可用整除分块跳过重复的 \(k\)。已是非降的数组答案 0;首元素也可拆(前沿含其各分段)。若考官限定「每项至多拆一次」,把 \(k\) 循环删到只剩 \(k \le 2\) 即可,其余不变。
例题 11(源题 74):产品有价格与类别,可自选卖出顺序;卖出一件产品的利润 = 价格 ×(截至并包含该件的已售类别数)。求最大总利润。
建模。设共 \(C\) 类。类别「首秀」依次把乘数从 1 抬到 \(C\);非首秀产品只有在全部 \(C\) 类亮相之后卖出才能拿到最大乘数 \(C\)。因此最优结构:每类挑最便宜的一件当首秀(共 \(C\) 件,按价格升序卖出,第 \(k\) 件乘 \(k\)),其余全部压后乘 \(C\)。
交换论证。其一,首秀件换成本类更便宜的件不减少其余乘数、只降首秀总价;其二,两件首秀「贵者先卖」会以大乘数配小价、小乘数配大价,交换后总和只增不减(排序不等式);其三,任何在首秀完成前卖出的非首秀件,挪到全部首秀之后乘数不减。
long long maxCategoryProfit(const vector<long long>& price,
const vector<int>& cat) {
map<int, long long> cheapest; // 每类最便宜的产品打头阵
long long total = 0;
for (size_t i = 0; i < price.size(); ++i) {
total += price[i];
if (!cheapest.count(cat[i])) cheapest[cat[i]] = price[i];
else cheapest[cat[i]] = min(cheapest[cat[i]], price[i]);
}
vector<long long> d;
for (auto& [c, p] : cheapest) d.push_back(p);
sort(d.begin(), d.end()); // 最便宜的先卖,乘小系数
long long C = d.size(), debut = 0, ans = 0, k = 0;
for (long long p : d) { ans += p * ++k; debut += p; } // 第 k 个首秀乘 k
ans += (total - debut) * C; // 其余产品乘 C
return ans;
}
数值结果。价格 \([10,5,8]\)、类别 \([A,A,B]\):A 的首秀取 5、B 取 8,卖出顺序 \(5 \times 1 + 8 \times 2 + 10 \times 2 = 41\),优于把 8 放首位的 38。
复杂度与边界。时间 \(O(n \log n)\)(排序首秀价)、空间 \(O(C)\)。乘积用 long long;若价格可为负,应讨论「不卖」的口径(题意通常默认全卖);单类别(\(C=1\))时全部乘 1,直接返回总价。
#例题六:区间计数与三个速答(源题 65、70、38、45、68)
例题 12(源题 65 与 70 为一体两面):给定一组闭区间与一组数。源题 70 问每个数落在多少个区间内;源题 65 把区间视为过滤器,问多少信号满足所有过滤器。
思路。源题 70 是端点差分:区间 \([l, r]\) 在 \(l\) 处 \(+1\)、\(r+1\) 处 \(-1\),前缀和展开后每点得到覆盖数,查询二分定位。源题 65 更简单:全部区间的交集是 \([\max l, \min r]\),统计落在交集内的信号即可。
// 源题 70:每个数落在多少个闭区间内(端点差分 + 前缀展开 + 二分)
vector<int> intervalCoverage(vector<vector<int>>& iv, const vector<int>& xs) {
map<long long, long long> diff; // [l,r] 记 +1@l、-1@(r+1)
for (auto& e : iv) { diff[e[0]] += 1; diff[e[1] + 1] -= 1; }
vector<pair<long long, long long>> pre; // (坐标, 自该点起生效的覆盖数)
long long run = 0;
for (auto& [p, d] : diff) { run += d; pre.push_back({p, run}); }
vector<int> res;
for (long long x : xs) {
auto it = upper_bound(pre.begin(), pre.end(), make_pair(x, LLONG_MAX));
res.push_back(it == pre.begin() ? 0 : prev(it)->second);
}
return res;
}
// 源题 65:多少信号落在所有区间(即区间交集)内
int signalsInAll(vector<vector<int>>& iv, const vector<int>& sig) {
long long lo = LLONG_MIN / 2, hi = LLONG_MAX / 2;
for (auto& e : iv) { lo = max(lo, (long long)e[0]); hi = min(hi, (long long)e[1]); }
int ans = 0;
for (int x : sig) ans += (lo <= x && x <= hi);
return ans; // 交集为空时自然得 0
}
数值结果。区间 \(\{[1,3],[2,5]\}\):点 2 覆盖数 2,点 4 覆盖数 1;交集 \([2,3]\),信号 \(\{2, 3, 4\}\) 中 2 个合规。
复杂度与边界。源题 70:\(O((|\text{iv}| + q)\log |\text{iv}|)\);源题 65:\(O(|\text{iv}| + |\text{sig}|)\)。闭区间端点计入(\(r+1\) 处减一是关键,写成 \(r\) 会漏掉右端点);坐标很大时用有序 map 而非数组差分;交集为空返回 0。
例题 13(源题 38,金融口径):计算夏普比率时删去收益为 0 的日子会怎样?若 \(n\) 天非零、\(n\) 天为零,正确与错误口径之比的范围是多少?
推导。在日均值远小于日波动的常规近似下(\(\mu \approx 0\)),删去 \(N\) 天中的 \(k\) 个零日:均值量级不变,而日度标准差按 \(\sqrt{N/(N-k)}\) 放大,于是错误夏普 / 正确夏普 \(\approx \sqrt{N/(N-k)}\)。对半情形 \(k/N = 1/2\) 时比值恰为 \(\sqrt 2\);一个零日都不删时为 1。结论一句话:删零收益日只会高估夏普,对半零日时虚高 \(\sqrt 2\) 倍,一般界于 \([1, \sqrt 2]\)(当 \(\mu\) 与 \(\sigma\) 可比时偏差还会更大,完整口径在 037 章夏普比率专题展开)。
面试怎么讲。先点破本质:零日不贡献均值却贡献「分母里的天数」,删零等于偷偷换年化与波动口径;再给出对半情形的 \(\sqrt 2\) 与区间 \([1, \sqrt 2]\);最后主动补一句「极端行情下 \(\mu\) 不可忽略时公式要重推」。
例题 14(源题 45,概率题):8 男 9 女随机排成一排,求异性相邻对(可重叠)的期望个数。
推导。17 人共 16 个相邻对。对任一指定相邻对,其两位置为一男一女的概率是 \(2 \cdot \frac{8}{17} \cdot \frac{9}{16} = \frac{9}{17}\)。由期望的线性(指示变量求和,重叠不影响):
检验。男女各半时该概率应为 \(\frac12\),此处 \(\frac{9}{17} \lt \frac12\) 略小(少数性别占比略低),方向正确。指示变量法的完整工具箱见 021 章。
例题 15(源题 68,概率题):\(X, Y\) 独立同服从标准正态,求 \(P(X + Y \gt 0 \mid X \gt 0)\)。
推导。旋转变换 \(U = \frac{X+Y}{\sqrt 2},\ V = \frac{X-Y}{\sqrt 2}\):\((U, V)\) 仍是独立标准正态(正交变换保联合正态与各向同性)。条件 \(X \gt 0\) 即 \(U + V \gt 0\),于是所求为 \(P(U \gt 0 \mid U + V \gt 0)\)。区域 \(\{U \gt 0\} \cap \{U + V \gt 0\}\) 是过原点两条直线夹出的、角度为 \(\frac{3\pi}{4}\) 的楔形;各向同性下点落在其中的概率正比于角度:\(P(U \gt 0,\ U+V \gt 0) = \frac{3\pi/4}{2\pi} = \frac38\),而 \(P(U + V \gt 0) = \frac12\),故
检验。\(X+Y\) 与 \(X\) 正相关,条件应把概率抬到 \(\frac12\) 之上;\(\frac34\) 合理。旋转与楔形角度是条件概率对称性工具的标准应用,更多例子见 020 章。
#误区与边界
一、精度口径:sqrt 的第 \(p\) 位是截断还是四舍五入,先问再写,逐位法天然截断;二、浮点极限:double 约 15–16 位有效数字,\(p\) 更大要换整数定点或高精度;三、二分上界:\(n \lt 1\) 时开方结果大于 \(n\),上界取 \(\max(1, n)\);四、终止条件:以「区间宽度小于 \(\varepsilon\)」或「残差平方小于 \(\varepsilon\)」停机,别用「恰好相等」——浮点里等不来。
一、最短窗口不是最长窗口:达标即收缩,条件 \(\text{distinct} \ge k\);二、源题 60 的循环移位是整行一起动,别按列独立处理;三、源题 59/72 的拆分题,「能不拆就不拆」的贪心是错的(\([1,6,4]\) 反例),必须用「末段—操作数」的 Pareto 权衡。另外,网格开洞(源题 48)与区间口径(65/70 的闭端点)都属于「先复述模型再动笔」型——把口径说清本身就是得分点。
#检查清单
- 我能写出二分、牛顿、逐位三种开方并口述复杂度 \(O(\log n + p)\)、\(O(\log p)\)、\(O(10p)\) 与截断/舍入口径。
- 我能推导蒙特卡洛估 π 的标准差 \(\approx 1.64/\sqrt n\),并回答「每多一位精度要一百倍样本」。
- 我知道用 mt19937 而非 rand(),固定种子可复现,并能提一句对偶变体降方差。
- 我能用埃氏筛生成前 n 个非素数,并用筛出的「尾 3 素数」步长集做最大得分路径 DP。
- 我能区分最长与最短滑动窗口的收缩条件,并解释「最短且至少 k 个不同值必然恰好 k 个」。
- 我能写花束 DP 的一个变体并说明换花束清单即得另一变体;能用三行写青蛙过河的等待 DP。
- 我能解释拆分非降题为何贪心失效、如何用「末段—操作数」Pareto 前沿求解。
- 我能用交换论证证明类别利润题的结构(最便宜首秀升序卖、其余压后乘 C),并用端点差分做区间覆盖计数。