在信息学竞赛中,有一类与数位有关的区间统计问题。这类问题往往具有比较浓厚的数学味道,无法暴力求解,需要在数位上进行递推等操作——刘聪《浅谈数位类统计问题》
例题1:[bzoj1833]数字计数(模板题)
题意:求区间内0-9各出现了几次
1.做一个差分,[a,b]的统计可以理解为[1,b]-[1,a)的统计,问题转化为求前缀的0-9个数
2.要对0-9每一个数字分别dp(也可以dp多加一维状态),问题转化为求前缀的1个数(以1为例)
3.数位dp+记忆化搜索,记录五个状态(具体见代码)(还有一种dp预处理+乱搞计算,比较复杂其实是我太菜了)

1 #include<bits/stdc++.h>
2 using namespace std;
3 #define ll long long
4 int a[21];
5 ll x,y,f[21][11][21];//f[i][j][k]表示之前已经有k个j,最后i位任意填,一共有几个j(本题中可以优化,但大部分题目不能)
6 ll dfs(int k,int t,int s,int p1,int p2){//k表示dp到第i位(从高到低),t表示统计t,s表示已经有多少t,p1表示是否有非0数,p2表示是否能取到上限
7 if (!k)return s;//dp完成,返回t的个数
8 if ((p1)&&(p2)&&(f[k][t][s]))return f[k][t][s];//记忆化
9 int ma=9;//通常可以取0-9
10 if (!p2)ma=a[k];//有限制
11 ll ans=0;//答案清0
12 for(int i=0;i<=ma;i++)ans+=dfs(k-1,t,s+((i==t)&&((t)||(p1))),p1|(i>0),p2|(i<ma));//转移到下一个状态(写得比较丑)
13 if ((p1)&&(p2))f[k][t][s]=ans;//存储答案
14 return ans;//返回答案
15 }
16 ll calc(ll k,int p){
17 a[0]=0;
18 memset(f,0,sizeof(f));//初始化
19 while (k){//取出k的每一位(因为要从高到低)
20 a[++a[0]]=k%10;
21 k/=10;
22 }
23 return dfs(a[0],p,0,0,0);//记忆化搜索
24 }
25 int main(){
26 scanf("%lld%lld",&x,&y);
27 for(int i=0;i<9;i++)printf("%lld ",calc(y,i)-calc(x-1,i));//差分
28 printf("%lld",calc(y,9)-calc(x-1,9));
29 }
例题2:[hdu2089]不要62(需要记录其他状态)
题意:多组数据,求区间不含62或4的数个数
1.同样做差分,转换为前缀
2.在上一题的基础上,记录上一个数字即可(详见代码)

1 #include<bits/stdc++.h>
2 using namespace std;
3 int x,y,a[11],f[11][2];//f[i][j]表示任意i位数上一位状态为j(是否等于6)的合法数数量
4 int dfs(int k,int t,int p){//k表示dp到第k位,t表示上一个数字是否为6,p表示是否要小于等于ai
5 if (!k)return 1;//1个合法数字
6 if ((p)&&(f[k][t]))return f[k][t];//记忆化
7 int ma=9,ans=0;//清空答案
8 if (!p)ma=a[k];//求出上限
9 for(int i=0;i<=ma;i++)
10 if ((i!=4)&&((i!=2)||(!t)))ans+=dfs(k-1,i==6,p|(i<ma));//判断是否可行&累计答案
11 if (p)f[k][t]=ans;//存储答案
12 return ans;//返回答案
13 }
14 int calc(int k){//计算1-k的答案
15 a[0]=0;
16 memset(f,0,sizeof(f));//清空
17 while (k){//转化为十进制(要从高到低)
18 a[++a[0]]=k%10;
19 k/=10;
20 }
21 return dfs(a[0],0,0);//记忆化搜索
22 }
23 int main(){
24 while (scanf("%d%d",&x,&y)!=EOF){
25 if ((!x)&&(!y))return 0;
26 printf("%d\n",calc(y)-calc(x-1));//差分
27 }
28 }
类似题目:
1.hdu3555:记录上一个数
2.bzoj1026:记录上一个数
3.hdu3652:记录上一个数和余数
4.hdu3709:记录上一个数和权值差
例题3:[bzoj3329]Xorequ(二进制下的数位dp)
题意:t组数据,求小于等于n/2^n的满足x^3x=2x的数个数
1.问题转化为2x^x=3x(异或满足自反性),在分析发现x满足条件当且仅当二进制下没有相邻1
2.类似于上题(比上题还简单)的dp,第二问需要矩阵乘法优化dp,与数位dp无关故不讲

