数位统计/数位DP 专题

本质上是暴力模拟计算,逐位统计、以及如何寻找不被约束的状态来简化计算 是关键点。

例题1 ural 1057.

  1. 树形结合, 按位统计. 若当前位为1, 则取0, 剩下的位任意取都比其小, ans += f[ L ][ k-tot ],  L表示剩下长度. 论文这个地方写的感觉不对- -...

#include<cstdio>

#include<cstdlib>

#include<cstring>

#include<string>

#include<algorithm>

using namespace std;



int f[32][32];



void init(){

    memset(f,0,sizeof(f));    

    f[0][0] = 1;

    for(int i = 1; i <= 31; i++){

        f[i][0] = f[i-1][0];

        for(int j = 1; j <= i; j++)

            f[i][j] = f[i-1][j-1] + f[i-1][j];

    }

}

int get(int n,int b){

    string s;

    while( n ){

        s = char((n%b)+'0') + s;

        n /= b;

    }

    for(int i = 0; i < (int)s.size(); i++){

        if( s[i]-'0' > 1 ){

            for(int j = i; j < (int)s.size(); j++)

                s[j] = '1';

            break;    

        }    

    }

    int x = 0;

    for(int i = 0; i < (int)s.size(); i++)

        x = (x<<1)|(s[i]-'0');

    return x;

}

int calc(int n,int k){

    int tot = 0, res = 0;

    for(int i = 30; i >= 0; i--){

        if( (n&(1<<i)) ){

            if( tot <= k )    

                res += f[i][(k-tot)];    

            tot += 1;    

        }    

    }    

    if( tot == k ) res++;

    return res; 

}

int main(){

    init();    

    int x, y, b, k;

    scanf("%d%d%d%d",&x,&y,&k,&b);

    x = get(x,b), y = get(y,b);

    printf("%d\n", calc(y,k) - calc(x-1, k) );

    return 0;

}
View Code

  2. 还是比较喜欢记忆化的写法, 比较通用.   Count( bool less, int Length, int cur_num, int target_num, int x ) 

#include<cstdio>

#include<cstdlib>

#include<cstring>

#include<string>

#include<algorithm>

using namespace std; 

int get(int n,int b){

    string s;

    while( n ){

        s = char((n%b)+'0') + s;

        n /= b;

    }

    for(int i = 0; i < (int)s.size(); i++){

        if( s[i]-'0' > 1 ){

            for(int j = i; j < (int)s.size(); j++)

                s[j] = '1';

            break;    

        }    

    }

    //printf("s = %s\n", s.c_str() );    

    int x = 0;

    for(int i = 0; i < (int)s.size(); i++)

        x = (x<<1)|(s[i]-'0');

    return x;

} 

int dp[50][50];

// dfs( false, 31, 0, k, x )

int dfs(bool less, int L, int tot, int k, long long x){ //x本身不被计算

    //printf("less = %d, L = %d, tot = %d, k = %d, x = %lld\n", less, L, tot, k, x );

    if( L == 0 )

        return less && (tot==k);

    if( less && (dp[L][k-tot]!=-1) ) 

        return dp[L][k-tot];

    int res = 0, d = (x&(1ll<<(L-1))) > 0 ? 1 : 0;    

    for(int i = 0; i <= 1; i++){

        if( (less==false) && (i > d) ) continue;    

        res += dfs( less||(i<d), L-1, tot + (i==1), k, x );    

    }    

    if(less) dp[L][k-tot] = res;

    return res;

}

void solve(int x,int y,int k){

    memset(dp,0xff,sizeof(dp));

    long long a = x, b = y+1; 

    printf("%d\n", dfs(false,32,0,k,b) - dfs(false,32,0,k,a) );

}

int main(){ 

    int x, y, b, k;

    scanf("%d%d%d%d",&x,&y,&k,&b);

    x = get(x,b), y = get(y,b);

    solve( x, y, k ); 

    return 0;

}
View Code

 

[1,n] 区间中二进制形式下1的数量

// If No Limit 

// dp[i]: I位1的数量和

// dp[i] = dp[i-1] + dp[i-1] + 2^(i-1)

#include<cstdio>

#include<cstring>

const int N = int(1e5)+10;

int f[N];

int dfs(bool less,int x,int L){//32---1

//    printf("L = %d, less = %d\n", L,less); getchar();getchar();

    if( L == 1 ) return less;

    if( less && (f[L]!=-1) ) return f[L];



    int res = 0, d = (x&(1<<(L-1)))?1:0;

    for(int j = 0; j < 2; j++){

        if( !less && (j>d) ) continue;



        res += dfs( less||(j<d), x, L-1);

        if( j == 1 ){

            if(less) res += 1<<(L-1);

            else{

                for(int i = 1; i < L; i++)

                    if( x&(1<<(i-1)) ) res+= 1<<(i-1);

            }

        }

    }

    if( less ) f[L] = res;

    return res;

}



