尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

前缀和与哈希表:高效解决子数组和等于k的幂次问题

前缀和与哈希表:高效解决子数组和等于k的幂次问题 1. 问题背景与核心挑战最近在整理一些经典的算法竞赛题目打算给团队里的新人做做训练翻到了Codeforces Round #400的这道C题题目叫“Molly‘s Chemicals”。这道题在当年比赛时卡住了不少人包括一些经验丰富的选手原因在于它表面上看起来是一个简单的子数组求和问题但数据范围和约束条件让它变得非常棘手。很多人的第一反应是暴力枚举所有子数组这在n最大为1e5的情况下O(n²)的复杂度是绝对会超时的。这道题的精妙之处在于它要求你快速判断一个数组里有多少个连续子数组的和恰好等于某个整数k的幂次k^0, k^1, k^2, ...。这不仅仅是考察前缀和更是对问题转化、边界处理和数据结构哈希表应用能力的综合考验。我带着团队复盘这道题时发现即使理解了前缀和哈希表的大方向在实现细节上依然有很多坑比如k1或k-1时的特判以及如何高效枚举所有可能的“目标值”。今天我就结合这道具体的题目把从问题抽象、思路推导到代码实现和调试的完整过程拆解一遍希望能帮你彻底吃透这类“前缀和配合特定目标值计数”的问题模型。2. 问题模型抽象与暴力解法的局限性题目给我们的核心信息是有一个长度为nn ≤ 1e5的整数数组a和一个整数k|k| 1 或 k ±1。我们需要找出这个数组里有多少个连续子数组即a[l], a[l1], ..., a[r]其元素之和等于k的某个整数次幂包括零次幂即1。也就是说目标值可以是..., k^{-2}, k^{-1}, 1, k, k², k³, ...中的任何一个。最直观的想法就是暴力枚举。我们枚举所有可能的子数组起点l和终点r计算sum(a[l..r])然后判断这个和是否在k的幂次集合里。计算子数组和如果每次都从头累加复杂度是O(n³)如果使用前缀和预处理使得计算任意子数组和的时间降到O(1)那么总复杂度是O(n²)。对于n1e5n²是1e10这在2秒的典型竞赛时间限制内是绝对无法通过的。所以暴力枚举的路被堵死了。我们必须寻找O(n log n)甚至O(n)的算法。这里的关键洞察在于“连续子数组和”与“前缀和”的关系。定义前缀和数组pref[i] a[0] a[1] ... a[i-1]通常pref[0]0。那么子数组a[l..r]的和就等于pref[r1] - pref[l]。我们的目标就是找到满足pref[r1] - pref[l] target的(l, r)对的数量其中target是k的某个幂次。将等式变形pref[r1] pref[l] target。也就是说对于当前遍历到的位置i对应pref[i]如果我们想知道以i-1为结尾的子数组有多少个和等于target我们只需要看看在i之前的前缀和pref[j]j i中有多少个的值等于pref[i] - target。因为pref[i] - target pref[j]就意味着子数组a[j..i-1]的和恰好是target。于是问题转化为遍历前缀和数组对于每个pref[i]我们需要快速查询在它之前出现过的所有pref[j]中值等于pref[i] - target的j有多少个。并且我们需要对每一个可能的target即k的幂次都进行这样的查询和累加。这自然让我们联想到使用哈希表在C中是unordered_map在Python中是dict或defaultdict来记录某个前缀和值之前出现的次数。这样每次查询的复杂度是O(1)。3. 算法核心前缀和与哈希表的配合基于上面的分析我们可以勾勒出算法的骨架预处理计算前缀和数组pref其中pref[0] 0pref[i] pref[i-1] a[i-1]。初始化创建一个哈希表count_map用于记录遍历过程中某个前缀和值出现的次数。初始时count_map[0] 1这对应着空子数组起点在0之前的前缀和为0。遍历与查询从i 1到n对应原数组从a[0]到a[n-1]进行遍历 a. 对于当前pref[i]我们需要枚举所有可能的target值即k的幂次。 b. 对于每一个target计算need pref[i] - target。 c. 在哈希表count_map中查询need出现的次数这个次数就代表了以i-1为结尾、和为target的子数组个数。将这个次数累加到最终答案ans中。 d. 将当前前缀和pref[i]加入哈希表即count_map[pref[i]]供后续位置查询。这个思路的核心时间复杂度取决于内层循环——即对于每个位置i我们需要枚举多少个target如果k的幂次可以无限增长那岂不是要枚举无限多个这里就需要利用题目条件进行剪枝。4. 目标值枚举的边界与特判处理target是k^p其中p是整数。由于数组元素和前缀和都是整数题目给定数组元素范围为[-1e9, 1e9]target也必须是整数。同时数组所有元素的和是有限的最极端情况每个元素1e9n1e5总和约为1e14。因此有效的target值必须在[-1e14, 1e14]这个数量级内。这给了我们枚举p的边界。我们需要枚举所有满足|k^p| max_possible_sum且|k^p|是整数的p。其中max_possible_sum可以粗略估计为n * max(|a[i]|) 1e5 * 1e9 1e14。但更精确和安全的做法是在枚举过程中如果k^p的绝对值超过了可能的最大前缀和范围就停止枚举。由于前缀和可能为负我们关心的是绝对值。这里有两个非常棘手的特殊情况k 1和k -1。当 k 1 时k^p永远等于1。也就是说target只有一个值1。我们只需要在算法中寻找和为1的子数组即可。枚举p的循环会变成无限循环因为1的任何次幂都是1所以必须单独处理。当 k -1 时k^p在1和-1之间交替。即target的可能值只有两个1 和 -1。同样枚举p的循环也会在两个值间无限振荡需要单独处理。当 |k| 1 时k^p的绝对值会随着|p|增大而指数级增长。由于前缀和总和有限p的枚举范围其实很小。我们可以从p0开始计算k^p只要其绝对值不超过一个设定的上限比如1e14就将其加入目标值集合然后p继续计算。但要注意k可能是负数所以k^p可能正负交替。同时我们还需要考虑负指数幂吗即p可以为负吗题目描述中“某个整数次幂”通常指非负整数次幂但为了严谨我们看样例如果k2,p-1得到0.5不是整数不符合。只有当k1或k-1时负指数幂才是整数±1但这已经包含在上述特例中。对于其他k负指数幂是分数不可能等于整数前缀和之差。因此我们只需要枚举p 0的情况。然而还有一个隐藏的坑整数溢出。当k较大例如k1e9且p2时k^p就达到了1e18这已经超出了64位有符号整数long long的范围约±9e18虽然还没到我们设定的上限1e14但计算过程中可能溢出。更稳妥的做法是在循环计算k^p时每次乘完k之后就判断其绝对值是否超过一个合理的上限例如1e14 * 2留一些余量如果超过就停止枚举。因为一旦k^p的绝对值远大于所有可能的前缀和它就不可能成为某个子数组的和。具体实现时我们可以这样处理目标值枚举将可能的目标值收集到一个列表targets中。首先加入1即k^0。如果k 1列表就只有[1]结束。如果k -1列表为[1, -1]结束。对于其他k初始化val 1然后循环val val * k如果abs(val) 1e14跳出循环否则将val加入targets。注意因为k可能是负数val可能正负交替但绝对值增长是指数级的所以循环次数很少因为2^50就超过1e14了所以循环最多几十次。5. 完整算法步骤与数据结构选择现在我们可以整合出完整的算法步骤输入读取整数n和k以及长度为n的数组a。生成目标值列表初始化一个向量/列表targets [1]。若k 1保持targets [1]。若k -1设置targets [1, -1]。否则初始化val k当abs(val) 1e14时将val加入targets然后val * k。初始化计算前缀和数组pref长度为n1pref[0]0。初始化哈希表cnt记录前缀和出现的频率。cnt[0] 1。初始化答案ans 0。主循环遍历i从1到n当前前缀和current_sum pref[i]。遍历targets列表中的每一个target计算need current_sum - target。ans cnt[need]如果need不在cnt中则加0。cnt[current_sum]。输出打印ans。数据结构选择哈希表是关键。在C中使用std::unordered_maplong long, int。在Python中使用collections.defaultdict(int)。选择long long或Python的int是因为前缀和可能很大最大约1e14。int类型可能溢出。复杂度分析生成targets列表由于|k|1时k^p指数增长列表长度是O(log(max_sum))最多几十个。主循环外层遍历n次内层遍历targets列表常数次最多几十次。哈希表的插入和查询是平均O(1)。因此总时间复杂度是O(n * |targets|)近似O(n)完全能处理n1e5。空间复杂度O(n)用于存储前缀和O(n)用于哈希表最坏情况所有前缀和都不同。6. 代码实现细节与常见错误这里给出一个C的实现示例并附上关键注释#include iostream #include vector #include unordered_map #include cmath using namespace std; int main() { int n; long long k; cin n k; vectorint a(n); for (int i 0; i n; i) { cin a[i]; } // 1. 生成所有可能的目标值 (k的幂次) vectorlong long targets; targets.push_back(1); // k^0 // 处理 k 1 和 k -1 的特殊情况 if (k 1) { // 只有1什么都不用做 } else if (k -1) { targets.push_back(-1); // k^1 } else { long long val k; // 注意循环条件绝对值不超过一个足够大的数这里取1e15比可能的最大前缀和(1e14)大 while (abs(val) 1e15) { targets.push_back(val); val * k; // 防止无限循环如果k的幂次增长太快可能溢出long long但我们的条件会跳出 } } // 2. 计算前缀和 vectorlong long pref(n 1, 0); for (int i 0; i n; i) { pref[i 1] pref[i] a[i]; } // 3. 使用哈希表进行统计 unordered_maplong long, int cnt; cnt[0] 1; // 空前缀和 long long ans 0; for (int i 1; i n; i) { long long current_sum pref[i]; for (long long target : targets) { long long need current_sum - target; // 如果need在哈希表中存在则加上其出现次数 if (cnt.find(need) ! cnt.end()) { ans cnt[need]; } } // 将当前前缀和加入哈希表供后面的位置查询 cnt[current_sum]; } cout ans endl; return 0; }常见错误与调试要点整数溢出这是最大的坑。pref[i]、targetk^p、need都必须使用64位整数C中的long long。在计算k^p时即使k和p本身不大连续乘法也可能导致中间结果溢出。因此在生成targets的循环中判断条件abs(val) LIMIT要在乘法val * k之前进行或者使用__int128来安全计算后再判断。上面的代码在while条件中使用了abs(val) 1e15并在循环内进行乘法对于k较大且p较小时val可能在乘法前就超过了1e15从而不会进入循环加入无效的大数。这是一种可行的防护。哈希表查找在C的unordered_map中直接使用cnt[need]来累加虽然简洁但如果need不存在会插入一个默认构造的键值对(need, 0)这可能会轻微影响性能并增加哈希表大小。使用find方法先检查是否存在是更规范的做法如上例所示。在Python中使用defaultdict(int)则可以直接ans cnt[need]因为不存在的键会返回0。初始状态cnt[0] 1至关重要。它代表了从数组开头开始的子数组即l0。当我们计算need pref[i] - target时如果need为0就说明从开头到i-1的子数组和等于target这个情况需要被统计到。目标值去重对于k-1我们手动添加了1和-1。对于其他k在循环生成targets时理论上不会产生重复值除非k0但题目规定|k|1。不过即使有重复放入列表也不会影响结果正确性只是多了几次无效循环。为了极致优化可以用set存储targets再去重但鉴于列表长度很短影响不大。边界值测试n1,k1,a[0]。答案应该是多少所有子数组[0]和为0不等于1所以答案是0。n1,k2,a[1]。子数组[1]和为1是2^0所以答案是1。n3,k-1,a[0, 0, 0]。所有子数组和都是0。目标值是1和-1。没有子数组和为1或-1答案是0。构造一个大数据测试确保不会超时。7. 思路延伸与同类问题对比解决这个问题后我们可以把它归纳到更广泛的问题模型给定一个数组求有多少个子数组的和等于给定集合中的某个值。本题的特殊性在于这个“集合”是k的幂次并且我们需要高效枚举这个集合。如果这个集合是任意的、有限的我们只需要遍历这个集合作为target即可算法框架完全不变。如果这个集合很大但满足某种规律如等差数列、等比数列我们可以根据规律生成target列表。另一个著名的同类问题是“和为K的子数组个数”LeetCode 560那里target只有一个固定的值K。我们今天的算法可以看作是它的一个扩展。在LeetCode 560中我们只需要一个target内层循环不需要遍历targets列表因此复杂度是严格的O(n)。此外还可以思考如果数组中的元素可以是任意实数或者k的幂次可以是分数次幂问题会变得如何复杂。通常竞赛题会限制在整数范围内使得问题可以通过离散化的数据结构来解决。在团队训练中我常强调这类问题的核心公式pref[r] - pref[l] target的变形pref[r] pref[l] target。它把“子数组和”问题转化为了“两数之差”问题而哈希表正是为了快速匹配这种关系。掌握这个转化是解决一大批前缀和问题的钥匙。最后在实现时务必注意数据范围和溢出问题这是竞赛中非常常见的失分点。对于涉及幂次、乘积的计算一定要预先估算值的范围并设置合理的循环终止条件。像本题中对k1和k-1的特判也体现了对问题边界情况的深思熟虑这是写出鲁棒代码的关键。
返回列表