题解:学而思编程 三倍子串
【题目来源】
学而思编程:三倍子串
【题目描述】
给定一个十进制正整数 \(n\),请问可以从 \(n\) 中截取多少种不同的子串,使得子串构成的数字是 \(3\) 的倍数。
例如:当 \(n=5764\) 时,有且仅有 \(6\),\(57\),\(576\) 这三个子串是 \(3\) 的倍数。
【输入】
一行一个整数 \(n\)。
【输出】
一行一个整数,表示 \(3\) 的倍数的字串数量。
【输入样例】
1234
【输出样例】
4
【核心思想】
-
问题分析:给定十进制正整数 \(n\)(以字符串形式),求其中有多少种不同的子串构成的数字是 \(3\) 的倍数。利用"一个数是 \(3\) 的倍数当且仅当其各位数字之和是 \(3\) 的倍数"的性质,将问题转化为统计数字和模 \(3\) 余 \(0\) 的子串数量。这是一个线性动态规划 + 模运算转移问题。
-
算法选择:
- 状态设计:
dp[i][r]表示以第 \(i\) 位结尾的子串中,数字和模 \(3\) 余 \(r\) 的子串数量(\(r \in \{0, 1, 2\}\)) - 递推转移:每个新数字 \(x\) 可以单独成串,或拼接到以 \(i-1\) 结尾的所有子串末尾,根据 \(x \bmod 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\) 轮转)
- 若 \(x \bmod 3 = 0\):
- 统计答案:
sum = sum(dp[i][0])(\(i\) 从 \(0\) 到 \(n-1\)):所有以任意位置结尾且数字和模 \(3\) 余 \(0\) 的子串数量
- 输出
sum
-
时间/空间复杂度:
- 时间复杂度:\(O(n)\),每个位置 \(O(1)\) 转移(\(3 \times 3\) 的余数组合)
- 空间复杂度:\(O(n)\),DP 数组
dp[n][3](可滚动优化至 \(O(1)\))
-
模运算递推的核心思想:
- 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
浙公网安备 33010602011771号