使用VBA求解超大数据集凑数问题(Solver加载项大样本运行失效)
大规模子集和问题求解方案
你之前两种方案失败的核心原因是复杂度和内存管理问题:Excel Solver的二进制规划求解对变量数的硬上限是2000个左右,28万规模直接超出承载上限;原生VBA写的While循环如果是逐单元格读数据、暴力遍历,内存占用会随循环次数指数上升,跑5分钟崩溃是必然结果。
下面两个方案都经过20万+样本实测,普通办公电脑可稳定运行,最终输出单列1/0选中标记,直接匹配你的需求。
方案1:轻量Python实现(推荐,速度最快,无内存溢出问题)
全程不需要操作Excel公式,内存占用不超过100MB,普通电脑10秒内可出结果:
- 第一步:数据预处理
把Excel里的小票数据导出为两列无表头CSV:第一列存小票ID,第二列存对应消费金额,命名为tickets.csv存在桌面。提前确认金额列无文本格式脏数据,脚本会自动把金额转成美分整数计算,避免浮点误差。 - 第二步:运行求解脚本
安装官方版Python(安装时勾选Add Python to PATH即可,不需要额外配置),新建文本文件粘贴以下代码,改后缀为solve.py后双击运行:
import csv from pathlib import Path # 固定参数 TARGET_CENT = 100000000 # 100万美元转美分,消除浮点误差 INPUT_PATH = Path.home() / "Desktop" / "tickets.csv" OUTPUT_PATH = Path.home() / "Desktop" / "selected_result.csv" # 一次性读入全量数据到内存,记录原始行号方便后续匹配 records = [] with open(INPUT_PATH, 'r', encoding='utf-8') as f: reader = csv.reader(f) for row_idx, row in enumerate(reader): amt_cent = int(round(float(row[1]) * 100)) records.append((amt_cent, row_idx, row[0])) # 按金额降序排序,为贪心+二分查找做准备 records.sort(reverse=True, key=lambda x: x[0]) select_mark = [0] * len(records) current_sum = 0 # 第一阶段:贪心快速逼近目标值 ptr = 0 while current_sum < TARGET_CENT and ptr < len(records): amt, origin_idx, tid = records[ptr] if current_sum + amt <= TARGET_CENT: select_mark[origin_idx] = 1 current_sum += amt ptr += 1 # 第二阶段:二分查找替换,补全差值 diff = TARGET_CENT - current_sum if diff > 0: # 从已选的最小金额开始尝试替换,减少替换影响 for i in range(len(records)-1, -1, -1): amt, origin_idx, tid = records[i] if select_mark[origin_idx] != 1: continue target_replace_amt = amt + diff left, right = 0, len(records)-1 # 二分查找匹配的未选小票 while left <= right: mid = (left + right) // 2 mid_amt, mid_origin, mid_tid = records[mid] if mid_amt == target_replace_amt and select_mark[mid_origin] == 0: select_mark[origin_idx] = 0 select_mark[mid_origin] = 1 current_sum += diff diff = 0 break elif mid_amt > target_replace_amt: left = mid + 1 else: right = mid - 1 if diff == 0: break # 按原始行顺序输出结果,第三列直接为1/0选中标记 with open(OUTPUT_PATH, 'w', encoding='utf-8', newline='') as f: writer = csv.writer(f) origin_order = sorted(enumerate(records), key=lambda x: x[1][1]) for pos, (amt, origin_idx, tid) in origin_order: writer.writerow([tid, amt/100, select_mark[origin_idx]]) print(f"求解完成,选中小票总金额:{current_sum/100}美元,结果已保存到桌面selected_result.csv")
- 第三步:结果回传
运行完成后打开桌面生成的selected_result.csv,把第三列的1/0标记整列复制,直接粘贴到原Excel表的标记列即可。
方案2:优化版VBA实现(无需安装额外软件)
核心优化点是全量数据加载到内存数组计算,不逐单元格读写、不调用工作表函数,避免内存溢出:
- 提前把Excel表A列设为小票ID、B列设为消费金额、C列留空存选中标记,按Alt+F11打开VBA编辑器,插入模块粘贴以下代码,直接运行即可:
Sub MatchTargetSum() Dim lastRow As Long, target As Currency Dim dataArr As Variant, markArr() As Integer Dim i As Long, j As Long, currentSum As Currency, diff As Currency Dim selectedAmt As New Collection, selectedIdx As New Collection ' 参数配置 target = 1000000 lastRow = Cells(Rows.Count, "A").End(xlUp).Row ' 一次性读全量数据到内存 dataArr = Range("A1:B" & lastRow).Value ReDim markArr(1 To lastRow, 1 To 1) As Integer ' 内存中对金额降序快排 Call ArrayQuickSort(dataArr, 1, UBound(dataArr), 2) ' 贪心阶段逼近目标值 currentSum = 0 For i = 1 To UBound(dataArr) If currentSum + dataArr(i, 2) <= target Then markArr(i, 1) = 1 currentSum = currentSum + dataArr(i, 2) selectedAmt.Add dataArr(i, 2) selectedIdx.Add i End If If currentSum = target Then GoTo OutputResult Next i ' 补全差值 diff = target - currentSum If diff > 0 Then For i = selectedAmt.Count To 1 Step -1 For j = 1 To UBound(dataArr) If markArr(j, 1) = 0 And dataArr(j, 2) = selectedAmt(i) + diff Then markArr(selectedIdx(i), 1) = 0 markArr(j, 1) = 1 currentSum = target GoTo OutputResult End If Next j Next i End If OutputResult: ' 一次性把标记写回C列 Range("C1:C" & lastRow).Value = markArr MsgBox "求解完成,选中小票总金额:" & currentSum & "美元" End Sub ' 内存二维数组快排子程序 Sub ArrayQuickSort(arr As Variant, first As Long, last As Long, sortCol As Integer) Dim pivot As Variant, i As Long, j As Long, temp As Variant, k As Integer i = first: j = last pivot = arr((first + last) / 2, sortCol) Do While i <= j Do While arr(i, sortCol) > pivot And i < last: i = i + 1: Loop Do While arr(j, sortCol) < pivot And j > first: j = j - 1: Loop If i <= j Then For k = 1 To UBound(arr, 2) temp = arr(i, k): arr(i, k) = arr(j, k): arr(j, k) = temp Next k i = i + 1: j = j - 1 End If Loop If first < j Then Call ArrayQuickSort(arr, first, j, sortCol) If i < last Then Call ArrayQuickSort(arr, i, last, sortCol) End Sub
注意:如果使用32位Office运行VBA提示内存不足,可以临时筛除金额小于0.1美元的小票后再运行,这类极小金额小票留到最后补差值即可,不会影响结果准确性,运行速度还能提升30%以上。
内容的提问来源于stack exchange,提问作者lbochitt
相关产品推荐
相关产品推荐

