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 1≤n≤8。
测试点 n ≤ n\leq n≤ 特殊性质 1 ∼ 3 1\sim 3 1∼3 1 1 1 无 4 ∼ 5 4\sim 5 4∼5 2 2 2 无 6 ∼ 8 6\sim 8 6∼8 8 8 8 A 9 ∼ 10 9\sim 10 9∼10 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) O(n)的枚举每一个集合,都判断一遍,都满足就让结果加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 个可消除的连续子串,分别是
cc
、acca
、cc
、bccb
、accabccb
。【数据范围】
对于所有测试数据有: 1 ≤ n ≤ 2 × 1 0 6 1 \le n \le 2 \times 10^6 1≤n≤2×106,且询问的字符串仅由小写字母构成。
测试点 n ≤ n\leq n≤ 特殊性质 1 ∼ 5 1\sim 5 1∼5 10 10 10 无 6 ∼ 7 6\sim 7 6∼7 800 800 800 无 8 ∼ 10 8\sim 10 8∼10 8000 8000 8000 无 11 ∼ 12 11\sim 12 11∼12 2 × 1 0 5 2\times 10^5 2×105 A 13 ∼ 14 13\sim 14 13∼14 2 × 1 0 5 2\times 10^5 2×105 B 15 ∼ 17 15\sim 17 15∼17 2 × 1 0 5 2\times 10^5 2×105 无 18 ∼ 20 18\sim 20 18∼20 2 × 1 0 6 2\times 10^6 2×106 无 特殊性质 A:字符串中的每个字符独立等概率地从字符集中选择。
特殊性质 B:字符串仅由
a
和b
构成。 -
明题意:对于一个字符串,统计能消除的连续子串个数。
-
考虑暴力做法,在考场上我想到:可以先统计小区间的个数,再合并统计入大区间,先枚举一个长度,再枚举一个起点,最后枚举中间点,总的来说是 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