【咕咕东的奇妙序列】二分+数字模拟

题目

POJ 1019 Number Sequence

题目大意

题目给出一种序列表示方式,可以表示为 1 12 123 1234 … 12345678910 …(序列不带空格)。也就是说序列可被分组,第 i 组包含的是1到 i 的所有数字。题目给出多个询问要求每次得到序列中第 k 位的数字。注意 k 要能取到 10^18.

解题思路

首先说明题目序列的含义和询问方式,k的最大取值就是序列的最长长度,并且本题中要按照 k 位来查找,这就使得序列中两位以上的数字很难处理,也就是要将序列分为若干个大组,每个大组包含多个小组。大组可以按照1~9,10~99,100~999,…这样的方法来分,因为每一小组内最大数字的位数相同,同时这样分组可以降低之后定位是的时间复杂度,这一点对于10^18的数据量非常关键。
分好组后,我们需要得到每一大组及其之前大组所包含的元素总和,以便定位 k 所在的大组,这一点可以用等差数列的求和思路来做。同时我们还要能确定每一大组的小组内,不同位数数字的分界点,以便找到 k 对应的位置所对应数字的位数,方便从这个数字中取一位(思路复杂,反复琢磨)。需要用到二分查找的地方就是要去除 k 对应大组内所有在 k 对应小组之前的小组的元素数。之后由上面说过的方法,再去除 k 对应小组内所有位数小于 k 对应数字位数的元素和,这样 k 表示的值就是 k 对应数字的从左往右第 k 位。之后只要找到这一位就可以了,注意细心。
本题在CSP模拟时的数据量十分庞大,要远大于POJ上的情况,因此POJ上的题解是不能直接使用的。据我观察,POJ上的大多题解是直接按照小组来存贮组内元素个数,然后寻找 k 对应的小组,这样自然是比直接暴力寻找快一些,但还是达不到本次题目要求,所以必须要采用两次分组。但是两次分组更加繁琐易错,一定要十分细致准确理解数组的含义和上述确定数字的方法。

具体代码

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <climits>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <queue>
#include <vector>
#define ll long long
#define MAXN 10005
#define inf 1e9

using namespace std;

ll q,l,r,k,t,mid,L,R,cnt;
ll a[MAXN],b[MAXN],c[MAXN],d[MAXN];

void in(long long &x){
    long long y=1;char c=getchar();x=0;
    while(c<'0'||c>'9'){if(c=='-')y=-1;c=getchar();}
    while(c<='9'&&c>='0'){ x=(x<<1)+(x<<3)+c-'0';c=getchar();}
    x*=y;
}
void out(long long x){
    if(x<0){putchar('-');x=-x;}
    if(x>9) out(x/10);
    putchar(x%10+'0');
}

void init()
{
	c[1] = 1;
	for(ll i = 2; i <= 9; ++i)
	{
		c[i] = c[i-1] * 10;
	}
	k = 9;
	for(ll i = 1; i <= 9; ++i)
	{
		l = r + i;
		r = (k - 1) * i + l;
		a[i] = a[i-1] + (l + r) * k / 2;  //a[i]每次加上第10^(i-1)组到第10^i-1组之间总元素个数,因此表示第10^i-1组及之前总元素个数 
		k *= 10;
	}
	k = 9;
	for(ll i = 1; i <= 9; ++i)
	{
		b[i] = b[i-1] + k * i;  //b[i-1]+i表示第10^i组起点编号
		k *= 10;                //b[i]还可以表示最大元素为i+1位数的组内所有位数小于i的数的元素个数和  
	}
}

bool check(ll x)  //确定x是否是k之前包含的该大段内的组数 
{
	ll l = b[t-1] + t, r = l + (x - 1) * t;
	return ((l + r) * x / 2) < k;
}

ll sum(ll x)  //减去k之前包含的该大段内的组数的总元素数 
{
	ll l = b[t-1] + t, r = l + (x - 1) * t;
	return (l + r) * x / 2;
}

void sol(ll x)  //将连续数字分解 
{
	if(x > 9)
	{
		sol(x/10);
	}
	d[++cnt] = x % 10;
}

int main()
{
	init();
    in(q);
    while(q--)
    {
    	in(k);
    	for(ll i = 1; i <= 9; ++i)
    	{
    		if(a[i] >= k)
    		{
    			t = i;
    			break;
			}
		}
		k -= a[t-1];
		l = 1; r = 1e9;
		while(l < r)
		{
			mid = (l + r) >> 1;
			if(check(mid)) l = mid + 1;
			else r = mid;
		}
		k -= sum(l-1);
		for(ll i = 1; i <= 9; ++i)
		{
			if(b[i] >= k)
			{
				t = i;
				break;
			}
		}
		k -= b[t-1];
		L = k / t;
		R = k % t;  //位数相同的第L个数字倒数第R位 
		if(R == 0)
		{
			ll tmp = c[t] + L - 1;
			out(tmp%10);
			putchar('\n');
		}
		else
		{
			cnt = 0;
			ll tmp = c[t] + L;
			sol(tmp);  //把数字分解成数组 
			out(d[R]);  //取分解到数组中的数的第R位,就是对应序号k的结果 
			putchar('\n');
		}
	}
    return 0;
}
原创文章 46 获赞 1 访问量 1502

猜你喜欢

转载自blog.csdn.net/weixin_43676449/article/details/105406172