int main(){

    int x;

    memset(f,-1,sizeof(f));

//    for(x = 1; x <= 1000; x++)

//        printf("n = %d,res = %d\n", x, dfs(false,x,32) );

    while( scanf("%d", &x) != EOF){

        printf("%d\n", dfs(false,x,32) );

    }

    return 0;

}
View Code

 

例题2 spoj 1182 Sorted bit sequence  求区间 [x,y] 中数位和从小到大,相同则数值大小排序的第K个数

  f( i, j ) 表示后i位任意,数位和为j的方案数 

  count(i,j) 表示 [1,i)间数位和为j的方案数  

  1. 首先利用 count() 求出第k的数位和 sum.

  2. 然后再逐位确定,同样利用count();

负数可以选择 与整数分开,单独做, 或者通过转换例如在33位+1之类的 与整数一起做。

 

例题3  spoj 2319 Sequence 

  因为对于一个最大值 S, 其分组数有一个最小值x, 若x<=M 则意味着S还可以减小。 所以可以二分找出最合适的S。

  对于每次二分得到的最大值S,求分数数量时,每次让分组尽量满,即可得到最小分组数量X

  函数 count( n ) 表示 [1,n] 中所有数二进制形式下 1的数量总和

  假定目前分组左边界是 a, 我们需要求最大的右边界满足 count(b) - count(a-1) <= S.

  这里需要通过逐位确定的方法的求解b的值

 

例题4 sgu 390 Tickets

  sumOf( x ) 表示x的数位和

  最好理解成 按顺序枚举 x 属于 [L,R] , 然后一个一个分组添加。 若当前 数位和为 remain, 当加入x进来时, remain + sumOf(x) >= K时,此时就会多一个分组,

否则当前未满的分组 remain = remain + sumOf(x). 

  我们可以提取一个不受约束的状态来 简化计算。 

  dp[ L ][ sum ][ left ]  :  表示 后L位取所有值,且L位之前的数位和为sum,且之前最后一个分组未满时已有的数位和left, 分组总数 与 最后一个未满分组剩余量

  然后进行按位处理以及合并状态。

#include<cstdio>

#include<cstring>

#include<cstdlib>

#include<algorithm>

#include<cmath>

using namespace std;



typedef long long LL;

const int Length = 19;



int K ,low[20], high[20];



void get( LL x, int *r ){

    int cnt = 0;

    for(int i = 0; i < Length; i++)

        r[i] = x%10, x /= 10;

//    for(int i = 0; i < Length; i++)

//        printf("%d ", r[i] ); puts("");

}



struct Node{

    int left; LL tot;

    Node(){}

    Node(int _l,LL _t):left(_l),tot(_t){}

};

Node dp[20][200][1001];

bool vis[20][200][1001];



Node count(bool isGreater,bool isLess,int L,int sum,int start){

    if( L == 0 ){

        if( isLess && isGreater ){

            if( start+sum >= K ) return Node(0,1);

            return Node(start+sum,0);

        }

        return Node(start,0);

    }

    int left = start; LL tot = 0;

    if( isLess && isGreater && vis[L][sum][left] ) return dp[L][sum][left];

    for(int x = 0; x < 10; x++){

        if( !isGreater && (x<low[L-1]) ) continue;

        if( !isLess && (x>high[L-1]) ) continue;



        Node t = count( isGreater||(x>low[L-1]),isLess||(x<high[L-1]),L-1,sum+x,left );

        tot += t.tot;

        left = t.left;

    }

    Node ret = Node(left,tot);

    if( isGreater && isLess ) dp[L][sum][start] = ret, vis[L][sum][start] = true;

    return ret;

}

int main(){

    LL L, R;

    while( scanf("%lld%lld%d",&L,&R,&K) != EOF ){

        get( L-1, low ); get( R+1, high );

        memset(vis,0,sizeof(vis));

        Node ans = count(false,false,Length,0,0);

        printf("%lld\n", ans.tot );

    }

    return 0;

}
View Code

 

例题5 zoj 2599  Graduated Lexicographical Ordering

  将[1,n]中的所有数排序, 第一关键字是 数位和从小到大, 第二关键字是字典序。 

  求[1,n]中 值K在第几位, 以及第K位的值是多少。

  f[ L, sum ]  表示后L位任意,数位和为sum的方案数量。

  count( n, sum ) 表示 [1,n] 数位和为sum的方案数量.

  对于第一问,首先求出 {1,sumOf(K)-1} 的数量, 然后再统计 数位和为sumOf(k) 且字典序小于K的数量,相加即为结果。 对于统计数位和为 sum(k) 且 字典序小于K的数量, 因为长度不同,我们没法直接求。 我们可以枚举 x 的长度范围 [1, l ength(n) ] 来计算。

  \sum {  count( k, length, sum(k) ) - f( length-1, sum(K) ) }   d

  对于第二问也是利用上面的 方式来逐位确定 第K的数的每一位, 细节比较多。 

#include<cstdio>

#include<cstring>

#include<cstdlib>

#include<iostream>

#include<algorithm>

using namespace std;



typedef long long LL;

const int Length = 19;



LL n, K;



