csp-s复盘

CSP-S2023解题思路

  • 复盘考试,并尝试探索正确分析思路的过程。

T1

  • [CSP-S 2023] 密码锁

    题目描述

    小 Y 有一把五个拨圈的密码锁。如图所示,每个拨圈上是从 0 0 0 9 9 9 的数字。每个拨圈都是从 0 0 0 9 9 9 的循环,即 9 9 9 拨动一个位置后可以变成 0 0 0 8 8 8

    因为校园里比较安全,小 Y 采用的锁车方式是:从正确密码开始,随机转动密码锁仅一次;每次都是以某个幅度仅转动一个拨圈或者同时转动两个相邻的拨圈。

    当小 Y 选择同时转动两个相邻拨圈时,两个拨圈转动的幅度相同,即小 Y 可以将密码锁从 0    0    1    1    5 \tt{0\;0\;1\;1\;5} 00115 转成 1    1    1    1    5 \tt{1\;1\;1\;1\;5} 11115,但不会转成 1    2    1    1    5 \tt{1\;2\;1\;1\;5} 12115

    时间久了,小 Y 也担心这么锁车的安全性,所以小 Y 记下了自己锁车后密码锁的 n n n 个状态,注意这 n n n 个状态都不是正确密码。

    为了检验这么锁车的安全性,小 Y 有多少种可能的正确密码,使得每个正确密码都能够按照他所采用的锁车方式产生锁车后密码锁的全部 n n n 个状态。

    输入格式

    输入的第一行包含一个正整数 n n n,表示锁车后密码锁的状态数。

    接下来 n n n 行每行包含五个整数,表示一个密码锁的状态。

    输出格式

    输出一行包含一个整数,表示密码锁的这 n n n 个状态按照给定的锁车方式能对应多少种正确密码。

    样例 #1

    样例输入 #1

    1
    0 0 1 1 5
    

    样例输出 #1

    81
    

    提示

    【样例 1 解释】

    一共有 81 81 81 种可能的方案。

    其中转动一个拨圈的方案有 45 45 45 种,转动两个拨圈的方案有 36 36 36 种。

    【数据范围】

    对于所有测试数据有: 1 ≤ n ≤ 8 1 \leq n \leq 8 1n8

    测试点 n ≤ n\leq n特殊性质
    1 ∼ 3 1\sim 3 13 1 1 1
    4 ∼ 5 4\sim 5 45 2 2 2
    6 ∼ 8 6\sim 8 68 8 8 8A
    9 ∼ 10 9\sim 10 910 8 8 8

    特殊性质 A:保证所有正确密码都可以通过仅转动一个拨圈得到测试数据给出的 n n n 个状态。

  • 首先,搞清楚题意:给出n个集合,每个集合有5个点,点数为0~9中任意一个数,对于每个集合,只能改变一位上的数,变成与原来不同的任意0 ~ 9之间的数,或只能改变相邻两位数至原来不同,且两位数之间差值不变,问对于n个集合中每个集合的所有结果,都包含结果的个数

  • 然后分析样例:(考场思路,贪心)

1

0 0 1 1 5

可以发现,这是特殊情况n == 1,在这种情况下,答案就是这个集合的所有结果。对于每一位,都有9种改变的方式(不能为原来的数),共有5位,所以只改变一个的情况下有 9 × 5 = 45 9 \times 5 = 45 9×5=45个结果;对于每两位,两位共有9种改变的方法(两位两位改,只能改四次)故有 9 × 4 = 36 9 \times 4 = 36 9×4=36个结果,一共81个结果。

2

0 0 1 1 5

0 0 1 1 4

对于这种情况,按位分析的话,相同的位并不用考虑, 看最后两位,只修改一种的情况,最后一位,除了4,5以外有8种情况符合题意。修改两位的情况下,一定不可能是上下两个集合都同时修改相邻两个,所以一定是一个移动了两位,一个移动了一位,所以分别令第一个的差值和第二个差值为移动两个值,更改另外的一个值,会有两种情况,举个例子:假定第一个移动两位,第二个移动一位,那么差值为4,所以可以让数字不同的位减去差值4得到的结果为:00104,刚好可以满足,同理另一个为00125。

