1. 项目概述:当“凑数”成为日常刚需
你有没有遇到过这样的场景?手头有一堆零散的发票,需要凑出一个特定的报销金额;或者是一系列产品的成本价,要组合出一个目标总价的报价方案;又或者是在做数据分析时,需要从一长串数字里找出几个,让它们的和刚好等于某个关键指标。这种“凑数字”的需求,在财务、审计、采购、库存管理甚至个人生活中都无处不在。它本质上是一个“子集和问题”,即从给定的一组数字中,找出所有和等于目标值的组合。
过去,面对这种问题,很多人要么是手动一个个试,效率低下且容易出错;要么是求助于编程,写一段VBA或Python脚本。但对于绝大多数日常使用Excel或WPS表格的办公人员、财务人员来说,学习编程门槛太高,而手动计算又太折磨人。这个项目的核心,就是完全依托于Excel或WPS表格的内置功能与函数,不依赖任何编程或第三方插件,构建一套普通人也能轻松上手的“凑数”解决方案。我们将从最基础的公式凑数法,一直讲到相对高级的自定义函数设计,让你在面对“凑金额”、“凑发票”、“凑数据”这类难题时,能从容地让电子表格替你完成繁重的计算工作。
2. 核心思路与方案选型:为什么不用VBA也能搞定?
在深入具体操作之前,我们先理清解决“凑数求和”问题的几种常见思路,并说明为什么我们主要选择函数公式方案。
2.1 常见方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 手动尝试/心算 | 无需学习新工具 | 效率极低,易出错,数字一多几乎不可能 | 极少量数据(<5个) |
| 规划求解(Solver) | Excel/WPS内置,可找最优解 | 一次只能找一组解,设置复杂,对非线性问题支持弱 | 寻找单一最优组合(如成本最低组合) |
| VBA宏编程 | 灵活强大,可找出所有组合 | 需要编程知识,启用宏有安全风险,文件兼容性可能有问题 | 需要自动化、重复性高、组合数量大的复杂场景 |
| 函数公式组合 | 无需编程,安全通用,可随数据动态更新,能展示多组解思路 | 在数据量极大时可能有性能压力,公式设置需要一定逻辑 | 绝大多数日常办公场景,追求安全、可移植、易维护 |
2.2 为什么选择函数公式作为主力?
对于大多数非程序员出身的表格使用者,VBA像是一道带锁的门,虽然门后风景很好,但钥匙不好找。而“规划求解”更像一个精密但操作复杂的仪器,每次使用都要重新调校。函数公式方案的优势在于:
- 零门槛:只需要理解基础的Excel/WPS函数(如SUM, SUMPRODUCT, INDEX, MATCH等),无需面对代码。
- 高安全:文件保存为标准的
.xlsx格式,在任何电脑上打开都能正常计算,不存在宏安全警告或禁用问题。 - 可追溯:每一步计算都由单元格内的公式清晰呈现,方便检查、审计和修改逻辑。
- 动态联动:当源数据或目标值发生变化时,结果能自动重新计算并更新。
我们的核心思路是:利用辅助列和数组公式,构建一个“筛选-验证-输出”的流水线。通过辅助列生成所有可能的组合标识,用函数验证其总和是否等于目标,最后将符合条件的组合清晰地提取出来。下面,我们就从最简单的场景开始,一步步搭建这套系统。
3. 基础构建:单解搜索与“规划求解”的妙用
在追求多解之前,我们先解决一个更常见的问题:快速找到任意一组可行的解。这对于“凑发票报销”这类需求通常就足够了。这里,Excel和WPS内置的“规划求解”工具是一个被低估的利器。
3.1 启用“规划求解”加载项
首先,确保你的工具可用。
- 在Excel中:点击“文件” -> “选项” -> “加载项”。在底部“管理”下拉框中选择“Excel加载项”,点击“转到…”。在弹出的对话框中勾选“规划求解加载项”,点击“确定”。
- 在WPS中:WPS表格的“规划求解”功能可能需要单独安装加载项。你可以点击“数据”选项卡,查看是否有“规划求解”按钮。如果没有,可访问WPS官网的插件平台搜索并安装“规划求解”插件。WPS专业增强版通常已内置。
注意:WPS不同版本和安装环境下,“规划求解”的可用性差异较大。如果找不到,完全不用担心,我们后续的函数方法更通用。这里先以Excel环境为例讲解。
3.2 单目标凑数实战:凑齐568元的餐费发票
假设我们有6张餐费发票,金额分别为:120, 85, 210, 65, 300, 98。需要从中凑出总金额恰好为568元的组合。
步骤一:搭建数据模型
- 在A列输入发票金额:A2:A7 分别写入 120, 85, 210, 65, 300, 98。
- 在B列建立“决策变量”单元格:B2:B7,这些单元格将由规划求解填写0或1,1代表选中该发票,0代表不选。初始值可以设为0或留空。
- 在C列计算单项贡献:C2单元格输入公式
=A2*B2,并向下填充至C7。这里计算的是每张发票是否被计入总金额。 - 在E2单元格设定目标值:输入 568。
- 在F2单元格计算实际总和:输入公式
=SUM(C2:C7)。这个值需要被规划求解调整到等于E2。
步骤二:配置并运行规划求解
- 点击“数据”选项卡下的“规划求解”。
- 设置目标:选择单元格
$F$2。 - 到:选择“目标值”,并输入
=$E$2(或直接输入568)。 - 通过更改可变单元格:选择
$B$2:$B$7。 - 添加约束:
- 点击“添加”,在“单元格引用”中选择
$B$2:$B$7,中间下拉框选择“bin”(二进制),点击“确定”。这个约束强制B列的值只能为0或1。 - (可选)如果需要避免所有都不选,可以添加约束
$F$2 >= 1。
- 点击“添加”,在“单元格引用”中选择
- 选择求解方法:保持默认的“单纯线性规划”即可。
- 点击“求解”。规划求解会开始计算,成功后点击“确定”保留解。
此时,B列中显示为1的发票即被选中。例如,结果可能是选中了210、300、65这三张发票(210+300+65=575≠568?等等,这里我故意举了一个可能无解的例子,用于引出重要讨论)。
实操心得:规划求解很可能提示“找不到可行解”。这引出了一个关键点:不是所有目标值都能被凑出。当遇到无解时,我们可以退一步,寻找最接近目标值的组合。只需在规划求解对话框中,将“到”设置为“最大值”或“最小值”,然后为目标单元格(F2)添加一个约束,例如
$F$2 <= $E$2(总和不超过目标值),再求解最大值,就能找到小于等于目标值的最大组合。这对于预算不足时的“最大化利用”场景非常有用。
3.3 规划求解的局限性
尽管规划求解很强大,但它一次运行通常只返回一组解(且不一定是所有解中的第一个)。如果你想知道是否还有其他组合也能凑出568元,它就无能为力了。这时,我们就需要转向更强大的函数公式方案,来探索“多解”的奥秘。
4. 进阶攻略:函数公式法实现多解枚举与展示
这是本项目的核心精华。我们将创建一个能自动列出所有可能解的模板。思路是:利用二进制思想,为每个数字生成一个是否被选中的状态(0/1),然后计算所有可能状态下的总和,并筛选出等于目标值的组合。
4.1 构建二进制选择器模型
假设我们有N个待凑数字。所有可能的组合数量是2^N种(每个数字要么选,要么不选)。我们需要一个方法来系统地生成这2^N种选择状态。
步骤一:准备数据源与参数
- 待凑数字列表:放在A列,例如A2:A6,假设有5个数字:{12, 25, 8, 31, 19}。
- 目标值:放在单元格E1,例如 50。
- 计算组合总数:在单元格E2输入公式
=2^COUNT(A2:A6),这里结果是32。我们将在辅助区域生成这32种组合。
步骤二:生成所有组合的二进制矩阵我们需要一个辅助区域来代表每种组合的选择状态。假设我们从G列开始。
在G1单元格输入“组合序号”,H1到L1分别输入“数字1”、“数字2”…“数字5”(对应A列的5个数字)。你也可以用公式引用A列的标题。
在G2单元格输入数字1,G3输入2,然后选中G2:G3向下拖动填充柄,一直填充到第33行(对应32种组合+标题行)。或者使用序列填充功能。
关键步骤:生成二进制位。在H2单元格输入以下公式,然后向右拖动填充至L2,再向下拖动填充至第33行。
=MOD(INT(($G2-1)/(2^(COLUMNS($H:H)-1))), 2)公式原理解读:
COLUMNS($H:H):随着公式向右填充,这部分会变成COLUMNS($H:I), COLUMNS($H:J)...,分别返回1,2,3...,代表二进制位的权重(2^0, 2^1, 2^2...)。2^(COLUMNS($H:H)-1):计算当前列的权重,即1,2,4,8,16...($G2-1):组合序号减1,因为我们要从0开始(代表全不选)。INT(($G2-1)/权重):将(序号-1)除以权重后取整,得到该权重位上的商。MOD(..., 2):对商取模2,结果只能是0或1,完美代表了该数字在当前组合中是否被选中。
操作后,你会看到一个32行5列的矩阵,由0和1组成,每一行都唯一对应一种选择组合。
步骤三:计算每种组合的总和在M列(或二进制矩阵右侧的下一列)进行计算。
- 在M1单元格输入“组合总和”。
- 在M2单元格输入数组公式(在较新版本的Excel或WPS中,直接按Enter即可;旧版本可能需要按Ctrl+Shift+Enter):
这个公式将A列的数字数组与H2:L2的0/1选择器数组对应相乘后求和。向下填充至M33。=SUMPRODUCT($A$2:$A$6, H2:L2)
步骤四:筛选并展示所有符合目标值的组合现在,我们需要从M列中找出所有等于目标值(E1)的组合,并把它们提取出来。
- 标识有效行:在N2单元格输入公式
=IF($M2=$E$1, ROW(), ""),向下填充。这个公式会标记出总和等于目标值的组合所在的行号。 - 提取有效组合序号:为了整洁地列出所有解,我们需要一个动态列表。假设从P列开始展示结果。
- 在P1输入“解序号”,Q1输入“组合详情”,R1输入“总和验证”。
- 在P2输入公式
=IFERROR(INDEX($G$2:$G$33, SMALL(IF($N$2:$N$33<>"", ROW($N$2:$N$33)-ROW($N$2)+1), ROW(A1))), "")。这同样是一个数组公式。IF($N$2:$N$33<>"", ROW(...)-ROW(...)+1):构建一个数组,里面是所有有效行在G2:G33区域内的相对位置。SMALL(..., ROW(A1)):依次提取第1小、第2小(随着公式下拉)的相对位置。INDEX(...):根据相对位置,从G列取出对应的组合序号。IFERROR(..., ""):当没有更多解时,显示为空。
- 在Q2输入公式,用于将组合详情转换为易读的格式(如“12+25+13”):
这也是一个数组公式。它先根据解序号P2,用MATCH定位到具体行,然后取出该行的0/1数组,与原始数字相乘(IF函数实现),最后用TEXTJOIN将非空的数字用“+”连接起来。=IF(P2="", "", TEXTJOIN("+", TRUE, IF(INDEX($H$2:$L$33, MATCH(P2, $G$2:$G$33, 0), 0), $A$2:$A$6, ""))) - 在R2输入公式
=IF(P2="", "", $E$1),用于验证总和,或者直接引用=IF(P2="", "", INDEX($M$2:$M$33, MATCH(P2, $G$2:$G$33, 0)))。
现在,P、Q、R列就会动态地列出所有能够凑出目标值的组合了。更改A列的数字或E1的目标值,结果会自动更新。
注意事项:这个方法在数字数量(N)较大时,组合数(2^N)会指数级增长,可能导致表格运行缓慢。建议N控制在15-20以内,用于处理日常的发票、单据凑数完全足够。对于更大的数据集,需要考虑更优化的算法,但这通常已超出纯函数公式的舒适区。
5. 效能提升与自定义函数思路
虽然上述公式矩阵法功能强大,但设置过程略显复杂,且表格看起来有很多辅助列。如果你需要更频繁地使用此功能,将其封装成一个自定义函数(LAMBDA)会是更优雅的选择,尤其是在支持动态数组的Excel 365或新版WPS中。
5.1 使用LET和LAMBDA创建简洁解决方案
我们可以创建一个名为FINDSUBSET的自定义函数,它接收“数字数组”和“目标值”两个参数,直接返回所有符合条件的组合文本数组。
步骤:在名称管理器中定义自定义函数
按
Ctrl+F3打开名称管理器,点击“新建”。名称:输入
FINDSUBSET(或其他你喜欢的名字)。引用位置:输入以下复杂的LAMBDA公式:
=LAMBDA(numbers, target, LET( n, ROWS(numbers), totalCombs, 2^n, seq, SEQUENCE(totalCombs, 1, 1, 1), binMatrix, --(INT((seq-1)/2^(SEQUENCE(1, n, 0)))/2=INT(INT((seq-1)/2^(SEQUENCE(1, n, 0)))/2)), sums, MMULT(binMatrix, numbers), matches, FILTER(seq, sums=target), resultIdx, IF(ISERROR(matches), "", matches), resultTxt, IF(resultIdx="", "", MAKEARRAY( COUNTA(resultIdx), 1, LAMBDA(r,c, TEXTJOIN("+", TRUE, FILTER(numbers, INDEX(binMatrix, INDEX(resultIdx, r), 0), "") ) ) ) ), resultTxt ) )
公式深度解析:
n:数字个数。totalCombs:总组合数2^n。seq:生成1到totalCombs的序列,代表每种组合的ID。binMatrix:这是核心。它利用数学运算和比较,生成一个totalCombs行n列的0/1矩阵,替代了之前辅助列的手工生成。公式利用了二进制转换的原理。sums:使用MMULT函数进行矩阵乘法,计算每一种组合(binMatrix的每一行)与原始数字数组的点积,即总和。matches:使用FILTER函数,筛选出总和等于目标值的组合ID序列。- 后续部分:将匹配的组合ID转换为易读的文本字符串(如“12+25+13”)。
使用方法: 在任意单元格输入=FINDSUBSET(A2:A6, E1),如果存在解,结果将自动溢出显示在下方的单元格中。一个公式搞定所有!
实操心得:这个自定义函数公式非常精炼,但构造和理解难度较高。它最大的优点是将复杂性封装在内部,对外提供极其简单的接口。对于初学者,建议先从理解前面“辅助列矩阵法”开始,那是所有逻辑的基础。当你熟练后,可以尝试使用这个自定义函数来提升工作效率和表格的整洁度。WPS最新版本也已支持LAMBDA函数,但函数支持度可能略有差异,建议先在小范围数据测试。
5.2 处理无解与近似解
在实际工作中,“恰好等于”往往过于理想。我们需要考虑“找不到精确解怎么办?”。
- 寻找最接近解:可以修改上面的自定义函数,或者单独建立一个模型。思路是计算所有组合总和与目标的绝对差
ABS(sums - target),然后使用MIN函数找到最小差值,再反向找出对应的组合。这可以在之前的数据模型基础上,增加一列“差值”,然后排序筛选。 - 设定容差范围:有时不一定要求绝对相等。可以在筛选条件中,将
sums=target改为ABS(sums-target)<=tolerance,其中tolerance是你设定的容差值(如0.5)。这样就能找出所有落在目标值附近范围内的组合。
6. 经典场景应用与避坑指南
掌握了核心方法后,我们来看几个变种场景和常见问题。
6.1 场景一:凑发票金额(带抬头限制)
假设你不仅需要总金额对,还需要发票抬头都是同一个公司。这时,你的数据源需要增加一列“发票抬头”。解决方案:在生成所有组合并计算总和后,增加一个校验条件。例如,在计算组合总和的同时,用另一个数组公式检查该组合中所有发票的抬头是否一致。可以使用SUMPRODUCT配合COUNTIF的数组形式来实现,只筛选出“总和达标”且“抬头唯一”的组合。这相当于在筛选条件中增加了一个“与”(AND)逻辑。
6.2 场景二:从大数据集中快速筛选可能组合
当待选数字很多(比如超过20个),2^N的枚举法计算量巨大。此时,可以先用排序和粗略筛选来缩小范围。
- 将数字从大到小排序。
- 先排除那些大于目标值本身的单个数字(它们不可能出现在任何解中,除非单独等于目标值)。
- 可以尝试“贪婪算法”思路的近似搜索:从最大的数字开始,如果它小于剩余目标值,则尝试加入组合,然后更新剩余目标值,继续找下一个更小的数字。这种方法用函数实现稍复杂,但可以快速找到一个近似解(不一定是最优或完整解),适合对结果要求不严苛的初步筛选。
6.3 常见问题排查
公式返回
#VALUE!或#N/A错误:- 检查数组公式:旧版本Excel中,部分公式需要按
Ctrl+Shift+Enter三键输入。如果输入后单元格显示公式本身而非结果,且左上角有{},说明已正确输入。 - 检查区域大小:确保
SUMPRODUCT、MMULT等函数涉及的数组维度匹配(例如,MMULT的第一个矩阵列数必须等于第二个矩阵的行数)。 - 检查数据类型:确保用于计算的单元格都是数字格式,而非文本。文本数字看起来像数字,但参与计算会导致错误。
- 检查数组公式:旧版本Excel中,部分公式需要按
运行速度极慢或Excel无响应:
- 数据量过大:这是最可能的原因。立即按
Esc键中断计算。回顾一下,你的待选数字是否超过20个?尝试减少数据量,或采用“场景二”的预筛选方法。 - 公式引用整个列:避免使用
A:A这种整列引用,改为精确的范围如A2:A100,可以显著提升计算效率。 - 关闭自动计算:在操作大量公式前,可以点击“公式”选项卡 -> “计算选项” -> 改为“手动”。待所有公式设置完毕后,再按
F9键手动重算。
- 数据量过大:这是最可能的原因。立即按
自定义函数(LAMBDA)不工作:
- 版本支持:确认你的Excel是Microsoft 365或2021版及以上。WPS需确认版本是否支持LAMBDA。
- 名称管理器定义错误:确保在名称管理器中定义的LAMBDA函数语法正确,参数数量与调用时一致。
- 溢出区域被阻挡:如果自定义函数返回的是数组,确保其下方有足够的空白单元格用于“溢出”,否则会返回
#SPILL!错误。
找不到解,但怀疑有解:
- 检查目标值是否过小或过大:目标值小于最小数字,或大于所有数字之和,肯定无解。
- 浮点数精度问题:如果数字是带有小数的金额,由于计算机浮点数计算存在微小误差,严格相等
=比较可能失败。此时应使用容差比较,如ABS(实际总和 - 目标值) < 0.000001。
这个“凑数”项目从一次手工计算的痛苦出发,逐步深入到利用表格软件的规划求解、函数矩阵乃至自定义函数,构建了一套从简到繁的解决方案。它最宝贵的价值在于提供了一种用确定性工具解决模糊性需求的思路。当你下次再面对一堆需要组合的数字时,不必焦虑或蛮干,只需按照这里的框架,将数据填入,让公式和逻辑为你工作。真正的效率提升,不在于软件有多先进,而在于你是否能将重复、耗神的思考过程,转化为一次性的、可复用的规则与模型。