数位dp:处理数字数位关系的一种dp方式。
一般的题目特征十分明显:
1.一般和数字本身有很大关系。
2.一般求数字在区间L,R中的一些信息
3.L,R一般很大,通常能达到long long级别。
dp方式也比较有套路:
一般有三种方法:
本质上的相似之处,都是集中在处理“填数有无限制”,“填数无限制情况下的固定方案数”,“某些已经搜出来的固定方案数”
1.记忆化搜索(没用过)
是一种倒着记忆的方法。
2.递推(我基本都是这个方法)
正着递推出答案。
一般会从高位向低位递推,讨论高位的填数方法,从而递推出低位的限制与否。
从高位到低位可以通过填的位数处理限制问题。
用一个[0/1]表示,填完这一位之后,是否对后面的数有限制。
填一个数,从k<num,k==num,k>num 讨论。
注意处理可能出现的前导零的锅。
3.预处理一些固定没有限制的一些位置,然后从高位开始填,
没有限制的话,直接查表。
有限制的话,继续填下一位。
注意处理已经填好的位置的固定影响。
经典入门例题:
[ZJOI2010]数字计数
给定两个正整数a和b,求在[a,b]中的所有整数中,每个数码(digit)各出现了多少次。
直接做就好了。
可以处理1~9,1~99,。。的固定答案,或者直接递推下去。
处理1~9,1~99,1~999...的方法:
Code:
#include<bits/stdc++.h> #define ll long long using namespace std; const int N=20; ll a,b; ll cnta[20],cntb[20]; ll f[20],ten[20]; void query(ll x,ll *cnt) { int len=0; int num[20]; ll k=x; while(k) { num[++len]=k%10; k/=10; } for(int i=len;i>=1;i--) { for(int j=0;j<=9;j++) cnt[j]=cnt[j]+f[i-1]*num[i]; for(int j=0;j<num[i];j++) cnt[j]+=ten[i-1]; ll num2=0; for(int j=i-1;j>=1;j--) { num2=num2*10+num[j]; } num2++; cnt[num[i]]+=num2; cnt[0]-=ten[i-1]; } } int main() { scanf("%lld%lld",&a,&b); ten[0]=1; for(int i=1;i<=15;i++) { f[i]=f[i-1]*10+ten[i-1]; ten[i]=ten[i-1]*10; } query(a-1,cnta); query(b,cntb); for(int i=0;i<=9;i++) printf("%lld ",cntb[i]-cnta[i]); return 0; }
数字计数1
递推处理:
Code:
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=15; struct node{ ll a[11]; void init(){ memset(a,0,sizeof a); } node operator +(const node &b){ node c;c.init(); for(int i=0;i<=9;i++) c.a[i]=a[i]+b.a[i]; return c; } node operator *(const int &x){ node c;c.init(); for(int i=0;i<=9;i++) c.a[i]=a[i]*x; return c; } node operator -(const node &b){ node c;c.init(); for(int i=0;i<=9;i++) c.a[i]=a[i]-b.a[i]; return c; } void op(){ cout<<endl; for(int i=0;i<=9;i++) cout<<" "<<a[i];cout<<endl;cout<<endl; } }f[N][2],ans,kk; int num[N],cnt; ll ten[N]; ll pre[N]; ll A,B; void sol(){ for(int i=0;i<=cnt+1;i++) for(int j=0;j<=1;j++) f[i][j].init(); for(int i=cnt;i>=1;i--){ //cout<<num[i]<<" dig"<<endl; f[i][1]=f[i+1][1];f[i][1].a[num[i]]++; f[i][0]=(f[i+1][1]*num[i])+(f[i+1][0]*10); for(int j=0;j<=9;j++) { if(j<num[i]) f[i][0].a[j]+=(pre[i+1]+1); else f[i][0].a[j]+=pre[i+1]; } //f[i][1].op(); //f[i][0].op(); } for(int i=cnt;i>=1;i--){ f[1][0].a[0]-=ten[i-1]; } } int main() { scanf("%lld%lld",&A,&B);ten[0]=1; for(int i=1;i<=13;i++)ten[i]=ten[i-1]*(ll)10; while(B){ num[++cnt]=B%10; pre[cnt]=B;B/=10; } //pre[cnt+1]=1; sol(); ans=ans+f[1][0]+f[1][1]; cnt=0;memset(num,0,sizeof num); memset(pre,0,sizeof pre); A--; while(A){ num[++cnt]=A%10; pre[cnt]=A; A/=10; } //pre[cnt+1]=1; sol(); kk=kk+f[1][0]+f[1][1]; ans=ans-kk; for(int i=0;i<=9;i++){ printf("%lld ",ans.a[i]); } return 0; }
数字计数2
[SCOI2009]windy数
windy定义了一种windy数。不含前导零且相邻两个数字之差至少为2的正整数被称为windy数。 windy想知道,
在A和B之间,包括A和B,总共有多少个windy数?
Solution:
直接记录最后一个数是什么,要记录是否已经出现了非零数字,即前导零。处理前导零bug
因为前导零不处理,可能会认为最后一位填的是0,从而不能填1、0了。
f[i][last][0/1限制][0/1有无出现非零数字]
Code:
#include<bits/stdc++.h> using namespace std; const int N=12; int f[N][N][2][2]; int num[N],cnt; int A,B; int wrk(){ memset(f,0,sizeof f); for(int i=1;i<num[cnt];i++) f[cnt][i][0][1]=1; f[cnt][num[cnt]][1][1]=1; f[cnt][0][0][0]=1; for(int i=cnt;i>=2;i--){ for(int j=0;j<=9;j++){ for(int k=0;k<=9;k++){ if(abs(k-j)>=2){ if(j==num[i]&&k==num[i-1]) f[i-1][k][1][1]+=f[i][j][1][1]; if(j==num[i]&&k<num[i-1]) f[i-1][k][0][1]+=f[i][j][1][1]; f[i-1][k][0][1]+=f[i][j][0][1]; if(j==0) f[i-1][k][0][1]+=f[i][j][0][0]; } } } f[i-1][0][0][0]+=f[i][0][0][0]; f[i-1][1][0][1]+=f[i][0][0][0]; } int ret=0; for(int j=0;j<=9;j++){ ret+=f[1][j][0][1]+f[1][j][1][1]; } //ret+=f[1][0][0][0]; return ret; } int main() { scanf("%d%d",&A,&B); while(B){ num[++cnt]=B%10;B/=10; } int ansB=wrk(); A--;cnt=0; while(A){ num[++cnt]=A%10;A/=10; } int ansA=wrk(); //cout<<ansB<<" "<<ansA<<endl; printf("%d",ansB-ansA); return 0; }
windy数
SAC#1 - 萌数
辣鸡蒟蒻SOL是一个傻逼,他居然觉得数很萌!
好在在他眼里,并不是所有数都是萌的。只有满足“存在长度至少为2的回文子串”的数是萌的——也就是说,101是萌的,因为101本身就是一个回文数;110是萌的,因为包含回文子串11;但是102不是萌的,1201也不是萌的。
现在SOL想知道从l到r的所有整数中有多少个萌数。
由于答案可能很大,所以只需要输出答案对1000000007(10^9+7)的余数。
Solution:
抓住问题本质,长度至少为2的回文子串,只要有2或者3就一定满足。
所以,只要记录上一位,上上位填的数即可。
还要记录是否已经出现过长度为2或者3的回文串。
同样,前导零的锅也要处理的。
Code:
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=1000+10; const int mod=1e9+7; int n,m; char a[N],b[N]; int la,lb; ll ans; ll r,l; ll f[N][2][2][12][12][2];// lim ; has huiwen ; has >=1 ll wrk(char *s,int len){ memset(f,0,sizeof f); for(int j=0;j<=s[1]-‘0‘;j++){ if(j!=0){ if(j<s[1]-‘0‘)f[1][0][0][10][j][1]=1; else f[1][1][0][10][j][1]=1; } else f[1][0][0][10][10][0]=1; } if(len==1) return 0; for(int i=2;i<=lb;i++){ for(int j=0;j<=10;j++){ for(int k=0;k<=10;k++){ for(int p=0;p<=9;p++){ if(p<s[i]-‘0‘){ if(p==k||p==j) (f[i][0][1][k][p][1]+=f[i-1][0][0][j][k][1]+f[i-1][1][0][j][k][1])%=mod; else (f[i][0][0][k][p][1]+=f[i-1][0][0][j][k][1]+f[i-1][1][0][j][k][1])%=mod; (f[i][0][1][k][p][1]+=f[i-1][0][1][j][k][1]+f[i-1][1][1][j][k][1])%=mod; } else if(p==s[i]-‘0‘){ if(p==k||p==j) (f[i][0][1][k][p][1]+=f[i-1][0][0][j][k][1])%=mod; else (f[i][0][0][k][p][1]+=f[i-1][0][0][j][k][1])%=mod; (f[i][0][1][k][p][1]+=f[i-1][0][1][j][k][1])%=mod; if(p==k||p==j) (f[i][1][1][k][p][1]+=f[i-1][1][0][j][k][1])%=mod; else (f[i][1][0][k][p][1]+=f[i-1][1][0][j][k][1])%=mod; (f[i][1][1][k][p][1]+=f[i-1][1][1][j][k][1])%=mod; } else{ if(p==k||p==j) (f[i][0][1][k][p][1]+=f[i-1][0][0][j][k][1])%=mod; else (f[i][0][0][k][p][1]+=f[i-1][0][0][j][k][1])%=mod; (f[i][0][1][k][p][1]+=f[i-1][0][1][j][k][1])%=mod; } } } } for(int p=1;p<=10;p++){ if(p==10) (f[i][0][0][10][p][0]+=f[i-1][0][0][10][10][0])%=mod; else (f[i][0][0][10][p][1]+=f[i-1][0][0][10][10][0])%=mod; } } ll ret=0; for(int j=0;j<=9;j++) for(int k=0;k<=9;k++){ (ret+=f[len][1][1][j][k][1]+f[len][0][1][j][k][1])%=mod; } return ret; } int main() { scanf("%s",a+1);scanf("%s",b+1); la=strlen(a+1),lb=strlen(b+1); l=wrk(a,la); r=wrk(b,lb); bool fl=false; for(int i=2;i<=la;i++){ if(a[i]==a[i-1]||a[i]==a[i-2]) fl=true; } ans=(r+fl-l+mod)%mod; printf("%lld",ans); return 0; }
萌数
bzoj 3329 Xorequ
Description
Input
第一行一个正整数,表示数据组数据 ,接下来T行
每行一个正整数N
Output
2*T行
第2*i-1行表示第i个数据中问题一的解,
第2*i行表示第i个数据中问题二的解,
HINT
x=1与x=2都是原方程的根,注意第一个问题的解
不要mod 10^9+7
1<=N<=10^18
1<=T<=1000
Solution:
首先一定要化简x^3x=2x
即:3x=x^2x ; x+2x=x^2x 说明,2倍的x和x没有公共1位置,否则就会变小了。
2x就是x二进制下左移一位,所以,x的条件其实是,x的二进制表示下没有连续的两个1.
对于第一问,记录上一位填的是0/1即可转移。
对于第二问,发现,是2的整次幂。2^n显然满足条件,
这里假设可以是0,不需要正整数条件,而0显然也满足条件。
最后也不用减,因为2^n本来就要加1个。
即处理有n位可以填0/1。
设f[i]表示,i位随便填0/1,符合的数的个数。
第i位填1的话,i-1位只能填零,方案数就是f[i-2]
第i位填0的话,对i-1位没有影响,就是f[i-1]
所以f[i]=f[i-1]+f[i-2],f[1]=2,f[2]=3
一个斐波那契数列。
矩阵乘法优化一下即可。
Code:(多组数据记得memset)
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=66; const int mod=1e9+7; ll f[N][2][2]; int num[N],cnt; ll wrk1(){ memset(f,0,sizeof f); f[cnt+1][1][0]=1; for(int i=cnt;i>=1;i--){ for(int k=0;k<=1;k++){ if(k<num[i]){ if(k==1) f[i][0][k]+=f[i+1][0][0]+f[i+1][1][0]; else f[i][0][k]+=f[i+1][0][0]+f[i+1][0][1]+f[i+1][1][0]+f[i+1][1][1]; } else if(k==num[i]){ if(k==1) f[i][1][k]+=f[i+1][1][0],f[i][0][k]+=f[i+1][0][0]; else f[i][1][k]+=f[i+1][1][1]+f[i+1][1][0],f[i][0][k]+=f[i+1][0][0]+f[i+1][0][1]; } else { if(k==1) f[i][0][k]+=f[i+1][0][0]; else f[i][0][k]+=f[i+1][0][0]+f[i+1][0][1]; } } } ll ret=0; ret=f[1][0][0]+f[1][0][1]+f[1][1][0]+f[1][1][1]; return ret-1; } struct tr{ ll a[3][3]; void pre(){ memset(a,0,sizeof a); } void init(){ for(int i=1;i<=2;i++) a[i][i]=1; } tr operator *(const tr &b){ tr c;c.pre(); for(int i=1;i<=2;i++) for(int k=1;k<=2;k++) for(int j=1;j<=2;j++){ (c.a[i][j]+=a[i][k]*b.a[k][j]%mod)%=mod; } return c; } }A,S,B; tr qm(tr x,ll y){ tr ret;ret.pre();ret.init(); while(y){ if(y&1) ret=ret*x; x=x*x; y>>=1; } return ret; } int t; ll n; int main() { scanf("%d",&t); A.a[1][1]=3,A.a[1][2]=2; B.a[1][1]=1,B.a[1][2]=1; B.a[2][1]=1,B.a[2][2]=0; while(t--){ scanf("%lld",&n); ll nn=n; cnt=0; while(nn){ num[++cnt]=nn%2;nn/=2; } ll ans1=wrk1(); printf("%lld\n",ans1); ll ans2; if(n==1){ ans2=2; } else if(n==2){ ans2=3; } else{ S=A*qm(B,n-2); ans2=S.a[1][1]%mod; } printf("%lld\n",ans2); } return 0; }
Xorequ
CF55D Beautiful numbers
Volodya是一个很皮的男孩。他认为一个能被它自己的每一位数上的数整除(非0)的数是很妙的。我们先忽略他的想法的正确性,只回答在l到r之间有多少个很妙的数字。
输入输出格式
输入:总共有t个询问:
第一行:t;
接下来t行:每行两个数l和r。
注意:请勿使用%lld读写长整型(虽然我也不知道为什么),请优先使用cin(或者是%I64d)。
输出:t行,每行为一个询问的答案。
Solution:
比较巧妙的状态设计,还有优化 的题目。
直接记录被1,2,3..除是肯定不行的,。
发现,lcm(1,2.。。9)=2520
所以,如果最后能被1,2.。。9整除,那么把这个数对2520取模,不会影响整除与否的。
所以,f[i][2520][S]表示,填到i位,对2520取模结果,出现数字的状压集合。
但是,这个题有10组数据,就T飞了。
考虑优化没用的状态。
S大小是256的,不用记0,不用记1,要记8个。
但是,最后要找的还是S出现数字的lcm能否整除x
所以,就直接记录lcm就行了。
2250=2^3*3^2*5*7,一共只有48种lcm,离散化记录,256->48
但是还是过不去(可能我的代码常数太大?)
发现,记录mod 2250的余数时,假设之前是j,
那么,新的=j*10+k mod 2250(k是当前位的数)
设x=a2250+j;
每次j要乘10,是不是记录mod 225即可?
x=a‘225 + j‘
x‘=a‘2250+10j‘+k
发现,x mod 2250 的只和j‘有关,
所以,记录j‘即可,每次mod 225记录新的j
最后一次,mod 2250 就是mod 2250 的余数了。
Code:(luogu最优解)
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=25; const int up=252; ll L,R; int a[N],cnt; ll f[N][252][2][50]; ll g[2525][2][50]; int lcm[1026],tot; int id[2530]; int to[50][12]; int gcd(int p,int q){ return q?gcd(q,p%q):p; } void dfs(int x,int lc){ //cout<<" x lc "<<x<<" "<<lc<<endl; if(x==10){ lcm[++tot]=lc; return; } dfs(x+1,lc); dfs(x+1,lc*x/gcd(x,lc)); } ll wrk(){ ll ret=0; memset(f,0,sizeof f); memset(g,0,sizeof g); f[cnt+1][0][1][1]=1; for(int i=cnt;i>=1;i--){ for(int j=0;j<252;j++){ for(int k=1;k<=tot;k++){ for(int p=0;p<=9;p++){ if(i!=1){ if(p<a[i]){ f[i][(j*10+p)%up][0][to[k][p]]+=f[i+1][j][0][k]+f[i+1][j][1][k]; } else if(p==a[i]){ f[i][(j*10+p)%up][0][to[k][p]]+=f[i+1][j][0][k]; f[i][(j*10+p)%up][1][to[k][p]]+=f[i+1][j][1][k]; } else { f[i][(j*10+p)%up][0][to[k][p]]+=f[i+1][j][0][k]; } } else{ if(p<a[i]){ g[(j*10+p)%2520][0][to[k][p]]+=f[i+1][j][0][k]+f[i+1][j][1][k]; } else if(p==a[i]){ g[(j*10+p)%2520][0][to[k][p]]+=f[i+1][j][0][k]; g[(j*10+p)%2520][1][to[k][p]]+=f[i+1][j][1][k]; } else { g[(j*10+p)%2520][0][to[k][p]]+=f[i+1][j][0][k]; } } } } } } for(int j=0;j<2520;j++){ for(int k=1;k<=tot;k++){ if(j%lcm[k]==0){ ret+=g[j][1][k]+g[j][0][k]; } } } return ret; } int T; ll ansl,ansr; int main() { dfs(1,1); sort(lcm+1,lcm+tot+1); //cout<<tot<<endl; tot=unique(lcm+1,lcm+tot+1)-lcm-1; for(int i=1;i<=tot;i++) id[lcm[i]]=i; for(int i=1;i<=tot;i++){ to[i][0]=i; for(int j=1;j<=9;j++){ to[i][j]=id[lcm[i]*j/gcd(lcm[i],j)]; } } /*cout<<" tot "<<tot<<endl; for(int i=1;i<=tot;i++){ cout<<lcm[i]<<" "<<id[lcm[i]]<<" "<<endl; for(int j=0;j<=9;j++){ cout<<" with "<<j<<" : "<<to[i][j]<<endl; } }*/ scanf("%d",&T); while(T--){ scanf("%I64d%I64d",&L,&R); ansl=ansr=0; L--; if(L==0) ansl=1; else{cnt=0; while(L){ a[++cnt]=L%10;L/=10; }ansl=wrk(); } cnt=0; while(R){ a[++cnt]=R%10;R/=10; }ansr=wrk(); printf("%I64d\n",ansr-ansl); } return 0; }
Beautiful numbers
[SCOI2014]方伯伯的商场之旅
非常麻烦的数位dp了。
见另外一篇博客:
[SCOI2014]方伯伯的商场之旅
原文地址:https://www.cnblogs.com/Miracevin/p/9534844.html