[UOJ]#300【CTSC2017】密钥——前缀和

一个密钥是一个长度为 n=2k+1 的字符串,它包含 1 个字母X、k 个字母 A 和 k 个字母 B。例如 k=3时,BAXABAB 就是一个密钥。

如下图所示,可以按顺时针顺序把这 2k+1个字母排成一个圈:
在这里插入图片描述

在 k 个字母 A 中,有一部分可以定义为 “强的’’。具体来说,从 X 出发顺时针走到某个 A 时,如果途中 A 的数目严格多于B的数目,则称此字母 A 为强的。

对于上面的例子来说,顺时针方向从字母 X 数起第 1 个和第 2 个字母 A 是强的,而第 3 个字母 A 不是强的。

一个密钥的特征值就是其中包含的强的字母 A 的个数。

天才小朋友 KT 给出了一个结论:

假设 k 个字母 A 所在的位置已经固定,但是剩下的 k 个 B 和 1 个 X 的位置是未知的。(注意,满足这样要求的密钥一共有 k + 1 个,因为字母 X 还剩下 k + 1 个可能的位置。)

可以证明:所有这 k + 1 个可能的密钥的特征值是各不相同的,它们恰好为0, 1, 2, …, k。

下面的图是一个具体的示例,从左到右的四个子图中分别有 3 个,2 个,1 个,0个字母 A 是强的。
在这里插入图片描述
类似地,如果固定 k 个字母 B 的位置,那满足条件的所有 k + 1 个密钥的特征值也各不相同,恰好为 0, 1, …, k。

现在你需要解决以下三个问题:

给定密钥中所有 A 的位置,当密钥的特征值为 0 时,请问 X 在哪个位置。

给定密钥中所有 A 的位置,当密钥的特征值为 S 时,请问 X 在哪个位置。

给定密钥中所有 B 的位置,当密钥的特征值为 S 时,请问 X 在哪个位置。

注意:字符串的 2k + 1 个字母的位置由 1 到 2k + 1 编号。

【例子 1】

假定 k = 3, S = 2。那么:

当 A 的位置是 {2,4,6} 且特征值为 0 时,X 的位置在 7;

当 A 的位置是 {2,4,6} 且特征值为 2 时,X 的位置在 3;

当 B 的位置是 {2,4,6} 且特征值为 2 时,X 的位置在 5。

【例子 2】

假定 k=9。S=7。那么:

当 A 的位置是 {3,4,5,9,10,12,13,16,19} 且特征值为 0 时,X 的位置在 14;

当 A 的位置是 {3,4,5,9,10,12,13,16,19} 且特征值为 7 时,X 的位置在 18;

当 B 的位置是 {3,4,5,9,10,12,13,16,19} 且特征值为 7 时,X 的位置在 17。
输入输出格式
输入格式:

只包含一组测试数据。

第一行包含一个整数 k,意义如题所述。

第二行包含一个整数 seed,这个数将用于生成一个 k 元集合 P。

第三行包含一个整数 S,意义如题所述。

保证 0 ≤ S ≤ k ≤ 107。1 ≤ seed ≤ 10000。

在 cipher/下,包含两个用于生成输入数据的文件 cipher.cpp/pas。其中读入部分已经完成,在数组 p[] 中,若 p[i] = 0,表示 i 不属于集合 P,否则,i 属于集合P。

//生成p数组的代码,不是我的代码
#include <stdio.h>
#include <string.h>
int p[20000005];
int seed, n, k, S;
int getrand() {
    seed = ((seed * 12321) ^ 9999) % 32768;
    return seed;
}
void generateData() {
    scanf( "%d%d%d", &k, &seed, &S );
    int t = 0;
    n = k * 2 + 1;
    memset(p, 0, sizeof(p));
    for( int i = 1; i <= n; ++i ) {
        p[i] = (getrand() / 128) % 2;
        t += p[i];
    }
    int i = 1;
    while( t > k ) {
        while ( p[i] == 0 ) ++i;
        p[i] = 0;
        --t;
    }
    while( t < k ) {
        while( p[i] == 1 ) ++i;
        p[i] = 1;
        ++t;
    }
}
int main() {
    generateData();
    return 0;
}

输出格式:

输出三行,每行一个数,依次对应问题描述中的三个子问题的答案。

即:

第一个数表示当 k 元集合 P 代表 A 的位置且特征值为 0 时 X 的位置。

第二个数表示当 k 元集合 P 代表 A 的位置且特征值为 S 时 X 的位置。

第三个数表示当 k 元集合 P 代表 B 的位置且特征值为 S 时 X 的位置。

输入输出样例
输入样例#1:

5
3344
2

输出样例#1:

10
1
2

输入样例#2:

