二分数字组合 | 豆包MarsCode AI刷题

88 阅读5分钟

具体描述

image.png

代码

#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的目标是将给定数组分成两组,使得:

  1. 一组的和的个位数等于目标值 A;
  2. 另一组的和的个位数等于目标值 B;
  3. 允许其中一组为空,但剩下的一组和的个位数必须等于 A 或 B。

问题分解

  1. 特殊情况:如果整个数组的和的个位数等于 A 或 B,则可以将所有元素归入其中一组,另一组为空。
  2. 动态规划问题:检查是否可以将数组分为两组,满足一组的和为 A (mod 10),另一组的和为 B (mod 10)。

解法分析

主要步骤:

  1. 预处理:将数组中的每个元素取模 10,将问题转化为在个位数(0-9)范围内操作。

  2. 总和检查

    • 如果整个数组的和(取模 10)等于 A 或 B,直接返回1(只有一种分法)。
  3. 动态规划

    • 定义 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;

特殊情况处理

  1. 如果 totalSumMod10 等于 AAA 或 BBB
    数组本身已经满足要求,可以将所有元素放入一组,另一组为空,满足分组条件,返回1。
  2. 如果 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 的动态规划数组。

总结

通过定义状态和构建转移方程,我们能够将复杂的分组问题分解为子问题,有效地解决。在状态转移时,关键是明确每个状态代表的含义,并找到它与前一状态的关系。

在本题中,动态规划表的每一行只依赖于上一行,可以使用滚动数组优化空间复杂度。这种技巧在处理状态转移依赖较少的动态规划问题时非常有用。