具体描述
代码
#include <iostream>
#include <vector>
#include <numeric>
using namespace std;
int solution(int n, int targetA, int targetB, vector<int>& numbers) {
// 将所有元素规范化到 [0, 9] 范围内
for (int& num : numbers) {
num %= 10;
}
// 计算所有元素的和并取模 10
int totalSumMod10 = accumulate(numbers.begin(), numbers.end(), 0) % 10;
// 检查总和是否符合要求
if (totalSumMod10 == targetA || totalSumMod10 == targetB)
return 1; // 直接满足
if (totalSumMod10 != (targetA + targetB) % 10)
return 0; // 无法分组满足条件
// 初始化动态规划数组:f[i][j] 表示前 i 个数字能否组成和为 j (mod 10) 的子集
vector<vector<int>> dp(n + 1, vector<int>(10, 0));
dp[0][0] = 1; // 初始状态:和为 0 是可能的
// 动态规划填充数组
for (int i = 1; i <= n; ++i) {
for (int remainder = 0; remainder < 10; ++remainder) {
// 不选第 i 个数字
dp[i][remainder] = dp[i - 1][remainder];
// 选择第 i 个数字
int newRemainder = (remainder - numbers[i - 1] + 10) % 10;
dp[i][remainder] += dp[i - 1][newRemainder];
}
}
// 返回最终结果:是否可以分组使得第一组的和为 targetB (mod 10)
return dp[n][targetB];
}
int main() {
vector<int> array1 = {1, 1, 1};
vector<int> array2 = {1, 1, 1};
vector<int> array3 = {1, 1};
cout << (solution(3, 1, 2, array1) == 3) << endl;
cout << (solution(3, 3, 5, array2) == 1) << endl;
cout << (solution(2, 1, 1, array3) == 2) << endl;
return 0;
}
思路
小F的目标是将给定数组分成两组,使得:
- 一组的和的个位数等于目标值 A;
- 另一组的和的个位数等于目标值 B;
- 允许其中一组为空,但剩下的一组和的个位数必须等于 A 或 B。
问题分解
- 特殊情况:如果整个数组的和的个位数等于 A 或 B,则可以将所有元素归入其中一组,另一组为空。
- 动态规划问题:检查是否可以将数组分为两组,满足一组的和为 A (mod 10),另一组的和为 B (mod 10)。
解法分析
主要步骤:
-
预处理:将数组中的每个元素取模 10,将问题转化为在个位数(0-9)范围内操作。
-
总和检查:
- 如果整个数组的和(取模 10)等于 A 或 B,直接返回1(只有一种分法)。
-
动态规划:
-
定义
dp[i][j]表示前 i 个元素中,是否存在一个子集,使得子集的和模 10 等于 j。 -
状态转移方程:对于当前数字
numbers[i-1]:- 不选它:
dp[i][j] = dp[i-1][j] - 选它:
dp[i][j] = dp[i-1][(j - numbers[i-1] + 10) % 10]
- 不选它:
-
分步解析
Step 1: 预处理输入数组
为了减少复杂度,我们只需要考虑数字的个位数。因此,将每个元素都取模10,这样每个元素的值都在0,90, 90,9之间。
for (int& num : numbers) {
num %= 10;
}
Step 2: 计算数组和的个位数
计算整个数组的和,并取模10,得到 totalSumMod10。
int totalSumMod10 = accumulate(numbers.begin(), numbers.end(), 0) % 10;
特殊情况处理:
- 如果
totalSumMod10等于 AAA 或 BBB:
数组本身已经满足要求,可以将所有元素放入一组,另一组为空,满足分组条件,返回1。 - 如果
totalSumMod10 ≠ (A + B) % 10:
数组的整体和无法分为两组满足要求,直接返回0。
Step 3: 动态规划
定义动态规划状态
- 状态定义:
dp[i][j]表示前 iii 个元素中,是否存在一个子集,其和的个位数等于 jjj。 - 初始化:
dp[0][0] = 1,表示前0个元素和为0的子集存在。
状态转移方程
对于当前元素 numbers[i−1]numbers[i-1]numbers[i−1]:
- 不选择当前元素:当前和等于前 i−1i-1i−1 个元素和: dp[i][j]=dp[i−1][j]dp[i][j] = dp[i-1][j]dp[i][j]=dp[i−1][j]
- 选择当前元素:新的和为原来的和加上当前元素: dp[i][j]∣=dp[i−1][(j−numbers[i−1]+10)dp[i][j] |= dp[i-1][(j - numbers[i-1] + 10) % 10]dp[i][j]∣=dp[i−1][(j−numbers[i−1]+10)
代码实现
vector<vector<int>> dp(n + 1, vector<int>(10, 0));
dp[0][0] = 1;
for (int i = 1; i <= n; ++i) {
for (int remainder = 0; remainder < 10; ++remainder) {
dp[i][remainder] = dp[i - 1][remainder]; // 不选当前元素
int newRemainder = (remainder - numbers[i - 1] + 10) % 10;
dp[i][remainder] |= dp[i - 1][newRemainder]; // 选当前元素
}
}
Step 4: 检查最终结果
目标是确保可以将数组分成两组:
- 第一组的和 % 10 == AAA
- 剩余元素的和 % 10 == BBB
动态规划完成后:
-
dp[n][targetA]表示是否存在一个子集,使和的个位数为 AAA。 -
如果存在满足条件的子集,检查剩余元素的和是否等于 BBB:
-
计算剩余元素的和的个位数: remainingSumMod10=(totalSumMod10−targetA+10)%10\text{remainingSumMod10} = (totalSumMod10 - targetA + 10) % 10remainingSumMod10=(totalSumMod10−targetA+10)%10
-
检查是否与 BBB 相等:
if (dp[n][targetA] && remainingSumMod10 == targetB) re
-
关键点
-
个位数运算:通过将所有数字取模10,限制了问题的状态空间(从0到9)。
-
动态规划应用:将问题转化为子集和问题,通过二维数组记录前 iii 个元素组成的各个位数和状态。
-
分组验证:动态规划确定是否存在子集和满足 AAA,然后验证剩余元素是否满足 BBB。
时空复杂度
-
时间复杂度:O(n×10)=O(n)O(n \times 10) = O(n)O(n×10)=O(n)
- 因为动态规划表有 n+1n + 1n+1 行和 101010 列,每个状态的计算复杂度为常数级。
-
空间复杂度:O(n×10)=O(n)O(n \times 10) = O(n)O(n×10)=O(n)
- 使用了一个大小为 (n+1)×10(n+1) \times 10(n+1)×10 的动态规划数组。
总结
通过定义状态和构建转移方程,我们能够将复杂的分组问题分解为子问题,有效地解决。在状态转移时,关键是明确每个状态代表的含义,并找到它与前一状态的关系。
在本题中,动态规划表的每一行只依赖于上一行,可以使用滚动数组优化空间复杂度。这种技巧在处理状态转移依赖较少的动态规划问题时非常有用。