51Nod3236-第k小生成数

0.题目大意

        给定三个正整数m,n,k(2≤m≤1e7,1≤k≤n≤5e7)。已知一个长度为n的序列a满足a_{i}=m^{i} %mod(mod为题目给出的常数,为1e9+7),求a中第k小的数字。

(注意,本序列从a_{0}开始,即a_{0}=m^{0}%mod

1.分析题目

      ①初步分析

        首先,开一个大小为5e7的整形数组再排序是不现实的。题目的内存上限是313072KB,而一个大小为5e7的整形数组所占用的内存为195312.5KB。显然,MLE了。

        同时,n的上限是5e7,而时限为1.5s。这说明什么?这说明只能用O(n)的算法通过。甚至连常数打了都不行。虽然题目的标签有一个基数排序,但是实际上这道题不能直接使用基排。(你想想,不仅不能使用数组存储数字;而且基排的常数是比较大的,不能保证卡过时限)

      ②打开思路

        那能怎么办?如果你的思路是比较开阔的,你应该想到一道类似的题目:给出一个数组,求第k小的数字。那题数据很小,所以使用各种方法都能过。而其中最正统的写法就是运用了快排的思想:

        每次选择一个“哨兵”,大于它的数字排在它左边,小于它的数字在它的右边。计算两边的元素个数,可以缩小第k小数字的范围。

        但是,这个算法有一个缺点:其最优的时间复杂度为O(n),可是但凡你的运气差一点,就很可能导致两边的数字个数相差甚远。比如,你运气奇差或者题目数据有些变态,导致你每次选中的哨兵都是当前序列中的最大或最小值,那么该算法就会退化至O(n^{2})。这是不能接受的。

        所以,我们要使用另一种思路:因为这些数字的值域都很小,所以我们大可以使用一个bool数组来记录数字i是否在序列中出现过。再求一个前缀和,这题目就能轻松搞定了。

        这个思路是可以借鉴的。但是在本题中,a_{i} 的理论最大值为mod-1,即1e9+6。这个数字还是太大了——5e7的数组都存不下,更何况1e9呢!思路进入了死胡同。

        但是,这个“死胡同”中,并非无路可走:既然1e9的数组存不下,那我就把数字分为两部分——低五位(即个位、十位、百位、千位、万位)与高五位(十万、百万、千万、亿、十亿)。只要我们能求出第k小数字的高五位与低五位,问题就能得到解决。因为高五位的权值更大,所以我们先处理高五位,求出第k小的数字的高五位是多少。再求低五位,就能轻松搞定了

2.最终代码

     ①Q:但是,我们怎么求每个数字的高五位与低五位呢?

        A:(精华在于注释,下同)

const int mod = 1e9 + 7, num = 1e5;
int m, n, k;
int cnt[num + 1];
cin>>m>>n>>k;
int last = 1, x, h; //h是前面5位
cnt[last/num+1]++;
for (int i = 2; i <= n; i++) {
	long long now=1ll*x*last
    now%=mod;
    x=now;
	cnt[x / num + 1]++; //注意,这里有一个加一,为了方便cnt[0]的处理
	last = x;
}

     ②Q:我们怎么存储个数呢?

        A:

int cnt[num + 10];//cnt[i]表示值为i的个数出现的次数

     ③Q:我们怎么确定第k小数字的前五位呢?

        A:

for (int i = 1; i <= num; i++) {
	cnt[i] += cnt[i - 1];
	if (cnt[i] >= k) {
		h = i - 1;//h表示第k小数字的高五位。-1是因为处理cnt[0]时会数组越界,就整体都加1
		k -= cnt[i - 1];//这一步不能忘。因为第一轮筛选过后k对于高五位为i的数字内部来说k应该减去cnt[i-1]
		break;
	}
}

     ④ Q:我悟了……

        A:悟了就好,自己写完之后来核对一下代码或者自己直接交。

​
#include<iostream>//有缘千里来相见,万年不变头文件
#include<stdio.h>
#define int long long//十年OI一场空,不开long long见祖宗
using namespace std;
const int mod = 1e9 + 7, num = 1e5;//酒逢知己千杯少,定义常量莫忘了
int m, n, k;
int cnt[num + 1];
signed main() {
	scanf("%lld%lld%lld",&m,&n,&k);
	int last = 1, x, h;
	cnt[last/num+1]++;
	for (int i = 2; i <= n; i++) {
		x = last * m % mod;//这里开了long long,就不用担心爆int了
		cnt[x / num + 1]++; 
		last = x;
	}
	for (int i = 1; i <= num; i++) {
		cnt[i] += cnt[i - 1];
		if (cnt[i] >= k) {
			h = i - 1;
			k -= cnt[i - 1];
			break;
		}
	}
//	printf("h:%lld k:%lld\n",h,k);
	for (int i = 0; i <= num; i++) {
		cnt[i] = 0;
	}
	last = 1;
	if(last/num==h) cnt[last%num+1]++;
	for (int i = 2; i <= n; i++) {
		x = last * m % mod;
		if(x/num==h)
			cnt[x%num+1]++;
		last = x;
	}
	for(int i=1;i<=num;i++){
		cnt[i]+=cnt[i-1];
		if(cnt[i]>=k){
			int answer=h*num+i-1;
			printf("%lld",answer);
			return 0;
		}
	}
}

​

完结撒花!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值