Zoj 2599 (数位dp,数位统计)

来源:互联网 发布:sql 给字段设置默认值 编辑:程序博客网 时间:2024/06/08 15:03

    这个题纠结死我了,最开始是在高逸涵的论文《数位计数问题解法研究》中看到的,论文中只说了这个题的思路,没有代码实现,所以我自己按照他得思路写了好久,又Debug了好久好久,最后终于出来了。纠结到死....

题意:定义两个数的比较方法,各位数字之和大的数大,如果数字和相等则按字典序比较两个数的大小。输入n,k,要求输出两个结果:

     1. 将1~n的数排序后,数字k的排名;

     2. 将1~n的数排序后的第k个数;

他得思路就是写5个函数

1. getSum1(int L, int sum); 数字和为 sum 的 L 位数字的个数(以0为前缀的也算数)

2. getSum2(LL n, int sum); 返回 1~n 中数字和为 sum 的数的个数

3. getSum3(LL n, LL prefix, int sum); 返回 1~n 中数字和为 sum 前缀为 prefix 的数的个数

4. getSum4(LL n, LL k, int sum); 返回 1~n 中数字和为 sum 且字典序小于k的数的个数

5. getSum5(LL n, LL k); 返回 k 在 1~n 中的位置


之后又看到刘聪的论文《浅谈数位类统计问题》中也有这个题,但方法貌似不太一样。所以又按照他的方法又写了一个,他的方法中有个神奇的地方就是,有 “补零”“删尾” 两个操作,就简化了过程。写出来的代码确实比前面一个短。

不过提交后AC的时间:第一种10ms,第二种500ms.

下面分别是两个版本的代码:

/**    在论文《数位计数问题解法研究——高逸涵》中找到的,    论文中没有代码实现,自己写代码用了很久,又Debug了很久,纠结死了...    题目的第二问,不能用二分,    我之前一直用二分 wrong answer了,    因为1~n不是按照题目规则有序的,所以不能二分。    纠结死我了。。。。。**/#include <iostream>#include <cstdio>#include <cstring>using namespace std;typedef long long LL;LL  dp[20][200];  // dp[L][sum]表示数字位为L 数字和为sum 的数字的个数,(前导零也算)// 数字和为 sum 的 L 位数字的个数(以0为前缀的也算数)LL getSum1(int L, int sum){    if ( sum > 9*L || L < 0 || sum < 0 )        return 0;    if ( dp[L][sum] )        return dp[L][sum];    if ( L == 0 && sum == 0 )        return 1;    for (int i = 0; i <= 9; i++)    {        if ( sum-i < 0 ) break;        dp[L][sum] += getSum1(L-1, sum-i);    }    return dp[L][sum];}// 返回 1~n 中数字和为 sum 的数的个数LL getSum2(LL n, int sum){    int digit[20], L = 0;    while ( n )    {        digit[L++] = n%10;        n /= 10;    }    LL  res = 0LL;    for (int i = L-1; i >= 0; i--)    {        for (int j = 0; j < digit[i]; j++)            res += getSum1(i, sum--);    }    res += getSum1(0, sum);    return res;}// 返回 1~n 中数字和为 sum 前缀为 prefix 的数的个数LL getSum3(LL n, LL prefix, int sum){    char sn[21] = {0}, sp[21] = {0};    int  ln, lp;    sprintf(sn, "%lld", n);    sprintf(sp, "%lld", prefix);    ln = strlen(sn);    lp = strlen(sp);    for (int i = 0; i < lp; i++)        sum -= sp[i] - '0';    int i;    for (i = 0; i < lp; i++)    {        if ( sn[i] != sp[i] )            break;    }    if ( i < lp )    {        LL  res = 0LL;        if ( sn[i] < sp[i] )  ln--;        for (i = ln-lp; i >= 0; i--)            res += getSum1(i, sum);        return res;    }    LL  t = 0LL, res = 0LL;    for (i = lp; i < ln; i++)        t = 10*t + sn[i] - '0';    res = getSum2(t, sum);    for (i = ln-lp-1; i >= 0; i--)        res += getSum1(i, sum);    return res;}// 返回 1~n 中数字和为 sum 且字典序小于k的数的个数LL getSum4(LL n, LL k, int sum){    int digit[20], L = 0;    while ( k )    {        digit[L++] = k % 10;        k /= 10;    }    LL  prefix = 1LL, res = 0LL;    int t = 1;    for (int i = L-1; i >= 0; i--)    {        for (int j = t; j < digit[i]; j++)        {            res += getSum3(n, prefix++, sum);        }        prefix = 10*prefix;        t = 0;    }    // 如果 k=3000; 小于k的数有3,30,300;而上面的计算不包括这些    // 所以下面特殊车里这种数据    for (int i = 0; i < L; i++)    {        if (digit[i] == 0) res++;        else break;    }    return res;}// 返回 k 在 1~n 中的位置LL getSum5(LL n, LL k){    int sum = 0;    LL  _k = k;    while ( _k )    {        sum += _k % 10;        _k /= 10;    }    LL  res = 0LL;    for (int i = 1; i < sum; i++) // 数字和小于sum的数的个数        res += getSum2(n, i);    res += getSum4(n, k, sum);   // 数字和为sum且字典序小于k的数的个数    return res + 1;}int main(){    LL  n, k;    while ( cin >> n >> k && n )    {        cout << getSum5(n, k) << " ";        int sum = 1, preSum;        LL  t, pre;        while ( (t = getSum2(n, sum)) < k )        {            sum++;            k -= t;        }        pre = 1;        preSum = 1;        while ( true )        {            while ( (t = getSum3(n, pre, sum)) < k )            {                pre++;                preSum++;                k -= t;            }            if ( preSum == sum ) break;            else pre *= 10;        }        while ( --k ) pre *= 10;        cout << pre << endl;    }    return 0;}

