- 科学计算
【免费下载链接】codeforces-go
算法竞赛模板库 by 灵茶山艾府 💭💡🎈
本篇文章以 leetcode/biweekly/177/README.md 为骨架,完整梳理力扣第 177 场双周赛(Biweekly Contest 177)的四道题:Smallest Pair with Different Frequencies、Merge Close Characters、Minimum Operations to Make Array Parity Alternating与Sum of K-Digit Numbers in a Range。文章将逐题给出思考过程、正确性论证、Python / Java / C++ / Go 四语言实现与复杂度分析,并结合当前 codeforces-go 仓库中对应的 Go 源码(a/a.go、b/b.go、c/c.go、d/d.go)与测试用例(a/a_test.go 等)做源码级佐证。读完本文,你将掌握"统计频次后的贪心选择""栈模拟消除类问题""奇偶交替的通用化建模"以及"贡献法 + 快速幂 + 模逆元"四类高频竞赛技巧。
赛题总览
本场双周赛的四道题由易到难,恰好覆盖了算法竞赛中四类最常用的思维工具:
| 题号 | 题目核心 | 核心算法 | 时间复杂度 |
|---|---|---|---|
| Q1 | 找出现次数不同且值最小的数对 | 哈希表计数 + 贪心 | O(n) |
| Q2 | 按间隔规则合并/消除靠近字符 | 栈模拟 + 位置记录 | O(n) |
| Q3 | 用最少操作把数组改成奇偶交替 | 贪心 + 分类讨论 | O(n) |
| Q4 | 求 k 位数区间内所有数字之和 | 贡献法 + 快速幂 + 逆元 | O(log k) |
仓库中每题均配有独立的说明文档(a/、b/、c/、d/目录下的 README.md)、Go 实现(*.go)与测试文件(*_test.go),测试由 copypasta/template/leetcode/generator_test.go 生成的骨架驱动,通过testutil.RunLeetCodeFuncWithFile配合题目的输入输出样例文件(如a.txt)自动校验。
Q1:Smallest Pair with Different Frequencies —— 频次不同的最小数对
题意与无解判断
题目要求在数组中找出两个数x < y,且x与y在数组中的出现次数(频次)不同。若所有数的出现次数都相同,则无解。
无解的判定:如果nums中每个数的出现次数都一样,那么任意两个数的频次都相等,直接返回[-1, -1]。
贪心构造:为什么取全局最小值为 x 一定最优
核心观察来自 a/README.md:
- 只要有任意两个出现次数不同的元素,把全局最小值
min(nums)作为x即可满足条件。因为x是最小值,任何另一个出现次数不同的数必然大于x,题目要求的x < y自动成立。 - 对于
y,只需在nums中选出出现次数不等于x的出现次数的最小元素即可。
这样既保证了x < y(构造上天然成立),又让(x, y)在满足条件的所有数对中字典序最小。统计出现次数用哈希表(或用数组,当值域较小时)即可。
四语言实现
Python 版核心逻辑(Counter 统计 + 生成器筛选):
class Solution: def minDistinctFreqPair(self, nums: List[int]) -> List[int]: cnt = Counter(nums) mn = min(nums) cnt_min = cnt[mn] min_y = min((y for y, c in cnt.items() if c != cnt_min), default=None) if min_y is None: return [-1, -1] return [mn, min_y]Go 版(与仓库 a/a.go 一致,注意minY == math.MaxInt作为"不存在"的哨兵):
func minDistinctFreqPair(nums []int) []int { cnt := map[int]int{} mn := math.MaxInt for _, x := range nums { cnt[x]++ mn = min(mn, x) } cntMin := cnt[mn] minY := math.MaxInt for y, c := range cnt { if c != cntMin { minY = min(minY, y) } } if minY == math.MaxInt { return []int{-1, -1} } return []int{mn, minY} }复杂度
- 时间复杂度:O(n),其中 n 是
nums的长度。 - 空间复杂度:O(n)(哈希表开销)。
仓库中对应的测试入口位于 a/a_test.go,样例数据见a.txt,可直接go test ./leetcode/biweekly/177/a/验证。
Q2:Merge Close Characters —— 栈模拟的"靠近字符合并"
问题模型:这不是真正的"消除"
本题的"合并/消除"规则与力扣常见的邻项消除问题(如 1047 删除字符串中的所有相邻重复项)思路同源,但判定条件改为:新遍历到的字符下标与它在栈(保留串)中最后一次出现的最大下标之差 > k 时,才把该字符入栈,否则忽略。详细推导见 b/README.md。
用栈保存未被消除的字符时,未被消除的字符都在栈中,因此新遍历到的字符的下标就是栈的大小len(st)——这是本题建模的关键一步,它把"原始下标"问题转化成了"栈大小"问题。
判定式:
len(st) - last[ch] > k其中last[ch]记录字符ch在栈中的最新位置。为了 O(1) 查询,可以用长度 26 的数组(或哈希表)保存每个字符在栈中的最新下标。
正确性直觉
逐个字符扫描,凡是"离同类字符太近"(距离 ≤ k)的字符都会被丢弃,最终栈内保留下来的字符满足任意两个相同字符在保留串中的间距都严格大于 k。这是典型的在线贪心:对每个字符只做一次入栈/丢弃决策,无需回溯。
实现细节:初始值的选取
Go 实现在仓库 b/b.go 中有一个值得注意的细节:last数组初始化为-k-1,保证首次遇到某个字母时len(ans)-last[i] > k恒为 true,从而必然入栈:
func mergeCharacters(s string, k int) string { last := [26]int{} for i := range last { last[i] = -k - 1 // 保证首次遇到字母 i 时,len(ans)-last[i] > k 是 true } ans := []byte{} for _, ch := range s { // ch 在 ans 中的下标是 len(ans) if len(ans)-last[ch-'a'] > k { last[ch-'a'] = len(ans) ans = append(ans, byte(ch)) } } return string(ans) }Python/Java/C++ 版本则使用-inf(或Integer.MIN_VALUE / 2、INT_MIN / 2)作为初始哨兵,效果相同。注意 Java/C++/Go 中使用MIN_VALUE / 2而非MIN_VALUE,是为了避免后续减法运算溢出。
复杂度
- 时间复杂度:O(n)(使用定长 26 数组)或 O(n + |Σ|),其中 |Σ| = 26 是字符集大小(创建数组本身需要 O(|Σ|) 时间)。
- 空间复杂度:O(|Σ|)(返回值不计入)。
顺带一提,仓库 b/4019/README.md 注明本题与力扣第 3853 题"合并靠近字符"完全相同,属于同一道题的换壳复现,值得在题单中互相参照。
Q3:Minimum Operations to Make Array Parity Alternating —— 奇偶交替的最小操作数与极差
两种目标奇偶模式
要把nums变成奇偶交替,只可能对应两种全局模式:
- 偶奇偶奇偶奇……
- 奇偶奇偶奇偶……
因此可以枚举这两种情况分别计算,再取最优(详见 c/README.md)。
核心贪心事实:每个元素至多操作一次
遍历过程中,若nums[i]的奇偶性不等于目标奇偶性,则操作一次(+1或-1)后其奇偶性必定反转,从而必然等于目标奇偶性。因此每个元素要么不操作,要么恰好操作一次,不需要考虑多次操作。
通用做法 vs 特殊做法
文档给出了两条路线:
通用做法(可迁移到 632. 最小区间):对每个x = nums[i],不操作视作列表[x],操作视作列表[x-1, x+1],于是得到 n 个列表;问题等价于找一个最短的值域范围[a, b]覆盖每个列表中的至少一个数——这正是 632. 最小区间 的模型,可排序后滑动窗口求解。
针对本题的特殊做法(O(n) 贪心):设全局最小值gMin = min(nums)、全局最大值gMax = max(nums),分类讨论修改策略:
- 若
n = 1:无需修改,返回[0, 0]。 - 若
gMin = gMax:规定要修改的数统一加一,则最终极差为 1(若有的加一有的减一,极差会变成 2,不优)。 - 若
gMin + 1 = gMax:等于gMin的数加一,等于gMax的数减一,最终极差为 1。 - 若
gMin + 1 < gMax:等于gMin的加一、等于gMax的减一;区间[gMin+1, gMax-1]内的中间值保持不动即可,因为它们总可以落在新的最小最大值之间,不影响极差(n ≥ 2 的奇偶交替数组极差天然至少为 1)。
结论:对需要修改的数,等于gMin则加一,等于gMax则减一,其余情况不修改。
奇偶性判定的位运算技巧
对目标模式target(0 表示以偶开头,1 表示以奇开头),位置i上应有的奇偶性是target ^ (i % 2)。代码中统一使用等价写法:
if (x-i)&1 != target { // 等价于 x&1 != target ^ i%2 op++ ... }这一写法避免了在每个位置重新计算target ^ i%2,同时保持逻辑完全等价。仓库实现见 c/c.go。
Go 实现(双模式枚举 + 极差兜底)
func makeParityAlternating(nums []int) []int { if len(nums) == 1 { return []int{0, 0} } gMin := slices.Min(nums) gMax := slices.Max(nums) f := func(target int) (int, int) { op, mn, mx := 0, math.MaxInt, math.MinInt for i, x := range nums { if (x-i)&1 != target { // 等价于 x&1 != target ^ i%2 op++ if x == gMin { x++ } else if x == gMax { x-- } } mn = min(mn, x) mx = max(mx, x) } return op, max(mx-mn, 1) // 在 n >= 2 的情况下,极差至少是 1 } op1, minD1 := f(0) op2, minD2 := f(1) if op1 < op2 || op1 == op2 && minD1 < minD2 { return []int{op1, minD1} } return []int{op2, minD2} }注意两点工程细节:一是返回max(mx-mn, 1)对极差做兜底(n ≥ 2 时奇偶交替数组极差至少为 1);二是两个模式都算完后,优先比较操作数,操作数相同再比较极差(Go 代码op1 < op2 || op1 == op2 && minD1 < minD2的短路语义即表达这一规则)。
复杂度
- 时间复杂度:O(n)。
- 空间复杂度:O(1)。
Q4:Sum of K-Digit Numbers in a Range —— 贡献法 + 快速幂 + 模逆元
贡献法:把"数位之和"拆成"每一位的贡献"
这是全场比赛最有思维含量的一题(推导过程见 d/README.md)。核心思路是贡献法:答案本质上是"一堆数字相加",其中大量数字是重复的,可以按位统计每个数位值出现了多少次,从而算出它对总和的贡献。
以k = 3(三位数)、ℓ = 2、r = 5为例:当十位数填 5 时,百位数有r - ℓ + 1 = 4种填法,个位数也有 4 种填法,共4² = 16个三位数的十位是 5。由于456 = 400 + 50 + 6,十位上的 5 实际代表 50,这 50 在 16 个不同的三位数中出现,因此十位填 5 的贡献是50 × 16 = 800。
一般化公式推导
一般地,在从低到高第i位(i 从 0 开始)上填x(ℓ ≤ x ≤ r),相当于填了x·10^i。其余k-1位每位都有m = r - ℓ + 1种填法,共m^(k-1)种填法,因此x对答案的贡献为:
x · 10^i · (r - ℓ + 1)^(k-1)枚举x与i并求和,利用等差数列求和(Σx)与等比数列求和(Σ10^i)可将双层枚举化简为闭式:
(ℓ + r)·m / 2 · (10^k − 1) / 9 · m^(k−1)其中(ℓ + r)·m / 2来自等差数列求和,(10^k − 1) / 9 = 1 + 10 + ... + 10^(k−1)来自等比数列求和。
模运算三件套:快速幂、除法转逆元、负数修正
由于答案可能非常大,需要在模1_000_000_007下计算,此时:
10^k与m^(k−1)需要快速幂(倍增法,O(log k));- 除以
2与除以9必须换成乘对应的模逆元:2 的逆元与9 的逆元。由于18 = 2 × 9,代码里直接乘pow(18, MOD-2)(费马小定理求逆元)一步到位; pow(10, k) - 1在取模后可能为负数,需要+ MOD修正。
仓库 Go 实现 d/d.go:
const mod = 1_000_000_007 func pow(x, n int) int { res := 1 for ; n > 0; n /= 2 { if n%2 > 0 { res = res * x % mod } x = x * x % mod } return res } func sumOfNumbers(l, r, k int) int { m := r - l + 1 return (l + r) * m * (pow(10, k) - 1 + mod) % mod * pow(18, mod-2) % mod * pow(m, k-1) % mod }Python 版更简洁,用内置pow(x, n, MOD)与pow(18, -1, MOD)直接求逆元:
class Solution: def sumOfNumbers(self, l: int, r: int, k: int) -> int: MOD = 1_000_000_007 m = r - l + 1 return (l + r) * m * (pow(10, k, MOD) - 1) * pow(18, -1, MOD) * pow(m, k - 1, MOD) % MOD复杂度
- 时间复杂度:O(log k)(快速幂主导)。
- 空间复杂度:O(1)。
总结:四题串起的四条方法论
- Q1演示了"先保证构造可行性(取最小值让 x < y 自动成立),再做局部最优选择"的贪心模板;
- Q2演示了栈模拟消除类问题的通用建模:用
len(st)代表当前新字符的下标,配一个last[]数组做 O(1) 的最近位置查询; - Q3演示了奇偶交替类问题的两种处理层级:通用化转化为"最小区间覆盖"模型(可迁移到 632 题),以及针对本题的 O(1) 空间的分类讨论贪心;
- Q4演示了贡献法的完整闭环:按位拆贡献 → 等差数列/等比数列求和化简 → 快速幂 + 模逆元落地。
如果你想进一步巩固这些技巧,仓库中 leetcode/biweekly/177/a/README.md、leetcode/biweekly/177/b/README.md、leetcode/biweekly/177/c/README.md、leetcode/biweekly/177/d/README.md 保留了逐题的完整推导;而各目录下的a.go/b.go/c.go/d.go与其*_test.go(例如 a/a_test.go)构成了"题解即代码、代码即测试"的可复现闭环——在仓库根目录执行go test ./leetcode/biweekly/177/...即可一键跑通本场全部样例,作为日常刷题与复盘的标准入口。
- 科学计算
【免费下载链接】codeforces-go
算法竞赛模板库 by 灵茶山艾府 💭💡🎈
相关推荐
力扣双周赛 159 Q1 题解:奇偶交替的最小相邻交换次数——codeforces-go 仓库源码解析
力扣双周赛 159 Q1 题解:奇偶交替的最小相邻交换次数——codeforces go 仓库源码解析 导读 本文基于 codeforces go 仓库中 双周
科学计算codeforces-go 仓库实战:力扣双周赛 104「英雄的力量」贡献法递推题解全解析
codeforces go 仓库实战:力扣双周赛 104「英雄的力量」贡献法递推题解全解析 导读 本篇技术指南以仓库中 双周赛 104 第四题题解 https:
科学计算力扣双周赛 134 题解:交替组计数、贪心取点与 AND 值为 k 的子数组枚举(codeforces-go 仓库配套实现)
力扣双周赛 134 题解:交替组计数、贪心取点与 AND 值为 k 的子数组枚举(codeforces go 仓库配套实现) 本篇文章以 leetcode/bi
科学计算
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考