【模板&2个套路】数位DP 从入门到放弃

最近囫囵吞枣,吃了几个dp类型的知识点。就算是临阵磨枪吧,毕竟神仙打架的杭州快开始了。 打算收官完dp,就去回头巩固那些思维上的东西了。 到时候算法题没做出来,我也不怪自己,毕竟时间少,积累少。 要是思维没想到,感觉接受不了。

.

  1. 数位dp
    :这类题 一般都是要求 l-r中 满足某一个条件的数有多少,一般这个条件都可以拆为: 以j为开头的i位的数 ,满足条件的有多少:dp[i][j][k]。
    第三位可以用来做别的事情。辅助去算那个满足的条件。

T1.hdu2089

题意:条件即为: 数字中不要出现 “62” 以及“4”。
Input
输入的都是整数对n、m(0< n≤m<1000000),如果遇到都是0的整数对,则输入结束。

Output
对于每个整数对,输出一个不含有不吉利数字的统计个数,该数值占一行位置。

这是一道很简单的入门题:
我们甚至只要二维就可以解决:
设置dp[i][j] 表示以j开头i位的满足条件的数字有多少?
在遍历过程中 ,符合条件的去做就可以了:

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
#include<stdlib.h>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<vector>
const double PI = acos(-1.0);
const double e = exp(1.0);
#define ll __int64
template<class T> T gcd(T a, T b) { return b ? gcd(b, a % b) : a; }
template<class T> T lcm(T a, T b) { return a / gcd(a, b) * b; }
using namespace std;
/*
一个一个去理解
比如统计15
第一位 dp[2][0]=9  即 00 01 02 03 05 06 07 08 09
第二位 dp[1][0~4]  有 0 1 2 3  有四个 ,而这四个 其实是: 10 11 12 13,是建立在前一个1上的
所以一共有13个
但是并没有去统计 15 这个本身的数
 */


int dp[10][10];  //dp[i][j]  以j开头 并且长度为i的 个数
void init(){
    memset(dp,0,sizeof(dp));
    for(int i=0;i<=9;i++)
        dp[1][i]=1;
    for(int i=1;i<=7;i++){  //  数字长度最多为7
        for(int j=0;j<=9;j++){   //第i位   j
            for(int k=0;k<=9;k++){  //第i-1 位 k
                if(j==4 || k==4) continue;
                if(j==6 && k==2) continue;
                dp[i][j]+=dp[i-1][k];  //如此一来  j......(i位)= (j) 0-9 .... (i-1位) 的和
            }
        }
    }
}
char a[16],b[15];
int count(char *str,int v){
    int len=strlen(str);
    int ans=0;
     for(int i=0;i<len;i++){
          int id=str[i]-'0';  // 第len-i 位 id
          //这一步 i没有建立 和i-1位的联系, 只是去计算开头为0-i-1 的多少位的合法数
          // 可以说这一步是建立在 a[i-1] 就是a[i-1]的情况下的
         for(int j=0;j<id;j++){  //统计所有这一位比 id 小的合法数字
             if(j!=4 && !(str[i-1]=='6' && j==2)){
               ans+=dp[len-i][j];
             }
         }
         if(str[i]=='4' || (str[i-1]=='6' && str[i]=='2')){ //后面都不用再统计了
             break;
         }
//         printf("v=%d i=%d %d\n",v,i,ans);
     }
     int flag=1;
     for(int i=0;i<len;i++){
            if(str[i]=='4')
                  flag=0;
            if(str[i]=='6' && str[i+1]=='2')
                  flag=0;
     }
     if(v==1)
         flag=0;
     ans+=flag;
     return ans;
}
int main(){
    //freopen("1.txt","r",stdin);
   while(~scanf("%s %s",a,b)){
       init();
       int lena=strlen(a);
       int lenb=strlen(b);
       if(lena==lenb && lena==1){
           if(a[0]=='0' && b[0]==a[0]){
               return 0;
           }
       }
       int l=0,r=0;
       l=count(a,1);
       r=count(b,2);
//       printf("%d %d\n",l,r);
       //判断 r 是否可行,需要这一步了,
      printf("%d\n",r-l);
   }
   return 0;
}

这是最普通,最简单,往往也是最直接有效的解题手段,根据自己的思维直接去解题即可。

hdu 3652
题意依然很简单: l,r中满足条件的数。
r< 1e9
条件:该数既要能被13整除,也要含有“13”;

这题比刚刚那题多了一个条件,除了一定包含XX,还需要能被某个数整除。
这里我们自然而然的多开一维,甚至多开两维,不影响大局,却还能够方便我们的操作,以及拓宽我们的思路。

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
#include<stdlib.h>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<vector>
const double PI = acos(-1.0);
const double e = exp(1.0);
#define ll __int64
template<class T> T gcd(T a, T b) { return b ? gcd(b, a % b) : a; }
template<class T> T lcm(T a, T b) { return a / gcd(a, b) * b; }
using namespace std;

