数位dp

Posted pywbktda

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数位dp相关的知识,希望对你有一定的参考价值。

  例题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 
View Code

 

例题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 
View Code

  类似题目: 

  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(ji1,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 
View Code

   其他题目:

  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 
View Code

  其他题目:

  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 
View Code

  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)即可

以上是关于数位dp的主要内容,如果未能解决你的问题,请参考以下文章

数位dp模板 [dp][数位dp]

数位DP入门

数位dp详解

数位dp总结 之 从入门到模板

动态规划专题——数位DP

数位dp小练