re:从零开始的数位dp

起源:唔,,前几天打cf,edu50那场被C题虐了,决定学学数位dp。(此文持续更新至9.19)

ps:我也什么都不会遇到一些胡话大家不要喷我啊。。。

数位dp问题:就是求在区间l到r上满足规定条件的数的个数。

ex1:hdu3555

  题意:给你n,求从一到n中有多少个数不包含“49”。(t<=1e4,n<=2^63-1)

  首先数位dp顾名思义就是对数位进行dp嘛,所以dp数组的第一维我们用来保存数字的位数,第二位我们用来判定当前位是否为4,

所以就是  dp[20][2];  这个样子。 在这之前我们先考虑一下常规的搜索思路,一件非常显然的事情,在[1,1000]和[1001,2000]以及所有类似区间里符合要求的数都是一样的,这样我们就可以通过记忆化的方式来保存某些结果。

先给出solve函数

1 ll solve(ll num){
2     int k = 0;//记录数位
3     while(num){
4         k++;
5         digit[k]=num%10;
6         num/=10;
7     }
8     return dfs(k,false,true);
9 }
View Code

 

这个很好理解嘛,保存这个数各位上的数字。然后我们就可以进行记忆化搜索了,

dp[20][2]:表示 1.有4的时候有几个含有49, 2.没有4的时候,有几个含有49。

ll dfs(int len,bool if4,bool limit){
//当前是第几位,上一位是否是4,上一位是否是上界
if(len==0)//统计完了直接返回1
return 1;
if(!limit&&dp[len][if4])//不是上界并且这种情况已经统计过
return dp[len][if4];
ll cnt=0,up_bound=(limit?digit[len]:9);//up_bound是当前位能满足的最大值,如果上一位是上界的话,当前位最大只能取到当前位的数字,如果不是,当前位可以从0取到9
for(int i=0;i<=up_bound;i++){
if(if4&&i==9)
continue;//上一位是4并且这一位是9,GG了啊
cnt+=dfs(len-1,i==4,limit&&i==up_bound);//上一位是上界的情况下我们才会考虑这一位是否是上界
}
if(!limit)//不是上界,属于通用的情况,我们进行赋值
dp[len][if4]=cnt;
return cnt;
}
最后结果差分一下就好。

ex2:hdu2089
和上道题几乎一样,条件是没有“4”并且没有“62”,
这时候我们掏出上一道题的板子了嘛肯定要,只需要在判断时加入一句话就行,在代码中加上注释了,就不做多解释了
#include <bits/stdc++.h>
using namespace std;
int n,m;
int digit[10];
int dp[10][2];
int dfs(int len,bool if6, bool limit){
    if(len==0)
        return 1;
    if(!limit&&dp[len][if6])
        return dp[len][if6];
    int cnt = 0,up_bound = (limit?digit[len]:9);
    for(int i=0;i<=up_bound;i++){
        if(i==4)//如果遇到四就直接GG
            continue;
        if(if6&&i==2)
            continue;
        cnt+=dfs(len-1,i==6,limit&&i==up_bound);
    }
    if(!limit)
        dp[len][if6]=cnt;
    return cnt;
}
int solve(int num){
    int k = 0;//记录数位
    while(num){
        k++;
        digit[k]=num%10;
        num/=10;
    }
    return dfs(k,false,true);
}