ll dp[12][12][13][2];
//dp 以j开头的i位数 余数为k 前面没有出现13 为0     ...前面已经出现过13 为1
/*
可以把这个dp  当做两个dp来看
 *
 */

int digit[12] , cnt ;
ll f(int n,int j)  //j *1e(n)
{
    ll i , k = j ;
    for(i = 0 ; i < n ; i++)
        k *= 10 ;
    return k ;
}
void init()
{
    int i , j , k , l , temp ;
    memset(dp,0,sizeof(dp)) ;
    for(j = 0 ; j < 10 ; j++)
        dp[1][j][ j%13 ][0] = 1 ; //初始化
    for(i = 2 ; i <= 10 ; i++)       //一共i位
    {
        for(j = 0 ; j < 10 ; j++)    //这一位为 j
        {
            temp = f(i-1,j) ;   //j*1e(i-1)  这一位贡献j00...0
            for(k = 0 ; k < 10 ; k++) //k 代表后一位的数字是多少  即:  jk......
            {
                for(l = 0 ; l < 13 ; l++)   //遍历 后一位所有的余数,得到这一位的余数
                {
                    dp[i][j][ (temp+l)%13 ][1] += dp[i-1][k][l][1] ;  //前面出现过,这里也出现
                    if( j == 1 && k == 3 )  // 这一次出现
                        dp[i][j][ (temp+l)%13 ][1] += dp[i-1][k][l][0] ; //前面没出现过,这里出现了
                    else
                        dp[i][j][ (temp+l)%13 ][0] += dp[i-1][k][l][0] ; //+前面也没有出现过的
                }
            }
        }
    }
    return ;
}
ll solve(ll temp)
{
    memset(digit,0,sizeof(digit)) ;
    cnt = 0 ;
    while( temp )
    {
        digit[++cnt] = temp%10 ;
        temp /= 10 ;
    }
    ll ans = 0 , i , j, k ;
    for(j = 0 ; j < digit[cnt] ; j++)   //这个不能忘掉
        ans += dp[cnt][j][0][1] ;
    temp = f(cnt-1,digit[cnt]) ;  // id * 1e(cnt-1)
    int flag=0;  //表示是否出现过13
    for(i = cnt-1 ; i > 0 ; i--)
    {
        for(j = 0 ; j < digit[i] ; j++)  //扫描0-id-1
        {
            for(k = 0 ; k < 13 ; k++)    //扫描所有余数
            {
                //扫描的数是: temp + j....(j往后一共i位) 余数为k
                if((temp+k)%13 != 0)
                    continue;
                ans += dp[i][j][k][1] ;    //+已经含有13,且当前余数k 的个数
                if( ( digit[i+1] == 1&& j == 3) ) //如果这里出现了13
                    ans += dp[i][j][k][0] ;   //那就可以+前面 没有出现过13 的
                else if( flag )
                    ans += dp[i][j][k][0];
            }
        }
        if( digit[i+1] == 1 && digit[i] == 3 )
            flag = 1 ;
        temp = temp + f(i-1,digit[i]) ;  //temp 更新。 +当前id
    }
    return ans ;
}
int main()
{
    ll n ;
    init() ;
    int i , j , k ;
    while( scanf("%I64d", &n) != EOF )
    {
        printf("%I64d\n", solve(n+1) ) ;
    }
    return 0;
}

可以总结一下,这两题表明的这种做法,大概是这么个套路:
1.先预处理(init):把所有的满足条件的一些dp[i][j][k][l] 什么的都算出来,
2.然后再dp 统计一下(count),整个时间复杂度会很短。

codeforces 55D
这题题意:
条件: 一个数需要能够被组成他的每一位非0的数整除;
比如 abcd 要能够 abcd%a = abcd%b =abcd%c =abcd%d=0;

这题咋一想挺难的, 这个条件不给你明显的规则让你去 dp。所以我们需要去找规则,需要把已知的信息转换一下,转换成我们需要的。

比如abcd 要能够整除 a、b、c、d 那么也就是 %lcm(a,b,c,d)=0;
这一点很重要。 那加入一个数1-9去包括了,也就是lcm(1,2,3,4,5,6,7,8,9)=2520. 即我们最多要处理的余数也不过就是2520。

这个套路 的dp意义:dp[i][j][k] 就是 长度为i , (以j开头) 某个条件 ,(辅助条件)
这里我们第二个条件不需要用以j开头,因为 在dfs下 我们是一边dfs,一边就记录答案。为什么这样呢? 正是dfs 的回溯 属性,能够让我们在每一个dfs 下都能算出他后面的事情,自然也就能够得到这一层dfs 的贡献值。

