数位DP小结

写在前面

感觉写多了和模板似的……

UPD at 2018.3.8:其实一点也不模板,如果觉得数位dp很水的话,参见[SCOI2014]方伯伯的商场之旅

long long/*(或者int)*/ dfs ( int len, ...... ,limit)
if( !limit && dp != -1 ) return dp
int lim = limit ? W[len] : 9/*(这里依据题意,比如二进制dp这里就是1)*/
long long rt = 0 ;
for( int i = /*下限*/ ; i <= lim ; i ++ ){
    /*题目所需判断*/
    rt += dfs( len-1 , ...... , limit && i == lim ) ;
}
return limit ? rt : dp = rt ;

中间的根据题意写就好了…


什么是数位DP?

数位DP就是用DP去解决一些数字统计问题
比如说最经典的问题:
给定[L,R],询问这个区间内不含有”49”子串的数字的个数

上一位如果取了4,这一位就不能取9
于是定义dp[i]为长度为在i位及以下时,不含49的数字的个数
那么函数参数”……”里的东西就是bool is_four
“/* 题目所需判断 */”里就是if( is_four && i == 9 ) continue ;
递归的dfs里的”……”就是 i==4
然后这道题就做完了qwq


一些题

HDU2089

题意

求出给定[L,R]中,不含”62”和”4”的数字的个数

题解

= =这个题和上面说的不含49差不多,只需要再加一个i==4就continue即可

自带大常数的代码

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

int N , M , dp[25][2] , W[25] ;

int dfs( int len , int sta , bool limit ){
    if( len == 0 ) return 1 ;
    if( !limit && dp[len][sta] != -1 ) return dp[len][sta] ;
    int rt = 0 ;
    for( int i = 0 ; i <= ( limit ? W[len] : 9 ) ; i ++ ){
        if( i == 4 ) continue ;//4不合法,因此直接continue
        if( sta && i == 2 ) continue ;//如果前一个是6并且当前2,仍然continue
        rt += dfs( len-1 , i == 6 , limit && i == W[len] ) ;
    }
    return limit ? rt : dp[len][sta] = rt ;
}

int solve( int Emm ){
    memset( W , 0 , sizeof( W ) ) ;
    int x = 0 ;
    while( Emm ){
        W[++x] = Emm %10 ;
        Emm /= 10 ;
    }
    return dfs( x , 0 , true ) ;//因为是从高位向低位走,因此一开始limit为true
}

int main(){
    memset( dp , -1 , sizeof( dp ) ) ;
    while( scanf( "%d%d" , &N , &M ) && N && M ){
        printf( "%d\n" , solve( M ) - solve( N - 1 ) ) ;
    }
}

HDU4734

题意

对于一个十进制数,定义它的某一种权值f(x)为
他的十进制的第i为乘以2^(i-1)的和
比如1234,他的权值为1*2^3 + 2*2^2 + 3*2^1 + 4*2^0
给定A,B,求出f(x)<=f(A)并且0<=x<=B的数字的个数

题解

先算出f(A),然后传参到dfs。在dfs中,每选一位的数就剪掉一位的权值,如果选到最后剩余的权值大于等于0,说明这个数字是可以的,统计答案,完了。其余的也几乎是板子。

自带大常数的代码

    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    using namespace std ;

    int T , A , B , dp[12][5005] , W[12];

    int dfs( int len , int left , bool limit ){
        //printf( "(dfs ) len = %d  left = %d  limit = %d\n" , len , left , limit ) ;
        if( len == 0 ) return left >= 0 ;//已经选完了,left还有剩或者刚刚好为0
        if( !limit && dp[len][left] != -1 ) return dp[len][left] ;//记忆化,注意只有!limit才能记忆

        //剩下的都是普通操作 
        int lim = ( limit ? W[len] : 9 ) , rt = 0 ;
        for( int i = 0 ; i <= lim ; i ++ ){
            int nxt_left = left - i * ( 1<<(len-1) ) ;
            if( nxt_left < 0 ) break ;
            rt += dfs( len-1 , nxt_left , limit && ( i == lim ) ) ;
        }
        return limit ? rt : dp[len][left] = rt ;
    }

    int dvd( int b ){
        int x = 0 , rt = 0 ;
        while( b ){
            rt += ( b % 10 ) * ( 1 << x ) ;
            b /= 10 ; x ++ ;
        }
        //printf( "(div ) rt == %d\n" , rt ) ;
        return rt ;
    }

    int solve(){
        int cnt = 0 ;
        while( B ){
            W[++cnt] = B%10 ;
            B /= 10 ;
        }
        return dfs( cnt , dvd( A ) , true ) ;
    }

    int main(){
        memset( dp , -1 , sizeof( dp ) ) ;
        scanf( "%d" , &T ) ;
        for( int Emmmmm = 1 ; Emmmmm <= T ; Emmmmm ++ ){
            scanf( "%d%d" , &A , &B ) ;
            printf( "Case #%d: %d\n" , Emmmmm , solve() ) ;
        }
    }

