关于数位动规(入门到进阶,难度中档)

  数位动规,就是对于数位进行动规日常一句废话···

  刚好今天听数位dp,就总结一下最近写的题吧。郭神说要学懂数位dp,还是要搞懂它内部是怎么工作的。比如一个有大小的数,我们在这里剥夺它作为一个整数的所有标签,它现在只不过是几个位置,我们要在上面按照一定的限制放置数字,除了我给的限制之外,他们位与位之间没有任何影响

  比如15651,38983,对于我的限制来说,这两个数并没有什么本质区别,他们都是有五个位置的,不含前导零,并且完全回文的数字串,除此之外,什么也不是。

  所以,我们就要想办法把某些限制的数筛出来,这就是数位dp。对于数位dp,我见过循环递推的写法,当然在我这边更流行的是记忆化搜索,今天讲的重点也是记忆化搜索;通过我对数位dp的理解,我整理了一个类似于记忆化搜索模板的东西,请大家先从下面看一两道题之后再来看这个模板:

  这个dfs函数包含几个参数,比如说当前决策的位(len)、题目中给的限制条件所需要的一两个参数、当前是否在数值范围的上界、有时还有前导零问题。具体题目还需具体分析。请看下图:

这只是一个比较笼统的模板,具体题目更要灵活应用!

下面分享几道水题:

一、HDU 3555 Bomb

这个题一句话就是说求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 }
数位dp

代码很短,也好写,尤其是,还很模板,作为入门题在适合不过啦!

 

二、HDU 2089 不要62

这个题乍一看根上一题一模一样!仔细一看也是一模一样,状态里还是记两位,只不过在判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 }
数位dp

这道题根上一道题我用的方法不同,这是用循环做的,有兴趣的同学可以试着用模板虐一下这题,也可以学学循环写法拓宽思路。

 

三、HDU 3709 Balance Number

这个题也很模板,平衡的数嘛,我们就枚举作为支点的那个数,然后一个参数记录题目限制,直接套用模板。但是这个题对于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 }
数位dp

也特别短,几乎就是模板!

 

四、luoguP3413 萌数

这个题需要稍微稍微动下脑筋,也特别简单,含长度至少为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 }
数位dp

我记得好像写过这题题解。额不对,我写的是Round Number!

 

 

五、当然是Round Number

 甭解释了,写的非常清楚了!!

 

六、luogu 2657 Windy数

这个提也比较模板,可以说是只记录两个数就好,但我用的貌似是循环做法,可以看一下。

请看代码:

 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 }
数位dp

处理边界的那一段长一些。

 

七、luogu 2602 数字计数

这个题就不是纯模板了,放在这里,权当开发智力!这个题需要试几组比较整的数来找找规律,同时还要注意比如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 }
数位dp

 

八、如果说前面都是水题,那这一道算是不那么水的题。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,都是通过模板进行一个变形,万变不离其宗,有的时候一些玄学优化会加大他的难度,所以数位dp说难也难,说不难也不难请各位勇敢的直面它。

转载于:https://www.cnblogs.com/Alan-Luo/articles/9514428.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值