int main(){
    while (scanf("%d%d",&n,&m)&&(n+m)) {
        cout << solve(m) - solve(n - 1) << endl;
    }
}
View Code

  

  ex3:codeforces1036C,也就是EDU50的C题嘛,一道非常简单的板子题,(问题是我当时还没听说过数位dp,,真的,,不然就可以骑学长了,,哭)

  条件是 零的个数不大于3个。

  很显然我们只要把dp数组的第二维开到3就可以了嘛,,然后就是套板子,, 我这里把0的情况单独拿出来了,因为1到9都可以一起考虑嘛

  

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int digit[20];
ll dp[20][4];
ll dfs(int len,int not0,bool limit){
    if(len==0)//完了
        return 1;
    if(!limit&&dp[len][not0])//已经统计过
        return dp[len][not0];//
    ll cnt=0,up_bound=(limit?digit[len]:9);//
    cnt+=dfs(len-1,not0,limit&&digit[len]==0);//
    for(int i=1;i<=up_bound;i++){
        if(not0==3)
            continue;
        cnt+=dfs(len-1,not0+1,limit&&i==up_bound);
    }
    if(!limit)
        dp[len][not0]=cnt;
    return cnt;
}
ll solve(ll num){
    int k = 0;//记录数位
    while(num){
        k++;
        digit[k]=num%10;
        num/=10;
    }
    return dfs(k,0,true);
}
int t;ll l,r;
int main(){
    ios::sync_with_stdio(false);
    cin>>t;
    while (t--){
        cin>>l>>r;
        cout<<(solve(r)-solve(l-1))<<endl;
    }
}
View Code

 

ex4:

  hdu3652

  条件:包括“13”并且能被13整除

  首先我们想到用余数来分类嘛,然后结合最初的板子,开出来的dp数组就是这样的 dp[17][17][3];//分别是数位,余数,对于“13”的三种状态(包括1,包括13,啥都没有)

  还有取模的那个地方需要稍微理解一下,剩下的也就是板子了

  

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 int digit[17];
 4 int dp[17][17][3];
 5 int dfs(int len,int mod,int have,int limit){
 6     if(len==0)
 7         return mod==0&&have==2;
 8     if(!limit&&dp[len][mod][have])
 9         return dp[len][mod][have];
10     int cnt = 0,up_bound=(limit?digit[len]:9);
11     for(int i=0;i<=up_bound;i++){
12         int mod_ = (mod*10+i)%13;
13         int tmp = have;
14         if(have==0&&i==1)
15             tmp = 1;
16         if(have==1&&i!=1)
17             tmp = 0;
18         if(have==1&&i==3)
19             tmp = 2;
20         cnt+=dfs(len-1,mod_,tmp,limit&&i==up_bound);
21     }
22     if(!limit)
23         dp[len][mod][have] = cnt;
24     return cnt;
25 }
26 
27 int solve(int num){
28     int k = 0;
29     while (num){
30         k++;
31         digit[k] = num%10;
32         num/=10;
33     }
34     return dfs(k,0,0,1);
35 }
36 int n;
37 int main(){
38     ios::sync_with_stdio(false);
39     while (scanf("%d",&n)!=EOF){
40 //    scanf("%d",&n);
41         cout<<solve(n)<<endl;
42     }
43 }
View Code

 

ex5: codeforces 55 d,  是上一道题略微进化版   条件“这个数能被他的每个非零位的最小公倍数整除”;

首先我们还是想到对模数来分类,这是非常显然的嘛,除此之外,我们需要保存“最小公倍数”,因为转移的时候必须要用到之前的,这样子我们开出来的数组是 dp[20][2520][2520],好恭喜你MLE on test 1,这时就需要一些奇淫技巧。我们会发现,lcm的个数非常有限吧,实际上只有48个,所以我们可以离散化嘛。然后就与上一题非常相似了,我们保存 “presum”和“prelcm”,进行记忆化搜索即可。

为什么可以%2520呢,

  • 首先我们能够知道如果这个数能够整除它的每个数位上的数字,那么它一定能够整除他们的最小公倍数,是充要的。
  • 那么我们定义状态dp[i][j][k]代表i位在任意组合下,得到的所有数位的数字的最小公倍数为j,且该数%2520为k的方案数。
  • 我们可以知道任意多个1-9之间的数的公倍数最大不会超过2520,而且他们都是2520的约数,所以(一个数%2520)能够被该数所有数位的数字的最小公倍数整除,那么该数就能整除自己每个数位上的数字。
