模n下最小和为0子集的非暴力PARI/GP程序实现(SSP问题)
模n下最小零和子集的非暴力解法(PARI/GP实现)
暴力枚举所有子集确实在t和n增大时完全不可行,下面我给你分享两种高效的非暴力思路,并用PARI/GP实现出来,你可以根据场景选择合适的方法。
方法一:BFS最短路径法
这个思路把问题转化为图的最短路径问题,天然适配「找最小长度」的需求:
- 把模n的每个余数(0到n-1)看作图中的节点
- 集合S里的每个元素s,相当于一条从节点
r到(r+s) mod n的边,边的权重是1(代表添加一个元素) - 我们要找的就是从节点0出发,经过至少一条边后回到节点0的最短路径——这条路径对应的元素集合就是最小长度的零和子集
因为BFS是按层遍历的,第一次找到的回0路径必然是最短的,完全不用考虑更长的情况。
PARI/GP实现代码
min_zero_subset_bfs(S, n) = { t = #S; // dp[r] 存储:[到达该余数的最小长度, 前驱余数索引, 已用元素的位掩码] dp = vector(n, i, [-1, -1, 0]); dp[1][1] = 0; // 余数0的初始长度为0 queue = [1]; // 队列存余数的1-based索引(对应0~n-1) while (#queue > 0) { current_r_idx = queue[1]; queue = queue[2..#queue]; current_r = current_r_idx - 1; current_len = dp[current_r_idx][1]; for (i=1; t; i++) { // 跳过已经使用过的元素 if (dp[current_r_idx][3] & (1 << (i-1))) continue; s = S[i]; new_r = (current_r + s) % n; new_r_idx = new_r + 1; new_len = current_len + 1; // 更新该余数的最短路径信息 if (dp[new_r_idx][1] == -1 || new_len < dp[new_r_idx][1]) { dp[new_r_idx][1] = new_len; dp[new_r_idx][2] = current_r_idx; dp[new_r_idx][3] = dp[current_r_idx][3] | (1 << (i-1)); queue = concat(queue, new_r_idx); // 找到零和子集,立即回溯返回 if (new_r == 0 && new_len > 0) { subset = []; idx = new_r_idx; while (dp[idx][3] != 0) { prev_idx = dp[idx][2]; // 找出本次新增的元素索引 bit_diff = dp[idx][3] ^ dp[prev_idx][3]; elem_idx = logint(bit_diff, 2) + 1; subset = concat(subset, S[elem_idx]); idx = prev_idx; } return subset; } } } } return []; // 无零和子集(当t<n时可能出现) }
代码说明
- 用位掩码记录已使用的元素,避免重复选择
- 一旦找到回到余数0的路径,立刻回溯提取元素,BFS的特性保证这是最短的子集
方法二:Meet-in-the-Middle分治法
当集合元素个数t较大(比如t=40左右),BFS的效率会下降,这时候分治法更合适:把集合分成前后两半,分别计算每一半的所有子集的和模n、长度和元素列表,然后找两半中互补的和(a + b ≡ 0 mod n),取总长度最小的组合。
这种方法的时间复杂度是O(t*2^(t/2)),比暴力的O(2^t)高效得多,能处理更大规模的集合。
PARI/GP实现代码
min_zero_subset_mitm(S, n) = { t = #S; if (t == 0, return []); // 分割集合为前后两半 mid = floor(t/2); S1 = S[1..mid]; S2 = S[mid+1..t]; // 计算第一半的所有子集信息:(和模n, 长度, 元素列表) subsets1 = []; for (mask=0; 2^mid -1; mask++) { sum = 0; len = 0; elem = []; for (i=0; mid-1; i++) { if (mask & (1 << i)) { sum = (sum + S1[i+1]) % n; len++; elem = concat(elem, S1[i+1]); } } subsets1 = concat(subsets1, [[sum, len, elem]]); } // 计算第二半的子集,按余数分组并保留最小长度的子集 subsets2_map = vector(n, i, []); for (mask=0; 2^(t-mid) -1; mask++) { sum = 0; len = 0; elem = []; for (i=0; (t-mid)-1; i++) { if (mask & (1 << i)) { sum = (sum + S2[i+1]) % n; len++; elem = concat(elem, S2[i+1]); } } r = sum % n; if (#subsets2_map[r+1] == 0 || len < subsets2_map[r+1][1]) { subsets2_map[r+1] = [len, elem]; } } // 寻找最小长度的零和子集 min_len = infinity; best_subset = []; // 检查第一半内部的零和子集 for (s in subsets1) { if (s[1] > 0 && s[1] < min_len && s[1] == 0) { min_len = s[1]; best_subset = s[3]; } } // 检查第二半内部的零和子集 for (r=0; n-1; r++) { if (r == 0 && #subsets2_map[r+1] > 0 && subsets2_map[r+1][1] >0 && subsets2_map[r+1][1] < min_len) { min_len = subsets2_map[r+1][1]; best_subset = subsets2_map[r+1][2]; } } // 检查跨两半的互补组合 for (s in subsets1) { if (s[1] == 0) continue; target_r = (-s[1]) % n; if (#subsets2_map[target_r+1] == 0) continue; total_len = s[2] + subsets2_map[target_r+1][1]; if (total_len < min_len) { min_len = total_len; best_subset = concat(s[3], subsets2_map[target_r+1][2]); } } return best_subset; }
代码说明
- 对第二半的子集按余数分组,只保留每个余数对应的最短子集,减少后续查找的工作量
- 分别检查两半内部的零和子集,再检查跨两半的互补组合,确保找到最小长度的结果
额外提示
- 根据鸽巢原理,当
t ≥ n时,必然存在非空零和子集,算法一定能返回结果 - 两种方法各有侧重:BFS适合n较小的场景,Meet-in-the-Middle适合t较大的场景
内容的提问来源于stack exchange,提问作者J. Linne
相关产品推荐
相关产品推荐

