大家好,歡迎大家閱讀周末算法題專題。
今天我們選擇的題目是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...)}}