另外一个很机智的点就是:
把0-2520 ,分为几段, 这里只能说Notonlysuccess 毕竟不是我等弱鸡能够看得懂的, 只能知道这么用确实会很好用,这个条件到底如何想出来,可能我还没精通到那个地步。

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
#include<stdlib.h>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<vector>
const double PI = acos(-1.0);
const double e = exp(1.0);
#define ll __int64
#define LL __int64
template<class T> T gcd(T a, T b) { return b ? gcd(b, a % b) : a; }
template<class T> T Lcm(T a, T b) { return a / gcd(a, b) * b; }
using namespace std;

/*
 如果一个数s能被组成他的所有非0的数字:(a,b,c,d)整除,
 也就意味着:
 s % lcm(a,b,c,d)=0
 lcm(1,2,3.....9)=2520,所以余数最大也就是这个数
 2520又可以简化 为252 。
 */
ll dp[19][2521][55];  // 长度为 i ,余数为0-2520,前面有k个能整除2520 , 有多少个
int dig[20];
int index[2521];  //存前面所有数字 前面 有多少能整除252的数
int gcd(int a,int b) {
    return b == 0 ? a : gcd(b , a % b);
}
ll dfs(int pos,int sum,int lcm, bool U) { //上一段累计下来的lcm,sum(%2520)。 U代表当前dig是否为原数字
    if (pos == -1)
        return sum % lcm == 0; //扫到最后一位,计算f(x)如果x满足则还可以+1
    if (!U && dp[pos][sum][index[lcm]] != -1)  //如果当前需要处理的东西已经有答案了
        return dp[pos][sum][index[lcm]];       //相当于记忆化搜索

    ll ret = 0;  //没搜过,那我们就来算一下这:长度为pos,上一段遗留下来的sum,lcm 下面能有多少个满足的数
    int end = U ? dig[pos] : 9;  //若U为真,只遍历到dig[pos],否则0-9都遍历
    /*
     这一步是为什么呢?   比如19.....
     第一位的时候,我们只算0.....
     第二位的时候,我们只算0..... 1.... 2.... 3..... 4..... 5..... 6.... 7..... 8....
     原理就和初始化做是差不多的,这也是为什么后来的U为真时  不更新dp;
     */
    for (int i = 0 ; i <= end ; i ++) {
        int Nlcm = lcm;    //之前的lcm
        if (i)
            Nlcm = lcm / gcd(lcm , i) * i;   //lcm=a*b/gcd, Nlcm:当前lcm
        int Nsum = sum * 10 + i;             //更新sum
        if (pos)  //扫到最后一位了
            Nsum %= 252;
        ret += dfs(pos - 1 , Nsum , Nlcm, U && i == end); //统计下面的有多少个满足这个条件
    }
    if (!U)   //这个数本身   不+ 上来
        dp[pos][sum][index[lcm]] = ret;
    return ret;
}
ll func(ll num) {  //实质就是直接去计算0-n, 可是想想预处理 感觉又不行,所以就用记忆化搜索,类似预处理!
    int n = 0;
    while (num) {
        dig[n++] = num%10;  //dig 从0开始 存低位
        num /= 10;
    }
    return dfs(n - 1 , 0 , 1 , 1);  //长度
}

void init() {
    int sz = 0;
    for (int i = 1 ; i <= 2520 ; i ++)
        if (2520%i == 0)
            index[i] = sz ++;  // 就表示i前面有几个能整除252的数
    memset(dp , -1 , sizeof(dp));
}
int main() {
    init();

    int T;
    cin >> T;
    while (T --) {
        ll a , b;
        cin >> a >> b;
        cout << func(b) - func(a - 1) << endl;
    }
    return 0;
}

dfs 套路的马上实践题:
ICPC 沈阳网络赛的题
hdu 5989
条件: 奇数段的长度为偶数,并且偶数段的长度为奇数。
比如:11222 、 357924

当时 按着这个套路慢慢写,确实debug了一会,因为那个 pos=-1的情况 ,有几种情况我都是特判的。
还有一个v+1我写成了v++ 也是醉了

但是实践证明 这个写法确实是有迹可循,可以模仿!

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
#include<stdlib.h>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<vector>
const double PI = acos(-1.0);
const double e = exp(1.0);
#define ll __int64
#define LL __int64
template<class T> T gcd(T a, T b) { return b ? gcd(b, a % b) : a; }
template<class T> T Lcm(T a, T b) { return a / gcd(a, b) * b; }
using namespace std;

