牛客竞赛记录——小紫的优势博弈(Python3题解)
题目来源
链接:https://ac.nowcoder.com/acm/contest/103948/D
来源:牛客网
题目描述
定义一个字符串是“双生串”,当且仅当字符串中每一种字符出现的次数都是偶数次。
现在,小紫拿到了一个长度为 n n n( 1 ≤ n ≤ 1 0 6 1\leq n\leq10^6 1≤n≤106),仅由字符‘0’和‘1’组成的字符串 s s s,她准备和小红玩一个游戏:
- 小红先手操作,删除该串的一个非空前缀;
- 小紫紧接着操作,删除该串的一个后缀(可以是空串)。
如果最终可以生成一个非空双生串,那么小紫将获得最终的胜利。
小紫发现这个游戏自己非常劣势,因为小红可以删除到只剩下一个字符导致自己必输,于是她强制让小红随机删除一个前缀(即删除 1 1 1 到 n n n 个字符)。请你计算小紫最终获胜的概率。
输入描述
第一行输入一个正整数
n
n
n(
1
≤
n
≤
1
0
6
1\leq n\leq10^6
1≤n≤106) 代表字符串的长度。
第二行输入一个长度为
n
n
n,由字符‘0’和‘1’组成的字符串
s
s
s,代表初始字符串。
输出描述
在一行上输出一个实数,代表最终小紫获胜的概率。
由于实数的计算存在误差,当误差的量级不超过 1 0 − 6 10^{-6} 10−6 时,您的答案都将被接受。具体来说,设您的答案为 a a a ,标准答案为 b b b ,当且仅当 ∣ a − b ∣ max ( 1 , ∣ b ∣ ) ≤ 1 0 − 6 \frac{|a - b|}{\max(1,|b|)}\leq10^{-6} max(1,∣b∣)∣a−b∣≤10−6 时,您的答案将被接受。
示例1
输入
5
10010
输出
0.2
说明
在这个样例中,小红一共有五种删除前缀的方式:
- 删除前一个字符,得到 “0010”;此时,小紫只需要删除最后的两个字符,得到 “00”,此时字符串是双生串,小紫可以获胜;
- 删除前两个字符,得到 “010”;
- 删除前三个字符,得到 “10”;
- 删除前四个字符,得到 “0”;
- 删除前五个字符,得到 “”;
其中,对于后四种情况,小紫无论怎么删除后缀,都无法得到双生串,所以,小紫获胜的概率为 1 5 = 0.2 \frac{1}{5}=0.2 51=0.2。
题解
我这里准备提供两个题解,一个比较正规的,一个比较神秘的。(笑~~)
答案不是原创的,但是原创不好找,通过的代码里好多是一样的。
题解一:
来自:不玩原神也能学算法吗QAQ
def main():
import sys
input_data = sys.stdin.read().split()
if not input_data:
return
n = int(input_data[0])
s = input_data[1].strip()
# 状态用整数 0~3 表示,编码方式:state = p0*2 + p1,其中 p0, p1 分别表示'0'和'1'出现次数的奇偶性
P = [0]*(n+1)
# 初始状态为 0 (对应 (0,0))
# 变量 p0, p1 表示当前奇偶性
p0, p1 = 0, 0
for i in range(n):
if s[i] == '0':
p0 ^= 1 # 异或1即取反
else: # s[i]=='1'
p1 ^= 1
P[i+1] = p0*2 + p1
# 记录每个状态最后出现的位置
last_occ = [-1]*4
for i in range(n+1):
last_occ[P[i]] = i
# 对于每个可能的小红操作(删除前缀长度 i,i=1..n-1,因为i=n时剩空串),判断是否存在 j>i 使得 P[j]==P[i]
win_count = 0
for i in range(1, n):
if last_occ[P[i]] > i:
win_count += 1
# 小红有 n 种选择(包括删除整个串的情况),但删除后空串无法构成胜利,所以分母为 n
prob = win_count / n
# 输出概率,满足误差要求
print(prob)
if __name__ == '__main__':
main()
这个解法比较正规,他在第一次遍历输入数组的时候,巧妙的用4个二进制数,(就是他的编码)来记录0和1的奇偶性。
之后遍历小红随机删除的所有可能,只要当前这个位置状态不在最后位置,就是可以得到双生子串的。
这里可能有人不明白,那么简单解释一下:奇数状态的两倍 = 偶, 偶数状态的两倍 = 偶。既然当前状态不在最后位置,那我把这个状态最后位置以后的删除了,不就得到两倍的当前状态吗?
什么,你问最后4个状态里有偶数怎么办?
那可以再加个判断呗。可惜的是这种题数据都比较多,你最后剩下那两种偶数状态,算误差里面去了。
-
时间复杂度:
- 第一个循环用于计算状态编码,遍历字符串一次,时间复杂度为 O ( n ) O(n) O(n)。
- 第二个循环用于记录每个状态的最后出现位置,同样遍历一次,时间复杂度为 O ( n ) O(n) O(n)。
- 第三个循环用于计算小紫获胜的情况数,遍历一次,时间复杂度为 O ( n ) O(n) O(n)。
- 总体时间复杂度为 O ( n ) O(n) O(n),因为代码中没有嵌套循环,主要操作都是线性的。
-
空间复杂度:
P
列表长度为n + 1
,last_occ
列表长度为 4,其余变量为常数级空间。- 因此,空间复杂度为
O
(
n
)
O(n)
O(n),主要由
P
列表占用的空间决定。
解法二:神秘代码
n = int(input())
s = input()
k = 0
for i in range(n):
a, b = 0, 0
for j in range(i + 1, n):
if s[j] == "0":
a += 1
else:
b += 1
if (a % 2 == 0 and b % 2 == 0):
k += 1
break
ans = k / n
print(ans)
这个解法就比较简单直白了。
-
输入读取:
n = int(input())
:从用户输入中读取一个整数n
,该整数代表字符串的长度。s = input()
:读取一个长度为n
的字符串s
,该字符串仅由字符'0'
和'1'
组成。
-
初始化计数器:
k = 0
:初始化一个计数器k
,用于记录满足特定条件的子串的数量。
-
嵌套循环遍历字符串:
- 外层循环
for i in range(n)
:遍历字符串s
的每个位置i
,作为子串的起始位置。 - 对于每个起始位置
i
,内层循环for j in range(i + 1, n)
从i + 1
开始遍历到字符串末尾,构建以i
为起始位置的子串。 - 在内层循环中,使用
a
和b
分别记录子串中字符'0'
和'1'
的出现次数。 - 每次内层循环更新
a
和b
后,检查a
和b
是否都为偶数。如果是,则将计数器k
加 1,并使用break
语句跳出内层循环,不再继续检查以当前起始位置i
开始的更长子串。
- 外层循环
-
计算概率并输出结果:
ans = k / n
:将满足条件的子串数量k
除以字符串的长度n
,得到概率。print(ans)
:输出计算得到的概率。
复杂度分析
-
时间复杂度
- 外层循环遍历字符串的每个位置,循环次数为
n
。 - 对于每个外层循环的位置
i
,内层循环从i + 1
开始遍历到字符串末尾,内层循环的平均次数约为n/2
。 - 因此,总的时间复杂度为 O ( n 2 ) O(n^2) O(n2),因为嵌套循环的总执行次数近似为 ∑ i = 0 n − 1 ( n − i − 1 ) ≈ n ( n − 1 ) 2 \sum_{i = 0}^{n - 1} (n - i - 1) \approx \frac{n(n - 1)}{2} ∑i=0n−1(n−i−1)≈2n(n−1)。
- 外层循环遍历字符串的每个位置,循环次数为
-
空间复杂度
- 代码中只使用了常数级的额外变量,如
n
、s
、k
、a
和b
,不随输入字符串长度的增加而增加。 - 因此,空间复杂度为 O ( 1 ) O(1) O(1),表示只使用了常数级的额外空间。
- 代码中只使用了常数级的额外变量,如
为什么说它比较神秘呢?
哎,因为这个代码不稳定。不是说代码本身有什么问题,运行会出错什么的,而是说在牛客网上不稳定。你在提交的时候,可以会告诉你85%或者90%用例通过,运行超时,请检查什么什么的。但是有的时候却能通过。
所以,这个故事告诉我们,问了问题可以多找找别人的问题(坏笑)
好吧,开个玩笑,这个可以告诉我们下次参加竞赛的时候,如果告诉你运行超时,你可以多提交几次,说不定就过了。