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