数位动规,就是对于数位进行动规(日常一句废话···)
刚好今天听数位dp,就总结一下最近写的题吧。郭神说要学懂数位dp,还是要搞懂它内部是怎么工作的。比如一个有大小的数,我们在这里剥夺它作为一个整数的所有标签,它现在只不过是几个位置,我们要在上面按照一定的限制放置数字,除了我给的限制之外,他们位与位之间没有任何影响。
比如15651,38983,对于我的限制来说,这两个数并没有什么本质区别,他们都是有五个位置的,不含前导零,并且完全回文的数字串,除此之外,什么也不是。
所以,我们就要想办法把某些限制的数筛出来,这就是数位dp。对于数位dp,我见过循环递推的写法,当然在我这边更流行的是记忆化搜索,今天讲的重点也是记忆化搜索;通过我对数位dp的理解,我整理了一个类似于记忆化搜索模板的东西,请大家先从下面看一两道题之后再来看这个模板:
这个dfs函数包含几个参数,比如说当前决策的位(len)、题目中给的限制条件所需要的一两个参数、当前是否在数值范围的上界、有时还有前导零问题。具体题目还需具体分析。请看下图:
这只是一个比较笼统的模板,具体题目更要灵活应用!
下面分享几道水题:
这个题一句话就是说求1~n中含49的数字有多少,那我们直接在状态中记录决策时的最后两位,出现四十九的最后返回1,否则0;大家看这道题,前导零不前导零就没什么影响,因为49又不可能出现在前导零中,所以既不会出现多余的答案,也不会漏掉正确的答案!
请看代码:
1 #include<iostream> 2 #include<cstdio> 3 #include<cmath> 4 #include<cstring> 5 #include<algorithm> 6 using namespace std; 7 const int MAXN=21;char ch; 8 long long f[MAXN][10][2],n,m,k,dig[MAXN],len1; 9 long long dfs(int len,int la1,int la2,bool flag,bool ap){ 10 if(len>len1) return (int)ap; 11 if(!flag&&~f[len][la2][(int)ap]) return f[len][la2][(int)ap]; 12 long long tot=0;int end=flag?dig[len]:9; 13 for(int i=0;i<=end;i++) 14 tot+=dfs(len+1,la2,i,flag&&(i==end),((la2==4)&&(i==9))||ap); 15 if(!flag) f[len][la2][(int)ap]=tot;return tot; 16 } 17 int main(){ 18 scanf("%lld",&k); 19 while(k--){ 20 len1=0;ch=getchar();long long ans=0; 21 while(!isdigit(ch)) ch=getchar(); 22 while(isdigit(ch)) dig[++len1]=ch-'0',ch=getchar(); 23 memset(f,-1,sizeof(f));ans=dfs(1,0,0,1,0); 24 printf("%lld\n",ans); 25 } 26 }
代码很短,也好写,尤其是,还很模板,作为入门题在适合不过啦!
这个题乍一看根上一题一模一样!仔细一看也是一模一样,状态里还是记两位,只不过在判62的基础上判一下有没有4就好了!
请看代码:
1 #include<iostream> 2 #include<cstdio> 3 #include<cstdlib> 4 #include<cmath> 5 #include<algorithm> 6 #include<cstring> 7 #include<string> 8 #include<queue> 9 using namespace std; 10 long long f[10][10],d[10]; 11 long long n,m,k,l,r; 12 void take() 13 { 14 f[0][0]=1; 15 for(int i=1;i<=7;i++) 16 for(int j=0;j<=9;j++) 17 for(int k=0;k<=9;k++) 18 if(j!=4&&!(j==6&&k==2)) 19 f[i][j]+=f[i-1][k]; 20 return ; 21 } 22 int solve(int n) 23 { 24 int ans=0,len=0; 25 while(n){ 26 d[++len]=n%10; 27 n /= 10; 28 } 29 d[len+1]=0; 30 for(int i=len;i>=1;--i){ 31 for(int j=0;j<d[i];j++){ 32 if(d[i+1]!=6||j!=2) 33 ans+=f[i][j]; 34 } 35 if(d[i]==4||(d[i+1]==6&&d[i]==2)) break; 36 } 37 return ans; 38 } 39 int main() 40 { 41 take(); 42 while(~scanf("%d%d",&m,&n)&&m&&n){ 43 printf("%d\n", solve(n+1)-solve(m)); 44 } 45 return 0; 46 }
这道题根上一道题我用的方法不同,这是用循环做的,有兴趣的同学可以试着用模板虐一下这题,也可以学学循环写法拓宽思路。
这个题也很模板,平衡的数嘛,我们就枚举作为支点的那个数,然后一个参数记录题目限制,直接套用模板。但是这个题对于0来说就用一点影响了,000000这种数,每一位都是支点,都会被计算一次,最后要从答案中把多计算的减去!
请看代码:
1 #include<iostream> 2 #include<cmath> 3 #include<cstdio> 4 #include<cstring> 5 #include<algorithm> 6 #define ll long long 7 using namespace std; 8 const int MAXN=20; 9 ll f[MAXN][MAXN][MAXN*100],l,r;int t,dig[MAXN]; 10 ll dfs(int pos,int x,int s,bool flag){ 11 if(pos<=0) return !s;if(s<0) return 0; 12 if(!flag&&~f[pos][x][s]) return f[pos][x][s]; 13 int end=flag?dig[pos]:9;ll ans=0; 14 for(int i=0;i<=end;i++) 15 ans+=dfs(pos-1,x,s+i*(pos-x),flag&&(i==end)); 16 if(!flag) f[pos][x][s]=ans;return ans; 17 } 18 ll solve(ll x){ 19 if(x<0) return 0;int len=0;ll ans=0; 20 while(x) dig[++len]=x%10,x/=10; 21 for(int i=1;i<=len;i++) ans+=dfs(len,i,0,1); 22 return ans-len+1; 23 } 24 int main(){ 25 memset(f,-1,sizeof(f)); 26 scanf("%d",&t);while(t--){ 27 scanf("%lld%lld",&l,&r); 28 printf("%lld\n",solve(r)-solve(l-1)); 29 }return 0; 30 }
也特别短,几乎就是模板!
这个题需要稍微稍微动下脑筋,也特别简单,含长度至少为2的回文部分,我们可以简单地想成两种情况!分别是奇回文和偶回文,我们想,一个奇回文一定含有一个长度为3的回文部分(我称之为回文核),一个偶回文一定含有一个长度为2的回文核,那么这个题就解决了,只需要记录当前决策前两个数的状态,分情况判断。但是这个题有一个问题,前导零个数超过2的都会被记录,需要处理前导零的问题。
请看代码:
1 #include<cstdio> 2 #include<iostream> 3 #include<string.h> 4 #include<algorithm> 5 using namespace std; 6 struct data{ 7 long long a,b;//a表示所有情况,b表示已有回文 8 }d[1005][11][11]; 9 char s1[1005],s2[1005]; 10 int nl,nr,len,i,dp[1005][11][11],digit[8000],mod=1000000007;//dp数组表示是否记录过,digit表示数位数字 11 inline data dfs(int len,int state,int ss,bool flag,bool ff){//len表示当前位置,state,ss分别表示两位数字,flag表示是否要枚举到9,ff表示当前是否是这个数的第一个数字,即我说的那个注意点 12 if(len==0){ 13 data zs; zs.a=1; zs.b=0; 14 return zs;}//边界 15 if(dp[len][state][ss]&&flag==false&&ff==false)return d[len][state][ss]; data z; z.a=z.b=0;//如果已经访问过直接返回 16 int meiju=flag?digit[len]:9; //确定枚举范围 17 for(int i=0;i<=meiju;i++){ 18 data zs=dfs(len-1,ff&i==0?10:i,ff&&i==0?10:state,flag&&(i==digit[len]),ff&&(i==0)); 19 z.a+=zs.a; z.b+=zs.b; if(state==i||ss==i)z.b+=zs.a-zs.b; z.a%=mod; z.b%=mod;//是否产生回文 20 } 21 if(flag==false&&ff==false){dp[len][state][ss]=true;d[len][state][ss]=z;} //存储 22 return z; 23 } 24 int main(){ 25 memset(dp,0,sizeof(dp)); 26 scanf("%s%s",&s1,&s2); 27 nl=strlen(s1); nr=strlen(s2);len=nl; 28 for(i=0;i<nl;i++)digit[len-i]=s1[i]-'0'; int flag=false; 29 for(i=0;digit[i]==0;i++); digit[i]--; if(i==1&&digit[i]==1)nl--; for(i--;i;i--)digit[i]=9;//将l减1 30 long long zs=dfs(len,10,10,true,true).b; 31 len=nr; 32 for(int i=0;i<nr;i++)digit[len-i]=s2[i]-'0'; 33 cout<<(dfs(len,10,10,true,true).b-zs+mod)%mod<<endl; 34 }
我记得好像写过这题题解。额不对,我写的是Round Number!
五、当然是Round Number咯
甭解释了,写的非常清楚了!!
这个提也比较模板,可以说是只记录两个数就好,但我用的貌似是循环做法,可以看一下。
请看代码:
1 #include<iostream> 2 #include<cstdio> 3 #include<cmath> 4 #include<cstdlib> 5 #include<cstring> 6 #include<string> 7 #include<algorithm> 8 #include<queue> 9 using namespace std; 10 int f[15][15],n,m,l,k,r; 11 int a[15]; 12 void init() 13 { 14 for(int i=0;i<=9;i++) f[1][i]=1; 15 for(int i=2;i<=10;i++){ 16 for(int j=0;j<=9;j++){ 17 for(int k=0;k<=9;k++){ 18 if(abs(j-k)>=2) f[i][j]+=f[i-1][k]; 19 } 20 } 21 } 22 } 23 int solve(int x) 24 { 25 memset(a,0,sizeof(a)); 26 int len=0,ans=0; 27 while(x){ 28 a[++len]=x%10; 29 x/=10; 30 } 31 for(int i=1;i<=len-1;i++){ 32 for(int j=1;j<=9;j++){ 33 ans+=f[i][j]; 34 } 35 } 36 for(int i=1;i<a[len];i++){ 37 ans+=f[len][i]; 38 } 39 for(int i=len-1;i;i--){ 40 for(int j=0;j<=a[i]-1;j++){ 41 if(abs(j-a[i+1])>=2) ans+=f[i][j]; 42 } 43 if(abs(a[i+1]-a[i])<2) break; 44 } 45 return ans; 46 } 47 int main() 48 { 49 init(); 50 scanf("%d%d",&n,&m); 51 cout<<solve(m+1)-solve(n)<<endl; 52 return 0; 53 }
处理边界的那一段长一些。
这个题就不是纯模板了,放在这里,权当开发智力!这个题需要试几组比较整的数来找找规律,同时还要注意比如0,1之类的细节,思虑周全再写会更好!
请看代码:
1 #include<iostream> 2 #include<cstdio> 3 #include<cmath> 4 #include<algorithm> 5 #include<cstdlib> 6 #include<cstring> 7 #include<string> 8 #include<queue> 9 using namespace std; 10 long long a,b; 11 long long ten[20],f[20],cnta[20],cntb[20]; 12 void solve(long long x,long long *cnt) 13 { 14 long long num[20]; 15 int len=0; 16 while(x){ 17 num[++len]=x%10; 18 x/=10; 19 } 20 for(int i=len;i;i--){ 21 for(int j=0;j<=9;j++){ 22 cnt[j]+=f[i-1]*num[i]; 23 } 24 for(int j=0;j<num[i];j++){ 25 cnt[j]+=ten[i-1]; 26 } 27 long long num2=0; 28 for(int j=i-1;j>=1;j--){ 29 num2=num2*10+num[j]; 30 } 31 cnt[num[i]]+=num2+1; 32 cnt[0]-=ten[i-1];//处理前导零问题; 33 } 34 } 35 int main() 36 { 37 scanf("%lld%lld",&a,&b); 38 ten[0]=1;//辅助数组,代表10的i次方 39 for(int i=1;i<=15;i++){ 40 f[i]=f[i-1]*10+ten[i-1]; 41 ten[i]=ten[i-1]*10; 42 } 43 solve(a-1,cnta); 44 solve(b,cntb); 45 for(int i=0;i<=9;i++) { 46 cout<<cntb[i]-cnta[i]<<" "; 47 } 48 puts(""); 49 return 0; 50 }
八、如果说前面都是水题,那这一道算是不那么水的题。luogu 3286 方伯伯的商场之旅
我们考虑代价是怎样计算的,从Balanced Number那里,我们可以得到一些启发,大概也是要找某个算起来比较平衡的数位来贡献代价!我们仔细观察,假如我们要把支点从当前位向右移动一位,代价会怎样变化?于是乎我们想到了用前缀和来计算支点的偏移,这样,我们通过两个dfs函数,首先,假定所有数的支点都在第一位(最左边一位),然后一位一位向右偏移,如果当前偏移能减小代价,那么就用答案减去这个值,否则就答案减去0;一直推到最后一位,这个答案就确定了!附一详细题解:errrrrrrrr
请看代码:
1 //%%%%%%%尹兄 2 #include<iostream> 3 #include<cstdio> 4 #include<cstring> 5 #include<cmath> 6 #include<algorithm> 7 #define ms(a,x) memset(a,x,sizeof(a)) 8 #define ll long long 9 using namespace std; 10 const int MAXN=60; 11 ll f[MAXN][MAXN*50],l,r; 12 int k,n=0,dig[MAXN]; 13 ll dfs1(int len,int s,bool flag){ 14 if(len<=0) return s; 15 if(!flag&&~f[len][s]) return f[len][s]; 16 int end=flag?dig[len]:k-1;ll ans=0; 17 for(int i=0;i<=end;i++) 18 ans+=dfs1(len-1,s+i*(len-1),flag&&i==end); 19 if(!flag) f[len][s]=ans;return ans; 20 } 21 ll dfs2(int len,int s,int m,bool flag){ 22 if(s<0) return 0;if(len<=0) return s; 23 if(!flag&&~f[len][s]) return f[len][s]; 24 int end=flag?dig[len]:k-1;ll ans=0; 25 for(int i=0;i<=end;i++) 26 ans+=dfs2(len-1,s+(len>=m?i:-i),m,flag&&i==end); 27 if(!flag) f[len][s]=ans;return ans; 28 } 29 ll solve(ll x){ 30 n=0;while(x) dig[++n]=x%k,x/=k; 31 ms(f,-1);ll ans=dfs1(n,0,1); 32 for(int i=2;i<=n;i++) 33 ms(f,-1),ans-=dfs2(n,0,i,1); 34 return ans; 35 } 36 int main(){ 37 scanf("%lld%lld%d",&l,&r,&k); 38 printf("%lld\n",solve(r)-solve(l-1)); 39 return 0; 40 }
可以看到,两个函数都是由模板改过来的,所以这题也没有那么难
通过上述这些题目我们可以看到,大部分的数位dp,都是通过模板进行一个变形,万变不离其宗,有的时候一些玄学优化会加大他的难度,所以数位dp说难也难,说不难也不难请各位勇敢的直面它。