2026-07-17:统计满足数位和数组的非递减数组数目。用go语言,给定一个长度为 n 的整数数组 digitSum。 需要统计能构造出多少个长度为 n 的数

25 阅读7分钟

2026-07-17:统计满足数位和数组的非递减数组数目。用go语言,给定一个长度为 n 的整数数组 digitSum。

需要统计能构造出多少个长度为 n 的数组 arr,同时满足:

  • arr 的每一项都是 0 到 5000 之间的整数。

  • arr 是非递减的,即 arr[0] ≤ arr[1] ≤ … ≤ arr[n-1]。

  • 对于每个位置 i,arr[i] 的十进制各位数字之和必须等于 digitSum[i]。

最终返回不同有效数组的个数,结果对 10⁹ + 7 取模。

1 <= digitSum.length <= 1000。

0 <= digitSum[i] <= 50。

输入: digitSum = [25,1]。

输出: 6。

解释:

数位和为 25 的数字有 799、889、898、979、988 和 997。

数位和为 1 且可以出现在这些值之后同时保持数组非递减的唯一数字是 1000。

因此,有效数组为 [799, 1000]、[889, 1000]、[898, 1000]、[979, 1000]、[988, 1000] 和 [997, 1000]。

因此,答案是 6。

题目来自力扣3883。

1. 预处理:按数位和分组数字

  • 枚举从 05000 的每一个整数 x,利用递推公式 digSum[x] = digSum[x/10] + (x % 10) 快速算出它的十进制各位数字之和。
  • 最大数位和出现在 4999,其值为 4+9+9+9 = 31。因此所有数字被分到 32 个桶中,每个桶对应一个数位和 0~31
  • 最终得到 sumToNums 数组,其中 sumToNums[s] 存放所有数位和为 s 的数字,并且由于枚举顺序是从小到大,每个桶内的数字天然保持升序。这部分计算只执行一次,与输入无关。

2. 动态规划准备

  • 创建一个长度为 5001 的数组 ff[x] 表示以数字 x 结尾的有效非递减数组的数量
  • 为了统一处理第一个元素,设置一个虚拟起点:令 f[0] = 1,相当于认为在前一个位置存在一个数位和为 0 的数字 0(它小于等于任何有效数字)。这样在计算第一个位置时,可以无缝复用后续的转移逻辑。
  • 变量 pre 记录前一个位置的数位和,初始设为 0

3. 逐位计算有效数组数量

对输入序列 digitSum 中的每个目标数位和 cur 依次处理:

3.1 可行性检查
如果 cur > 31,说明要求的数位和不可能由 0~5000 内的数字实现,直接返回 0

3.2 获取相关数字列表

  • a = sumToNums[pre]:前一个位置允许的所有数字(已按升序排列)。
  • 当前列表 b = sumToNums[cur]:当前位置允许的所有数字(同样升序)。

3.3 双指针累加转移

  • 初始化指针 j = 0 指向 a 的开头,累加和 sum = 0
  • 遍历当前列表 b 中的每一个候选数字 x
    • 因为 ab 都已升序,我们可以利用双指针技巧:不断右移 j,将 a 中所有满足 a[j] <= xf[a[j]] 累加到 sum 中。这样保证了非递减约束——前一个数字只要小于等于 x 就能接上。
    • 当所有符合条件的 a[j] 都处理完后,sum 就代表了所有以 x 结尾的当前序列的个数。将它对 10^9+7 取模后赋给 f[x]
  • 处理完当前列表后,f 数组就更新为以各个 x(数位和为 cur)结尾的有效序列数量。

3.4 进入下一轮
pre = cur,为下一个位置的转移做好准备。


4. 汇总最终答案

  • 遍历完所有位置后,pre 中存储的是最后一个目标数位和。
  • 将所有数位和为 pre 的数字 x 对应的 f[x] 累加起来,就得到了整个序列的不同有效数组总数。
  • 最后再取一次模,返回结果。

复杂度分析

  • 时间复杂度
    预处理部分只需遍历 5001 个数字,复杂度 O(5000)
    动态规划部分,对每个 digitSum 元素,我们遍历了前一个数位和的数字列表和当前数位和的数字列表。尽管有双重循环,但双指针的内层 while 在每个状态转移中总共只将前一个列表扫描一遍。因此,总操作次数等于相邻两步列表长度之和的总和。所有桶的数字总数不超过 5001,假设 digitSum 长度为 n(最大 1000),总时间复杂度为 O(5000 + n * L_max),其中 L_max 是单个数位和的最大数字个数(远小于 5000)。实际运行极其迅速。
  • 额外空间复杂度
    全局的 sumToNums 保存了所有数字的分组,占用 O(5000) 空间;动态规划数组 f 同样占用 O(5000) 空间。除输入外,总额外空间为 O(5000),即常数级,非常节省。

