问题描述
小F面临一个有趣的挑战:给定一个数组,她需要将数组中的数字分为两组。分组的目标是使得一组数字的和的个位数等于给定的 A,另一组数字的和的个位数等于给定的 B。除此之外,还有一种特殊情况允许其中一组为空,但剩余数字和的个位数必须等于 A 或 B。小F需要计算所有可能的划分方式。
例如,对于数组 [1, 1, 1] 和目标 A = 1,B = 2,可行的划分包括三种:每个 1 单独作为一组,其余两个 1 形成另一组。如果 A = 3,B = 5,当所有数字加和的个位数为 3 或 5 时,可以有一组为非空,另一组为空。
问题分析
读题时
- 这道题是典型的 子集和问题 变体,所以我考虑用 dp 来做。
- 我注意到,题目要求关注两组数字和的 个位数 ,因此其他高位部分的数字就无关紧要了,可以对每个数字 取10的模 再dp。
解题时
状态方程推导
令 表示 前 个数字能否组成一个子集,其和的个位数为 。对于当前数字 ,
如果不选,那么当前 就直接由前 个数字的状态继承。对应的状态转移是:
如果选,则 应该等于前 个数字和的个位数加上 后的结果。由于 ,对应状态转移为:
其中 是为了保证结果在 的范围内,避免负数。
从以上两种情况,可以得到总的状态转移方程
边界条件和目标状态
若不选任何数字,和为 0,即 , 1 表示可以通过空集达到这个状态。和不可能为非 0 ,因此
要判断是否存在一种分组,使得两组数字和的个位数分别为 和 ;或者一组为空时,另一组的数字和的个位数为 或 。那么如果 或 ( 为数组长度),则说明至少存在一种分组方式满足条件。
数学约束
题目要求将数组分成两组,假设两组的和分别为 , ,则满足以下条件
- (记为 )
- 且 ,或者反过来 且
将这两个条件结合,使用模运算的性质:
由于 和 ,所以有:
根据这个约束可以提前剪枝,减少不必要的计算。
代码分析及注释
#include <bits/stdc++.h>
using namespace std;
// 计算满足条件的划分方式
int solution(int n, int A, int B, vector<int>& array_a) {
// 将所有数组元素取模 10,标准化为个位数
for (int& x : array_a) {
x %= 10;
}
// 计算总和的个位数
int sum_unit = accumulate(array_a.begin(), array_a.end(), 0) % 10;
// 检查总和是否满足 A 或 B,若满足,直接返回 1 种划分方式
if (sum_unit == A || sum_unit == B) {
return 1;
}
// 提前剪枝
if (sum_unit != (A + B) % 10) {
return 0;
}
vector<vector<int>> dp(n + 1, vector<int>(10, 0));
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < 10; j++) {
dp[i][j] += dp[i - 1][j];
dp[i][j] += dp[i - 1][(j - array_a[i - 1] + 10) % 10];
}
}
return dp[n][B];
}
int main() {
// 测试用例,此处省略
return 0;
}
复杂度分析
- 时间复杂度:
O(n * 10),其中n为数组长度,10是和的个位数范围。 - 空间复杂度:
O(n * 10),使用二维dp数组存储结果。
总结
这道题表面上看起来像是一个传统的分组问题,但由于问题专注于个位数的和,这使得其可以通过模运算巧妙处理个位数的和。我利用了这一特殊要求,在设计状态转移方程时,将原本复杂的分组问题简化为一个易于计算的状态空间。同时使用模运算提前剪枝和压缩状态空间,提高了程序效率。