#include <cstdio>
using namespace std;
typedef long long ll;
int t;ll l,r;
const int mod = 2520;
ll dp[20][mod][50];
int ind[mod+5];
int digit[20];
void init(){
    int cnt = 0;
    for(int i=1;i<=mod;i++){
        if(mod%i==0)
            ind[i]=cnt++;
    }
}

int gcd(int a, int b){
    return b==0?a:gcd(b,a%b);
}

int lcm(int a, int b){
    return a/gcd(a,b)*b;
}
ll dfs(int len,int presum,int prelcm,bool limit){
    if(len==0)
        return presum%prelcm==0;
    if(!limit&&dp[len][presum][ind[prelcm]])
        return dp[len][presum][ind[prelcm]];
    ll cnt = 0;int up_bound = limit?digit[len]:9;
    for(int i=0;i<=up_bound;i++){
        int nowsum = (presum*10+i)%mod;
        int nowlcm = prelcm;
        if(i!=0)
            nowlcm = lcm(nowlcm,i);
        cnt+=dfs(len-1,nowsum,nowlcm,limit&&i==up_bound);
    }
    if(!limit)
        dp[len][presum][ind[prelcm]] = cnt;
    return cnt;
}
ll solve(ll num){
    int k = 0;
    while (num){
        k++;
        digit[k]=num%10;
        num/=10;
    }
    return dfs(k,0,1,1);
}

int main(){
    init();
    scanf("%d",&t);
    while (t--){
        scanf("%I64d%I64d",&l,&r);
        printf("%I64d\n",solve(r)-solve(l-1));
    }
}
View Code

 

ex6:poj 3286  求区间里包含多少个零

我随手一写竟然过了。网上题解貌似没看到和我的写法一样的。所以讲的详细些,还是套板子,dfs里的四个参数分别表示 位数,零的个数,是否前导零,是否上界

然后就很简单了嘛。注意到l可以取到0,然后我们的dfs对于零来说是没有计算在内的,所以要加上1。

#include <iostream>
using namespace std;
typedef long long ll;
int digit[20];
int dp[20][20];
ll l,r;
ll dfs(int len,int count,bool zero,bool limit){
    if(len==0)
        return count;
    if(!zero&&!limit&&dp[len][count])
        return dp[len][count];
    ll cnt=0;int up_bound=limit?digit[len]:9;
    cnt+=dfs(len-1,count+(zero?0:1),zero,limit&&digit[len]==0);
    for(int i=1;i<=up_bound;i++){
        cnt+=dfs(len-1,count,false,limit&&i==up_bound);
    }
    if(!limit&&!zero)
        dp[len][count]=cnt;
    return cnt;
}
ll solve(ll num){
    int k = 0;
    while (num){
        k++;
        digit[k]=num%10;
        num/=10;
    }
    return dfs(k,0,1,1);
}
int main(){
    ios::sync_with_stdio(false);
    while (1) {
        cin >> l >> r;
        if(l==-1||r==-1)
            return 0;
        if (l == 0)
            cout << solve(r) + 1 << endl;
        else
            cout << solve(r) - solve(l - 1) << endl;
    }
}
View Code

 

ex7:poj2282&&bzoj1833&&luogu2602&&zjoi1010

求区间里包含“0,1,2,3,4,5,6,7,8,9”的个数分别是多少

和ex6一毛一样嘛,,一个很直观的思路就是我们把ex6跑十遍吧,嗯正解就是这样(囍的不行),我觉着求“1,2,3,4,5,6,7,8,9”还要比“0”简单咯,不用考虑前导0,然后把代码稍微一改就可以了。哦对了!他这个鬼输入竟然l还能大于r,所以要判断swap一下,,,我一开始测样例输出了一堆负数让我受到了很大的惊吓。。。

  