1 #include<bits/stdc++.h>
2 using namespace std;
3 #define ll long long
4 #define mod 1000000007
5 struct ji{
6 int a[2][2];
7 ji operator * (const ji &b){
8 ji c;
9 for(int i=0;i<2;i++)
10 for(int j=0;j<2;j++)
11 c.a[i][j]=(1LL*a[i][0]*b.a[0][j]+1LL*a[i][1]*b.a[1][j])%mod;
12 return c;
13 }
14 }p;
15 int t,ans,a[101];
16 ll n,f[101][2];
17 ll dfs(int k,int t,int p){//记忆化搜索
18 if (!k)return 1;//1种答案
19 if ((p)&&(f[k][t]))return f[k][t];//记忆化
20 int ma=1;
21 ll ans=0;//清空答案
22 if (!p)ma=a[k];//确定上限
23 for(int i=0;i<=ma;i++)
24 if ((!i)||(!t))ans+=dfs(k-1,i,p|(i<ma));//判断&统计答案
25 if (p)f[k][t]=ans;//存储答案
26 return ans;//返回答案
27 }
28 ll calc(ll k){
29 a[0]=0;
30 memset(f,0,sizeof(f));//清空答案
31 while (k){//转化为二进制
32 a[++a[0]]=k%2;
33 k/=2;
34 }
35 return dfs(a[0],0,0);//计算答案
36 }
37 ji ksm(ji n,ll m){
38 if (m==1)return n;
39 ji s=ksm(n,m>>1);
40 s=s*s;
41 if (m&1)s=s*n;
42 return s;
43 }
44 int main(){
45 scanf("%d",&t);
46 while (t--){
47 scanf("%lld",&n);
48 printf("%lld\n",calc(n)-1);//计算答案,不能为0
49 if (n==1){
50 printf("2\n");
51 continue;
52 }
53 p=ksm(ji{1,1,1,0},n-1);
54 ans=0;
55 for(int i=0;i<2;i++)
56 for(int j=0;j<2;j++)ans=(ans+p.a[i][j])%mod;
57 printf("%d\n",ans);
58 }
59 }
其他题目:
1.poj3252:记录0和1的数量差(需要+40防止为负)
2.cf809C:发现规律——$A_{i,j}=(i-1)^(j-1)+1$(从1开始),然后把(i-1)^(j-1)和1分离计算即可
3.bzoj3209:枚举含有多少个1,然后即求二进制中有k个1的个数,数位dp
例题4:[hdu4352]XHXJ's LIS(数位dp+状压dp)
题意:t组数据,求区间内把一个数每一位分开后LIS为k的数个数
1.做差分转化为前缀问题
2.考虑LIS的一种求法:维护每一个长度的最小结尾,可以用这个序列来表示答案
3.这个序列只与每一个数字有没有出现有关,用2^10状压
4.必须要在记忆化中存储LIS的长度,否则每一次都清空会tle

