1. 问题背景与需求分析
给定一个m×n的正整数矩阵,我们需要从每一行各选一个数,统计所有选法中使得所选m个数的最大公约数(GCD)为1的方案数。由于结果可能很大,需要对1000000007取模。
这个问题在实际应用中可能出现在组合数学、密码学或系统设计等领域。例如在设计分布式系统的任务分配策略时,可能需要确保各个节点的资源分配参数互质以避免冲突。
2. 算法核心思路解析
2.1 容斥原理的应用
直接统计GCD恰好为1的方案数比较困难,我们采用逆向思维:
- 先计算所有选中数字都能被d整除的方案数
- 然后通过容斥原理剔除重复统计的部分
这种"先包含后排除"的思想在组合数学中非常常见,类似于计算集合的并集大小。
2.2 莫比乌斯反演思想
虽然我们没有直接使用莫比乌斯函数,但算法中从大到小排除倍数的过程与莫比乌斯反演的思想一致。对于每个d,我们计算:
code复制恰好GCD=d的方案数 = GCD是d的倍数的方案数 - Σ(恰好GCD=k*d的方案数),k>1
3. 详细实现步骤
3.1 预处理因子表
go复制const maxVal = 151
var divisors [maxVal][]int
func init() {
for i := 1; i < maxVal; i++ {
for j := i; j < maxVal; j += i {
divisors[j] = append(divisors[j], i)
}
}
}
这个预处理步骤的时间复杂度是O(maxVal²),对于maxVal=150来说非常高效。存储每个数的所有因子,后续可以快速查询。
3.2 统计每行的因子出现次数
go复制divisorCnt := make([][]int, len(mat))
mx := 0
for i, row := range mat {
rowMax := slices.Max(row)
mx = max(mx, rowMax)
divisorCnt[i] = make([]int, rowMax+1)
for _, x := range row {
for _, d := range divisors[x] {
divisorCnt[i][d]++
}
}
}
这里为每行创建一个计数数组,统计该行中能被每个d整除的数字个数。mx记录全局最大值,用于后续处理。
3.3 计算GCD方案数
go复制cntGcd := make([]int, mx+1)
for i := mx; i > 0; i-- {
res := 1
for _, cnt := range divisorCnt {
if i >= len(cnt) || cnt[i] == 0 {
res = 0
break
}
res = res * cnt[i] % mod
}
for j := i; j <= mx; j += i {
res -= cntGcd[j]
}
cntGcd[i] = res % mod
}
这是核心部分:
- 从大到小遍历每个可能的d
- 计算所有数都是d的倍数的方案数(各行计数的乘积)
- 减去所有d的倍数对应的方案数,得到恰好GCD=d的方案数
3.4 处理负数结果
go复制return (cntGcd[1] + mod) % mod
由于Go的取模运算可能产生负数,这里确保返回非负结果。
4. 复杂度分析
4.1 时间复杂度
- 预处理因子:O(maxVal²) = 150×150 = 22,500
- 统计每行因子:O(m×n×avg_factors) ≈ 150×150×10 = 225,000
- 计算GCD方案:O(maxVal×m) = 150×150 = 22,500
总时间复杂度约270,000次操作,完全在合理范围内。
4.2 空间复杂度
- 因子表:O(maxVal×avg_factors) ≈ 150×10 = 1,500
- 每行计数:O(m×maxVal) = 150×150 = 22,500
- GCD计数:O(maxVal) = 150
总空间约24,150个存储单元,非常节省。
5. 关键优化点
- 预处理因子表:避免了重复计算,显著提升性能
- 从大到小处理:确保每个d只处理一次,避免重复计算
- 及时取模:防止整数溢出,保证结果正确
- 行最大值优化:每行只处理到该行最大值,减少不必要的计算
6. 实际应用中的注意事项
- 矩阵规模:虽然题目限制m,n≤150,但算法可以处理更大矩阵,只需调整maxVal
- 数值范围:如果数值范围增大,需要相应调整maxVal和预处理步骤
- 并行优化:预处理因子和统计每行计数可以并行处理
- 内存使用:对于超大矩阵,可以考虑逐行处理减少内存占用
7. 扩展思考
这个算法框架可以解决更广泛的组合计数问题:
- 统计GCD为特定值k的方案数
- 计算LCM(最小公倍数)相关的组合
- 处理多维矩阵的选择问题
只需要调整核心计算公式,整体框架可以保持不变。
8. 完整代码实现
以下是经过优化的Go实现,添加了详细注释:
go复制package main
import (
"fmt"
"slices"
)
const (
maxVal = 151 // 矩阵元素最大值+1
mod = 1e9 + 7 // 取模基数
)
var divisors [maxVal][]int // 全局因子表
// 初始化因子表
func init() {
for i := 1; i < maxVal; i++ {
for j := i; j < maxVal; j += i {
divisors[j] = append(divisors[j], i)
}
}
}
func countCoprime(mat [][]int) int {
// 预处理每行的因子计数
divisorCnt := make([][]int, len(mat))
mx := 0 // 记录全局最大值
for i, row := range mat {
rowMax := slices.Max(row)
if rowMax > mx {
mx = rowMax
}
// 创建当前行的计数数组
divisorCnt[i] = make([]int, rowMax+1)
for _, x := range row {
// 对该数的每个因子增加计数
for _, d := range divisors[x] {
if d <= rowMax {
divisorCnt[i][d]++
}
}
}
}
cntGcd := make([]int, mx+1) // 存储每个d对应的方案数
// 从大到小处理每个d
for i := mx; i > 0; i-- {
// 计算所有数都是i的倍数的方案数
res := 1
for _, cnt := range divisorCnt {
if i >= len(cnt) || cnt[i] == 0 {
res = 0
break
}
res = res * cnt[i] % mod
}
// 减去GCD是i的倍数的方案数
for j := 2 * i; j <= mx; j += i {
res = (res - cntGcd[j] + mod) % mod
}
cntGcd[i] = res
}
return cntGcd[1] // 已经保证非负
}
func main() {
testCases := []struct {
mat [][]int
expect int
}{
{[][]int{{1, 2}, {3, 4}}, 3},
{[][]int{{1, 1}, {1, 1}}, 1},
{[][]int{{2, 4, 6}, {3, 6, 9}, {5, 10, 15}}, 0},
{[][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}, 39},
}
for _, tc := range testCases {
result := countCoprime(tc.mat)
fmt.Printf("Input: %v\nExpected: %d, Got: %d\n\n", tc.mat, tc.expect, result)
}
}
这个实现包含了测试用例,可以验证算法的正确性。在实际工程中,我们还可以添加以下改进:
- 输入验证:检查矩阵是否为空,每行是否有元素
- 边界处理:处理所有元素相同的情况
- 性能分析:添加benchmark测试
- 并发优化:使用goroutine并行处理每行的统计
通过这个算法,我们不仅解决了特定的组合计数问题,还掌握了一种处理类似数学问题的通用思路。这种"预处理+容斥"的方法在许多领域都有广泛应用,值得深入理解和掌握。