首页 > 代码库 > ZOJ2599:Graduated Lexicographical Ordering(非常经典的数位DP)

ZOJ2599:Graduated Lexicographical Ordering(非常经典的数位DP)

Consider integer numbers from 1 to n. Let us call the sum of digits of an integer number its weight. Denote the weight of the number x as w(x).

Now let us order the numbers using so called graduated lexicographical ordering, or shorter grlex ordering. Consider two integer numbers a and b. If w(a) < w(b) then a goes before b in grlex ordering. If w(a) = w(b) then a goes before b in grlex ordering if and only if the decimal representation of a is lexicographically smaller than the decimal representation of b.

Let us consider some examples.

  • 120 < grlex4 since w(120) = 1 + 2 + 0 = 3 < 4 = w(4).
  • 555 < grlex78 since w(555) = 15 = w(78) and "555" is lexicographicaly smaller than "78".
  • 20 < grlex200 since w(20) = 2 = w(200) and "20" is lexicographicaly smaller than "200".

Given n and some integer number k, find the position of the number k in grlex ordering of integer numbers from 1 to n, and the k-th number in this ordering.


Input

There are several lines in the input file, and each line stands two integers n and k (1 <= k <= n <= 1018). A line with n = k = 0 ends up the input.


Output

For each line in the input, output one line in the output file. First print the position of the number k in grlex ordering of integer numbers from 1 to n, and then the integer that occupies the k-th position in this ordering.


Sample Input

20 10
0 0

Sample Output

2 14

题意:先把1~n内的数按照数位和排一次序,然后数位和相等的按照字典序拍一次序,然后输出k的位置和第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 中的位置

用这五个函数解决这道题
在ZOJ提交后惊讶的发现自己排在d第2,而且与第1的运行时间和内存是一样的,只是他是08年提交的,我是14年提交的
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
#define ll __int64
#define up(i,x,y) for(i=x;i<=y;i++)
#define down(i,x,y) for(i=x;i>=y;i--)
#define mem(a,b) memset(a,b,sizeof(a))
#define w(x) while(x)
ll n,k;
ll dp[20][200];
//数字和为 sum 的 L 位数字的个数(以0为前缀的也算数)
ll  getSum1(int L, int sum)
{
    if(sum>9*L || sum<0 || L<0)
        return 0;
    if(dp[L][sum])
        return dp[L][sum];
    if(!L&&!sum)
        return 1;
    int i;
    up(i,0,9)
    {
        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 bit[20],i,len=0,j;
    ll ans=0;
    w(n)
    {
        bit[len++]=n%10;
        n/=10;
    }
    down(i,len-1,0)
    {
        up(j,0,bit[i]-1)
        ans+=getSum1(i,sum--);
    }
    ans+=getSum1(0,sum);
    return ans;
}
//返回 1~n 中数字和为 sum 前缀为 prefix 的数的个数
ll getSum3(ll n,ll prefix,int sum)
{
    char sn[20],sp[20];
    int ln,lp,i,j;
    ll ans = 0;
    sprintf(sn,"%I64d",n);//将数字转化为字符串
    sprintf(sp,"%I64d",prefix);
    ln=strlen(sn),lp=strlen(sp);
    up(i,0,lp-1)
    sum-=sp[i]-'0';
    up(i,0,lp-1)
    if(sn[i]!=sp[i])
        break;
    if(i<lp)
    {
        if(sn[i]<sp[i]) ln--;//如果前缀的这一位大于n,那么从第二高位匹配,看作n的长度减去1
        down(i,ln-lp,0)
        ans+=getSum1(i,sum);
        return ans;
    }
    ll tem=0;
    up(i,lp,ln-1)
    tem=tem*10+sn[i]-'0';
    ans+=getSum2(tem,sum);
    down(i,ln-lp-1,0)
    ans+=getSum1(i,sum);
    return ans;
}
//返回 1~n 中数字和为 sum 且字典序小于k的数的个数
ll getSum4(ll n,ll k,int sum)
{
    int bit[20],i,len=0,j,t=1;
    ll ans=0,pre=1;
    w(k)
    {
        bit[len++]=k%10;
        k/=10;
    }
    down(i,len-1,0)//枚举前缀
    {
        up(j,t,bit[i]-1)
        {
            ans+=getSum3(n,pre++,sum);
        }
        pre*=10;
        t = 0;
    }
    //例如1000,小于k的有1,10,100这些是在前面统计过程中不涉及的
    up(i,0,len-1)
    {
        if(bit[i]==0)ans++;
        else break;
    }
    return ans;
}
//返回 k 在 1~n 中的位置
ll getSum5(ll n,ll k)
{
    ll sum = 0,tem=k,ans=0,i;
    while(tem)
    {
        sum+=(tem%10);
        tem/=10;
    }
    up(i,1,sum-1)//计算所有和小于sum的个数
    ans+=getSum2(n,i);
    ans+=getSum4(n,k,sum);//和等于sum,但是字典序小于k的个数
    return ans+1;//位于那些数的后一个位置
}
int main()
{
    mem(dp,0);
    w((scanf("%I64d%I64d",&n,&k),n+k))
    {
        printf("%I64d ",getSum5(n,k));
        ll pre=1,presum=1,sum=1,t;
        w(((t=getSum2(n,sum))<k))//统计数位和,从1开始,确定第k个位置的数位和
        {
            sum++;
            k-=t;
        }
        w(1)
        {
            w(((t=getSum3(n,pre,sum))<k))//确定k位置的数位和后,再比较前缀,找出k位置的数
            {
                pre++;
                presum++;
                k-=t;
            }
            if(presum==sum) break;//数位和确定
            pre*=10;
        }
        w(--k)pre*=10;//因为数位和确定,且前缀相同的情况还有可能是1,10,100这些类型,也要考虑
        printf("%I64d\n",pre);
    }

    return 0;
}