/**    这个代码是按照论文《浅谈数位类统计问题——刘聪》来写的。**/#include <iostream>#include <cstdio>#include <cstring>#include <algorithm>using namespace std;typedef unsigned long long ULL;ULL f[20][200];  // f[i][j]表示长度为i数字和为j的数的个数(包含前缀为0的)void init(){f[0][0] = 1;for (int i = 1; i < 20; i++)for (int j = 0; j <= i*9; j++)for (int k = 0; k <= 9; k++)if (j >= k)f[i][j] += f[i-1][j-k];}// 返回n的数位和inline int sumof(ULL n){int sum = 0;while ( n ){sum += n%10;n /= 10;}return sum;}// 返回数字n的长度inline int lenof(ULL n){int ans = 0;while ( n ){ans++;n /= 10;}return ans;}// 返回1~n中的数位和为sum的数的个数ULL count(ULL n, int sum){int digit[20], L = 0;ULL ans = 0;while ( n ){digit[L++] = n%10;n /= 10;}for (int i = L-1; i >= 0; i--){for (int j = 0; j < digit[i]; j++)        {            ans += f[i][sum--];            if ( sum <= 0 ) goto loop;        }}loop:ans += f[0][sum];return ans;}// 返回第一问的结果:在1~n中数位和小于sum,和数位和等于sum且字典序小于k的数的个数。// 由第二问可知,这里的sum值不一定是sumof(k)ULL find(ULL n, int sum, ULL k){    int ln = lenof(n), lk = lenof(k);ULL t, ans = 0;for (int i = 1; i < sum; i++)ans += count(n, i);    if ( k == 0 )        return ans;t = 10*k;for (int i = lk + 1; i <= ln; i++)  // 补零{ans += count( min(n, t-1), sum ) - f[i-1][sum];t *= 10;}t = k;for (int i = lk; i > 0; i--)     // 删尾{ans += count(t, sum) - f[i-1][sum];t /= 10;}return ans;}// 返回第二问的结果:1~n中按题意排序后,第k个数字是谁ULL find2(ULL n, ULL k){int i, sum = 1;ULL t, ans, _k = k;while ( (t = count(n, sum)) < _k ){sum++;_k -= t;}ans = 0;while ( find(n, sumof(ans), ans) != k ){ans *= 10;for (i = 0; i <= 9; i++)if ( find(n, sum, ans+i) >= k ) //这里是sum而不是sumof(ans+i)break;ans += i;if ( find(n, sumof(ans), ans) == k )break;ans--;}return ans;}int main(){init();ULL n, k;while ( cin >> n >> k && n ){cout << find(n, sumof(k), k) << " " << find2(n, k) << endl;}return 0;}