1. 程式人生 > >BZOJ 3110 [Zjoi2013]K大數查詢

BZOJ 3110 [Zjoi2013]K大數查詢

lin 時間 樹狀數組 永久 方便 大數 com 都差不多 樹狀

  這是一道非常經典的模版題,做法萬變不離其宗,但是還是有不少可記敘的。

法一:值域線段樹套序列線段樹

  這應該是見得很多的做法了,也是我最先寫的做法。值域線段樹上每個結點都對應了一棵序列線段樹,值域線段樹線段[lf,rg]上序列線段樹線段[L,R]的值表示[L,R]這段區間有多少個值域在[lf,rg]之間的值。

  修改時,因為是[l,r]每個位置都添入了權值c。於是就是值域線段樹走到c,中間每個線段[l,r]加上r-l+1。

  查詢時,就當是二分答案,答案區間就是值域線段樹的線段,每次取右兒子,看當前區間在右兒子值域中出現的數的個數,與k比較。如果k不足則向右走,否則向左走而且k相應減小。

  於是這樣下來,一個常數巨大的樹套樹就出來了,時間復雜度O(nlog2n),空間復雜度O(nlog2n)。我最開始是,內存池300*N,241080 kb,12872 ms

  確實太慢了。考慮優化,內層其實完全可以標記永久化,不用pushdown,一些多余結點的浪費也隨之消失。內存池200*N,201236 kb,7852 ms

  但這還不夠,考慮到值域線段樹所有的左兒子其實都沒有用,於是在修改時可以直接跳過,時空復雜度更小。內存池100*N,101624 kb,5556 ms

法二:整體二分套序列樹狀數組

  整體二分這個方法讓我迷亂了好久,最後才發現,其實就只是沒有建出值域線段樹而已。所有的操作的時間相對順序不變,solve(lf,rg,...)表示...這些操作都滾到了[lf,rg]這條線段上。於是我們只需要按照順序做,把相應的操作都丟到更下面的線段去。

  而很明顯,在這種情況下線段樹只留一棵也行。每一次處理完[lf,rg]的操作後,再把它清零。考慮線段樹的操作,區間加一個數,詢問區間的和。那這樣,動態開點線段樹就可以冬眠了~因為樹狀數組可以區修區詢啊!

  具體來說是這樣的。如果說我讓後綴[i,n]的所有位置都+x,之後詢問前綴[1,j]的和,那很明顯+x對詢問的貢獻就是x*(j-i+1)=x*(j+1)-x*i。而前面一半x*(j+1),宏觀下來就是(Σx)*(j+1)。後面一半x*i,則是只與修改有關而與詢問無關。當然,中間要求i<j。

  於是,做法出來了:使用兩棵序列樹狀數組。

技術分享圖片

  Succeed!時間復雜度O(nlog2

n),空間復雜度O(n)。3384 kb,1176 ms!!!

法三:序列樹狀數組套值域線段樹

  由法二的啟迪,想到這個做法也不是特別困難。外層的樹狀數組也可以使用同樣套路區間修改,而查詢的時候則可以把若幹個這樣的根拿來一起跳,加加減減。

  他們總說這是主席樹,Too Young Too Simple!

  最終時間復雜度O(nlog2n),空間復雜度O(nlog2n),跑下來效率還可以,內存池200*N,118652 kb,4852 ms

法四:序列線段樹套值域線段樹

  既然腦洞已經大開,那就怕是關不住了。修改的時候如果標記永久化,查詢時再把若幹個結點一起拿出來還加權,效果又會怎麽樣呢?

總結

  這道題目做了我很久,但其實各種做法都差不多。然而,編程復雜度各不相同,所耗內存各不相同,時間也各不相同。是為什麽呢?還是在於對題目的充分把握,還是在於對於數據結構本身的理解。像法三和法四關系如此,而法一卻不用序列樹狀數組,是因為要動態開點。同理,法三和法四也不能使用值域樹狀數組,除非你實在無聊把樹狀數組變成線段樹然後再來跳。而法一不使用值域樹狀數組是因為不方便二分,而其實最後我們只留了右兒子,這其實本質上就是樹狀數組了。最後一句話比較玄妙,但很有意思。

BZOJ 3110 [Zjoi2013]K大數查詢