2

0 0 1 1 5

0 0 1 2 3

分析最后两位,修改一位显然都无法满足,所以一定为修改了两位

  • 这是我在考场上的思路,可以发现这样思考将问题复杂化,从结果倒推开始,反而更难思考

1

0 0 1 1 5

枚举每一位,一共5位,一位有10种可能,一共100000种可能结果,但其中有大量不可能的情况,我们发现,只要一个结果与该集合中五个数有至少三个不同,就不合法。对于所有只有一位不相同的结果,合法,对于两个不相同,如果可以改一两位做到,就也合法。同理,我们只需要 O ( n ) O(n) On的枚举每一个集合,都判断一遍,都满足就让结果加1.

  • 可见这种思路更简单,清晰,对比上述的贪心思路,下面的显然更简便,代码也很清晰明了实现也很简单
  • 那么分析一下下面这个思路,枚举结果的过程,为 O ( 1 0 5 ) O(10 ^5) O(105),判断一个的过程为 O ( 5 ) O(5) O(5)(统计5位不同)同理,若判断n个集合,时间复杂度为 O ( n ) O(n) O(n),而看题目,$n\leq 8 $ ,很小 ,也就是,这个算法最多只要 O ( 4 × 1 0 6 ) O(4 \times 10 ^ 6) O(4×106)的复杂度,跑完绰绰有余。所以,想思路前一定要先看好数据规模。
void calc(int tot){
	if(tot == 6){
		int flag = 1;//标记n个集合是否都满足
		for(int i  = 1;i <= n;i++){
			int cnt = 0;
			for(int j = 1; j <= 5; j++) 
				if(f[j] != w[i][j]) cnt++;//统计有几位不同
			if(cnt > 2 || cnt == 0) flag = 0 , break;
			if(cnt == 2){//特判两位不同的情况
				int p,q; 
				cnt = 0;
				for(int j = 1; j <= 5; j++){//找到不相等的两位分别是p,q
					if(f[j] != w[i][j]){
						if(cnt == 0) p = j;
						else q = j;
						cnt++;
					}
				}
				if(p + 1!= q) flag = 0 , break;//p,q不相邻
				int k = 0;
				for(int j = 1; j <= 9; j++){//判断是否可以
					int x1 = (f[p] + j) % 10;
					int x2 = (f[q] + j) % 10;
					if(x1 == w[i][p] && x2 == w[i][q]) k = 1;
				}
				if(!k) flag = 0;
			}
		}
		if(flag) ans++;
		return;
	}
	for(int i = 0; i <= 9; i++){//先递归每一位填充
		f[tot] = i;
		calc(tot + 1);
	}
	return;
}
  • 这里还有一种思路

n个点集,可对每一个点集,将所有可能结果转换为一个int存储到一个数组,最后再,在这n个数组中求都包含的结果个数,也就是求交集。

