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

    这个题纠结死我了,最开始是在高逸涵的论文《数位计数问题解法研究》中看到的,论文中只说了这个题的思路,没有代码实现,所以我自己按照他得思路写了好久,又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;
}


你可能感兴趣的:(Zoj 2599 (数位dp,数位统计))