1. 问题分析

题目要求对数组 进行 次操作,每次操作针对数组末尾的两个数进行加法模10或乘法模10,并将结果放回末尾。 我们来追踪数组的变化过程(假设数组长度为4,元素为 ):

  1. 操作对象是末尾的 。假设运算结果为 。 数组变为:
  2. 操作对象是末尾的 。假设运算结果为 。 数组变为:
  3. 操作对象是末尾的 。假设运算结果为 。 数组变为:

由此可见,该操作序列在数学上等价于一个右结合的嵌套表达式: 其中,每一个 都可以独立地选择是 加法() 还是 乘法(),且所有运算均在模 10 意义下进行。

  1. 数据规模 可达 。这意味着不能使用指数级的暴力搜索( 种方案),算法复杂度必须控制在
  2. 状态空间:尽管输入 很大,但根据模运算性质 ,我们只需要关注 的值。因此,任意时刻中间结果的值域仅为
  3. 计算方向:由于运算结构是右结合的(Suffix Structure),即 总是与 “ 这个后缀计算出的结果” 进行运算。因此,算法处理顺序应从数组末尾向前遍历。

2. 线性动态规划

鉴于子问题的重叠性质(后缀 的结果无论是由何种操作组合产生,只要最终数值相同,对于 来说就是等价的),以及最优子结构特性,本题适合采用 动态规划

状态定义

定义 为: 仅考虑数组后缀 ,经过 次操作后,最终结果为数字 () 的方案总数

状态转移方程

对于位置 (),其结果依赖于 与后缀 的结果 进行合并。 对于每一个可能的后缀结果 (即 ), 有两种组合方式:

  1. 加法方案:结果变为 。 对新状态的贡献:
  2. 乘法方案:结果变为 。 对新状态的贡献:

注意:以上累加过程均需对 取模。

边界条件

对于最后一个元素 ,不需要进行操作,它本身就是后缀的结果。

3. 代码实现

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

static constexpr ll MOD = 1e9 + 7;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vector<ll> a(n);
    for (int i = 0; i < n; i++)
        cin >> a[i];

    if (n == 1) {
        for (int i = 0; i <= 9; i++) {
            if (a[0] == i)
                cout << 1 << (i == 9 ? "" : " ");
            else
                cout << 0 << (i == 9 ? "" : " ");
        }
        cout << "\n";
        return 0;
    }

    vector<vector<ll>> dp(n, vector<ll>(10, 0));
    dp[n - 1][a[n - 1] % 10] = 1LL;

    for (int i = n - 2; i >= 0; i--) {
        for (int j = 0; j <= 9; j++) {
            int v1 = (a[i] + j) % 10;
            dp[i][v1] = (dp[i][v1] + dp[i + 1][j]) % MOD;
            int v2 = (a[i] * j) % 10;
            dp[i][v2] = (dp[i][v2] + dp[i + 1][j]) % MOD;
        }
    }

    for (int i = 0; i <= 9; i++) {
        cout << dp[0][i] << " ";
    }
    cout << "\n";
}

4. 复杂度分析

时间复杂度

  • 复杂度
  • 分析:我们需要从 遍历到 。在每一步迭代中,内层循环固定执行 10 次(遍历数字 0-9),且循环体内的操作均为常数级(加法、乘法、取模)。

空间复杂度

  • 复杂度
  • 分析:虽然输入数组需要 的存储,但动态规划的核心逻辑只需要两个长度为 10 的数组来维护状态(滚动数组思想)。
  • 内存对比:相比于 的完整 DP 表格,空间占用极小。

特判

  • 单元素数组:如果 ,操作次数为 0,需特殊处理。