#include <iostream>
#include <cstring>
using namespace std;
typedef long long ll;
int digit[10];
int dp[10][10][10];
int ans[10][2];
ll l,r;
ll dfs0(int len,int count,bool zero,bool limit){
    if(len==0)
        return count;
    if(!zero&&!limit&&dp[len][count][0])
        return dp[len][count][0];
    ll cnt=0;int up_bound=limit?digit[len]:9;
    cnt+=dfs0(len-1,count+(zero?0:1),zero,limit&&digit[len]==0);
    for(int i=1;i<=up_bound;i++){
        cnt+=dfs0(len-1,count,false,limit&&i==up_bound);
    }
    if(!limit&&!zero)
        dp[len][count][0]=cnt;
    return cnt;
}
ll dfs(int len,int count,int num,bool limit){
    if(len==0)
        return count;
    if(!limit&&dp[len][count][num])
        return dp[len][count][num];
    ll cnt = 0;int up_bound=limit?digit[len]:9;
    for(int i=0;i<=up_bound;i++){
        cnt+=dfs(len-1,count+(i==num),num,limit&&i==up_bound);
    }
    if(!limit)
        dp[len][count][num]=cnt;
    return cnt;
}
void solve(ll num,int ind){
    int k = 0;
    while (num){
        k++;
        digit[k]=num%10;
        num/=10;
    }
    ans[0][ind]=dfs0(k,0,1,1);
    for(int i=1;i<=9;i++)
        ans[i][ind]=dfs(k,0,i,true);
}
void init(){
    memset(dp,0, sizeof(dp));
    memset(digit,0, sizeof(digit));
}
int main(){
    ios::sync_with_stdio(false);
    while (cin>>l>>r&&l&&r) {
        if(l>r)
            swap(l,r);
        init();
        solve(r,0);
        init();
        solve(l-1,1);
        for(int i=0;i<10;i++)
            cout<<ans[i][0]-ans[i][1]<<" ";
        cout<<endl;
    }
}
View Code

 

ex8:8102上海大都会赛J题 

能被各位数字和整除 数据范围(N<=1e12)

  首先我们应该会求 “被1整除”,“被二整除”,“被三整除”这样子的吧,然后神妙的战法就出现了,我们可以枚举各位数字和,,,12*9=128,也就是跑100来遍就行了吧。。。

初始化要初始化成-1,因为可能有很多状态本身就没有符合条件的数。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int t;ll n;
int digit[17];
ll dp[17][120][120];//第i位,之前数位之和位j,对某个mod余数为k的满足条件的个数
int nowsum;
ll dfs(int len, int sum,int mod,bool limit){
    if(len==0)
        return (sum==nowsum&&mod==0);
    if(!limit&&dp[len][sum][mod]!=-1)
        return dp[len][sum][mod];
    ll cnt = 0;int up_bound=limit?digit[len]:9;
    for(int i=0;i<=up_bound;i++){
        if(sum+i>nowsum)
            break;
        cnt+=dfs(len-1,sum+i,(mod*10+i)%nowsum,limit&&i==digit[len]);
    }
    if(!limit)
        dp[len][sum][mod]=cnt;
    return cnt;
}
ll solve(ll num) {
    int k = 0;
    while (num) {
        k++;
        digit[k] = num % 10;
        num /= 10;
    }
    ll res = 0;
    for(int i=1;i<=9*k;i++) {
        nowsum = i;
        memset(dp,-1, sizeof(dp));
        res += dfs(k,0,0, true);
    }
    return res;
}

int main(){
    ios::sync_with_stdio(false);
    cin>>t;
    int cas = 0;
    while (t--){
        cin>>n;
        cout<<"Case "<<++cas<<": "<<solve(n)<<endl;
    }
    return 0;
}
View Code

 

 

   

 

posted @ 2018-09-11 18:26  MXang  阅读(178)  评论(0编辑  收藏  举报