题解:学而思编程 三倍子串

【题目来源】

学而思编程:三倍子串

【题目描述】

给定一个十进制正整数 \(n\),请问可以从 \(n\) 中截取多少种不同的子串,使得子串构成的数字是 \(3\) 的倍数。

例如:当 \(n=5764\) 时,有且仅有 \(6\)\(57\)\(576\) 这三个子串是 \(3\) 的倍数。

【输入】

一行一个整数 \(n\)

【输出】

一行一个整数,表示 \(3\) 的倍数的字串数量。

【输入样例】

1234

【输出样例】

4

【核心思想】

  1. 问题分析:给定十进制正整数 \(n\)(以字符串形式),求其中有多少种不同的子串构成的数字是 \(3\) 的倍数。利用"一个数是 \(3\) 的倍数当且仅当其各位数字之和是 \(3\) 的倍数"的性质,将问题转化为统计数字和模 \(3\)\(0\) 的子串数量。这是一个线性动态规划 + 模运算转移问题。

  2. 算法选择

    • 状态设计dp[i][r] 表示以第 \(i\) 位结尾的子串中,数字和模 \(3\)\(r\) 的子串数量(\(r \in \{0, 1, 2\}\)
    • 递推转移:每个新数字 \(x\) 可以单独成串,或拼接到以 \(i-1\) 结尾的所有子串末尾,根据 \(x \bmod 3\) 的值进行余数轮换
  3. 关键步骤

    • 初始化:读取数字字符串 \(s[0..n-1]\)
    • DP 初始化\(i = 0\),第一个字符):
      • \(x = s[0] - '0'\)
      • dp[0][x % 3] = 1:单个数字的子串
    • DP 转移\(i\)\(1\)\(n-1\)):
      • \(x = s[i] - '0'\)
      • 新子串dp[i][x % 3]++:以 \(s[i]\) 为起始的单个数字子串
      • 拼接转移:将 \(s[i]\) 拼接到以 \(i-1\) 结尾的所有子串末尾
        • \(x \bmod 3 = 0\)dp[i][r] += dp[i-1][r](余数不变,\(r = 0, 1, 2\)
        • \(x \bmod 3 = 1\)dp[i][0] += dp[i-1][2]dp[i][1] += dp[i-1][0]dp[i][2] += dp[i-1][1](余数 \(+1\) 轮转)
        • \(x \bmod 3 = 2\)dp[i][0] += dp[i-1][1]dp[i][1] += dp[i-1][2]dp[i][2] += dp[i-1][0](余数 \(+2\) 轮转)
    • 统计答案
      • sum = sum(dp[i][0])\(i\)\(0\)\(n-1\)):所有以任意位置结尾且数字和模 \(3\)\(0\) 的子串数量
    • 输出 sum
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n)\),每个位置 \(O(1)\) 转移(\(3 \times 3\) 的余数组合)
    • 空间复杂度:\(O(n)\),DP 数组 dp[n][3](可滚动优化至 \(O(1)\)
  5. 模运算递推的核心思想

    • 3 的倍数判定:数字和模 \(3\)\(0\) 等价于该数是 \(3\) 的倍数,将字符串子串问题转化为数字和模 \(3\) 问题
    • 余数状态机:拼接新数字 \(x\) 时,原余数 \(r\) 变为 \((r + x) \bmod 3\),形成确定的余数映射关系
    • 计数 DP:不枚举具体子串,而是统计以每个位置结尾、每种余数状态的子串数量,实现批量转移
    • 空间优化dp[i] 只依赖 dp[i-1],可用两个长度为 \(3\) 的数组滚动,空间降至 \(O(1)\)
    • 适用于"子串统计 + 模运算性质"类问题,将指数级枚举优化为线性动态规划

【算法标签】

递推

【代码详解】

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

int main()
{
    string s;
    cin >> s;  // 输入数字字符串
    int n = s.size();  // 字符串长度

    int dp[n][3] = {};  // dp[i][r] = 以s[i]结尾的子串中,数字和模3余r的子串数量

    for (int i = 0; i < n; i++)  // 遍历每个字符
    {
        int x = s[i] - '0';  // 当前数字

        if (i == 0)  // 第一个字符
        {
            dp[i][x % 3]++;  // 单个数字的子串
        }
        else
        {
            // 情况1:以s[i]为起始的新子串
            dp[i][x % 3]++;

            // 情况2:将s[i]拼接到前面子串的末尾
            if (x % 3 == 0)
            {
                // 当前数模3余0
                // 拼接后,余数不变
                dp[i][0] += dp[i-1][0];
                dp[i][1] += dp[i-1][1];
                dp[i][2] += dp[i-1][2];
            }
            else if (x % 3 == 1)
            {
                // 当前数模3余1
                // 拼接后余数变化:
                // 前余0 → 现余1
                // 前余1 → 现余2
                // 前余2 → 现余0
                dp[i][0] += dp[i-1][2];
                dp[i][1] += dp[i-1][0];
                dp[i][2] += dp[i-1][1];
            }
            else  // x%3 == 2
            {
                // 当前数模3余2
                // 拼接后余数变化:
                // 前余0 → 现余2
                // 前余1 → 现余0
                // 前余2 → 现余1
                dp[i][0] += dp[i-1][1];
                dp[i][1] += dp[i-1][2];
                dp[i][2] += dp[i-1][0];
            }
        }
    }

    long long sum = 0;
    for (int i = 0; i < n; i++)
    {
        sum += dp[i][0];  // 累加所有以s[i]结尾且数字和能被3整除的子串数量
    }
    cout << sum;

    return 0;
}

【运行结果】

1234
4
posted @ 2026-07-04 15:00  团爸讲算法  阅读(4)  评论(0)    收藏  举报