500000
4545
234567

输出样例#2:

999992
246922
753067

说明

【样例解释】

第一个样例中, P 数组为 1 的元素的下标分别为 5, 6, 7, 8, 9。

【数据范围与约定】

对于 30% 的数据,k ≤ 10^3。

对于 50% 的数据,k ≤ 10^5。

对于 100% 的数据,k ≤ 10^7。

对于每个测试点, 得分为以下三部分得分之和:

如果第一问回答正确,你将获得 3 分。

如果第二问回答正确,你将获得 4 分。

如果第三问回答正确,你将获得 3 分。

如果你仅仅知道部分答案,请也务必按此格式要求输出三个数。否则你可能会因格式错误无法得分。


题目要我们处理的是一个环,在环上进行操作显然不太容易,于是我们断环成链,将另一端1~n接在该断1 ~n的后面(这应该是一个常规操作)。
然后我们考虑如何统计答案,先考虑前两问,我们认为每个p[i]=1表示A。然后统计前缀和,每个位置若p[i]=1,则算作前缀和加一,否则减一。
for(int i=1;i<=2*n;i++) sum[i]=sum[i-1]+(p[i]?1:-1);
那么我们会发现,对于每一个p[i]=0的位置,如果它之后的n-1个数这段区间内,如果存在y个x,它们的p[x]都为1,并且它们的sum[x]>sum[i],那么将X放在位置i的特征值为x。
然而数据范围是 1 0 7 10^7 107,所以怎么在O(n)时间内求出每一段的x呢?我们发现相邻的两段,它们的段头的sum值差值为1,所以我们可以直接刷一遍更新,开一个桶记录每个数前缀和出现的次数,每次向右移动的时候就加或减一段前缀和的值,复杂度为O(n)。
		if(sum[i-1]>sum[i]) ans+=buck[sum[i-1]];  //向右移动更新
		else ans-=buck[sum[i]];
		if(p[i-1]){   //只有为A的点才能对答案有贡献
			buck[sum[i-1]]--;
			if(sum[i-1]>sum[i]) ans--;
		}
		if(p[i+n-1]){
			buck[sum[i+n-1]]++;
			if(sum[i+n-1]>sum[i]) ans++;
		}
还有一个问题就是把A变成B的,可以把sum全部变为原来的相反数,然后用同样的方法做一遍来求,但是这样比较麻烦,而我们这样考虑:把A全部变成B,那么sum全部变成原来的相反数,那么刚好我们求的区间就反过来,所以当x值为k-S时,当前区间就是A转成B的区间。所以就有了下面代码里的做法。
完整CODE:
#include<bits/stdc++.h>
using namespace std;
int p[40000005],sum[40000005],buck[40000025];
int seed, n, k, S,ans,a0,as,bs;
int getrand() {
	seed = ((seed * 12321) ^ 9999) % 32768;
	return seed;
}
void generateData() {
	scanf( "%d%d%d", &k, &seed, &S );
	int t = 0;
	n = k * 2 + 1;
	memset(p, 0, sizeof(p));
	for( int i = 1; i <= n; ++i ) {
		p[i] = (getrand() / 128) % 2;
		t += p[i];
	}
	int i = 1;
	while( t > k ) {
		while ( p[i] == 0 ) ++i;
		p[i] = 0;
		--t;
	}
	while( t < k ) {
		while( p[i] == 1 ) ++i;
		p[i] = 1;
		++t;
	}
}
int main()
{
	generateData();
	for(int i=n+1;i<=2*n;i++) p[i]=p[i-n];
	for(int i=1;i<=2*n;i++) sum[i]=sum[i-1]+(p[i]?1:-1);
	for(int i=1;i<=2*n;i++) sum[i]+=n;
	for(int i=1;i<=n;i++){
		if(!p[i]) continue;
		if(sum[i]>sum[1]) ans++;
		buck[sum[i]]++;
	}
	if(ans==0&&!p[1])a0=1;
	if(ans==S&&!p[1])as=1;
	if(ans==k-S&&!p[1])bs=1;
	for(int i=2;i<=n;i++){
		if(sum[i-1]>sum[i]) ans+=buck[sum[i-1]];
		else ans-=buck[sum[i]];
		if(p[i-1]){
			buck[sum[i-1]]--;
			if(sum[i-1]>sum[i]) ans--;
		}
		if(p[i+n-1]){
			buck[sum[i+n-1]]++;
			if(sum[i+n-1]>sum[i]) ans++;
		}
		if(ans==0&&!p[i])a0=i;
		if(ans==S&&!p[i])as=i;
		if(ans==k-S&&!p[i])bs=i;
	}
	printf("%d\n%d\n%d",a0,as,bs);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值