你們好,歡迎你們閱讀週末算法題專題。算法
今天咱們選擇的題目是codeforces上週比賽的C題,我上週原本想參賽的,都已經報名了。可是後來因爲身體不適,因此早早休息了,沒有參加。今天抽空作了一下上週的題目以後很是慶幸還好上週沒參加,否則的話rating確定要掉了。數組
題目連接:https://codeforces.com/contest/1405/problem/Cide
這道題有6800多人經過,怎麼看也不算是難題,可是我作了一上午都沒能AC。最後又苦思冥想了好久,才最終作出來。作出來以後的第一感受就是這道題太牛了,值得一說,算是那種誰都能看懂題意,都能想一想辦法,可是能作出來很不容易的問題。測試
仍是一如既往的codeforces賽題的風格,不嚴格考察算法,你作不出來大機率不是由於知道的算法不夠多,而是由於你思惟能力不夠。url
給定一個字符串,字符串當中只包含三種字符,分別是0,1和?。?表示既能夠是0也能夠是1。如今呢,給定一個整數k,k表示滑動窗口的長度。咱們須要從頭開始將一個滑動窗口向字符串末尾移動,很明顯,無論咱們怎麼移動,滑動窗口裏的字符的數量應該都是k個。spa
因爲存在?既能夠是0也能夠是1,咱們但願咱們能找到一種方案,把一部分?變成0,另一部分變成1。使得在這個窗口滑動的過程中,窗口裏的0的數量和1的數量相等。設計
給定字符串以及k,要求返回YES或NO,YES表示存在這樣的方案,NO表示不存在。3d
這是一道多組測試數據的問題,首先給定一個t表示數據組數。對於每一組數據首先給定n和k兩個整數,n表示字符串的長度,k表示滑動窗口的長度。接着給定一個字符串,保證字符串當中只有0,1和?,而且字符串的長度爲n。code
其中blog
首先經過給定的數據範圍咱們能夠肯定一點,就是若是咱們一個滑動窗口一個滑動窗口地判斷必定會超時。由於最壞狀況下,,這時滑動窗口的數量一共也是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')
我以爲今天的題挺難的,解題的思路繞了好幾個彎。從一開始的分析問題到後面嘗試解決,發現踩了坑,再繼續分析,繼續踩坑,最後發現了關鍵線索從而解出了問題。在問題解決以前百思不得其解是很痛苦的,可是想到了解法以後的成就感仍是很使人欣喜的。咱們作算法題鍛鍊本身的能力,其實就是在這兩種體驗之間來回搖擺,在這過程中得到成長。從這個角度來講這題的質量的確很高,是我我的認爲的高質量算法題。