You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

模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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:09:36