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

62 阅读2分钟

问题描述

小F面临一个有趣的挑战:给定一个数组,她需要将数组中的数字分为两组。分组的目标是使得一组数字的和的个位数等于给定的 A,另一组数字的和的个位数等于给定的 B。除此之外,还有一种特殊情况允许其中一组为空,但剩余数字和的个位数必须等于 A 或 B。小F需要计算所有可能的划分方式。

例如,对于数组 [1, 1, 1] 和目标 A = 1,B = 2,可行的划分包括三种:每个 1 单独作为一组,其余两个 1 形成另一组。如果 A = 3,B = 5,当所有数字加和的个位数为 3 或 5 时,可以有一组为非空,另一组为空。

问题分析

读题时

  • 这道题是典型的 子集和问题 变体,所以我考虑用 dp 来做。
  • 我注意到,题目要求关注两组数字和的 个位数 ,因此其他高位部分的数字就无关紧要了,可以对每个数字 取10的模 再dp。

解题时

状态方程推导

f[i][j]f[i][j] 表示 前 ii 个数字能否组成一个子集,其和的个位数为 jj 。对于当前数字 array[i1]array[i−1]

如果不选,那么当前 jj 就直接由前 i1i−1 个数字的状态继承。对应的状态转移是:

f[i][j]f[i1][j]f[i][j] \gets f[i−1][j]

如果选,则 jj 应该等于前 i1i−1 个数字和的个位数加上 array[i1]array[i−1] 后的结果。由于 (a+b)%10=(a%10+b%10)%10(a+b)\%10=(a\%10+b\%10)\%10 ,对应状态转移为:

f[i][j]f[i1][(jarray[i1]+10)%10]f[i][j] \gets f[i−1][(j−array[i−1]+10)\%10]

其中 (jarray[i1]+10)%10(j−array[i−1]+10)\%10 是为了保证结果在090 \sim 9 的范围内,避免负数。

从以上两种情况,可以得到总的状态转移方程

f[i][j]=f[i1][j]+f[i1][(jarray[i1]+10)%10]f[i][j]=f[i−1][j]+f[i−1][(j−array[i−1]+10)\%10]

边界条件和目标状态

若不选任何数字,和为 0,即 f[0][0]=1f[0][0]=1 , 1 表示可以通过空集达到这个状态。和不可能为非 0 ,因此 f[0][j]=0(j0)f[0][j]=0(j \neq 0)

要判断是否存在一种分组,使得两组数字和的个位数分别为 AABB;或者一组为空时,另一组的数字和的个位数为 AABB。那么如果f[n][A]1f[n][A] \geq 1f[n][B]1f[n][B] \geq 1nn 为数组长度),则说明至少存在一种分组方式满足条件。

数学约束

题目要求将数组分成两组,假设两组的和分别为 S1S_1 , S2S_2 ​,则满足以下条件

  1. S1+S2=数组总和S_1 + S_2 = \text{数组总和} (记为 sum_unit\text{sum\_unit}
  2. S1%10=AS_1\%10=AS2%10=BS_2 \% 10 = B,或者反过来 S1%10=BS_1\%10=BS2%10=AS_2 \% 10 = A

将这两个条件结合,使用模运算的性质:

(S1+S2)%10=sum_unit%10(S_1 + S_2) \% 10 = \text{sum\_unit} \% 10

由于 S1%10=AS_1 \% 10 = AS2%10=BS_2 \% 10 = B,所以有:

(S1+S2)%10=(A+B)%10(S_1 + S_2) \% 10 = (A + B) \% 10

根据这个约束可以提前剪枝,减少不必要的计算。

代码分析及注释

#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数组存储结果。

总结

这道题表面上看起来像是一个传统的分组问题,但由于问题专注于个位数的和,这使得其可以通过模运算巧妙处理个位数的和。我利用了这一特殊要求,在设计状态转移方程时,将原本复杂的分组问题简化为一个易于计算的状态空间。同时使用模运算提前剪枝和压缩状态空间,提高了程序效率。