险些翻车,差一点没做出来的基础算法题
大家好,欢迎大家阅读周末算法题专题。
今天我们选择的题目是codeforces 1405比赛的C题。
题目链接:https://codeforces.com/contest/1405/problem/C
这道题有6800多人通过,怎么看也不算是难题,但是我做了一上午都没能AC。最后又苦思冥想了很久,才最终做出来。做出来之后的第一感觉就是这道题太牛了,值得一说,算是那种谁都能看懂题意,都能想想办法,但是能做出来很不容易的问题。
还是一如既往的codeforces赛题的风格,不严格考察算法,你做不出来大概率不是因为知道的算法不够多,而是因为你思维能力不够。
题意
给定一个字符串,字符串当中只包含三种字符,分别是0,1和?。 ?表示既可以是0也可以是1。现在呢,给定一个整数k,k表示滑动窗口的长度。我们需要从头开始将一个滑动窗口向字符串末尾移动,很明显,不管我们怎么移动,滑动窗口里的字符的数量应该都是k个。
由于存在?既可以是0也可以是1,我们希望我们能找到一种方案,把一部分?变成0,另外一部分变成1。使得在这个窗口滑动的过程当中,窗口里的0的数量和1的数量相等。
给定字符串以及k,要求返回YES或NO,YES表示存在这样的方案,NO表示不存在。
这是一道多组测试数据的问题,首先给定一个t表示数据组数。对于每一组数据首先给定n和k两个整数,n表示字符串的长度,k表示滑动窗口的长度。接着给定一个字符串,保证字符串当中只有0,1和?,并且字符串的长度为n。
其中
样例
心路历程
首先通过给定的数据范围我们可以确定一点,就是如果我们一个滑动窗口一个滑动窗口地判断一定会超时。因为最坏情况下,,这时滑动窗口的数量一共也是k个,对于每一个窗口我们需要遍历一遍。所以整体的复杂度是,对于1e5的数据范围来说这一定是不能接受的。
于是我转变思路,决定从整体入手。怎么入手呢?
整体入手
对于每一个滑动窗口来说都要保证其中0和1的数量相等,我们观察一下会发现,每一个位置的字符一共出现的次数是不同的。比如10?1?0这个字符串,我们假设k=4。我们会发现第0位的字符1只在1个窗口出现,第1位的0会在两个滑动窗口出现。对于每一个窗口我们都要保证0和1的数量一样多,那么也就是说我们要保证这些窗口出现的0和1的总数累加在一起应该一样多。
所以对于字符串当中的每一位,我们都计算它们的贡献度,贡献度就是总共出现的次数。这个值其实很好算,就是。比如第0位的1只出现了一次,所以贡献度就是1,第1位的0出现了两次,贡献度就是2。对于?来说我们是不确定它们贡献是0还是1的,但可以肯定的是贡献度是确定的。所以我们用一个数组来存储下来它们的贡献度。
最终我们可以得到两个数,分别是0的所有贡献度,1的贡献度以及**?组成的贡献度数组**。我们要做的就是从?组成的贡献度数组当中选出一些来变成0,另外一些变成1,最后让0和1的贡献度相等。
其实问题就转变成了给定一个数组和一个target,要求我们能否从这些数组当中选出一部分来求和之后等于target。我们之前在LeetCode当中做过这样的题目,应该说是非常基础了,只需要用递归就可以实现了。
但很遗憾的是,我把代码写出来之后连样例都过不了。错在了这个样例:
6 2
????00
由于最后出现了两个0,所以对于最后一个窗口来说,是无论如何也是无法达成的。这个结论其实不难发现,观察一下样例就可以。
维护区间
发现了这个问题之后,于是我开始想办法打补丁,也就是设计一种方法能够解决这个问题。我于是想了一个办法,对于每一个窗口我都维护两个值。分别是应该赋值成1的?的数量和应该赋值成0的?数量,举个例子,比如说还是刚才那个例子,一开始遇到两个??,那么显然应该一个等于0一个等于1。
这样当我们移动窗口的时候,会移出去一个字符,移进来一个字符。对于每个字符来说都有三种可能,所以一共就有9种可能。这9种情况我们也很容易想明白,首先移出和移入相等的情况,一定是合法的。如果移出的和移入不相等,并且当中没有?的话,那么一定是非法的。
如果移出0,移入?,那么移入的?一定是0,也就是说确定是0的问号数量加一。如果移出的是1,那么说明移入的?是1。如果移出的是?,移入的是1,说明移出的?也是1,也就是消耗了一个确定是1的?,同理如果移入的是0,也是一样的。
这样我们可以维护窗口内确定是0和确定是1的?的数量,在变化的过程当中,只要有一个小于0,那么就说明情况是非法的,否则说明是合法的。
我原本以为这样的方案应该已经很完美了,但是最后还是没有AC。我仔细想了一下,其实这种方案还是存在漏洞,因为我们没办法判断是否会出现前后矛盾的情况。也就是说最好要把每一个?的取值确定下来,而不是模棱两可,因为模棱两可就意味着可能存在矛盾。
正解
但是理论上来说每一个?都有两种可能,我们怎么能确定下来?的取值呢?
如果是单单思考这个问题是很难的,但其实我们刚才已经距离正解非常接近了,因为我们在维护区间的时候发现了一个非常重要的特性。就是当我们移动窗口的时候,移出的字符必须和移入的一致,否则一定非法。而我们移动的窗口的长度是确定的,我们就可以得到一个性质: s[i] = s[i+k]。
我们看下上图,上图框起来的k个元素代表窗口,当我们窗口移动的时候会移入一个元素,也会移出一个元素。我们假设目前窗口内的元素是合法的,也就是0和1一样多。那么当我们移动之后如果也是合法的,必须要保证移入的和移出的元素一样,或者其中有一个是?。
我们进一步观察会发现i和i + k,它们关于k同余。说白了就是它们对k取模的余数一样,我们把所有关于k取模之后余数一样的数的集合称为剩余系。k的剩余系一共有k个,这个也很容易想明白,因为k的余数一共有0到k-1这k个。不管我们怎么移动窗口,窗口内的元素都是k个,并且是每一个剩余系各包含一个元素。所以我们可以检查每一个剩余系对应下标的元素是否全部相等或者是等于?,如果不满足那么一定非法。
检查完所有的剩余系之后,我们还要统计一下为0的剩余系以及为1的剩余系的数量。如果超过k的一半,那么也一定是非法的。如果你能把这些点全部想明白,那么这题的代码也就非常简单了。
t = int(input())
for _ in range(t):
n, k = list(map(int, input().split(' ')))
st = input()
if k % 2 == 1:
print('NO')
continue
zero, one = 0, 0
flag = True
# 检查所有剩余系
# 枚举对k取模之后的余数
for i in range(k):
# tmp存这个剩余系应该全部相等的字符
tmp = None
for j in range(i, n, k):
if st[j] != '?':
# 如果tmp是1遇到了0或者是tmp是0遇到了1
if tmp is not None and st[j] != tmp:
flag = False
break
tmp = st[j]
if not flag:
break
# 根据tmp判断是全部为0的剩余系+1,还是全部为1的剩余系+1
if tmp == '0':
zero += 1
elif tmp == '1':
one += 1
# 有一种剩余系的数量超过一半,那么一定无法构成平衡
if max(one, zero) > (k // 2):
flag = False
print('YES' if flag else 'NO')
我觉得今天的题挺难的,解题的思路绕了好几个弯。从一开始的分析问题到后面尝试解决,发现踩了坑,再继续分析,继续踩坑,最后发现了关键线索从而解出了问题。在问题解决之前百思不得其解是很痛苦的,但是想到了解法之后的成就感还是很令人欣喜的。我们做算法题锻炼自己的能力,其实就是在这两种体验之间来回摇摆,在这过程当中获得成长。从这个角度来说这题的质量的确很高,是我个人认为的高质量算法题。
希望大家都能享受算法题的快乐,祝大家周末愉快。
衷心祝愿大家每天都有所收获。如果还喜欢今天的内容的话,请来一个三连支持吧~(点赞、关注、转发)
本文使用 mdnice 排版
- END -{{uploading-image-692975.png(uploading...)}}