T2

  • [CSP-S 2023] 消消乐

    题目描述

    小 L 现在在玩一个低配版本的消消乐,该版本的游戏是一维的,一次也只能消除两
    个相邻的元素。

    现在,他有一个长度为 n n n 且仅由小写字母构成的字符串。我们称一个字符串是可消除的,当且仅当可以对这个字符串进行若干次操作,使之成为一个空字符串。

    其中每次操作可以从字符串中删除两个相邻的相同字符,操作后剩余字符串会拼接在一起。

    小 L 想知道,这个字符串的所有非空连续子串中,有多少个是可消除的。

    输入格式

    输入的第一行包含一个正整数 n n n,表示字符串的长度。

    输入的第二行包含一个长度为 n n n 且仅由小写字母构成的的字符串,表示题目中询问的字符串。

    输出格式

    输出一行包含一个整数,表示题目询问的答案。

    样例 #1

    样例输入 #1

    8
    accabccb
    

    样例输出 #1

    5
    

    提示

    【样例 1 解释】

    一共有 5 5 5 个可消除的连续子串,分别是 ccaccaccbccbaccabccb

    【数据范围】

    对于所有测试数据有: 1 ≤ n ≤ 2 × 1 0 6 1 \le n \le 2 \times 10^6 1n2×106,且询问的字符串仅由小写字母构成。

    测试点 n ≤ n\leq n特殊性质
    1 ∼ 5 1\sim 5 15 10 10 10
    6 ∼ 7 6\sim 7 67 800 800 800
    8 ∼ 10 8\sim 10 810 8000 8000 8000
    11 ∼ 12 11\sim 12 1112 2 × 1 0 5 2\times 10^5 2×105A
    13 ∼ 14 13\sim 14 1314 2 × 1 0 5 2\times 10^5 2×105B
    15 ∼ 17 15\sim 17 1517 2 × 1 0 5 2\times 10^5 2×105
    18 ∼ 20 18\sim 20 1820 2 × 1 0 6 2\times 10^6 2×106

    特殊性质 A:字符串中的每个字符独立等概率地从字符集中选择。

    特殊性质 B:字符串仅由 ab 构成。

  • 明题意:对于一个字符串,统计能消除的连续子串个数

  • 考虑暴力做法,在考场上我想到:可以先统计小区间的个数,再合并统计入大区间,先枚举一个长度,再枚举一个起点,最后枚举中间点,总的来说是 O ( n 3 ) O(n ^ 3) O(n3)的复杂度,但是其实这种思路有问题,看样例而就能发现:

样例一:

8
accabccb

分析,对于i + 1 == j显然 f [ i ] [ j ] = 1 f[i][j] = 1 f[i][j]=1,

但对于 f [ 1 ] [ 4 ] f[1][4] f[1][4]应为2,但想要让小区间相加得出,必不可能,只有在加的时候特判,

同理,在求 f [ 1 ] [ 8 ] f[1][8] f[1][8]时,最优是从 f [ 1 ] [ 4 ] f[1][4] f[1][4] f [ 5 ] [ 8 ] f[5][8] f[5][8]推出,但因未考虑1~8本身就是,所以会少一,也要特判,那么,对于这个样例,特判两下就过了。但对于第二个样例(连续几个数相同),就不适用了。(现在想清楚考试时的未知错误的原因了)

5

aaaaa

在这种情况下,共有6个,分别是4个aa,两个aaaa,

假如先分成两个小区间,

aa 和 aaa

aa显然只有一个,aaa有两个,

合起来时,只有三个,如果想特判,显然是让第一个小区间的尾和第二个区间的头特判,让第一个区间的尾,和第二个区间的第三个特判也有一个,这样虽然最后能算出来,但算法的复杂度瞬间就退化了,思考也会更复杂。

  • 也就是,这个算法虽然能从小到大去考虑,但合并后,不同区间内会不可避免(所有数都相等的情况)产生新的合法的情况。(哈哈,考场还写了个假算法)
  • 观察题目,发现很类似于之前写过的一道题括号匹配,给一个字符串大意为判断字符串内’(‘与’)‘是否一对一匹配,’[‘与’]‘是否匹配,’{‘与’}'是否匹配。这道题相当于把括号换成了字母,把判断问题转换为统计类问题,那么可以尝试使用单调栈,怎么实现?

让字符串的字母顺序入栈,如果当前字母恰好与栈顶字母相同,就出栈,否则入栈,并把对于每一位的栈的状态记录下来。

这里有一个性质:假如一个区间[ l , r ]可以消除,那么,r对应的栈状态与 l - 1 对应的栈状态 应相同。所以,我们可以O ( n ) 枚举右端点,再枚举左端点, 匹配出可以消除的区间,累加答案。