HDU3709

题意

对于一个十进制数,我们给他指定一个支点,那么我们说这个数是平衡的,当且仅当他左边的权重等于右边的权重
举个栗子
12348,当支点为4时
左边的权值:1*3+2*2+3*1
右边的权值:8*1
因此这是一个平衡数

题解

很容易证明,一个非0的数字,如果他是平衡数,那么它的支点有且仅有一个,因为把这个支点往左或者往右都一定不可能再次平衡

然后我们就去枚举每个位置作为支点,最后求个和就好了
注意0这个数,无论支点在哪里都是平衡数,因此需要剪掉重复的
其他好像没有什么需要注意的地方

自带大常数的代码

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

int T , W[20] ;
long long dp[20][20][2000] , x , y ;

long long dfs( int len , int mid , int sum , int limit ){
    if( len == 0 ) return sum == 0 ;
    if( !limit && dp[len][mid][sum] != -1 ) return dp[len][mid][sum] ;
    int lim = ( limit ? W[len] : 9 ) ;
    long long rt = 0 ;
    for( int i = 0 ; i <= lim ; i ++ )
        rt += dfs( len - 1 , mid , sum + i * ( mid - len ) , limit && i == lim ) ;
    return limit ? rt : dp[len][mid][sum] = rt ;
}

long long solve( long long x ){
    if( x < 0 ) return 0 ;
    int cnt = 0 ;
    while( x ){
        W[++cnt] = x%10 ;
        x /= 10 ;
    }
    //printf( "(solve ) cnt = %d\n" , cnt ) ;
    long long rt = 0 ;
    for( int i = 1 ; i<= cnt ; i ++ )
        rt += dfs( cnt , i , 0 , true ) ;
    return rt - cnt + 1 ;
}

int main(){
    memset( dp , -1 , sizeof( dp ) ) ;
    scanf( "%d" , &T ) ;
    while( T-- ){
        scanf( "%lld%lld" , &x , &y ) ;
        printf( "%lld\n" , solve( y ) - solve( x - 1 ) );
    }
}

POJ3252

题意

求出给定[L,R]区间内的符合下列条件的数的个数
1,将该数写成二进制形式,0的个数不少于1

题解

这个题嘛,二进制的数位DP
注意的是这个题需要对前导0进行特殊判定
其他的好像也没什么

代码比较丑

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

int st , ed , dp[35][70] , W[35] ;

int dfs( int len , int Dv , bool limit , bool lead ){
    if( len == 0 ) return Dv >= 32 ;
    if( !limit && !lead && dp[len][Dv] != -1 ) return dp[len][Dv] ;
    int rt = 0 ;
    rt += dfs( len-1 , Dv + ( lead ? 0 : 1 ) , limit && W[len] == 0  , lead ) ;//取0 
    if( W[len] == 1 || !limit ) rt += dfs( len - 1 , Dv - 1 , limit && W[len] == 1 , false ) ;//取1 
    if( !limit && !lead ) dp[len][Dv] = rt ;
    return rt ;
}

int solve( int x ){
    memset ( W , 0 , sizeof( W ) ) ;
    int cnt = 1 ;
    while( x ){
        if( x&1 ) W[cnt] = 1 ;
        x >>= 1 ; ++cnt ;
    }
    return dfs( cnt - 1 , 32 , true , true ) ;
}

int main(){
    memset( dp , -1 , sizeof( dp ) ) ;
    while( scanf( "%d%d" , &st , &ed ) != EOF )
        printf( "%d\n" , solve( ed ) - solve( st - 1 ) ) ;
    return 0 ;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值