P3413 SAC#1 - 萌数
知识点: 数位DP
原题面
题目要求:
给定 两整数 \(l,r\)
求 \([l,r]\) 中有多少个 包含长度 \(\ge 2\) 的回文串 的数
\(l \le r \le 1e1000\)
分析题意:
可以发现 , 一些显然的结论 :
-
包含长度 \(\ge 2\) 的回文串 等价于:
包含 长度\(=2\) 或者 包含 长度\(=3\) 的回文串- 证明 :
最短的回文串只有: \(aa\) 或者 \(aba\) 两种形态
且 在一个回文串首尾 各添加一相同字母后 , 得到的仍为一回文串
若考虑 每一个数各位 的选择情况, 即可得知其是否合法
考虑数位DP - 证明 :
-
可以发现 , 对于一个合法的数 , 向其 尾部/首部 添加任意数量的任意数 ,
得到的新数 仍然是一个合法的数则可使用记忆化 \(DFS\) 实现数位 \(DP\) .
对于一个数 , 若其已知其最后一位 , 与倒数第二位,
则可以通过选择 向尾部添加的数 , 从而构造合法的数 -
开始构建状态:
设 \(f[i][j][k][0/1]\) : 枚举到第 \(i\) 位 , 第 \(i-1\) 位为 \(j\) , 第 \(i-2\) 位为 \(k\) , \(0/1\) 代表在第i位前是否有回文串-
为什么需要标记 是否有回文串?
-
对于 下列两种状态 :
\(54123 , 32123\) ,
尾部两元素相同, 但是前者不合法, 后者合法
则需要进行标记 , 防止错解发生 -
在实现记忆化时 , 对于 状态 \(f\text{[i][j][k][0/1]}\)
\(f\text{[i][j][k][0/1]}\) 用于记录 \(i\) 位之后 有/无 回文串的 串的个数
若不进行标记 , 则无法判断 \(i\) 位之后 有/无回文串的 串若 \(i\) 位之前 已有回文串 , 则无论 \(i\) 位之后 是否存在 回文串
构成的串 全部合法若 \(i\) 位之前 无回文串 , 则\(i\)位之后 必须存在回文串 , 否则构造的串不合法
若 不进行标记 , 则无法得到 \(i\) 位之后有回文串的 串的个数
直接进行转移 可能会构造出 不合法的 , 但是计入了答案的串
-
-
算法实现:
然后就可以套数位DP的 记忆化dfs模板
- 前缀和思想, 求 \(1 \sim l-1\) 的答案, 求 \(1 \sim r\) 的答案, 并进行相减
- 枚举每一位数, 判断是否全为前导零, 是否到达可选择的上界, 是否已为合法串
- 深入到 第 \(0\) 层时, 说明构造完成, 返回答案即可
- 注意进行记忆化
#include<cstdio>
#include<cstring>
#include<ctype.h>
#define ll long long
const int MARX = 1010;
const ll mod = 1e9+7;
//=============================================================
char num[MARX];
//f[i][j][k][0/1]: 枚举到第i位, 第i-1位为j, 第i-2位为k
ll ans1,ans2, lim[MARX],f[MARX][10][10][2];
//=============================================================
inline int read()
{
int s=1, w=0; char ch=getchar();
for(; !isdigit(ch);ch=getchar()) if(ch=='-') s =-1;
for(; isdigit(ch);ch=getchar()) w = w*10+ch-'0';
return s*w;
}
//分别代表: 枚举到的位数, 倒数第一个数, 倒数第二个数, 是否到达可选择的上界, 是否全为前导零, 是否为合法状态
int dfs(ll pos, ll last1, ll last2, bool limit, bool zero, ll flag)
{
ll sum = 0;
if(!pos) return flag;//搜到第 0位, 返回答案
if(!limit && (!zero) && (last1 != -1) && (last2 != -1) && f[pos][last1][last2][flag] != -1) return f[pos][last1][last2][flag];//已被搜过, 返回记忆化的值
for(int i = 0, up = (limit?lim[pos]:9); i <= up; i ++)//枚举每一位(注意判断上界
{
bool limit1 = (limit && i == up), zero1 = (!i && zero);//更新各变量
ll flag1 = (flag || (!zero && i == last1) || (!zero && i == last2));//判断是否合法
ll last11 = (zero1 ?-1 : i), last21 = (zero ? -1 : last1);//更新 倒数的数
ll ret = dfs(pos - 1, last11, last21, limit1, zero1, flag1) % mod;
sum = (sum + ret) % mod;//累加返回值
}
if(!limit && (!zero) && (last1 != -1) && (last2 != -1)) return (f[pos][last1][last2][flag] = sum);//合法状态,进行记忆化
return sum;
}
ll solve(char s[], ll op) //将字符串 转化为数字
{
ll lth = strlen(s+1), tot = 1;
memset(f,-1,sizeof(f));//初始化
for(ll i = 1; i <= lth; i ++) lim[lth - i + 1] = s[i] - '0';//倒序存储
while(lim[tot] - op < 0)//l-1的特殊操作
{
if(tot == lth) break;
lim[tot] = 9, tot ++;
}
lim[tot] -= op;//l-1的特殊操作
return dfs(lth, -1, -1, 1, 1, 0);
}
//=============================================================
signed main()
{
scanf("%s", num + 1); ans1 = solve(num, 1);//求解
scanf("%s", num + 1); ans2 = solve(num, 0);
printf("%lld",(ans2 - ans1 + mod) % mod);//前缀和
}