复杂度整体上是 O ( n 2 ) O(n ^ 2 ) O(n2)

  • 正解思路:
  • 先看一种题:
  • 给你n个数,让你求n个数中,数和等于k的区间的个数.
  • 对于这种题,有一种固定的套路;

1.枚举右端点R

2.累加区间和,并将所得区间和存到桶中,

3.对于每个右端点,通过桶,判断是否有区间和与R的区间和差刚好为k

  • 这种算法的复杂度是 O ( n ) O(n) O(n)。对比暴力,优化在于在找左端点时,将 O ( n ) O(n) O(n)的枚举变成 O ( 1 ) O(1) O(1)的判断。同理,本题可用这种方式解决。
  • 延续上述栈思路,可见,复杂度卡在:枚举左端点。根据上述套路,我们可以开一个map,将栈的状态记录下来,对于每个右端点,找和它栈相同的个数记录下来,直接累加。
  • 但是,map使用字符串为键值时,复杂度是字符串的长度,所以使用map相当于没有优化,还是 O ( n 2 ) O(n ^ 2) O(n2).
  • 不过,可以用字符串hash代替map,可将算法优化到 O ( n ) O(n) O(n)。 值得一提的是,因为字符串hash的计算方式,存储方向是要逆序的,这样才能保证唯一。
  • 可见,考试时还是要推推性质的,就算推不出正解,也有更优的暴力思路,就比如这一题栈的思路
#define ull unsigned long long//无符号longlong自动取模
const int p = 13331;
map< ull , int > mp;//统计出现hash个数 
int n ;
long long ans = 0;
ull f[2000010];
char w[2000010];
int len[2000010];
stack<int> s;
ull qpow(int a , int b){//快速幂
	if(b == 0) return 1;
	if(b % 2 == 1) return qpow(a , b - 1) * a;
	ull t = qpow(a , b / 2);
	return t * t;
}
void calc(int x , int y){//字符串hash
	len[y] = len[x] + 1;
	f[y] = f[x] + qpow(p , len[y]) * w[y];//逆序计算
}
int main()
{
	scanf("%d",&n);
	scanf("%s",w + 1);
	mp[ 0 ] = 1;
	for(int i = 1; i <= n; i++){//边枚举,边计算hash
		if(s.size() == 0){
			s.push( i );
			calc(0 , i);
		}
		else{
			int j = s.top();
			if(w[i] == w[j]){
				s.pop();
				f[i] = f[j - 1];
				len[i] = len[j - 1];
			}
			else{
				s.push(i);
				calc(j , i);
			} 
		}
		ans += mp[ f[i] ];
		mp[ f[i] ] ++ ;
	}
	printf("%lld",ans);//特殊构造的数据可能会爆int
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
CSP-S222是一个自测系统,它的主要目的是帮助用户评估和测试自己的计算机科学知识和能力。这个系统由中国计算机学会(CSP)开发,并且针对计算机科学相关的专业课程。 CSP-S222自测系统提供了一系列的问题和题目,涵盖了计算机科学的各个方面,例如算法、数据结构、编程语言、操作系统、数据库等。用户可以通过回答这些问题进行自我评估,了解自己在这些方面的水平和能力。 使用CSP-S222进行自测非常简单。用户可以通过注册并登录系统,然后选择相应的题目进行回答。这些题目既有选择题,也有编程题。用户可以根据自己的实际情况,选择合适的题目进行回答。在回答问题的过程中,系统会根据用户的答案自动评分,并提供相应的反馈和解析,帮助用户检查和纠正错误。 CSP-S222自测系统的好处是帮助用户系统地了解自己在计算机科学方面的知识和能力。通过自测,用户可以发现自己的薄弱环节,并针对性地进行学习和提高。此外,CSP-S222还提供了一些参考资料和学习资源,帮助用户进一步深入学习和掌握计算机科学相关的知识。 总之,CSP-S222自测系统是一个方便、实用的工具,可以帮助用户评估和测试自己在计算机科学方面的知识和能力。通过自测,用户可以更好地了解自己的水平,并进行针对性的学习和提高。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值