1 #include<bits/stdc++.h>
2 using namespace std;
3 #define ll long long
4 int t,k,a[21];
5 ll l,r,f[21][11][2005];//f[i][j][k]表示i位数初始状态为k有多少方案LIS为j
6 int count(int k){//统计1的个数
7 int ans=0;//清空答案
8 while (k){//类似树状数组的统计方法
9 k&=k-1;//去掉最后一个1
10 ans++;//答案+1
11 }
12 return ans;//返回答案
13 }
14 ll dfs(int k,int t,int s,int p1,int p2){//k表示位数,t表示LIS的目标长度,s表示LIS的状态,p1表示是否为0,p2表示是否要求取到上限
15 if (!k)return count(s)==t;//判断LIS是否符合条件
16 if ((p1)&&(p2)&&(f[k][t][s]>=0))return f[k][t][s];//记忆化
17 int ma=9,la=-1,ss;//la表示上一个s状态中非0位置
18 ll ans=0;//清空答案
19 if (!p2)ma=a[k];//计算上限
20 for(int i=9;i>ma;i--)
21 if (s&(1<<i))la=i;//初始化la
22 for(int i=ma;i>=0;i--){//枚举下一位
23 if (s&(1<<i))la=i;//修改la
24 ss=s;
25 if ((p1)||(i))ss+=(1<<i);
26 if (la>=0)ss-=(1<<la);//修改LIS的状态
27 ans+=dfs(k-1,t,ss,p1|(i>0),p2|(i<ma));//累计答案
28 }
29 if ((p1)&&(p2))f[k][t][s]=ans;//存储答案
30 return ans;//返回答案
31 }
32 ll calc(ll k,int p){//计算前缀答案
33 for(a[0]=0;k;k/=10)a[++a[0]]=k%10;//转化为十进制
34 return dfs(a[0],p,0,0,0);//记忆化搜索
35 }
36 int main(){
37 scanf("%d",&t);
38 memset(f,-1,sizeof(f));//清空,不能清为0,因为可能有很多答案都是0
39 for(int ii=1;ii<=t;ii++){
40 scanf("%lld%lld%d",&l,&r,&k);
41 printf("Case #%d: %lld\n",ii,calc(r,k)-calc(l-1,k));//差分
42 }
43 }
其他题目:
cf1073E:对每一个数字是否出现状压
好像就没什么题目了我太菜了
例题5:[cf908G]New Year and Original Order(优化/改变dp状态)
题意:f(x)表示将x各数位排序后的值,求小于等于n的x的f(x)之和,mod 1e9+7
1.发现0-9每一个数字相互独立,因此分别计算
2.状态中新增有多少个数字大于统计数字,多少个数字等于统计数字(小于可以计算得到),但状态太大
3.发现只需要统计大于等于该数字的数量,然后计算贡献即可,时间复杂度可以接受
PS:好像没人写记忆化?好吧那我也不写记忆化了……

1 #include<bits/stdc++.h>
2 using namespace std;
3 #define mod 1000000007
4 int n,ans,f[1005][1005][11][2];//f[i][j][k][l]表示前i位,有j个数大于等于k,能否取到上限(l)
5 char s[1005];
6 int main(){
7 scanf("%s",s);
8 n=strlen(s);
9 for(int i=0;i<10;i++)f[0][0][i][0]=1;//0位数有0个数大于等于某数
10 for(int i=0;i<n;i++){
11 int p=(s[i]-'0');
12 for(int j=0;j<=i;j++)
13 for(int k=0;k<10;k++)
14 for(int l=0;l<2;l++)//对应f状态的ijkl
15 for(int t=0;t<=max(p,9*l);t++){//枚举最后一位来转移
16 f[i+1][j+(k<=t)][k][l|(t<p)]+=f[i][j][k][l];//考虑对哪些状态有贡献来转移
17 f[i+1][j+(k<=t)][k][l|(t<p)]%=mod;//转移得到的状态
18 }
19 }
20 for(int i=1,s=0;i<=n;i++){
21 s=(10LL*s+1)%mod;//计算i个1的答案
22 for(int j=1;j<10;j++)
23 for(int k=0;k<2;k++)ans=(ans+1LL*s*f[n][i][j][k])%mod;//统计答案
24 }
25 printf("%d",ans);
26 }
hdu4734:发现f的值域约为1e4,存储状态(i,j,k)表示dp到i位,当前数是j,不能超过k,但这样会炸掉,发现答案只与j-k的差值有关,存储(i,j-k)即可
好像也没什么题目了还是我太菜了
总结(数位dp的基本套路):
1.差分,基本上所有区间的询问都要差分(也可能在二维甚至更高维差分)
2.记录有无前导0、是否能取到上限来判断(特殊题目可能还有别的)
3.多记录一些东西,用增加状态来减少清空复杂度(例4)
4.对0-9的数字分离,适用于数字间关系不大的情况
5.考虑优化/改变状态来达到节省空间/时间的作用(例5)
6.不要看到数位有关的问题就用数位dp(比如bzoj4292枚举f(n)即可)
