文章目录
零、题目描述
题目链接:Numehehaaaaaa
题目要求:
输入 t 组测试用例,每组给定一个整数 n,请输出大于等于 n 的最小整数,使得该整数的十进制表示中包含子串“114514”(即“114514”作为连续字符出现)。
示例(假设):
- 若输入
n = 114513,输出114514(直接包含模式串); - 若输入
n = 114515,输出1000000 + 114514 = 1114514(需找下一个包含模式串的数)。
一、算法概述
本题采用 “数位动态规划(数位DP)+ 状态机 + 二分查找” 的组合算法,核心思路如下:
- 数位DP + 状态机:设计数位DP统计“0到x中包含子串‘114514’的数的个数”。其中,状态机用于追踪当前数位匹配模式串的进度,确保高效识别模式串;
- 二分查找:利用“0到x中符合条件的数的个数”的单调性(x越大,个数越多),通过二分查找找到“大于等于n的最小x”,使得x是第一个包含“114514”的数。
该算法的优势在于:
- 数位DP解决了“大数字范围计数”的问题(避免直接枚举导致的超时);
- 状态机精准追踪模式串匹配进度,确保不遗漏符合条件的数;
- 二分查找将“找最小数”转化为“验证计数差”,效率极高。
二、算法思路
2.1 核心问题拆解
本题的核心是两个子问题:
- 子问题1:如何快速计算
f(x)= 0到x中包含“114514”的数的个数? - 子问题2:如何基于
f(x)找到大于等于n的最小x,使得f(x) - f(n-1) = 1(即x是n之后第一个包含模式串的数)?
2.2 子问题1:数位DP + 状态机实现 f(x)
2.2.1 状态机设计(模式串匹配核心)
目标是匹配子串“114514”,需设计状态机追踪当前匹配进度:
- 状态定义:用
data0表示当前匹配到模式串的第k个字符(k ∈ 0~6):0:初始状态(未匹配任何字符);1:匹配到第1个字符“1”;2:匹配到第2个字符“1”(即“11”);3:匹配到第3个字符“4”(即“114”);4:匹配到第4个字符“5”(即“1145”);5:匹配到第5个字符“1”(即“11451”);6:完全匹配(即“114514”,终止状态)。
- 转移规则:用二维数组
mat[7][10]存储状态转移,mat[cur_state][digit]表示“当前状态为cur_state时,输入数字digit后转移到的新状态”。例如:mat[0][1] = 1(初始状态遇“1”,匹配第1个字符);mat[5][4] = 6(匹配到“11451”后遇“4”,完全匹配);mat[6][d] = 1(完全匹配后,遇任何数字均回到“匹配第1个‘1’”的状态,不影响后续计数)。
2.2.2 数位DP状态设计
数位DP的递归状态需包含以下信息:
depth:当前枚举到的数位位置(从高位到低位);is_leadingzero:是否存在前导零(如“00123”的前导零不影响实际数值);is_limit:当前数位是否受原数x的限制(如x=123,枚举到百位为1时,十位最多为2;若百位为0,十位无限制);dpdata:自定义状态(含data0状态机进度、data1是否已找到模式串)。
2.2.3 数位DP流程
- 初始化:
dpdata初始化为data0=0(未匹配)、data1=0(未找到模式串); - 递归出口:当枚举完所有数位(
depth == num.size()),返回data1(1表示找到模式串,0表示未找到); - 记忆化剪枝:若当前状态已计算过,直接返回缓存结果(避免重复计算);
- 枚举数位:根据
is_limit确定当前数位的最大值,遍历所有可能的数字; - 状态转移:根据当前数字更新
is_leadingzero、is_limit和dpdata(通过getNextDpData函数更新状态机进度和data1); - 递归累加:累加所有子状态的结果,存入缓存并返回。
2.3 子问题2:二分查找找最小x
由于 f(x) 是单调递增函数(x越大,包含模式串的数越多),可通过二分查找定位最小x:
- 二分范围:
- 左边界
l = n(从n开始找); - 右边界
r = (n/1e6 + 1)*1e6 + 114514(保守上界,确保包含至少一个符合条件的数);
- 左边界
- 二分逻辑:
- 计算
lans = f(n)(0到n中符合条件的数的个数); - 取中点
mid = (l + r) / 2,计算cnt = f(mid) - lans; - 若
cnt >= 1:说明mid及左侧存在符合条件的数,调整r = mid; - 若
cnt < 1:说明mid左侧无符合条件的数,调整l = mid; - 当
l + 1 < r时重复,最终r即为最小x。
- 计算
三、代码实现
代码分为 模板框架 和 自定义逻辑 两部分,核心自定义逻辑集中在状态机和状态转移。
3.1 宏定义与结构体
#include <iostream>
#include <map>
#include <cstring>
#include <string>
using namespace std;
#define ll long long
#define maxd 65 // 最大数位(十进制65位足够覆盖大整数)
#define MEMORY_USE_MAP // 启用map记忆化(灵活处理状态,避免数组大小限制)
// 自定义DP状态:存储状态机进度和是否找到模式串
struct DpData {
ll data0; // 状态机进度(0~6,对应匹配模式串的0~6个字符)
ll data1; // 是否已找到模式串(1=是,0=否)
static ll K; // 预留参数(本题未用)
static ll base; // 进制(十进制=10)
// 构造函数
DpData(): data0(0), data1(0) { init(); }
DpData(ll d0, ll d1): data0(d0), data1(d1) {}
// map排序需要的比较运算符
bool operator<(const DpData& d) const {
return data0 != d.data0 ? data0 < d.data0 : data1 < d.data1;
}
// 自定义逻辑:初始化DP状态
void init() { data0 = 0; data1 = 0; }
// 自定义逻辑:递归出口返回值
ll dfsReturn(bool is_leadingzero) const {
return is_leadingzero ? 0 : data1; // 前导零视为无意义,返回0
}
// 自定义逻辑:状态转移
DpData getNextDpData(bool is_leadingzero, int digit) const;
};
// 静态成员初始化
ll DpData::K = 0;
ll DpData::base = 10;
3.2 状态机转移表与核心函数
// 状态机转移表:mat[当前状态][输入数字] = 下一状态
int mat[7][10] = {
{0,1,0,0,0,0,0,0,0,0}, // 状态0(初始):遇1→1,其他→0
{0,2,0,0,0,0,0,0,0,0}, // 状态1(匹配"1"):遇1→2,其他→0/1
{0,2,0,0,3,0,0,0,0,0}, // 状态2(匹配"11"):遇4→3,其他→0/1
{0,1,0,0,0,4,0,0,0,0}, // 状态3(匹配"114"):遇5→4,其他→0/1
{0,5,0,0,0,0,0,0,0,0}, // 状态4(匹配"1145"):遇1→5,其他→0/1
{0,2,0,0,6,0,0,0,0,0}, // 状态5(匹配"11451"):遇4→6,其他→0/1
{0,1,0,0,0,0,0,0,0,0} // 状态6(匹配"114514"):遇任何→1(已找到,不影响)
};
// 状态转移实现:根据当前状态和数字更新DP状态
DpData DpData::getNextDpData(bool is_leadingzero, int digit) const {
DpData ret = *this;
if (is_leadingzero) {
if (digit == 0) {
// 前导零+当前为0:保持前导零,状态机不变
ret.data0 = 0;
} else {
// 前导零+当前非0:取消前导零,更新状态机
ret.data0 = mat[0][digit];
}
} else {
// 无中导零:直接按状态机转移
ret.data0 = mat[ret.data0][digit];
}
// 若已找到模式串(data1=1),则保持1;否则检查是否刚匹配成功
ret.data1 = ret.data1 || (ret.data0 == 6);
return ret;
}
3.3 数位DP模板(dfs与getans)
// 记忆化数组/Map:dp[深度][前导零][限制][DP状态] = 计数
#ifdef MEMORY_USE_MAP
map<DpData, ll> dp[maxd][2][2];
#else
#define data0_max 7 // 状态机最多7个状态(0~6)
#define data1_max 2 // data1只有0/1
ll dp[maxd][2][2][data0_max][data1_max];
#endif
// 数位DP递归函数
ll dfs(const string& num, int depth, bool is_leadingzero, bool is_limit, DpData dpdata) {
// 1. 递归出口:枚举完所有数位
if (depth == num.size()) {
return dpdata.dfsReturn(is_leadingzero);
}
// 2. 记忆化返回
#ifdef MEMORY_USE_MAP
map<DpData, ll>& memo = dp[depth][is_leadingzero][is_limit];
auto it = memo.find(dpdata);
if (it != memo.end()) return it->second;
#else
ll& ans = dp[depth][is_leadingzero][is_limit][dpdata.data0][dpdata.data1];
if (ans != -1) return ans;
#endif
// 3. 枚举当前数位的可能值
ll ans = 0;
int max_digit = is_limit ? (num[depth] - '0') : (DpData::base - 1); // 数位上限
for (int digit = 0; digit <= max_digit; ++digit) {
// 更新子状态
bool new_leadingzero = is_leadingzero && (digit == 0);
bool new_limit = is_limit && (digit == max_digit);
DpData new_dpdata = dpdata.getNextDpData(is_leadingzero, digit);
// 递归累加子状态结果
ans += dfs(num, depth + 1, new_leadingzero, new_limit, new_dpdata);
}
// 4. 记忆化存储
#ifdef MEMORY_USE_MAP
memo[dpdata] = ans;
#endif
return ans;
}
// 计算[0, n]中包含模式串的数的个数
ll getans(ll n) {
// 初始化记忆化
#ifdef MEMORY_USE_MAP
for (int i = 0; i < maxd; ++i)
for (int j = 0; j < 2; ++j)
for (int k = 0; k < 2; ++k)
dp[i][j][k].clear();
#else
memset(dp, -1, sizeof(dp));
#endif
// 数字转字符串(高位在前)
string s;
if (n == 0) s = "0";
else {
int a[maxd], asize = 0;
while (n) { a[asize++] = n % 10; n /= 10; }
for (int i = asize - 1; i >= 0; --i) s.push_back('0' + a[i]);
}
// 启动数位DP
DpData dpd;
return dfs(s, 0, true, false, dpd);
}
// 计算[l, r]中包含模式串的数的个数
ll getans(ll l, ll r) {
return getans(r) - getans(l - 1);
}
3.4 主函数(二分查找逻辑)
int main() {
int t;
cin >> t;
while (t--) {
ll n;
scanf("%lld", &n);
// 二分查找边界:右边界确保包含答案
ll lans = getans(n);
ll l = n, r = (n / 1000000 + 1) * 1000000 + 114514;
// 二分查找最小x
while (l + 1 < r) {
ll mid = (l + r) >> 1;
if (getans(mid) - lans >= 1) {
r = mid; // mid左侧可能有答案
} else {
l = mid; // mid左侧无答案
}
}
printf("%lld\n", r);
}
return 0;
}
四、算法解释
4.1 状态机核心逻辑
状态机是本题识别模式串的关键,mat 数组的设计直接决定匹配准确性:
- 例如,当处于状态5(已匹配“11451”)时,若当前数字是4,
mat[5][4] = 6,data1设为1(标记找到模式串); - 若已找到模式串(
data1=1),后续状态转移不改变data1,确保即使后续数位不匹配,也不会“忘记”已找到的模式串。
4.2 前导零处理
前导零不影响实际数值(如“00114514”等价于“114514”),因此:
- 若存在前导零且当前数字为0,
is_leadingzero保持为true,data0仍为0(未开始匹配); - 若当前数字非0,
is_leadingzero设为false,开始按状态机匹配模式串。
4.3 记忆化的作用
数位DP的状态空间较小(maxd×2×2×7×2 ≈ 3.6×10³),记忆化可避免重复计算相同状态,将时间复杂度从“指数级”降至“多项式级”,确保处理大数字时不超时。
4.4 二分查找的合理性
f(x) 单调递增的证明:对任意 x1 < x2,[0, x1] 是 [0, x2] 的子集,因此 f(x1) ≤ f(x2)。基于单调性,二分查找可高效定位最小x。
五、复杂度分析
5.1 时间复杂度
-
数位DP单次计算(getans(x)):
- 状态总数:
depth × is_leadingzero × is_limit × data0 × data1=65 × 2 × 2 × 7 × 2 ≈ 3.6×10³; - 每个状态枚举10个数字(十进制),因此单次
getans(x)时间为O(3.6×10³ × 10) = O(3.6×10⁴)。
- 状态总数:
-
二分查找:
- 二分次数:
log2(r - l),其中r最多为n + 1e6,因此次数约为20; - 单次测试用例时间:
O(20 × 3.6×10⁴) = O(7.2×10⁵)。
- 二分次数:
-
多组测试用例:若
t ≤ 100,总时间为O(100 × 7.2×10⁵) = O(7.2×10⁷),远低于时间限制。
5.2 空间复杂度
- 记忆化空间:
- 若用Map:存储所有状态,空间为
O(3.6×10³)(每个状态的Map条目); - 若用数组:
dp[65][2][2][7][2],总大小约65×2×2×7×2 = 3640个ll,仅占3640×8 ≈ 29KB,空间开销极小。
- 若用Map:存储所有状态,空间为
1152

被折叠的 条评论
为什么被折叠?



