这道题是在前几天的省队集训上做的……大爷出的题……虽说考场上找到了规律,但是还是没写出来,细节挺多的,而且也比较纠结……
Description
将从1到N的数字一个接一个写下来,一位一位地看,对于奇数位,在前面添一个加号;对于偶数位,在前面添一个减号。这样可以得到一个很长的算式,求其值。N≤1015。
Analysis
这是一道数位统计题。首先我们可以得到一个暴力的算法,即顺次枚举每个数的每一位,再计算答案。但显然这个算法是不能通过的,不过我们可以用这个暴力程序来验证我们的正解的正确性。对于这类数位统计的题,对拍程序还是很有用的。
既然暴力会超时,我们就得换个角度来思考,即寻找数位之间的规律。
通过观察(考验狗眼的时候到了……)我们发现,可以把算式分成几段来求和。
例如,对于N=13827,我们可以分成[1,9]、[10,99]、[100,999]、[1000,9999]和[10000,13827]几段来求和。
这样分的好处有二:第一,除了第一段外,每一段都是以减号开头的,因为除了第一段每段的长度都是偶数;第二,除了最后一段外,每一段的和都是可以计算出来的,只要分奇数长度和偶数长度讨论即可,此处则略去公式。然后问题就变成了如何求最后一段的和。
我们对于最后一段分奇数长度和偶数长度进行讨论。
对于奇数长度的段,例如[10000,13827],我们写出中间几项:
-1 +2 -3 +4 -4
+1 -2 +3 -4 +5
-1 +2 -3 +4 -6
+1 -2 +3 -4 +7
可以发现,相邻的两个偶数和奇数抵消成了1,那么如果N为奇数,我们就可以直接计算出前面抵消而成的1的个数;如果N为偶数,我们只需在1的和上再加上N单独计算的和即可。
对于偶数长度的串,例如[1000,1382],我们也写出中间几项:
-1 +2 -3 +4
-1 +2 -3 +5
-1 +2 -3 +6
-1 +2 -3 +7
可以发现,每一位的符号都相同,那么我们可以分别对于每位求和,最后再加起来。这里有两种方法,我当时只想到了比较复杂的一种,其实还有一种比较简单的。这里先介绍一下我想到的复杂办法:
记某一位为从前往后的第X位,其数值为num[X],依然通过观察可以得到,每一位的和可以分成三部分:
1. 如果X位不是首位,那么我们一定可以找到某个数Q,使得Q的前X-1位比N的前X-1位小,且Q最大。那么在从段开始的数到Q为止的这一段中,X位一定是在从1到9循环,而且这个循环的长度以及和是可以求出来的。
2. 我们可以找到一个数P,使得P的前X位小于N的前X位,且P最大。那么在(Q,P]这一段中,X位的取值必然是[1,num[X]],那么这一部分的和也是可以求出来的。
3. 剩下的就是(P,N]这一段了,这一段X位的值为num[x],可以直接计算。
那么只要对于每一位分别计算这三部分的值,最后再求和即可。不过这个方法思维量略大,而且写起来细节有点麻烦。下面介绍更简洁的另一种方法:
我们依然使用分段的思想,按位来分段。比如,对于[100000,134012],我们将其分为[100000,109999]、[110000,119999]、[120000,129999]、[130000,130999]、[131000,131999]、[132000,132999]、[133000,133999]、[134000,134009]、[134010,134012]。那么这些段的求和就和之前的求[1000,9999]这种整段的求和一样了,只是多了一个前缀而已。这个前缀是很好求的,那么只要将这些段的和加起来即可。
至此,问题解决。
Code
我用的是我自己的那个较麻烦的方法,需要更为简洁的方法的代码的可以自行搜索其他题解。
//SPOJ1433; The Sum (KPSUM); 数位统计 #include <cstdio> #include <cstdlib> typedef long long ll; ll n, x, ans; int c; ll pow(int x, int t) { if (t <= 0) return 1; if (t == 1) return x; if (t & 1) return x * pow(x, t - 1); ll r = pow(x, t >> 1); return r * r; } int g(int x) { int ret = 0; for (int i = 1; i <= n; ++i) ret += (-1 + (i & 1) * 2) * i; return ret; } ll b(ll x) { return x < 0 ? 0 : x; } int main() { freopen("count.in", "r", stdin); freopen("count.out", "w", stdout); while (scanf("%I64d", &n)) { if (!n) break; ans = 0LL; for (x = n, c = 0; x; x /= 10, ++c) ; if (n < 10) { printf("%d\n", g(n)); continue; } ans += 5; for (int i = 2; i < c; ++i) if (i & 1) //Odd ans += pow(10, i - 3) * (ll)450; else //Even ans -= pow(10, i - 2) * (ll)45; ll left = n + 1 - pow(10, c - 1); if (left) { if (c & 1) //Odd { if (left & 1) for (int i = 1; i <= c; ++i) if (i & 1) ans -= n % pow(10, c - i + 1) / pow(10, c - i); else ans += n % pow(10, c - i + 1) / pow(10, c - i); ans += left >> 1; } else //Even { int num[20], tot = 0; for (x = n; x; num[++tot] = x % 10, x /= 10) ; ll nx = n - pow(10, tot - 1); for (int i = 1; i <= tot; ++i) { ll sign = -(-1LL + (ll)(i & 1) * 2LL); ans += sign * b(nx / pow(10, tot - i + 1)) * pow(10, tot - i) * 45LL; ans += sign * b((num[tot - i + 1]) * (num[tot - i + 1] - 1) >> 1) * pow(10, tot - i); ans += sign * num[tot - i + 1] * (n % pow(10, tot - i) + 1); } } } printf("%lld\n", ans); } }