SPOJ KPSUM 题解

 

这道题是在前几天的省队集训上做的……大爷出的题……虽说考场上找到了规律,但是还是没写出来,细节挺多的,而且也比较纠结……

 

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);
	}
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值