zoukankan      html  css  js  c++  java
  • 11 .3 数位dp

    数位dp是以数位上的关系为转移关系而进行的一种计数dp,题目基本类型是给定区间[l ,r] ,求l到r之间满足要求的数字的个数 .

    dp状态的转移方式通常是用 递归+记忆化搜索 ,转移顺序一般是由高数位转移到底数位 ,其中就是记忆化搜索保证了数位dp的高效率

    例如千位2到百位转移要枚举0,1,2,3 ...(2000,2100,2200,2300...) ,而千位3也是同样的(3000,3100,3200,3300...),其进行的都是对三位数000~999的统计,所以低位统计过程只用进行一次就可将结果应用于所有高位状态上,减少了重复过程的进行.

    结果的输出形式是 0~r 之间的dp 与 0~l之间的dp 进行相减 来求 l到r 之间的 dp.

            printf("%lld
    ",solve(r)-solve(l-1));

    值得注意的点是边界 l和r 不能进行记忆化搜索 ,比如 dp[2][sta] 记录的是 000~999(三位数) 中满足条件的数字的个数 ,而对于l = 2250 ,其在2000之后的三位数只有 100~250 ,所以这时候如果直接记忆化返回 dp[2][sta] 就会出现多记.

    有的题目对前导零有要求,有的没有,做的时候随机应变。

    例题:

    HDU 2089 不要62

    题解:转移状态很清晰明了的题目,主要通过此题了解 递归+记忆化的转移方式,对题目要求如何在dfs函数中进行处理 ,lim边界标记的使用和传递的方式.

    #include <iostream>
    #include <cstdio>
    #include <cmath>
    #include <algorithm>
    #include <set>
    #include <queue>
    #include <stack>
    #include <string>
    #include <cstring>
    #include <vector>
    #include <map>
    #include <unordered_map>
    #define mem( a ,x ) memset( a , x ,sizeof(a) )
    #define rep( i ,x ,y ) for( int i = x ; i<=y ;i++ )
    #define lson l ,mid ,pos<<1
    #define rson mid+1 ,r ,pos<<1|1
    #define Fi first
    #define Se second
    
    using namespace std;
    typedef long long ll ;
    typedef pair<int ,int> pii;
    typedef pair<ll ,int> pli;
    const ll inf = 0x3f3f3f3f;
    const int N = 1e5+5;
    const ll mod = 1e9+7;
    
    ll dp[10][2];
    int a[10] ,pos;
    
    ll dfs( int pos ,int pre ,int sta ,int lim ){
        //pos 当前数位 , pre 前一位的数字 ,sta 当前状态:前一位是否是6 ,lim 是否是在需要特判的边界上
       if( pos==-1 )return 1;
       if( !lim && dp[pos][sta]!=-1 )return dp[pos][sta];
       //在边界则不能进行记忆化
       int tmp = lim ? a[pos] : 9;
       ll now = 0;
       rep( i ,0 ,tmp ){
           if( i==4 )continue;
           //数位为4则不进行计数
           if( pre==6 && i==2 )continue;
           //前一位为6则不进行计数
           now += dfs(pos-1 ,i ,i==6 ,lim&&i==tmp);
           //
       }
       if( !lim )dp[pos][sta] = now;
       //在边界则不能进行记忆化
       return now;
    }
    
    ll solve( int x ){
       int cnt = 0;
       while(x){
         a[cnt++] = x%10; x/=10;
        }
       return dfs(cnt-1 ,-1 ,0 ,1 );
    }
    
    int main( ){
         int l ,r;
         while( scanf("%d%d" ,&l ,&r) ,r ){
             mem( dp ,-1 );
             printf("%lld
    " ,solve(r) - solve(l-1));
         }
         return 0;
    }

    常用优化:

    1.就算面对不同询问,数位的dp状态也往往是相同的,因此在约束条件普适于所有数字的条件下不用每次都用mem对dp进行初始化

    如果面对不同询问,在约束条件下产生的状态不普适的条件下(如 一个数是它自己数位和的倍数),可以将dp增加一维dp[pos][state][limit],或者直接每次都初始化dp

    2.状态的表示方法不同也会影响dp的适用范围,一种常见的状态表示是将和式的状态表示为 当前数位和 与 目标值 所需要拼凑的差值

    例如:HDU 4734

    F(x) = An * 2n-1 + An-1 * 2n-2 + ... + A2 * 2 + A1 * 1,Ai是十进制数位,给出a,b求区间[0,b]内满足f(i)<=f(a)的i的个数。

    如果正面思考,用数位和sum作为状态,逐位拼凑到a,则状态dp[pos][sum]无法复用,因为随着a的变化,sum<f(a)的数字个数也是不同的,状态增加一维表示a后才能覆盖所有状态,所需要空间是dp[10][4600][4600]

    不如反面思考,将初始与目标值的差值作为状态,初始状态为f(a),逐位相减,如果在dp转移到最后一位差值仍大于等于零则说明f(i)<=f(a),对于不同的a,如果减到某一位后他们的状态:与目标值的差值相同,则接下来的位数中满足相减完毕后结果大于等于0的方案数也相同 ,dp是可以复用的

    #include <iostream>
    #include <cstdio>
    #include <cmath>
    #include <algorithm>
    #include <set>
    #include <queue>
    #include <stack>
    #include <string>
    #include <cstring>
    #include <vector>
    #include <map>
    #include <unordered_map>
    #define mem( a ,x ) memset( a , x ,sizeof(a) )
    #define rep( i ,x ,y ) for( int i = x ; i<=y ;i++ )
    #define lson l ,mid ,pos<<1
    #define rson mid+1 ,r ,pos<<1|1
    #define Fi first
    #define Se second
    
    using namespace std;
    typedef long long ll ;
    typedef pair<int ,int> pii;
    typedef pair<ll ,int> pli;
    const ll inf = 0x3f3f3f3f;
    const int N = 1e5+5;
    const ll mod = 1e9+7;
    
    ll dp[10][10500];
    int num[10] ,cnt;
    
    void div( ll x ){
        cnt = 0;
        while( x ){
            num[cnt++] = x%10;
            x /= 10;
        }
    }
    
    int f( ll x ){
        int tmp = 0 ,ans = 0;
        while( x ){
            ans += (x%10)*(1<<tmp);
            tmp++; x/=10;
        }
        return ans;
    }
    
    int dfs( int pos ,int sta ,bool lim ){
        //sta 是与目标值的所需差值
        if( sta < 0)return 0;
        if( pos < 0 )return sta >= 0;
        if( !lim && dp[pos][sta] != -1 )return dp[pos][sta];
    
        int ans = 0 ,sum = sta;
        int tmp = lim ? num[pos]:9;
        rep( i ,0 ,tmp ){
           sta = sum - i*(1<<pos);
           if( sta < 0)break;
           ans += dfs( pos-1 ,sta ,lim && i==tmp );
        }
    
        if( !lim )dp[pos][sum] = ans;
        return ans;
    }
    
    int main( ){
         int t ,fa ,ks = 0;
         ll a ,b;
         scanf("%d" ,&t);
         mem( dp ,-1);
         while( t-- ){
             scanf("%lld %lld" ,&a ,&b);
             fa = f(a);
             div(b);
             printf("Case #%d: %d
    ", ++ks ,dfs(cnt-1, fa ,1) );
         }
    
         return 0;
    }

    其他题目:

    HDU 3709 这题就是要枚举中轴,然后数位dp

    #include <iostream>
    #include <cstdio>
    #include <cmath>
    #include <algorithm>
    #include <set>
    #include <queue>
    #include <stack>
    #include <string>
    #include <cstring>
    #include <vector>
    #include <map>
    #include <unordered_map>
    #define mem( a ,x ) memset( a , x ,sizeof(a) )
    #define rep( i ,x ,y ) for( int i = x ; i<=y ;i++ )
    #define lson l ,mid ,pos<<1
    #define rson mid+1 ,r ,pos<<1|1
    #define Fi first
    #define Se second
    
    using namespace std;
    typedef long long ll ;
    typedef pair<int ,int> pii;
    typedef pair<ll ,int> pli;
    const ll inf = 0x3f3f3f3f;
    const int N = 1e5+5;
    const ll mod = 1e9+7;
    
    int num[20] ,cnt;
    ll dp[20][20][2000];
    
    void div( ll x ){
        cnt = 0;
        while(x){
            num[cnt++] = x%10;
            x /= 10;
        }
    }
    
    ll dfs( int pos ,ll sum, int sta ,bool lim ){
         if( pos < 0 || sum < 0 )return sum == 0;
         if( !lim && dp[pos][sta][sum] != -1 )return dp[pos][sta][sum];
    
         int tmp = lim ? num[pos] : 9;
         ll ans = 0;
         rep( i ,0 ,tmp ){
             if( pos >= sta )ans += dfs( pos-1 ,sum + i*(pos-sta) ,sta ,lim&&i==tmp );
             if( pos < sta )ans += dfs( pos-1 ,sum - i*(sta-pos) ,sta ,lim&&i==tmp );
            }
         if( !lim )dp[pos][sta][sum] = ans;
         //cout<<pos<<" "<<sum<<" "<<sta<<" "<<ans<<endl;
         return ans;
    }
    
    ll sol( ll x ){
       div(x);
       ll ans = 0;
       rep( i ,0 ,cnt-1 ){
           ans += dfs( cnt-1 ,0 ,i ,1 );
       }
       //注意枚举中轴过程中0的重复计数
       return x >= 0 ? ans - cnt + 1 : 0;
    }
    
    int main( ){
         mem( dp ,-1 );
         int t;
         ll s ,e;
         scanf("%d" ,&t);
         while( t-- ){
             scanf("%lld%lld" ,&s ,&e );
             printf("%lld
    " ,sol(e) - sol(s-1) );
         }
         return 0;
    }

    这题需要注意的是在枚举中轴的过程中,对于数字0,无论中轴在哪一位都能满足,因此最后要减去0的重复计数

    HYSBZ - 1799 给出a,b,求出[a,b]中各位数字之和能整除原数的数的个数。

    数位和相较于原来的a,b较小,最大只有9*18 == 162 ,也就是说在a~b之间有很多数的数位和是相同的,可以直接枚举除数mod

    状态 dp[pos][val][mod],其中val是枚举到某一位的余数

    #include <iostream>
    #include <cstdio>
    #include <cmath>
    #include <algorithm>
    #include <set>
    #include <queue>
    #include <stack>
    #include <string>
    #include <cstring>
    #include <vector>
    #include <map>
    //#include <unordered_map>
    #define mem( a ,x ) memset( a , x ,sizeof(a) )
    #define rep( i ,x ,y ) for( int i = x ; i<=y ;i++ )
    #define lson l ,mid ,pos<<1
    #define rson mid+1 ,r ,pos<<1|1
    #define Fi first
    #define Se second
    
    using namespace std;
    typedef long long ll ;
    typedef pair<int ,int> pii;
    typedef pair<ll ,int> pli;
    const ll inf = 0x3f3f3f3f;
    const int N = 1e5+5;
    //const ll mod = 1e9+7;
    
    ll dp[20][200][200];
    int num[20] ,cnt;
    
    ll dfs( int pos ,int sum ,int val ,int mod ,bool lim ){
       if( sum - 9*(pos+1) > 0 )return 0;
       if( pos == -1 )return sum == 0 && val == 0;
       if( !lim && dp[pos][sum][val] != -1 )return dp[pos][sum][val];
       int up = lim ? num[pos] : 9;
       ll ans = 0;
       rep( i ,0 ,up ){
           if( sum - i < 0 )break;
           ans += dfs(pos-1 ,sum-i ,(val*10+i)%mod ,mod ,lim&&i==up );
       }
       if( !lim )dp[pos][sum][val] = ans;
       return ans;
    }
    
    void div( ll x ){
        cnt = 0;
        while( x ){
            num[cnt++] = x%10;
            x /= 10;
        }
    }
    
    ll solve( ll x ){
        div(x);
        ll ans = 0;
    
        rep( i ,1 ,cnt*9 ){
            mem( dp ,-1 );
            ans += dfs( cnt-1 ,i ,0 ,i ,1 );
        }
    
        return ans;
    }
    int main( ){
         ll l ,r;
         while( ~ scanf("%lld%lld" ,&l ,&r) ){
             printf("%lld
    " ,solve(r) - solve(l-1) );
         }
         return 0;
    }

    进阶:dp所求不是a~b之间数字个数的情况,例如求a~b之间满足条件数字的和,求a~b之间满足条件数字的平方和

    处理方法是在转移过程中考虑各位为dp结果产生的贡献,写出dp的转移方程

    和 : sum[pos] = sum[pos-1][0~9] + cnt[pos-1][0~9]*i*10^pos

    平方和:i为当前位上的数 ,假设b为低位上余下的数值,则将表达式展开(i*10^pos + b)^2 = (i*10^pos)^2 + 2*b*i*10^pos + b^2

    故转移方程 :

         sum_sq[pos] = cnt[pos-1][0~9]*(i*10^pos)^2 + 2*i*10^pos * sum[pos-1][0~9]  + sum_sq[pos-1][0~9];
         sum[pos] = sum[pos-1][0~9] + cnt[pos-1][0~9]*i*10^pos

    例题:HDU 4507

    简单的约束条件,但所求结果不是计数而是平方和

    #include <iostream>
    #include <cstdio>
    #include <cmath>
    #include <algorithm>
    #include <set>
    #include <queue>
    #include <stack>
    #include <string>
    #include <cstring>
    #include <vector>
    #include <map>
    #include <unordered_map>
    #define mem( a ,x ) memset( a , x ,sizeof(a) )
    #define rep( i ,x ,y ) for( int i = x ; i<=y ;i++ )
    #define lson l ,mid ,pos<<1
    #define rson mid+1 ,r ,pos<<1|1
    #define Fi first
    #define Se second
    
    using namespace std;
    typedef long long ll ;
    typedef pair<int ,int> pii;
    typedef pair<ll ,int> pli;
    const ll inf = 0x3f3f3f3f;
    const int N = 1e5+5;
    const ll mod = 1e9+7;
    
    ll ten[20];
    ll dp[20][10][10]; // dp[pos][digsum][num]
    ll sum[20][10][10] ,sum_sq[20][10][10];
    int num[20] ,cnt;
    ll sq (ll x){ return (x%mod)*(x%mod)%mod; }
    
    void init( ){
       ten[0] = 1;
       rep( i ,1 ,18 )ten[i] = 10ll*ten[i-1]%mod;
       mem( dp ,-1 );
    }
    
    ll dfs( int pos ,int sum7 ,int dig7 ,ll &s,ll &s_sq ,bool lim ){
       if( pos < 0 ){
         return (sum7) && (dig7);
       }
    
       if( !lim && dp[pos][sum7][dig7]!=-1 ){
         s = ( sum[pos][sum7][dig7])%mod;
         s_sq = ( sum_sq[pos][sum7][dig7])%mod;
         return dp[pos][sum7][dig7];
       }
    
       ll ans=0;
       ll up = lim ? num[pos] : 9;
       for( ll i = 0; i <= up; i++ ){
         if( i==7 )continue;
         ll cnt ;
         ll ans_s=0 ,ans_sq=0;
         cnt = dfs( pos-1 ,(sum7*10+i)%7 ,(dig7+i)%7 ,ans_s ,ans_sq , lim&&i==up )%mod;
         s_sq = (s_sq + cnt*sq(i*ten[pos])%mod + 2*i*ten[pos]%mod*ans_s%mod + ans_sq%mod)%mod;
         s = (s + cnt%mod * (i*ten[pos])%mod + ans_s%mod )%mod;
         ans = (ans + cnt)%mod;
       }
    
       if( !lim ){
            dp[pos][sum7][dig7] = ans%mod;
            sum[pos][sum7][dig7] = s%mod;
            sum_sq[pos][sum7][dig7] = s_sq%mod;
       }
       return ans%mod;
    }
    
    void div( ll x ){
        cnt = 0;
        while( x ){
            num[cnt++] = x%10;
            x /= 10;
        }
    }
    
    ll solve( ll x ){
        div( x );
        ll ans_sq = 0 ,ans_s = 0;
        dfs( cnt-1 ,0 ,0 ,ans_s ,ans_sq ,1 );
        return ans_sq%mod;
    }
    
    int main( ){
         int T;
         ll l ,r;
         //freopen( "Hdu 4507.in" ,"r" ,stdin );
         //freopen( "my - hdu 4507.txt" ,"w" ,stdout );
    
         init( );
         scanf("%d" ,&T);
         while( T-- ){
            scanf("%I64d%I64d" ,&l ,&r );
            printf("%I64d
    " ,(solve(r) - solve(l-1) + mod)%mod );
         }
         return 0;
    }

    方法就是考虑每一位的贡献,转移方程就是上面推的

    复制下别人的题解:

    关于数字7的限制其实不难实现,难的是要求平方和,考虑2XX这个数,也就是百位为2的数,它满足限制条件的平方和是多少?
    假设满足条件的数有234,245,266,那么 234^2 + 245^2 + 266^2 = (200 + 34)^2 + (200 + 45)^2 + (200 + 66)^2 
    = 3*200^2 + 2*200*(34+45+66) + (34^2 + 35^2 + 66^2),因此在枚举到2的时候,表达式里只有3,(34 + 45 + 66),(34^2 + 35^2 + 66^2)不知道,
    因此我们可以定义dfs1(i)为求平方和,dfs2(i)为求和,dfs3(i)为求有多少个满足条件的数(也就是上述的3)。

    另外注意算的时候每一步都取模,不要爆longlong了
  • 相关阅读:
    致命错误 RC1004: 文件查找结束时有无法预知的错误(vc++)
    demo713总结
    图标,鼠标,字符串,音频..
    不同的色深条件(8、16、24、32),像素绘制方式
    SQL 保留两位小数的实现方式
    MVC4的REmote缺陷
    MVC4安装过程
    mongodb 的几种驱动
    iis7 web配置问题及解决办法
    Fast Binary File Reading with C#
  • 原文地址:https://www.cnblogs.com/-ifrush/p/11787634.html
Copyright © 2011-2022 走看看