题解 | #小红删数字#

小红删数字

https://www.nowcoder.com/practice/a246d1e2b38e454985ac1400591d6175

题目链接

[小红删数字]https://www.nowcoder.com/practice/a246d1e2b38e454985ac1400591d6175

题目描述

给定一个长度为 的整数数组 。需要进行恰好 次操作,直到数组中只剩下一个数字。

每次操作固定地针对当前数组的最后两个数 在前, 在后)进行,并二选一执行:

  1. 删除,并将 的结果插入到数组末尾。
  2. 删除,并将 的结果插入到数组末尾。

请统计在所有可能的操作序列下(即每次操作的两种选择),最终结果为 的方案数各有多少。答案需要对 取模。

解题思路

1. 分析操作结构

操作的顺序是固定的,总是从数组的末尾开始。我们来分析一下 的情况,数组为

  1. 第一次操作: 对 操作,结果为 。数组变为
  2. 第二次操作: 对 和第一次的结果 操作,结果为 。数组变为
  3. 第三次操作: 对 和第二次的结果 操作,最终结果为

这种固定的操作顺序产生了一种嵌套的表达式结构。这启发我们从后向前进行动态规划。关键在于,所有中间和最终结果都在 的范围内。

2. 动态规划设计

我们可以定义一个DP状态来表示对数组的一个后缀进行计算后,所有可能的结果及其对应的方案数。由于结果只可能是 ,DP状态可以是一个大小为10的数组。

  • 状态定义: 是一个大小为10的数组。 表示对数组后缀 进行所有可能的运算后,结果为 的方案数。

  • 状态转移: 我们要计算 ,可以利用 的结果。 存储了对后缀 运算的所有结果的方案数。现在我们引入 ,它将与 中的每一个结果进行一次 +* 的运算。

    具体来说,对于 中的每一个可能的结果 ,它有 种方案。这 种方案都会为 贡献两种新结果:

    1. 加法: 新结果为
    2. 乘法: 新结果为

    所以,我们可以通过遍历 来计算

    dp[i] = array of size 10, initialized to 0
    for j from 0 to 9:
      count = dp[i+1][j]
      if count > 0:
        dp[i][(a_i' + j) % 10] += count
        dp[i][(a_i' * j) % 10] += count
    
  • 基本情况: 从数组的最后一个元素开始,当只考虑后缀 时,没有操作可进行,所以结果只有一个,就是 本身,方案数为 1。 因此, 是一个大小为10的数组,只有 的值为1。

  • 最终结果: 我们从 向下迭代到 ,最终 中就存储了对整个数组进行运算后,得到 的方案数。

  • 特殊情况 n=1: 当 时,需要进行 次操作,这意味着数组的初始状态即为最终状态。最终结果就是数组中唯一的数 。题目要求统计 的方案数。因此,如果 恰好在此区间内,则结果 的方案数为1;否则,所有我们关心的结果 () 的方案数都为0。

代码实现

#include <iostream>
#include <vector>
#include <numeric>

using namespace std;

const int MOD = 1000000007;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

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

    if (n == 1) {
        vector<int> result(10, 0);
        if (a[0] >= 0 && a[0] <= 9) {
            result[a[0]] = 1;
        }
        for (int i = 0; i < 10; ++i) {
            cout << result[i] << (i == 9 ? "" : " ");
        }
        cout << endl;
        return 0;
    }

    // dp[j] 表示当前后缀运算结果为 j 的方案数
    vector<int> dp(10, 0);

    // 基本情况:处理 a[n-1]
    dp[a[n - 1] % 10] = 1;

    // 从后向前递推
    for (int i = n - 2; i >= 0; --i) {
        vector<int> next_dp(10, 0);
        int current_val = a[i] % 10;
        for (int j = 0; j < 10; ++j) {
            if (dp[j] > 0) {
                // (a[i] + j) mod 10
                int res_add = (current_val + j) % 10;
                next_dp[res_add] = (next_dp[res_add] + dp[j]) % MOD;
                // (a[i] * j) mod 10
                int res_mul = (current_val * j) % 10;
                next_dp[res_mul] = (next_dp[res_mul] + dp[j]) % MOD;
            }
        }
        dp = next_dp;
    }
    
    for (int i = 0; i < 10; ++i) {
        cout << dp[i] << (i == 9 ? "" : " ");
    }
    cout << endl;

    return 0;
}
import java.util.Scanner;

public class Main {
    private static final int MOD = 1_000_000_007;

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        long[] a = new long[n];
        for (int i = 0; i < n; i++) {
            a[i] = sc.nextLong();
        }

        if (n == 1) {
            int[] result = new int[10];
            if (a[0] >= 0 && a[0] <= 9) {
                result[(int)a[0]] = 1;
            }
            for (int i = 0; i < 10; i++) {
                System.out.print(result[i] + (i == 9 ? "" : " "));
            }
            System.out.println();
            return;
        }

        // dp[j] 表示当前后缀运算结果为 j 的方案数
        int[] dp = new int[10];

        // 基本情况:处理 a[n-1]
        dp[(int)(a[n - 1] % 10)] = 1;

        // 从后向前递推
        for (int i = n - 2; i >= 0; i--) {
            int[] nextDp = new int[10];
            int currentVal = (int)(a[i] % 10);
            for (int j = 0; j < 10; j++) {
                if (dp[j] > 0) {
                    // (a[i] + j) mod 10
                    int resAdd = (currentVal + j) % 10;
                    nextDp[resAdd] = (nextDp[resAdd] + dp[j]) % MOD;
                    // (a[i] * j) mod 10
                    int resMul = (currentVal * j) % 10;
                    nextDp[resMul] = (nextDp[resMul] + dp[j]) % MOD;
                }
            }
            dp = nextDp;
        }
        
        for (int i = 0; i < 10; i++) {
            System.out.print(dp[i] + (i == 9 ? "" : " "));
        }
        System.out.println();
    }
}
import sys

def main():
    MOD = 1_000_000_007
    
    try:
        n_str = sys.stdin.readline()
        if not n_str: return
        n = int(n_str)
        a = list(map(int, sys.stdin.readline().split()))
    except (IOError, ValueError):
        return

    if n == 1:
        result = [0] * 10
        if 0 <= a[0] <= 9:
            result[a[0]] = 1
        print(*result)
        return

    # dp[j] 表示当前后缀运算结果为 j 的方案数
    dp = [0] * 10

    # 基本情况:处理 a[n-1]
    dp[a[n - 1] % 10] = 1
    
    # 从后向前递推
    for i in range(n - 2, -1, -1):
        next_dp = [0] * 10
        current_val = a[i] % 10
        for j in range(10):
            if dp[j] > 0:
                # (a[i] + j) mod 10
                res_add = (current_val + j) % 10
                next_dp[res_add] = (next_dp[res_add] + dp[j]) % MOD
                # (a[i] * j) mod 10
                res_mul = (current_val * j) % 10
                next_dp[res_mul] = (next_dp[res_mul] + dp[j]) % MOD
        dp = next_dp

    print(*dp)


if __name__ == "__main__":
    main()

算法及复杂度

  • 算法: 动态规划
  • 时间复杂度: ,其中 是数组的长度, 是可能的结果数量。外层循环 次,内层循环遍历大小为 的DP数组。总复杂度为 ,可以简化为
  • 空间复杂度: 。主要开销是存储输入数组 。DP数组的空间是 ,是常数级别的。
全部评论

相关推荐

自从我室友在计算机导论课上听说了“刷&nbsp;LeetCode&nbsp;是进入大厂的敲门砖”,整个人就跟走火入魔了一样。他在宿舍门口贴了一张A4纸,上面写着:“正在&nbsp;DP,请勿打扰,否则&nbsp;Time&nbsp;Limit&nbsp;Exceeded。”日记本的扉页被他用黑色水笔加粗描了三遍:“Talk&nbsp;is&nbsp;cheap.&nbsp;Show&nbsp;me&nbsp;the&nbsp;code。”连宿舍聚餐,他都要给我们讲解:“今天的座位安排可以用回溯算法解决,但为了避免栈溢出,我建议用动态规划。来,这是状态转移方程:dp[i][j]&nbsp;代表第&nbsp;i&nbsp;个人坐在第&nbsp;j&nbsp;个位置的最优解。”我让他去楼下取个快递,他不直接去,非要在门口踱步,嘴里念念有词:“这是一个图的遍历问题。从宿舍楼(root)到驿站(target&nbsp;node),我应该用&nbsp;BFS&nbsp;还是&nbsp;DFS?嗯,求最短路径,还是广度优先好。”和同学约好出去开黑,他会提前发消息:“集合点&nbsp;(x,&nbsp;y),我们俩的路径有&nbsp;k&nbsp;个交点,为了最小化时间复杂度,应该在&nbsp;(x/2,&nbsp;y/2)&nbsp;处汇合。”有一次另一个室友低血糖犯了,让他帮忙找颗糖,他居然冷静地分析道:“别急,这是一个查找问题。零食箱是无序数组,暴力查找是&nbsp;O(n)。如果按甜度排序,我就可以用二分查找,时间复杂度降到&nbsp;O(log&nbsp;n)。”他做卫生也要讲究算法效率:“拖地是典型的岛屿问题,要先把连通的污渍区块都清理掉。倒垃圾可以用双指针法,一个指针从左往右,一个从右往左,能最快匹配垃圾分类。”现在我们宿舍的画风已经完全变了,大家不聊游戏和妹子,对话都是这样的:“你&nbsp;Two&nbsp;Sum&nbsp;刷了几遍了?”“别提了,昨天遇到一道&nbsp;Hard&nbsp;题,我连暴力解都想不出来,最后只能看题解。你呢?”“我动态规划还不行,总是找不到最优子结构。今天那道接雨水给我整麻了。”……LeetCode&nbsp;真的害了我室友!!!
老六f:编程嘉豪来了
AI时代还有必要刷lee...
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务