Go完整代码如下:

package main

import (
	"fmt"
)

const mx = 5001
const maxDigitSum = 31 // 4999 的数位和最大
var sumToNums [maxDigitSum + 1][]int

func init() {
	digSum := [mx]int{}
	for x := range digSum {
		// 去掉 x 的个位,问题变成 x/10 的数位和,即 digSum[x/10]
		digSum[x] = digSum[x/10] + x%10
		sumToNums[digSum[x]] = append(sumToNums[digSum[x]], x)
	}
}

func countArrays(digitSum []int) (ans int) {
	const mod = 1_000_000_007
	f := [mx]int{1} // f[x] 表示以 x 结尾的有效数组的个数
	pre := 0

	for _, cur := range digitSum {
		if cur > maxDigitSum {
			return 0
		}
		a := sumToNums[pre]
		j, m := 0, len(a)
		sum := 0
		for _, x := range sumToNums[cur] {
			// 有效数组的前一个数只要 <= x 就行
			for ; j < m && a[j] <= x; j++ {
				sum += f[a[j]]
			}
			// sum 现在就是以 x 结尾的有效数组的个数
			f[x] = sum % mod
		}
		pre = cur // 记录上一个数位和
	}

	for _, x := range sumToNums[pre] {
		ans += f[x]
	}
	return ans % mod
}

func main() {
	digitSum := []int{25, 1}
	result := countArrays(digitSum)
	fmt.Println(result)
}

在这里插入图片描述

Python完整代码如下:

# -*-coding:utf-8-*-

from typing import List

MOD = 1_000_000_007
MX = 5001
MAX_DIGIT_SUM = 31  # 4999 的数位和最大为 31

# 预计算:按数位和分组数字 0..5000
sum_to_nums = [[] for _ in range(MAX_DIGIT_SUM + 1)]
dig_sum = [0] * MX
for x in range(MX):
    dig_sum[x] = dig_sum[x // 10] + (x % 10)
    sum_to_nums[dig_sum[x]].append(x)

def countArrays(digitSum: List[int]) -> int:
    f = [0] * MX
    f[0] = 1          # 以 0 结尾的方案数为 1(虚起点)
    pre = 0           # 上一个数位和,初始为 0

    for cur in digitSum:
        if cur > MAX_DIGIT_SUM:
            return 0
        a = sum_to_nums[pre]      # 前一个数位和对应的数字列表
        j = 0
        m = len(a)
        s = 0
        for x in sum_to_nums[cur]:
            # 累加所有满足 a[j] <= x 的 f[a[j]]
            while j < m and a[j] <= x:
                s += f[a[j]]
                j += 1
            f[x] = s % MOD
        pre = cur                 # 更新前一个数位和

    ans = 0
    for x in sum_to_nums[pre]:
        ans += f[x]
    return ans % MOD

# 示例测试
if __name__ == "__main__":
    digitSum = [25, 1]
    print(countArrays(digitSum))

在这里插入图片描述

C++完整代码如下:

#include <iostream>
#include <vector>
using namespace std;

const int MX = 5001;
const int MAX_DIGIT_SUM = 31;   // 4999 的数位和最大为 31
const int MOD = 1'000'000'007;

// 预计算:按数位和分组数字 0..5000(只初始化一次)
const vector<vector<int>>& getSumToNums() {
    static vector<vector<int>> sumToNums = []() {
        vector<vector<int>> res(MAX_DIGIT_SUM + 1);
        vector<int> digSum(MX, 0);
        for (int x = 0; x < MX; ++x) {
            if (x > 0) digSum[x] = digSum[x / 10] + (x % 10);
            res[digSum[x]].push_back(x);
        }
        return res;
    }();
    return sumToNums;
}

int countArrays(const vector<int>& digitSum) {
    const auto& sumToNums = getSumToNums();
    vector<int> f(MX, 0);
    f[0] = 1;           // 以 0 结尾的方案数为 1(虚起点)
    int pre = 0;        // 上一个数位和,初始为 0

    for (int cur : digitSum) {
        if (cur > MAX_DIGIT_SUM) return 0;

        const auto& a = sumToNums[pre];
        int j = 0, m = a.size();
        long long sum = 0;   // 用 64 位避免溢出

        for (int x : sumToNums[cur]) {
            // 累加所有满足 a[j] <= x 的 f[a[j]]
            while (j < m && a[j] <= x) {
                sum += f[a[j]];
                ++j;
            }
            f[x] = sum % MOD;
        }
        pre = cur;
    }

    long long ans = 0;
    for (int x : sumToNums[pre]) {
        ans += f[x];
    }
    return ans % MOD;
}

int main() {
    vector<int> digitSum = {25, 1};
    cout << countArrays(digitSum) << endl;
    return 0;
}

在这里插入图片描述