LL f[20][200]; //后i位任意,数位和为j 的方案总数

LL pow10[20];

int getbit(LL x){

    for(int i = Length; i >= 1; i--){

        if( x/pow10[i-1] ) return i;

    }

}

int SumOf(LL x){

    int re = 0; while(x) re+=x%10,x/=10; return re;

}

int get(int *r,LL x){

    for(int i = 0; i < Length; i++) r[i]=x%10,x/=10;

}

void init(){

    pow10[0] = 1;

    for(int i = 1; i < Length; i++) pow10[i]=pow10[i-1]*10;

    memset(f,0,sizeof(f));

    f[0][0] = 1;

    for(int i = 1; i <= Length; i++){

        for(int j = 0; j < 200; j++){

            for(int x = 0; x<10 && x<=j; x++)

                f[i][j] += f[i-1][j-x];

        }

    }

}

LL count(bool less, int *r, int L, int sum, int goal){ // [1,x) 数位和为goal的数量

    if( L == 0 ) return less && (sum==goal);

    if( less ) return f[L][goal-sum];

    LL res = 0;

    for(int x = 0; x < 10; x++){

        if( !less && (x>r[L-1]) ) continue;

        if( sum+x > goal ) continue;

        res += count(less||(x<r[L-1]),r,L-1,sum+x,goal);

    }

    return res;

}

LL Count(LL A, int L, int sum){ // 所有长度为L,数位和为sum, 且字典序小于A的前L位的数量

    LL t = A; bool flag = false;

    int r[20], k = getbit(t), tmp = 0;

    while( k > L ) t/=10,k--;

    while( k < L ){

        if( t<=n/10 && t*10<=n ) t*=10,k++;

        else{ t=n; flag=true; break; }

    }

//    printf("Count:: t = %I64d\n", t);

    memset(r,0,sizeof(r));

    int len = 0; LL remain = t;

    while(t) r[len]=t%10,t/=10,tmp+=r[len++];

    LL temp = count(false,r,L,0,sum)-f[len-1][sum];

    if( (L <= getbit(A)) || (remain==n && flag) ) temp += (tmp==sum);

//    printf("Count:: temp = %I64d\n", temp );

    return temp;

}

LL Deal_1(){

    LL tot = 0; int sum = SumOf(K);

    int r[20]; get(r,n+1);

    for(int i = 1; i < sum; i++ )

        tot += count(false,r,Length,0,i); //printf("Deal_1:: tot = %I64d\n", tot );

    for(int i = 1; i <= getbit(n); i++)

        tot += Count( K, i, sum );

//    puts("--->");

    return tot;

//    printf("%I64d\n", tot );

}



LL check(LL A,int x){

    if( A <= n/10 ) return min( A*10+x, n );

    else return n;

}



LL Deal_2(){

    LL rank = K, A; int sum, r[20]; get(r,n+1);

    for(sum=1;;sum++){

        LL tmp = count(false,r,Length,0,sum);

//        printf("Deal:: tmp_%d = %I64d, rank = %I64d\n", sum, tmp, rank ); getchar(); getchar();

        if( rank - tmp > 0 ) rank -= tmp;

        else break;

    }

//    printf("Deal_2:: rank = %I64d, sum = %d\n", rank, sum);

    int flag = 0;

    for(int i = 1; i <= 9; i++){

        LL tot = 0;

        for(int L = 1; L <= getbit(n); L++)

            tot += Count( i, L, sum );

//        printf("Deal_2:: i = %d, tot = %I64d\n", i, tot );

        if( tot >= rank ){

            flag = 1;

            if(tot==rank && i==sum) A = i, rank = 0;

            else A = i-1;

            break;

        }

    }

    if( !flag ) A = 9;

    if( !rank ) return A;

//    printf("First:: A = %I64d\n", A);

    for(int j,i = 2;i <= getbit(n);i++){ // 逐位确定

        LL tmp, x;

        for(j = 0; j < 10; j++){ //当前位的取值j

            tmp = 0, x = A;  // 计算当前位置取j时,长度相等字典序小于x,且数位和为sum的数量

            for(int L = 1; L <= getbit(n); L++) // 这里可能溢出

                tmp += Count( x*10+j, L, sum);

//            printf("Deal_2:: tmp = %I64d, j = %d\n", tmp, j );

            if( tmp >= rank ) break;

        }

        if( j < 10 ){

            if( (rank==tmp) && (SumOf(A*10+j)==sum) ) {A = A*10+j; break;}

            else A = A*10 + (j-1);

        }

        else A = A*10+9;

    }

    return A;

}

int main(){

//    freopen("1.in","r",stdin);

    init(); // 预处理f(i,j) 后i位任意,且数量和为j的总数

    while( cin >> n >> K )

//    while( scanf("%lld%lld",&n,&K) != EOF)

    {

        if( n+K == 0 ) break;

        LL a = Deal_1();

        LL b = Deal_2();

        cout << a << " " << b << endl;

//        printf("%lld %lld", a,b );

    }

    return 0;

}
View Code

 

你可能感兴趣的:(dp)