ll dp[20][10][20];  //长度为i 的,前面留过来的数 为奇/偶,且长度为k
int dig[20];
ll dfs(int pos,int v,int flag,bool U){ //flag代表从上一位的是奇数还是偶数,v代表长度
//    printf("find %d %d %d %d  ans=%d\n",pos,v,flag,U,flag%2!= v%2);
    if(pos==-1){
        if(flag==0 && v==0){
            return 1;
        }
        if(flag%2== v%2)
            return 0;
        else
            return 1;
    }
    if(!U && dp[pos][flag][v]!=-1)
        return dp[pos][flag][v];

    //下面都是没有搜过的
    ll ret=0;
    int end=U?dig[pos]:9;
    for(int i=0;i<=end;i++){
        if(v==0){
            if(i) ret+=dfs(pos-1,1,i,i==end && U);
            else ret+=dfs(pos-1,0,i,i==end && U);
            continue;
        }
        if(i&1){  //如果这一位是奇数
            if(flag%2==1){ //如果上一位是奇数
//                v++;//   debug 出要不能这么写
                ret+=dfs(pos-1,v+1,i,i==end && U);

            }
            else{
                if(v&1){
                    ret+=dfs(pos-1,1,i,i==end && U);
                }
            }
        }
        else{//,这一位是偶数
            if(flag%2){ //如果上一位是奇数
                if(v%2==0){
//                    printf("%d %d %d\n",i,flag,v);
                    ret+=dfs(pos-1,1,i,i==end && U);
                }
            }
            else{
                ret+=dfs(pos-1,v+1,i,i==end && U);
            }
        }
    }
    if(!U)
        dp[pos][flag][v]=ret;
    return ret;
}
ll func(ll num) {
    int n = 0;
    while (num) {
        dig[n++] = num%10;  //dig 从0开始 存低位
        num /= 10;
    }
    if(dig[n-1]%2)
        return dfs(n-1,0,0,1);
    else
        return dfs(n-1,0,0,1);
}
void init(){
    memset(dp,-1,sizeof(dp));
}
int main() {
    init();
    int T;
    int cas=1;
    scanf("%d",&T);
    while(T--){
        ll l,r;
        scanf("%I64d %I64d",&l,&r);
        printf("Case #%d: ",cas++); printf("%I64d\n",func(r)-func(l-1));
    }
    return 0;
}

2016 11-15补题
bzoj1833
纯手写入门数位dp

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
#include<stdlib.h>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<vector>
const double PI = acos(-1.0);
const double e = exp(1.0);
#define LL long long
using namespace std;

/*
 * 读错了一遍题。。。。  不是出现带1的数字有多少个,是1出现了多少次,比如11 就算1 出现了两次
 *
 *
 * */
//LL dp[13][15][15][2];
map< LL,map<LL,map<LL,map<LL,LL> > > >dp;
int dig[15];
LL dfs(int pos,LL flag,int x,int u,bool cnt){   //cnt代表 前面至少已经出现过一个>0的数
//  printf("%d %d %d %d %d \n",pos,flag,x,u,cnt);
    if(pos==-1){
       if(flag>0)
          return flag;
       if(!cnt && !x)   //就是找0的情况   0也算出现了一次0
           return 1;
       return  0;
    }
    if( !u && dp[pos][flag][x][cnt]){
        return dp[pos][flag][x][cnt];
    }
    LL ans=0;
    int end=u?dig[pos]:9;
    for(int i=0;i<=end;i++){  //假设这一位是i
        if(flag){
           if(i==x) ans+=dfs(pos-1,flag+1,x,(i==end&&u),1);
           else ans+=dfs(pos-1,flag,x,(i==end&&u),1);
        }
        else{
           if(i==x){
               if(x==0 && !cnt)
                   ans+=dfs(pos-1,flag,x,(i==end&&u),0);  //如果前面没有出现过大于0的数,且要找0
               else
                   ans+=dfs(pos-1,(LL)flag+1,x,(i==end&&u),1);
           }
           else{
                if(i>0) ans += dfs(pos - 1, flag, x, (i == end && u), 1);
                else ans += dfs(pos - 1, flag, x, (i == end && u), cnt);
           }
        }
    }
    if(!u)
       dp[pos][flag][x][cnt]=ans;
//  printf("%d %d %d %d %d = %I64d,%d\n",pos,flag,x,u,cnt, dp[pos][flag][x][cnt],ans);
    return ans;
}
LL func(LL num,int x) {
    int n = 0;
    while (num) {
        dig[n++] = num%10;  //dig 从0开始 存低位
        num /= 10;
    }
    if(n==0){
        if(x==0)
            return 1;
        return 0;
    }
    return dfs(n-1,0,x,1,0);
}

int main(){
    freopen("1.txt","r",stdin);
    LL a,b;
    cin>>a>>b;
    cout<<(func(b,0)-func(a-1,0));
        for(int i=1;i<=9;i++){
            printf(" ");
            cout<< (func(b,i)-func(a-1,i));
        }
        printf("\n");
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值