ACM-ICPC培訓資料匯編(3)數據結構、動態規划分冊


ACM-ICPC 培訓資料匯編
3
數據結構、動態規划分冊
(版本號 1.0.0
哈爾濱理工大學 ACM-ICPC 集訓隊
2012 12
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- I -

2012 5 月,哈爾濱理工大學承辦了 ACM-ICPC 黑龍江省第七屆大學生程序設計競
賽。做為本次競賽的主要組織者,我還是很在意本校學生是否能在此次競賽中取得較好成
績,畢竟這也是學校的臉面。因此,當 2011 10 月確定學校承辦本屆競賽后,我就給齊
達拉圖同學很大壓力,希望他能認真訓練參賽學生,嚴格要求受訓隊員。當然,齊達拉圖
同學半年多的工作還是很有成效,不僅帶着黃李龍、姜喜鵬、程憲慶、盧俊達等隊員開發
了我校的 OJ 主站和競賽現場版 OJ,還集體帶出了幾個比較像樣的新隊員,使得今年省賽
我校取得了很好的成績(當然,也承蒙哈工大和哈工程關照,沒有派出全部大牛來參
賽)。
2011 9 月之前,我對 ACM-ICPC 關心甚少。但是,我注意到我校隊員學習、訓練
沒有統一的資料,也沒有按照競賽所需知識體系全面系統培訓新隊員。 2011-2012 年度的
學生教練們做了一個較詳細的培訓計划,每周都會給 2011 級新隊員上課,也會對老隊員
進行訓練,辛辛苦苦忙活了一年——但是這些知識是根據他們個人所掌握情況來給新生講
解的,新生也是雜七雜八看些資料和做題。在培訓的規范性上欠缺很多,當然這個責任不
在學生教練。 2011 9 月,我曾給老隊員提出編寫培訓資料這個任務,一是老隊員人數
少,有的還要去百度等企業實習;二是老隊員要開發、改造 OJ;三是培訓新隊員也很耗費
精力,因此這項工作雖很重要,但卻不是那時最迫切的事情,只好被擱置下來。
2012 8 月底, 2012 級新生滿懷夢想和憧憬來到學校,部分同學也被 ACM-ICPC 深深
吸引。面對這個新群體的培訓,如何提高效率和質量這個老問題又浮現出來。市面現在已
經有了各種各樣的 ACM-ICPC 培訓教材,主要算法和解題思路都有了廣泛深入的分析和討
論。同時,互聯網博客、 BBS 等中也隱藏着諸多大牛對某些算法的精彩論述和參賽感悟。
我想,做一個資料匯編,采擷各家言論之精要,對新生學習應該會有較大幫助,至少一可
以減少他們上網盲目搜索的時間,二可以給他們構造一個相對完整的知識體系。
感謝 ACM-ICPC 先輩們作出的傑出工作和貢獻,使得我們這些后繼者們可以站在巨人
的肩膀上前行。
感謝校集訓隊各位隊員的無私、真誠和抱負的崇高使命感、責任感,能夠任勞任怨、
以苦為樂的做好這件我校的開創性工作。
唐遠新
2012 10
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- II -
編寫說明
本資料為哈爾濱理工大學 ACM-ICPC 集訓隊自編自用的內部資料,不作為商業銷售目
的,也不用於商業培訓,因此請各參與學習的同學不要外傳。
本分冊大綱由黃李龍編寫,內容由黃李龍、周洲、盧俊達、曹振海等分別編寫和校
核。
本分冊內容大部分采編自各 OJ、互聯網和部分書籍。在此,對所有引用文獻和試題的
原作者表示誠摯的謝意!
由於時間倉促,本資料難免存在表述不當和錯誤之處,格式也不是很規范,請各位同
學對發現的錯誤或不當之處向acm@hrbust.edu.cn郵箱反饋,以便盡快完善本文檔。在此對
各位同學的積極參與表示感謝!
哈爾濱理工大學在線評測系統( Hrbust-OJ)網址: http://acm.hrbust.edu.cn,歡迎各位
同學積極參與AC
國內部分知名 OJ
杭州電子科技大學: http://acm.hdu.edu.cn
北京大學: http://poj.org
浙江大學: http://acm.zju.edu.cn
以下百度空間列出了比較全的國內外知名 OJ:
http://hi.baidu.com/leo_xxx/item/6719a5ffe25755713c198b50
哈爾濱理工大學 ACM-ICPC 集訓隊
2012 12
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- III -
目 錄
…….......................................................................................................................................... I
編寫說明..................................................................................................................................... II
1 章 數據結構........................................................................................................................5
1.1 散列...................................................................................................................................5
1.1.1 散列表的概念............................................................................................................5
1.1.2 散列函數的構造方法................................................................................................6
1.1.3 處理沖突的方法........................................................................................................7
1.1.4 散列表上的運算......................................................................................................11
1.1.5 散列表的應用..........................................................................................................14
1.1.6 附:字符串哈希函數..............................................................................................18
1.2 並查集.............................................................................................................................19
1.2.1 並查集基本原理......................................................................................................19
1.2.2 並查集的時間復雜度分析和優化..........................................................................21
1.2.3 並查集樣例代碼......................................................................................................22
1.2.4 例題講解..................................................................................................................22
1.3 二叉堆.............................................................................................................................27
1.3.1 二叉堆的概念..........................................................................................................28
1.3.2 二叉堆的基本操作..................................................................................................28
1.3.3 堆排序......................................................................................................................30
1.3.4 經典題目..................................................................................................................30
1.4 樹狀數組.........................................................................................................................36
1.4.1 基本原理..................................................................................................................36
1.4.2 樹狀數組例題..........................................................................................................39
1.4.3 其他推薦例題..........................................................................................................44
1.5 線段樹.............................................................................................................................44
1.1.1 線段樹的介紹..........................................................................................................45
1.1.2 線段樹模板代碼......................................................................................................45
1.1.3 經典題目..................................................................................................................46
1.6 隨機平衡二叉查找樹.....................................................................................................53
1.6.1 概述..........................................................................................................................54
1.6.2 Treap基本操作..........................................................................................................54
1.6.3 Treap的操作..............................................................................................................54
1.7 Treap應用.........................................................................................................................57
1.8 總結.................................................................................................................................57
1.8.1 經典題目..................................................................................................................57
1.8.2 其他例題..................................................................................................................66
1.9 伸展樹(Splay Tree).........................................................................................................66
1.9.1 概述..........................................................................................................................67
1.9.2 基本操作..................................................................................................................67
1.9.3 在伸展樹中對區間進行操作..................................................................................69
1.9.4 實例分析—NOI 2005 維護數列( Sequence....................................................71
1.9.5 和線段樹的比較......................................................................................................76
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- IV -
1.9.6 伸展樹例題..............................................................................................................76
2 章 動態規划......................................................................................................................86
2.1 遞推.................................................................................................................................86
2.1.1 遞推原理..................................................................................................................86
2.1.2 一般的思路..............................................................................................................86
2.1.3 經典題目..................................................................................................................86
2.2 背包問題.........................................................................................................................87
2.2.1 背包的入門和進階..................................................................................................88
2.2.2 經典題目..................................................................................................................92
2.3 區間動態規划.................................................................................................................95
2.3.1 引子..........................................................................................................................95
2.3.2 NOIp2000 乘積最大................................................................................................95
2.3.3 POJ 1141 Brackets Sequence....................................................................................97
2.3.4 NOIp2006 能量項鏈................................................................................................99
2.3.5 NOI 2001 棋盤分割...............................................................................................101
2.3.6 其他題目................................................................................................................104
2.4 狀態壓縮動態規划.......................................................................................................104
2.4.1 狀態壓縮的原理....................................................................................................104
2.4.2 一般的解題思路....................................................................................................105
2.4.3 經典題目................................................................................................................105
2.4.4 擴展變型................................................................................................................ 111
2.5 樹形動態規划............................................................................................................... 111
2.5.1 樹形動態規划介紹................................................................................................ 111
2.5.2 解題思路................................................................................................................ 111
2.5.3 經典題目................................................................................................................ 111
2.6 利用單調性質優化動態規划.......................................................................................114
2.6.1 利用單調性優化最長上升子序列........................................................................114
2.6.2 單調隊列................................................................................................................115
2.6.3 直接利用單調隊列解題........................................................................................117
2.6.4 單調隊列優化動態規划........................................................................................119
2.6.5 利用斜率的單調性................................................................................................125
2.6.6 擴展推薦................................................................................................................129
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 5 -
1章 數據結構
1.1 散列
參考文獻:
《算法導論》
散列表 http://student.zjzk.cn/course_ware/data_structure/web/chazhao/chazhao9.4.1.htm
擴展閱讀:
整數哈希介紹: http://www.cnblogs.com/napoleon_liu/archive/2010/12/29/1920839.html
各個字符串哈希函數比較: http://www.byvoid.com/blog/string-hash-compare/
編寫:黃李龍 校核:黃李龍
1.1.1 散列表的概念
散列方法不同於順序查找、二分查找、二叉排序樹及 B-樹上的查找。它不以關鍵字的
比較為基本操作,采用直接尋址技術。在理想情況下,無須任何比較就可以找到待查關鍵
字,查找的期望時間為 O(1)
1、散列表
設所有可能出現的關鍵字集合記為 U(簡稱全集)。實際發生(即實際存儲)的關鍵字集合
記為 K|K||U|小得多)。
散列方法是使用函數 h U 映射到表 T[0..m-1]的下標上( m=O(|U|))。這樣以 U 中關
鍵字為自變量,以 h 為函數的運算結果就是相應結點的存儲地址。從而達到在 O(1)時間內
就可完成查找。
其中:
hU{012,…, m-1} ,通常稱 h 為散列函數(Hash Function)。散列函數 h
的作用是壓縮待處理的下標范圍,使待處理的|U|個值減少到 m 個值,從而降低空間開銷。
T 為散列表(Hash Table)
h(Ki)(KiU)是關鍵字為 Ki 結點存儲地址(亦稱散列值或散列地址)
④ 將結點按其關鍵字的散列地址存儲到散列表中的過程稱為散列(Hashing)
用散列函數 h 將關鍵字映射到散列表中
3、 散列表的沖突現象
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 6 -
1)沖突
列函數值相同,因而被映射到同一表位置上。該現象稱為
沖突
2)安全避免沖突的條件
法是安全避免沖突。要做到這一點必須滿足兩個條件:
計散列函數 h 有可
能完全避免沖突。
3)沖突不可能完全避免
|U|>m,故無論怎樣設計 h,也不可
能完 時盡可能使沖突最少。同時還需要確定解決沖突的
方法
示表長和表中填人的結點數,則將α =n/m 定義為散列表的裝填因子
(Loa 常取α≤1
1.1. 方法
簡單快速;
其映射到表空間的任
何一 均勻地分布在表的地址集{0
1,…
集合上。
方法:先通過求關鍵字的平方值擴大相近數的差別, 然后根據表長度取中間的幾
位數作為散列函數值。又因為一個乘積的中間幾位數和乘數的每一位都相關,所以由此產
生的散列地址較為均勻。
0102010010020010012321)
201020123)
1000//取中間三位數作為散列地址返回
}

兩個不同的關鍵字,由於散
(Collision)或碰撞。發生沖突的兩個關鍵字稱為該散列函數的同義詞(Synonym)
【例】上圖中的 k2k5,但 h(k2)=h(k5),故 k2 K5 所在的結點的存儲地址相同。

最理想的解決沖突的方
①其一是|U|m
②其二是選擇合適的散列函數。
這只適用於|U|較小,且關鍵字均事先已知的情況,此時經過精心設

通常情況下, h 是一個壓縮映像。雖然|K|m
全避免沖突。因此,只能在設計 h
,使發生沖突的同義詞能夠存儲到表中。
4)影響沖突的因素
沖突的頻繁程度除了與 h 相關外,還與表的填滿程度相關。
m n 分別表
d Factor)。α越大,表越滿,沖突的機會也越大。通
2 散列函數的構造
1、散列函數的選擇有兩條標准:簡單和均勻。
簡單指散列函數的計算
均勻指對於關鍵字集合中的任一關鍵字,散列函數能以等概率將
個位置上。也就是說,散列函數能將子集 K 隨機
m-1}上,以使沖突最小化。
2、常用散列函數
為簡單起見,假定關鍵字是定義在自然數
1)平方取中法
具體
【例】將一組關鍵字(01000110101010010111)平方后得
(0010000001210
若取表長為 1000,則可取中間的三位數作為散列地址集:
(100121
相應的散列函數用 C 實現很簡單:
int Hash(int key){ //假設 key 4 位整數
key*=keykey/=100//先求平方值,后去掉末尾的兩位數
return key
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 7 -
表長 m 來除關鍵字,取其余數作為散列地
址,即 h(key)=keym
【例】若選 m 是關鍵字的基數的冪次,則就等於是選擇關鍵字的最后若干位數字作為
址, 而與高位無關。 於是高位不同而低位相同的關鍵字均互為同義詞。
制整數, 其基為 10,則當 m=100 時, 159259359,…,等均互法包括兩個步驟: 首先用關鍵字

key 乘上某個常數 A(0<A<1),並抽取出 key.A 的小數部分;然后用
m 乘以該小數后取整。即:(
2)除余法該方法是最為簡單常用的一種方法。 它是以該方法的關鍵是選取

m。選取的 m 應使得散列函數值盡可能與關鍵字的各位相關。 m
最好為素數。地【 例】 若關鍵字是十進為同義詞。(



3)相乘取整法該方該方法最大的優點是選取

m 不再像除余法那樣關鍵。 比如, 完全可選擇它是 2 的整數次冪。雖然該方法對任何
A 的值都適用,但對某些值效果會更好。 Knuth 建議選取
t Hash(int key){
機函數值為它的散列地址, 即類方法處理沖突: 開放定址
(Open Addressing)法和拉鏈(Chaining)法。前者是將針放在散列表

T[0..m-1]中。放地址法解決沖突的方法用某種探查

(亦稱探測)技術在散列表中形成一個探查
()序列。沿此序列逐個單元地查找,直到找到給定的關鍵字,或者碰 元為空
)為止(若要插入,在探查到開放的地址,則可將待 表明表中無待查的關鍵字,即查找失敗。更該函數的



C 代碼為:
indouble d=key *A
//不妨設 A m 已有定義
return (int)(m*(d-(int)d))//(int)表示強制轉換后面的表達式為整數
}
4)隨機數法選擇一個隨機函數, 取關鍵字的隨

h(key)=random(key)
其中 random 為偽隨機函數,但要保證函數值是在 0 m-1 之間。
1.1.3 處理沖突的方法
通常有兩所有結點均存放在散列表
T[0..m-1]中;后者通常是將互為同義詞的結點鏈成一個單鏈表, 而將此鏈表的頭指

1、開放定址法(
1) 開用開放定址法解決沖突的做法是: 當沖突發生時, 使到一個開放的地址

(即該地址單插入的新結點存人該地址單元)。 查找時探查到開放的地址則注意:①用開放定址法建立散列表時, 建表前須將表中所有單元


( 嚴格地說, 是指單元中存儲的關鍵字
)置空。
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 8 -
"-1"來表示空單元,而關鍵字為字符串時,空單元應是總之:應該用一個不會出現的關鍵字來表示空單元。開放定址法的一般形式為:


hi=(h(key)+di)m 1im-1
中:
m 為表長。…,
hm-1 形成了一個探查序列。
i 0 開始,並令 d0=0,則 h0=h(key),則有:
im-1
查法、雙重想是:散列表


T[0..m-1]看成是一個循環向量,若初始探查的地址為 d(h(key)=d),則最長的

d-1:
探查時從地址 d 開始,首先探查 T[d],然后依次探查 T[d+1],…,直到 T[m-1],此后 到探查到
T[d-1]為止。;般形式,線性探查法的探查序列為:


9.1】已知一組關鍵字為(26364138441568120651),用除余法構

m=1(0
1021252
31
②空單元的表示與具體的應用相關。【例】關鍵字均為非負數時,可用空串。(


2)開放地址法的一般形式其①

h(key)為散列函數, di 為增量序列,②
h(key)是初始的探查位置,后續的探查位置依次是 hlh2,…, hm-1,即
h(key)hlh2,③若令開放地址一般形式的

hi=(h(key)+di)m 0≤探查序列可簡記為
hi(0im-1)。(
3)開放地址法堆裝填因子的要求開放定址法要求散列表的裝填因子α≤
l,實用中取α為 0.5 0.9 之間的某個值為宜。(

4)形成探測序列的方法按照形成探查序列的方法不同,可將開放定址法區分為線性探查法、二次探散列法等。①線性探查法


(Linear Probing)
該方法的基本思將探查序列為:


dd+ld+2,…, m-101,…,即又循環到

T[0]T[1],…,直探查過程終止於三種情況:

(1)若當前探查的單元為空,則表示查找失敗(若是插入則將 key 寫入其中);
(2)若當前探查的單元中含有 key,則查找成功,但對於插入意味着失敗
(3)若探查到 T[d-1]時仍未發現空單元也未找到 key,則無論是查找還是插入均意味着失敗
(此時表滿)。利用開放地址法的一

hi=(h(key)+i)m 0im-1 //di=i
利用線性探測法構造散列表【例造散列函數,用線性探查法解決沖突構造這組關鍵字的散列表。解答


:為了減少沖突,通常令裝填因子α <l。這里關鍵字個數 n=10,不妨取
3,此時α≈0.77,散列表為 T[0..12],散列函數為: h(key)=key13。由除余法的散列函數計算出的上述關鍵字序列的散列地址為

2612)
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 9 -
T[0]T[10)
T[2] T[12]T[5]中。字
15 時,其散列地址 2(h(15)=1513=2)已被關鍵字 41(15 41
互為
)13=1,此地址開放,可將 12 插入其中。動畫演示

http:
集或堆積現象
i,i+1,…, i+k 的位置上已有結點時,一個散列地址為 插入在位置
i+k+1 上。把這種散列地址不同的結點爭奪同一
(Clustering)。這將造成不是同義詞的結點也處在同 加了查找時間。若散列函數不好或【例】上例中,


h(15)=2h(68)=3,即 15 68 不是同義詞。但由於處理 15 和同義詞 ,這就使得插入
68 時,這兩個本來不應該發生沖突的非個順序的地址序列

(相當於順序查找
),而應使探查序列跳躍式地散列在整個散列表中。查法的探查序列是:的缺陷是不易探查到整個散列空間。


shing)
最好的方法之一,它的探查序列是:
=h(key)(d+h1(key))m(d+2h1(key))m,…,等。用了兩個散列函數
h(key)h1(key),故也稱為雙散列函數探查法。互素,因此,我們可以簡單地將它定義為:,我們可取


h(key)=key13,而 h1(key)=key11+1。前
5 個關鍵字插入時,其相應的地址均為開放地址,故將它們直接插,當插入第

6 個關鍵同義詞
)占用。故探查 h1=(2+1)13=3,此地址開放,所以將 15 放入 T[3]中。當插入第
7 個關鍵字 68 時,其散列地址 3 已被非同義詞 15 先占用,故將其插入到
T[4]中。當插入第
8 個關鍵字 12 時,散列地址 12 已被同義詞 38 占用,故探查 hl=(12+1)
13=0,而 T[0]亦被 26 占用,再探查 h2=(12+2
類似地,第 9 個關鍵字 06 直接插入 T[6]中;而最后一個關鍵字 51 插人時,因探查的地址
1201,…, 6 均非空,故 51 插入 T[7]中。構造散列表的具體過程【參見

//student.zjzk.cn/course_ware/data_structure/web/flashhtml/kaifang.htm】聚用線性探查法解決沖突時,當表中


ii+1,…, i+k+1 的結點都將個后繼散列地址的現象稱為聚集或堆積一個探查序列之中,從而增加了探查序列的長度,即增裝填因子過大,都會使堆積現象加劇。



41 的沖突時, 15 搶先占用了 T[3]
同義詞之間也會發生沖突。為了減少堆積的發生,不能像線性探查法那樣探查一②二次探查法

(QuadraticProbing)
二次探
hi=(h(key)+i*i)m0im-1//di=i2
即探查序列為 d=h(key)d+12d+22,…,等。該方法③雙重散列法

(DoubleHa
該方法是開放定址法中
hi=(h(key)+i*h1(key))m0im-1//di=i*h1(key)
即探查序列為:
d
該方法使注意:定義

h1(key)的方法較多,但無論采用什么方法定義,都必須使 h1(key)的值和 m 互素,才能使發生沖突的同義詞地址均勻地分布在整個表中,否則可能造成同義詞地址的循環計算。【例】若


m 為素數,則 h1(key)1 m-1 之間的任何數均與 mh1(key)=key
(m-2)+1
【例】對例 9.1
【例】若 m 2 的方冪,則 h1(key)可取 1 m-1 之間的任何奇數。
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 10 -

T[i]為頭指針的單鏈表中。 T 中各分量的初值均應為空指針。在拉鏈法中,裝填因子α可以大於
1,但一般均取α≤1。例
9.2】已知一組關鍵字和選定的散列函數和例 9.1 相同,用拉鏈法解決沖突構造這組關鍵字的散列表。解答:不妨和例

9.1 類似,取表長為 13,故散列函數為 h(key)=key13,散列表為
T[0..12]。注意:當把

h(key)=i 的關鍵字插入第 i 個單鏈表時,既可插入在鏈表的頭上,也可以插在鏈表的尾上。這是因為必須確定
key 不在第 i 個鏈表時,才能將它插入表中,所以也就知道鏈尾結點的地址。若采用將新關鍵字插入鏈尾的方式,依次把給定的這組關鍵字插入表中,則所得到的散列表如下圖所示。具體構造過程【參見動畫演示



http://student.zjzk.cn/course_ware/data_structure/web/flashhtml/llf.htm】。
2、 拉鏈(
1)拉鏈法解決沖突的方法拉鏈法解決沖突的做法是:將所有關鍵字為同義詞的結點鏈接在同一個單鏈表中。若選定的散列表長度為

m,則可將散列表定義為一個由 m 個頭指針組成的指針數組 T[0..m-1]
。凡是散列地址為 i 的結點, 均插入到以【拉鏈法構造散列表示例(


2)拉鏈法的優點與開放定址法相比,拉鏈法有如下幾個優點:

(1)拉鏈法處理沖突簡單,且無堆積現象, 即非同義詞決不會發生沖突, 因此平均查找長度較短;各鏈表上的結點空間是動態申請的,故它更適合於造表前無法確定表, 拉鏈法中增加的指針域可忽略不計, 因此節省空間;





(2)由於拉鏈法中長的情況;

(3)開放定址法為減少沖突,要求裝填因子α較小,故當結點規模較大時會浪費很多空間。而拉鏈法中可取α≥
1, 且結點較大時
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 11 -
的散列表中,刪除結點的操作易於實現。只要簡單地刪去鏈表上相應的結點即可。而對開放地址法構造的散列表,刪除結點不能簡單地將被刪結點的空間置件。因此在用開放地址法處理沖突的散列表上執行刪除操作,只能在被刪結點上做刪除標記,而不能真正刪除結點。定址法較為節省空間,而若將節省的指針空間用來擴大散列表的規模,可使裝填因子變小,這又減少了開放定址法中的沖突,從而提高平均查找速度。







1.1.
列表上的運算有查找、插入和刪除。其中主要是查找, 這是因為散列表的目的主要是用於快速查找,且插入和刪除均要用到查找操作。

pedef struct{ //散列表結點類型
eyType key;建表時設定的散列函數

h,計算出散列地址與給

(查找失敗)或者關鍵字比較相等(查找成功)為止。開放地址法一般形式的函數表示


hi0im-1
是散列函數。 Increment

di
性探查的開放定址法處理沖突,則上述函求

KK
% ;
i(4)
在用拉鏈法構造為空,否則將截斷在它之后填人散列表的同義詞結點的查找路徑。這是因為各種開放地址法中,空地址單元

(即開放地址)都是查找失敗的條(
3)拉鏈法的缺點拉鏈法的缺點是:指針需要額外的空間,故當結點規模較小時,開放

4 散列表上的運算
1
、散列表類型說明:
#define NIL -1 //空結點標記依賴於關鍵字類型,本節假定關鍵字均為非負整數
#define M 997 //表長度依賴於應用,但一般應根據。確定 m 為一素數
tyKInfoType otherinfo

//此類依賴於應用
}NodeType
typedef NodeT pe HashTable[m y ]//散列表類型
2、基於開放地址法的查找算法散列表的查找過程和建表過程相似。假設給定的值為
K,根據
h(K),若表中該地址單元為空,則查找失敗;否則將該地址中的結點定值
K 比較。若相等則查找成功,否則按建表時設定的處理沖突的方法找下一個地址。如此反復下去,直到某個地址單元為空(

1
int Hash(KeyType kint i){ //
求在散列表 T[0..m-1]中第 i 次探查的散列地址
//下面的 h 是求增量序列的函數,它依賴於解決沖突的方
return(h(K)+Increment(i))m//Increment(i)相當於是
}
若散列函數用除余法構造,並假設使用線數中的
h(K)Increment(i)可定義為:
int h(KeyType K){ //用除余法 的散列地址
return m}int Increment(int i){//

用線性探查法求第 i 個增量 d
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 12 -
hTable TKeyType Kint *pos){ //
在散列表 T[0..m-1]中查找 K,成功時返回 1。失敗有兩種情況:找到一個開放地址

*pos=Hash(Ki)//求探查地址 hi
查找成功返回開放定址法,只要給出函數
Hash 中的散列函數 h(K)和增量函數
Incre 寫入算法
H
插入及建表中各結點的關鍵字清空,使其地址為開放的;然后調用插入算法將給定找算法, 若在表中找到待插入的關鍵字或表已滿,則插入失敗;若在表中找到一個開放地址,則將待插入的結點插入其中,即插入成功。




ashTable TNodeTypene w){ //
將新結點 new 插入散列表 T[0..m-1]
T 中查找 new 的插入位置
posw!")
//表滿錯誤,終止程序執行
eateHashTable(HashTable TNodeType A[]int n)freturn i; //

若用二次探查法,則返回 i*i}

2)通用的開放定址法的散列表查找算法:
int HashSearch(Has//
時返回 0,表滿未找到時返回-1*pos 記錄找到 K 或找到空結點時表中的位置
int i=0//記錄探查次數
do{if(T[*pos].key==K) return l
//if(T[*pos].key==NIL) return 0
//查找到空結點返回
}while(++i<m) //最多做 m 次探查
return -1//表滿且未找到時,查找失敗
} //HashSearch
注意:上述算法適用於任何

ment(i)即可。但要提高查找效率時,可將確定的散列函數和求增量的方法直接
ashSearch 中,相應的算法【參見習題】。
3、基於開放地址法的建表時首先要將表的關鍵字序列依次插入表中。插入算法首先調用查



void Hashlnsert(Hint pos
sign
sign=HashSearch(Tnew.key&pos)//在表
if(!sign) //找到一個開放的地址
T[pos]=new//插入新結點 new,插入成功
else //插人失敗
if(sign>0)printf("duplicate key!")
//重復的關鍵字
else //sign<0Error("hashtableoverflo} //Hashlnsertvoid Cr{ //



根據 A[0..n-1]中結點建立散列表 T[0..m-1]int iif(n>m) //

用開放定址法處理沖突時,裝填因子α須不大於 1Error("Load actor>1")

for(i=0;i<mi++)T[i].key=NIL
//將各關鍵字清空,使地址 i 為開放地址
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 13 -
i=0;i<ni++) //依次將 A[0..n-1]插入到散列表 T[0..m-1]
} //CreateHashTable
其探查到 DELETED 標記時,將相應的表單元視為一個空單元,將新結點插入其中。這樣做無疑增加了時間開銷,並且查找時間不再依賴於裝填因子。刪除結點的操作時,一般是用拉鏈法來解決沖突。找操作的時間性能。關鍵字的比較就可找到待查關鍵字。但是由於沖突的存在, 散列表的查找過程仍是一個和關鍵字比較的過程 的查找要小得多。找成功的平均查找長度分別為:








1)/10=1.4 //拉鏈法
ASL=(1× l+2× 2+3× 4+4× 3)/10=2.9 //二分查找,可由判定樹求出該值找不成功的
ASL
。因此,在等概率情況下,也可將散列表在查找不成功時的平均查找長度,定義為查找不成功時對關鍵字需要執行的平均比較次性探查法和拉鏈散列表的平均查找長度不是結點個數




n 的函數,而是裝填因子α的函數。因此在設計散列表時可選擇α以控制散列表的平均查找長度。③ α的取值


for(Hashlnsert(T
A[i])
4、刪除基於開放定址法的散列表不宜執行散列表的刪除操作。若必須在散列表中刪除結點,則不能將被刪結點的關鍵字置為

NIL,而應該將其置為特定的標記 DELETED。因此須對查找操作做相應的修改,使之探查到此標記時繼續探查下去。同時也要修改插人操作,使因此,當必須對散列表做注意:用拉鏈法處理沖突時的有關散列表上的算法【參見練習】。





5、性能分析插入和刪除的時間均取決於查找,故下面只分析查雖然散列表在關鍵字和存儲位置之間建立了對應關系,理想情況是無須,不過散列表的平均查找長度比順序查找、二分查找等完全依賴於關鍵字比較(



1)查找成功的 ASL
散列表上的查找優於順序查找和二分查找。【例】在例
9.1 和例 9.2 的散列表中,在結點的查找概率相等的假設下,線性探查法和拉鏈法查

ASL=(1× 6+2× 2+3× l+9× 1)/10=2.2 //線性探查法
ASL=(1× 7+2× 2+3×而當
n=10 時,順序查找和二分查找的平均查找長度(成功時)分別為:
ASL=(10+1)/2=5.5 //順序查找(
2) 查對於不成功的查找,順序查找和二分查找所需進行的關鍵字比較次數僅取決於表長,而散列查找所需進行的關鍵字比較次數和待查結點有關數。【例】例



9.1 和例 9.2 的散列表中,在等概率情況下,查找不成功時的線法的平均查找長度分別為:

ASLunsucc=(9+8+7+6+5+4+3+2+1+1+2+1+10)/13=59/134.54ASLunsucc=(1+0+2+1+0+1+1+0+0+0+1+0+3)/13
10/130.77
注意:①由同一個散列函數、不同的解決沖突方法構造的散列表,其平均查找長度是不相同的。②



哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 14 -
的機會就小,但α過小,空間的浪費就過多。只要α選擇合適,散列表上的平均查找長度就是一個常數,即散列表上查找的平均時間為
O(1)。有
"=""<"">"三種可能,且每次比較后均能縮小下次的查找范圍,故查找速度更快,其平均時間為
O(lgn)。而散列法是

1.1.
)之間的隨機整數(
N 對於其中重復的數字,只保留一個,把其余相同的數去掉。然后再把這些數從小到大排序。請你完成“去重”與“排序”的工作。

1 行為 1 個正整數,表示所生成的隨機數的個數:的正整數,為所產生的隨機數。

300 400 15400

很直白。數據是隨機給出的,散列函數使用取余法,可以認為數據是平均分布的,沖突問題使用拉鏈法解決。當然也可以直接使用
C++ STLset容器,時間復雜度是
N 間能夠承受。
>ostream>gorithm>e std;t




α越小,產生沖突④ 散列法與其他查找方法的區別除散列法外,其他查找方法有共同特征為:均是建立在比較關鍵字的基礎上。其中順序查找是對無序集合的查找,每次關鍵字的比較結果為


"=""!="兩種可能,其平均時間為
O(n);其余的查找均是對有序集合的查找,每次關鍵字的比較根據關鍵字直接求出地址的查找方法,其查找的期望時間為
O(1)
5 散列表的應用
1.1.5.1 Hrbustoj 1287 數字去重和排序 II
用計算機隨機生成了 N 0 1000000000(包含 0 1000000000
5000000),輸入有
2 行,第
N
2 行有 N 個用空格隔開輸出也是
2 行,第 1 行為 1 個正整數 M,表示不相同的隨機數的個數。第 2 行為 M
個用空格隔開的正整數,為從小到大排好序的不相同的隨機數。
Sample Input1020 40 32 67 40 20 89Sample Output815 20 32 40 67 89 300





思路: 題意
log N 2 ,題目所給時
C++代碼:
#include <stdio.h>#include <string.h#include <i#include <alusing namespacconst int MP = 1007;struct Node {in d;Node* next;};Node* pnd[MP+1];Node nd[MP+1];int n_cnt;int a[1000+7+10];int a_cnt;int main(){
















哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 15 -
{ems eof(pnd));nd[n_cnt].d = d;= pnd[p];pnd[p] = &nd[n_cnt];= d;





n1000
)對於每組測試數據,輸出組成的正方形數量。組成正方形肯定超時的,不可取。兩個點,若存在,說明有一個正方形。的坐標從小到大排序,



x 優先,之后是 y,這一步只是從插入順序上優化一下之后的哈希查找,哈希函數使用取余法,把
x+y 和除 MOD 取余。的兩點坐標,然后計算出另外兩點。證明就不具體展開了,可以參考下圖,已知兩個點,然后做出兩個全等三角形。之后就得出結論

(x1+|y1-y2|, y1+|x1-x2|),(x2+|y1-
。當然這只是一種情況,其他情況類似。
int n, d, p;while (EOF != scanf("%d", &n))m et(pnd, 0, sizn_cnt = 0;a_cnt = 0;for (int i = 0; i < n; ++i) {scanf("%d", &d);p = d % MP;bool found = false;Node *pt = pnd[p];while (pt) {if (pt->d == d) {found = true;break;}pt = pt->next;}if (!found) {nd[n_cnt].nextn_cnt++;a[a_cnt++]}}sort(a, a+a_cnt);printf("%d\n%d", a_cnt, a[0]);for (int i = 1; i < a_cnt; ++i) {printf(" %d", a[i]);}printf("\n");}return 0;}































1.1.5.2 POJ 2002 Squares
題意:在平面內給出 n 個點,問你這些點一共能組成幾個不相等的正方形?輸入有多組測試數據,每組測試數據的第一行是一個整數
n,表示 n 個點,接下來行,每行兩個整數,表示一個坐標點,這
n 個點都不相同。( 1<=n<=
思路:直接枚舉四個點判斷能否普遍的做法是先枚舉兩個點,通過數學公式得到另外
2 個點,使得這四個點能夠成正方形。然后檢查散點集中是否存在計算出來的那先按他們枚舉正方形最左邊的



y2|,y2+|x1-x2|)
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 16 -
沖突的解決方法使用拉鏈法。在枚舉和統計的過程中, 會重復統計。 枚舉方式的不同, 統計結果也不一樣, 下面代碼的


b)b.y;tp.x + tp.y) % M;}bool hash_search(const Point &tp){ode(tp);return false;v{y = hashcode(p[i]);next[i] = hash[key];











枚舉方式使得需要將統計的結果除以 2。代碼:

#include <stdio.h>#include <string.h>#include <iostream>#include <algorithm>using namespace std;const int M = 1031;struct Point{int x, y;};Point p[1024];int n;int hash[M+8], next[1024];bool cmp(const Point& a, const Point&{if (a.x != b.x)return a.x < b.x;return a.y <}int hashcode(const Point &tp){return abs(int key = hashcint i = hash[key];while (i != -1){if (tp.x == p[i].x && tp.y == p[i].y)return true;i = next[i];}}oid insert_hash(int i)int ke






























哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 17 -
;(1 == scanf("%d", &n) && n){-1, sizeof(hash));sizeof(next));i < n; i++){i].x, &p[i].y);p+n, cmp);t i = 0; i < n ; i++) ///






排完后進行插入
_hash(i);ans = 0;i < n; i++){i+1; j < n; j++){nt dx = p[j].x - p[i].x;nt dy = p[j].y - p[i].y;p3.x = p[i].x + dy;p3.y = p[i].y - dx;if (hash_search(p3)){p4.x = p[j].x + dy;p4.y = p[j].y - dx;)











1.1
3+a3*x33+a4*x43+a5*x53=0x1x2x3x4x5 都就在區間 的整數,且
x1x2x3x4x5 都不等於 0。問:給定a1,a2,a3,a4,a5 的情況

5 個整數,表示 a1a2a3a4
a5。大,大概
1005次,肯定會TLE。可以將式子變化一下:
a1* 3+a5*x53),先把所有a1*x13+a2*x23 結果算出來,放進哈希表里
*x43+a5*x53),枚舉x3x4x5,然后再哈希表里查找枚舉時計算的結果即可。

const int maxn = 5;const int prime = 1280519;s }Hash* hash[prime+1];int c[maxn];hash[key] = i;}int main(){Point p3, p4;int dx, dy, answhilememset(hash,memset(next, -1,for (int i = 0;scanf("%d %d", &p[}sort(p,for (ininsertfor (int i = 0;for (int j =iiif (hash_search(p4)ans++;}}}printf("%d\n", ans/2);}return 0;}
































.5.3 POJ 1840 Eqs
有以下等式: a1*x13+a2*x2[-50
50]之間下,
x1,x2,x3,x4,x5 共有多少種可能的取值?輸入有多組測試數據,每組測試數據有一行,包含對於每組測試數據輸出一行,表示解的數量。思路:直接枚舉



x1 x5 量會非常
x13+a2*x23 = -(a3*x33+a4*x4
,然后再根據-(a3*x33+a4
代碼:
#include <stdio.h>#include <string.h>truct Hash{int v;Hash* next;;





哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 18 -
int t, p;){f(hash));-50; x[0] <= 50; x[0]++) if (x[0])for (x[1] = -50; x[1] <= 50; x[1]++) if (x[1]){[1]*x[1]*c[1]);p ;phph->next = hash[p];ans = 0;2] <= 50; x[2]++)if (x[2])3]++)if (x[3])4]++)if (x[4]){x[4]*c[4];];}}"%d\n", ans);0;




















OJ 2503 Babe
請參閱此文章:《各種字符串Hash函數比較》 http://www.byvoid.com/blog/string-hashint x[maxn];int ans;int main(){Hash * ph;for (int i = 0; i < maxn; i++scanf("%d", c+i);}memset(hash, 0, sizeofor (x[0] =t = -(x[0]*x[0]*x[0]*c[0] + x[1]*x= (t>0?t:-t) % primeph = new Hash;->v = t;hash[p] = ph;}for (x[2] = -50; x[for (x[3] = -50; x[3] <= 50; x[for (x[4] = -50; x[4] <= 50; x[t = x[2]*x[2]*x[2]*c[2] + x[3]*x[3]*x[3]*c[3] + x[4]*x[4]*p = (t>0?t:-t) % prime;ph = hash[pwhile (ph){if (ph->v == t)++ans;ph = ph->next;printf(return}




























其他題目:
POJ 3349 Snowflake Snow SnowflakesP lfishPOJ 3274 Gold Balanced Lineup


1.1.6 附:字符串哈希函數
compare/,主要是各種哈希函數的評測,下面是兩個字符串哈希函數,推薦BKDR哈希函數,

// ELF Hash Functionunsigned int ELFHash(char *str){unsigned int hash = 0;unsigned int x = 0;while (*str){hash = (hash << 4) + (*str++);if ((x = hash & 0xF0000000L) != 0){hash ^= (x >> 24);hash &= ~x;}}













哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 19 -
/unsigned int BKDRHash(char *str){int seed = 131; // 31 131 1313 13131 131313 etc..{hash = hash * seed + (*str++);}(hash & 0x7FFFFFFF);







http://hi.baidu.com/bobo__bai/item/fbf57d110b72650fb88a1a09
1.2.1 並查集基本原理
素構成一個單元素的 的集合合並,其間要反復查找一個元素在 復雜,但數據量極大,若用正常的數據結構來描述的話,往往在空間上過大,計算機無法承受;即使在空間上勉強通過,運行的時間復雜度也極高,根本就不可能在比賽規定的運行時間(



13 秒)內計算出試題需要的結果,只能采用一種全新的抽象的特殊數據結構——並查集來描述。初始化每個集合:

init ();初始時每個節點(元素)都是獨立的。查找這個節點所在集合:
find(v);我們用一個節點的標號表示這個節點處在哪個集合里,這個函數會返回
v 最上層的節點,也就是根。合並兩個不相交集合:
join(x, y) y 加到 x 集合里。判斷兩個元素是否屬於同一個集合:
is_same(x, y),如果 x y 在同一集合內,則返回真,否則為假。最后還會講到一個簡單高效的並查集優化。


fa 記錄每個節點的父節點, fa[i]i
時,節點 i 的父節點是他本身,那么 i 的所在的樹的根上就始化,

fa[i] = i,他們分別是自己的父點,現在他們是各自獨立的。
fa[1] = 1; fa[2] = 2; fa[3] = 3; fa[4] = 4; fa[5] = 5;
return (hash & 0x7FFFFFFF);}/ BKDR Hash Functionunsignedunsigned int hash = 0;while (*str)return}







1.2 並查集
參考文獻:《算法導論》編寫:黃李龍 校核:黃李龍在一些有


N 個元素的集合應用問題中,我們通常是在開始時讓每個元集合,然后按一定順序將屬於同一組的元素所在哪個集合中。這一類問題其特點是看似並不並查集是一種樹型的數據結構,用於處理一些不相交集合的合並問題。並查集的主要操作有:並查樣例以





5 個節點為例,圓圈內為節點的標號,我們用數組表示
i 節點的父節點標號,當 fa[i]=
i。初

哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 20 -
合並 1 2 時, 2 的父節點變為 1fa[2] = 1,此時有:果
1 節點所在樹的根節點是 12 所在的樹的根節點是

f ] = 1; fa[2] = 1; fa[3] = 3; fa[4] = 4; fa[5] = 5;
現在我們分別查找 1 2 的所在樹的根節點,
a[1

1,說明 1 2 是在同同一棵樹內,也就是說 1 2 是在同一集合內的。接下來我們合並
1 3,讓 fa[3] = 1,即使得 3 的父節點為 1,有:
fa[1] = 1; fa[2] = 1; fa[3] = 1; fa[4] = 4; fa[5] = 5;
很明顯能知道 1,2,3 的最頂層節點都是 1。我們再合並
4 5,讓 4 的父節點為 5,即 fa[4] = 5。最后, 我們合並
5 3,讓 5 的父節點為 3,即 fa[5] = 3。能得到下面的圖。我們根據
fa[i]的 以判定 2 5 是屬於同一棵樹內的,同一個集合的。現在你應該能寫出每個節點的
fa[i]值了。值, 能找到
4 所在樹的根節點是 12 的所在樹的根節點是 1, 則可那么現在開始寫代碼吧!我們先定義個常量,

MAX_SIZE,表示最后有多少個元素,用數字標號每個節點,在這里接下來我們事先查找一個節點所在樹的根節點的功能:我們的節點標號從


0 開始。
cons int MAX_SIZE = 100005;int fa[MAX_SIZE];

我們要有集合的初始化操作:
void init {int i;for (i = 0; i < MAX_SET_SIZE; ++i) fa [i] = i;}Ok



了,
int find(int v){

哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 21 -
(fa[v] == v) return v; // 如果 fa[v] = v,那么 v 就是樹的根節點了,返回 v
么簡單就 Ok 了!
[fx] = fy; // 我們把 y 的的根節點的父節點設為 x 的最上層節點就行了斷兩個節點在同一個集合,
Easy!(find (x) == find(y)); //
判斷它們的根節點是否一樣就行了
1.2.2 間復雜度分析和優化
t()操作循環 MAX_SIZE 次,時間復雜度 O(n)。操作,這個有些難確定,不過它的最壞情況還是能確定的,如果對於每個節點,都有 ,這棵樹在這種最壞情況下就退化成有

n 個元素,那么總的查找次數 :
1 + 2 + 3 + … + n,有(n+1)*n/2,時間復雜度 O(n*n),如果

n(x, y)操作,依賴於 find(v)操作,如果 find(v)快的話, join(x, y)也會很快。
find(v)join(x,y)操作 q 次,那么時間復雜度將達到
O(q* 常的慢壓縮

)函數的目的是查找到一個節點的根節點,那么找到一個節點
x = f,主要在再次查找 x 的父節點的時候,就能馬上找 既然這樣,我們就直接將查找路徑上所有節點的父節點都設為

f,這樣在查找這些節點的時候就能用兩次 find()函數調用就找到了節點的根節點,大大加快了速度。碼:


n v;fa [v]); //
路徑壓縮,直接賦值為找到的根節點用路徑壓縮后,每一次查詢所用的時間復雜度為增長極為緩慢的
ackerman 函數的反數是
Ackerman 函數的某個反函數,在很大的范圍內(人類目前觀

ifreturn find (fa [v]); //
上面一句沒成功,要找父節點的父節點
}
這還有一個合並集合的操作,我們把
y 並到 x 里:
void join(int x, int y){int fx = find(x), fy = find(y); //

先找到 x y 各自的根節點
if (fx != fy) { // 如果他們的根節點不一樣,就是不在同一集合內,可以合並了
fa}}



int is_same(int x, int y){return}



並查集的時
好了,我們來看看他們運行所需時間。
inifind()fa[i] = i – 1

fa[0] = 0
,也就是 find(v)調用次數為節點很多,運行時間就會很慢。

joi
一般來講, init()只操作一次,
n)如果 q*n 很大的話,程序會跑得非優化:路徑主要優化

find()函數。 find(
的根節點后 f,我們可以直接讓 fa[x]
x 節點的根節點。我們來實現代

int find(int v){if (fa[v] == v) returreturn fa[v] = find (}




采函數——α(
x)。這里α函測到的宇宙范圍估算有
10 80 次方個原子,這小於前面所說的范圍)這個函數的值
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 22 -
里就不作為重點寫出來了。

1.2.3
onst int MAX_SIZE = 100005;MAX_SET_SIZE; ++i) fa [i] = i;y);t x, int y)d(y);





1.2.4
分別是:
1.2.4 Hrbustoj 1073 病毒
種病毒襲擊了某地區,該地區有 N(1N50000)人,分別編號為 0,1,...,N-1,現在 0
友和間接朋友都要被隔離。例如: 0 1 是直接朋友, 1 2
你編程計算,有多少人要被隔離。第 ≤在接下來的

M 行中,每行表示一次接觸,;可以看成是不大於
4 的,所以並查集的操作可以看作是線性的。路徑壓縮可以寫成非遞歸形式,你可以自己想想怎么寫,這還有一個優化,就是根據樹的深度來合並集合,具體怎么實現,請你自己上網查一下,相信你能完成它。



並查集樣例代碼
c
據結構:
int father[MAX_SET_SIZE];
初始化
void init {int i;for (i = 0; i <}



查找
int find(int v){if (fa[v] == v) return v;return fa[v] = find (fa [v]);}




判斷是否在同一集合內
int is_same(int x, int{return (find (x) == find(y))}



並查集合並
void join(in{int fx = find(x), fy = finif (fx != fy)fa[fx] = fy;}





例題講解
下面講解三道樣例題目,
Hrbustoj 1073 病毒
POJ 2492 A Bug's LifePOJ 1182
食物鏈
.1
某號已被確診,所有
0 的直接朋是直接朋友,則
02 就是間接朋友,那么 012 都須被隔離。現在,已查明有 M(1M
10000)個直接朋友關系。如: 0,2 就表示 0,2 是直接朋友關系。請 輸入數據的 一行包含兩個正整數

N(1N50000),M(1 M100000),分別表示人數和接觸關系數量;

哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 23 -
有多組測試數據。含一個整數,為共需隔離的人數(包含
0 號在內)。路:有過直接或者間接接觸的都是需要隔離的人,簡單的並查集應用。:



lude <stdio.h>t int MAXN = 50005;f[MAXN];find(int x);oid union_set(int x, int y)if (px != py) f[px] = py;nt, f0, n, m, x, y;







1.2.4 's Life
聊的科學家說只有兩個不同性別的昆蟲能在一起,當然是在沒有同性戀的情況下。給你幾對能在一起的昆蟲,問里面有沒有同性戀,也就是交配是否有沖突。入一個數


t,表示測試組數。然后每組第一行兩個數字 n,mn 表示有 n 只昆蟲,編號 面要輸入
m 行交配情況,每行兩個整數,表示這兩個編號的昆蟲為異性 要 統計交配過程中是否出現沖突,即是否有兩個同性的昆蟲發生交配。每行包括兩個整數


U, V(0 <= U, V < N)表示一個直接朋友關系。注意輸出數據僅包思 與



0
代碼
#incconsintint{if (x != f[x]) f[x] = find(f[x]);return f[x]}v{int px = find(x);int py = find(y);}int main(){int cwhile (EOF != scanf("%d %d", &n, &m)) {for (int i = 0; i < MAXN; i++) f[i] = i;for (int i = 0; i < m; i++) {scanf("%d %d", &x, &y);union_set(x, y);}cnt = 1;f0 = find(0);for (int i = 1; i < n; i++) {if (f0 == find(i)) cnt++;}printf("%d\n", cnt);}return 0;}






























.2 POJ 2492 A Bug
題目大意:一個無輸 從

1n,m 表示下, 進行交配。 要求

Sample Input23 31 2



哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 24 -
332ut

路昆蟲之間的“偏移”關系,如果偏移次數是偶數次,說明是同一個性別的,奇數次則說明是異性。怎樣實現呢?節點的偏移次數,初始時


rel[i]=0。考慮第一對昆蟲
a 和 性別是不一樣的,如果把 a 作為根,那么 rel[b] = 1,這樣 b a 的偏移 是
l
同蟲的
rel[a]+rel[b]是偶數則兩只昆蟲是同性,說明找到了一對將要交配的同性昆蟲,否則什么也不做。偏移關系的維護,注意看代碼中的寫法。


h>5;N];= 0;fa find(a), fb = find(b);(fa != fb) {f[fb] = fa;for (int cs = 1; cs <= caseN; cs++) {







2141 23 4Sample OutpScenario #1:Suspicious bugs found!Scenario #2:No suspicious bugs found!









思 :昆蟲只有公和母兩種情況,可以考慮用一個

rel[i]數組們的表示

i 昆蟲相對根
b 的關系,他就
re [b] + rel[a],結果是奇數。在每次加入一個可以交配的昆蟲 a b 時,先判斷他們是否在 一集合內,不在則表示兩只昆蟲是可交配的,在同一個集合則要判斷兩只昆偏移關系

rel[a]rel[b],如果此題關鍵是對昆蟲

#include <stdio.const int MAXN = 200int f[MAXN], rel[MAXvoid init(int n){for (int i = 0; i <= n; i++) {f[i] = i;rel[i]}}int find(int x){if (x != f[x]) {int t = f[x];f[x] = find(f[x]);rel[x] = (rel[x] + rel[t]) % 2;}return f[x];}void union_set(int a, int b) // a, b are different gender{int =ifrel[fb] = (rel[a] - rel[b] + 1 + 2) % 2;}}int main(){int caseN;scanf("%d", &caseN);





























哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 25 -
int n, m;bool found = false;scanf("%d %d", &n, &m);nt i = 0, a, b; i < m; i++) {anf("%d %d", &a, &b);int ta = find(a), tb = find(b);if (ta != tb) {union_set(a, b);} else if (rel[a] == rel[b]) {found = true;}}printf("Scenario #%d:\n", cs);printf("%s\n", found ? "Suspicious bugs found!" : "No suspicious bugsfounif (cs < caseN) {ntf("\n");}return 0;


















1.2.4
B,C,這三類動物的食物鏈構成了有趣的環形。 A
B,說

K 行每行是三個正整數 DXY,兩數之間用一個空格隔開,其中 D 表示說法的種

=1,則表示 X Y 是同類。
Y。有一個整數,表示假話的數目。

init(n);for (iscd!");pri}}






.3 POJ 1182 食物鏈
題意:動物王國中有三類動物 A,B
CC A。現有
N 個動物,以 1N 編號。每個動物都是 A,B,C 中的一種,但是我們並不知道它到底是哪一種。有人用兩種說法對這

N 個動物所構成的食物鏈關系進行描述:第一種 法是
"1 X Y",表示 X Y 是同類。第二種說法是
"2 X Y",表示 X Y。此人對
N 個動物,用上述兩種說法,一句接一句地說出 K 句話,這 K 句話有的是真的,有的是假的。當一句話滿足下列三條之一時,這句話就是假話,否則就是真話。

1) 當前的話與前面的某些真的話沖突,就是假話;
2) 當前的話中 X Y N 大,就是假話;
3) 當前的話表示 X X,就是假話。你的任務是根據給定的
N1 <= N <= 50,000)和 K 句話( 0 <= K <= 100,000),輸出假話的總數。

Input
第一行是兩個整數 N K,以一個空格分隔。以下類。若


D
D=2,則表示 X
Output

Sample Input100 71 101 12 1 22 2 3




哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 26 -
mple Output
每句話判斷真假,同時記錄由真話構成的食物鏈的結構,的判斷上。系, 而非它們是什么物種。如果我們把相互間可以確際上 多個集合, 同時會出現需要將兩個集合合並的情況




(例如
a 屬 於集合 B,當 ab 的關系確定時,集合 AB 就需要合並為一個集合
),由這種動態處理集合的情況不難聯想到並查集。食結構。 當
f[a]=a 時,表示 a 是本集合的根結點, f[a]=b
表示 ab 間的捕食關系只要找到 ab 的根結點即 結點則可通過邏輯判斷確定
ab 關系, ab 有不同根節點,說明
ab 。 這樣我們還需要一個和 f[n]對應的存儲結構 vec[n]用以存儲 f[a]與其父結點的捕食關系。由於關系都是相對的,而且有向的,我們可以以向量的思想來確定。

=0 表示 a 結點與其父結點是同類, vec[a]=-1 表示 a 捕食其父結點,
vec[a getf(a)來得到 a 的根結點,構造 getv(a)來得到 的關系向量。

2 3 31 1 32 3 11 5 5Sa3





思路: 這一題的基本思路是對進行統計, 最后輸出統計結果。關鍵是對每句話可以看出


3 個物種之間的關系是相對的, 對稱的, 所以要記錄的是動物間的關定捕食關系的動物放在一個集合中,那么實在處理過程中我們可能會得到於集合


Ab 屬利用並查集來存儲已知的捕

ab 間有已經確定的捕食關系。 這樣想知道可,
ab 有相同根關系尚未確定我們以

vec[a]]=1
表示 a 被其父結點捕食。首先構造函數
a 與其根結點當得到輸入
d a b 時, 若 ab 有相同根結點,則如上圖,不難有 d-1==getv(b)-getv(a)時是真話

b 有不同根結點時,這句話為真,可以確定兩根結點的關系, a b 的關系向量參考代碼:代碼中用

pa 數組表示每個節點的父節點, ch 數組表示節點與它的父節點的關系,即向量的關系。同樣,

a,為
:getv(b)-(d-1)-getv(a);

以上就是以向量和並查集來考慮本題的思路。參考:
http://hi.baidu.com/bobo__bai/item/fbf57d110b72650fb88a1a09
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 27 -
>}int find_set(int x){if (pa[x] == x)return x;int xt = find_set(pa[x]);ch[x] = (ch[x] + ch[pa[x]] + 3) % 3;pa[x] = xt;return xt;}void union_set(int d, int x, int y) // d,










表示 x y 的關系, 0, 同類, 1x y{if (x > n || y > n ) {++lies;return;}int fx = find_set(x);int fy = find_set(y);if (fx == fy) {if ((ch[x] - ch[y] + 3) % 3 != d) {++lies;}} else {ch[fx] = (d + ch[y] - ch[x] + 6) % 3;pa[fx] = fy;}}int main(){int i, d, x, y;scanf("%d %d\n", &n, &k);init_set();for (i = 0; i < k; i++) {scanf("%d %d %d\n", &d, &x, &y);union_set(d - 1, x, y);}

























3_Find them, Catch them
1.3 叉堆
#include <stdio.h>#include <string.hconst int MAXN = 50005;int pa[MAXN], ch[MAXN];int n, k, lies;void init_set(){int i;for (i = 0; i < MAXN; i++) pa[i] = i;memset(ch, 0, sizeof(int)*MAXN);lies = 0;printf("%d", lies);return 0;}













其它並查集題目:
HDU 1213_How Many TablesPOJ 170Hrbustoj 1418

夏夜星空
POJ 1988 Cube Stacking

參考文獻:《算法導論》第
6 章 堆排序
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 28 -
二叉堆: http://www.nocow.cn/index.php/%E4%BA%8C%E5%8F%89%E5%A0%86
編寫:黃李龍 校核:黃李龍
1.3.
近似完全二叉樹。二叉堆滿足堆特性:父結點的鍵值總是大於或等於(小於或等於)任何一個子節點的鍵值,且每個結點的左子樹和右子樹都是一個二叉堆(都是最大堆或最小堆)。父 值總是大於或等於任何一個子節點的鍵值時為最大堆。 當父結點的鍵值總是小於或等於任何一個子節點的鍵值時為最小堆。




1 二叉堆的概念
二叉堆(Binary Heap)是一種特殊的堆,二叉堆是完全二叉樹或者是當 結點的鍵

(:一般把二叉堆簡稱為堆)
預備知識: 我們將一棵二叉樹從上到下, 從左到右編號,我們可以發現,第 i 個節點的兩 兒 的 ,
2*i+1。其 是 , 當且僅當它滿足以下兩個條件之一時, 才能稱之為堆:

1)a i*2]a[i]<=a[i*2+1](即小根堆,節點的值不大於兩個兒子的值)a[i]>=a[i*2+1](
即大根堆,節點的值不小於兩個兒子的值)
意:,類似地可定義
k 叉堆。
1.3 基本操作
個 子 編號分別為 2*i
堆 實 一顆完全二叉樹
( [i]<=a[(2)a[i]>=a[i*2]
且注①堆中任一子樹亦是堆。②以上討論的堆實際上是二叉堆


(Binary Heap)
.2 二叉堆的
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 29 -
組表示,下標從 1 開始計算,如果是 C\C++語言,則忽略下標為 0 的元素的兩個基本操作基本操作的實現:



a[i/2])比較,如果比父結點大(大

h_heap(int a[], int n) {int i = n;[n];> 1 && a[i/2] < x) {j]



改為 a[i]<a[j] )
下面的尾結點(堆數組最后一個)交換,然后刪除堆尾結點,(小根堆)再與節點進行比較大小,交換,更換節點編號,如此重復,直到滿足堆的性質。





C
把根為 m 的非大根堆調整成大根堆,前提條件是這個非大根堆的子堆,也就是根的兒子必須是大根堆。

id int n, int m) { // n 是數組下標范圍, m 是 需要調整的根的下標
tilm = m * 2;}a[m/2]) {m];a[m] = a[m/2];}







數據結構表示:使用一個一維數。使用

n 表示隊中元素的個數。堆堆關鍵是理解兩個


1、向堆插入一個結點(上升操作):在堆尾(
a[n])插入一個元素,然后不斷和父結點(根堆
])或小(小根堆)就交換,一直到堆頂或不再交換就結束。
C 語言代碼:
void pusint x = awhile (ia[i] = a[i/2];i /= 2;}a[i] = x;}(







以上代碼是大根堆的上升操作,小根堆只需將 a[i]>a[ 即可具體使用時,先在一維數組后面加入元素,然后調用
push_heap 函數進 整。如 行調代碼:

++n;a[n] = x;push_heap(a, n);2


、刪除結點:刪除堆頂結點
(下降操作):將堆頂結點(堆數組第一個) 和堆將交換后的節點和左右兒子比較大小,然后選擇兩個兒子較大者(大根堆)或較小者語言代碼:先給一個函數,函數功能是




vo heap(int a[],in t;wh e(m*2 <= n) {if (m < n && a[m] < a[m+1]) {m++;if (a[m] >t = a[a[m/2] = t;}elsebreak;}











下面的代碼是把一個堆的堆頂去掉,並調整堆。
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 30 -
void pop_heap(int a[], int n) {intn, 1);


的下降操作,小根堆只需將所有的‘ >'換為'<'即可)
的復雜度都是O(log2N)。:內容進行介紹,具體代碼請讀者自己實現,代碼不難,可以利用刪除堆頂 函數進行調整。內結點,則需先判斷是需要下降還是需要上升。替換需要刪除的節點,首先嘗試下降操作,如果無法下降,則嘗試上升






1.3
根堆)堆頂記錄的關鍵字最大(或最小)這一特征,使得在當前無序 鍵字的記錄變得簡單。

n]建成一個大根堆,此堆為初始的無序區字最大的記錄
a[1](即堆頂)和無序區的最后一個記錄 a[n]交換,由此得到新的無 序區
a[n],且滿足 a[1..n-1] a[n]
於交換后新的根 a[1]可能違反堆性質,故應將當前無序區 a[1..n-1]調整為堆。
a[1..n-1]中關鍵字最大的記錄 a[1]和該區間的最后一個記錄 a[n-1]交換,由此得到新的無序區
a[1..n-2]和有序區 a[n-1..n],且仍滿足關系 a[1..n-2]R[n-1..n],同樣要將復 只有一個元素為止,此時

a[1..n]就是一個有序的數組了。語


首先建堆,從最底層的元素開始建堆,一直遞推到堆頂,即下標為 1 的數據元素。
id ake int n) {tor i > 0; --i) heap(a, n, i);


堆排序
id ea ], int n) {i imake_heap(a, n); //

先把無序的元素建立成堆
or i = i) {eap );

1.3 經典題目
1.3.4
x;x = a[1];a[1] = a[n];a[n] = x;n--;heap(a,}






(以上代碼是大根堆不難得出,兩個操作刪除堆內結點該操作作為擴展元素時使用的



heap
如果要刪除的結點是堆算法是:使用末尾操作。


.3 堆排序
堆排序利用了大根堆(或小區中選取最大
(或最小)關①先將初始數組
a[1..
②再將關鍵序區
a[1..n-1]和有③由然后再次將


a[1..n-2]調整為堆。重 的步驟,直到無序 ②③ 區

C 言代碼:
//vo m _heap(int a[],in i;f (i = n/2;}//vo h dp_sort(int a[nt ;f ( n; i > 1; --int t = a[1];a[1] = a[i];a[i] = t;h (a, n - i, 1}}














可以得出堆排的時間復雜度約為 O(n*logn)
.4
.1 poj 3253 Fence Repair
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 31 -
度最小的木板,然后連接成一根木板,放入剩余的木板集合中,然后重復上述過程,知道剩余木板集合中只剩下一根木板,每次連接木板的花費的和就是所求


20000+2;id> l[lr])x lgap_sort(i);ans = 0;return 0;






題意:給幾根木板,要你把他們連接起來,每一次連接的花費是他們的長度之和。問最少需要多少錢?輸入數據的第一行是模板個數

N(1 N 20000),接下來有 N 行,第 i+1 行一個整數
Li 1 Li 50000),表示第 i 根模木板。輸出最少的花費。思路:跟哈夫曼編碼的過程相似,是貪心題,只是哈夫曼編碼跟這個相似。每次只取兩根長答案。代碼如下:





#include <stdio.h>#include <string.h>const int maxn =int l[maxn];int n, h, min;long long ans;vo heap_sort(int x){int lg, lr, t;while ((x<<1) <= h){lg = x << 1;lr = (x<<1)+1;if (lr <= h && l[lg]lg = lr;if (l[x] > l[lg]){t = l[x];l[x] = l[lg];l[lg] = t;= ;}elsebreak;}}int main(){int i;while (1== scanf("%d", &n)){for (i = 1; i <= n; i++)scanf("%d", &l[i]);h = n;for (i = n/2; i > 0; i--)hewhile (h > 1){min = l[1];l[1] = l[h];h--;heap_sort(1);min += l[1];l[1] = min;heap_sort(1);ans += min;}printf("%I64d\n", ans); // POJ










































Windows 平台,輸出 64 位整數需要 I64d}

哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 32 -
}
1.3.4
題,輸出最小的
n個和。小
n 元素,存到 A 數組中, A 數組中的數都是 k-1 個數的和。然后利用第個 k 序列的數生成
k 個數和, B 數組也有 n 個。轉移時,用第 k 個序列中的每一項 li( 1
中的每個數求和,若和比 B 數組中的最大值小,則需要更新 B 數組中數組輪流使用

A B 數組。為了簡便,直接使用 C++ STL 庫里的

e std;005, MAXM = 105;) scanf("%d", a[0] + i);r = 1; h < m; ++h, q = 1-q) {i < n; ++i) a[1-q][i] = a[q][i] + t;i = 1; i < n; ++i) {nf("%d", &t);(int j = 0; j < n; ++j) {int t2 = a[q][j] + t;if (t2 < a[1-q][0]) {pop_heap(a[1-q], a[1-q] + n);a[1-q][n-1] = t2;a[1-q] + n);++i) {n", a[q][n-1]);turn 0;















1.3 niversity - Financial Aid
分數 score[i],和需要的資費 aid[i],求上述 C 頭牛的一個
N 元子集,使得其中位數最大,而資費總和<=f(特定的值)
score 從小到大排序。是可以枚舉
[N/2..C-N/2]之間的每頭牛為中位數點,那么要滿足題目條件則有:
i 點之前的(n/2)financial aid before[i]before[i]
.2 POJ 2442 Sequence
意:有m個序列,每個序列 n個非負數,從每個序列中選擇一個數組成一個序列並計算和,總共有
nm 個和思路:直接計算復雜度太高。故考慮類似動態規划的思想。先求出前
k-1 個序列的最個
B
數組, B 數組是
<= i <= n ) A 數組的最小的
n 個元素。程序實現時,使用滾動二叉堆操作函數。


C++代碼:
#include <cstdio>#include <algorithm>using namespacconst int MAXN = 2int a[2][MAXN];int main() {int runs;scanf("%d", &runs);while (runs--) {int n, m;scanf("%d%d", &m, &n);for (int i = 0; i < n; ++imake_heap(a[0], a[0] + n);int q = 0;f (int h oint t;scanf("%d", &t);for (int i = 0;for (intscaforpush_heap(a[1-q],}}}}sort(a[q], a[q] + n);for (int i = 0; i < n-1;printf("%d ", a[q][i]);}printf("%d\}re}

































.4.3 POJ 2010 Moo U
題意:給定 C 頭牛的 CSAT
思路:先將牛以於選擇第

i 個牛為中位數點,那么設
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 33 -
不含 點之后的(n/2)financial aid after[i],不含 i 號牛,則有 fa[i](i 點的
fina 足條件。,遇到的第一個結果就是答案,時間復 度

(er[]
呢?別 最大堆的元素的數目為
(n/2) 個,那么每次更新一頭牛 更新
before[]afeter[])值即可。 體 現時間來維護這個

before[i]/after[i]值,故總的復雜度為O(Nlog2N)
ns nttor < (const Node &B) const {s < B.s;t before[MAXN];iint val =ile (c *int lcif (lc < heap_size && p[lc+1] > p[lc]) {++lc;}if (p[lc] > val) {p[c] = p[lc];c = lc;void init_heap(int l, int r){p_sum = 0;(int i = l; i <= r; ++i) {heap[i - l] = node[i].f;_sum += node[i].f;}t *p = heap - 1;(f < p[1]) {heap_sum -= p[1] - f;
























i 號牛,設 incial aid
值) + before[i] + afeter[i] <= F 的時候,滿由於牛的序列是以
score 遞增的, 從大到小掃一遍雜
O n)。那么如何確定
before[]aft
分 維護一個大根堆,初始的時候
i 的時 ,若這個點的 候 fa[i] > 大根堆堆頂點的 fa,那么具 實 看代碼。於是只需要

log(n)
C++代碼:
#include <cstdio>#include <cstring>#include <iostream>#i u <a ncl de lgorithm>using namespace std;co t i t MAXN = 100000 + 100;struct Node {in s, f;bool operareturn}} node[MAXN];innt heap[MAXN];int heap_size, heap_sum;void adjust(int c){int *p = heap - 1;

















 p[c]; wh  2 <= heap_size) {= c * 2;
} else {break;}}p[c] = val;}heaforheapfor (int i = heap_size / 2; i > 0; --i) {adjust(i);}}void update_heap(int f){inif
















哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 34 -
p[1] = f;%d %d %d", &n, &c, &f)) {[i].s, &node[i].f);0, heap_size - 1);i = heap_size; i < c - heap_size; ++i) {(node[i].f);for (int i = c - heap_size - 1; i >= heap_size; --i) {int t = before[i] + heap_sum + node[i].f;t && t <= f) {update_heap(node[i].f);}d\n", ans);











1.3 ueue
戶來辦理業務。每個客戶用一個正整數 K 標識,因為銀行人手不多,客戶在辦理時需要等待其他用戶辦理。在客戶辦理業務前,銀行為用戶提 先級來確定先處理哪個用戶的業務,並且有時候是優先級高的先服務,有時是優先級的先服務。銀行系統將對客戶的服務分為以下幾種請求:



 0  務停止系統的服
 1 K P  加入 K 用戶的業務請求,優先級為 P 2  先服務一個最高優先級的客戶 3  先服務一個最低優先級的客戶adjust(1);}}int main(){int n, c, f;while (EOF != scanf("for (int i = 0; i < c; ++i) {scanf("%d %d", &node}sort(node, node + c);heap_size = n / 2;init_heap(for (intbefore[i] = heap_sum;update_heap}int ans = -1;init_heap(c - heap_size, c - 1);if (0 <=ans = node[i].s;break;}printf("%}return 0;}


























.4.4 POJ 3481 Double Q
題 :在一個銀行中,每天都會有 意 客供一個優先級
P,根據優輸入:請求,並且最后一行的請求為

0,表示停止服務。輸入保證有類型為 1
的請求,每個客戶的標識都不一樣,並且沒有兩個請求的優先級是一樣的。客戶標識小於
1000000 P 小於 10000000,一個客戶可以有多次請求,每次請求的優先級都不同。出服務的客戶標識。如果沒有客戶在請求,則輸出路級,則該題就是簡單的堆的應用。除了去最高優先級外,還有娶最低優先級的操作,因為每個請求的優先級都不一樣,我們可以用優先級標記每一個請求是否已經處理過,即已經從隊列中刪除,在每次取最高優先級或者最低優先級每一行為一個,優先級輸出:對於每個類型為










2 或者 3 的請求,輸
0。思 :如果僅考慮一個取得最高的優先


哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 35 -
時,要判斷這個優先級的請求是否已經處理過了,處理過了的話則繼續取下一個優先級的客戶。取到后標記對應優先級的請求為刪除狀態即可。


*/ncnc ing>c e <utility>lgorithm>pace std;int, int> PII;const int MAXN = 1000006;sint fix;};struct greaterKP {) (const KP &a, const KP &b) const {l > b.val;}};sval < b.val;}};ib_min;], kp_min[MAXN];template <class cmp>, a+sz, cmp());}_max = sz_min = 0;x_cnt = 0;}/** POJ 3481 Double Queue*






























個 用關聯映射之間的值 堆,
*#i lude <cstdio>#i lude <cstr#in lud#include <ausing namestypedef pair<truct KP {int val, id;bool operator (return a.vatruct lessKP {bool operator () (const KP &a, const KP &b) const {return a.nt fix_cnt;ool del[MAXN];int sz_max, szKP kp_max[MAXNint get(KP a[], int &sz) {while (sz > 0 && del[a[0].fix]) {pop_heap(a, a + sz, cmp());--sz;}if (sz == 0) return 0;else {del[a[0].fix] = 1;return a[0].id;}}template <class cmp>void ins(KP a[], int &sz, const KP &tmp) {a[sz++] = tmp;push_heap(avoid init() {szfiint main() {int cmd;while (EOF != scanf("%d", &cmd)) {if (cmd == 0) {continue;}if (cmd == 1) {










































哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 36 -
KP t;del[fix_cnt++] = 0;ins<lessKP>(kp_max, sz_max, t);ins<greaterKP>(kp_min, sz_min, t);} else if (cmd == 2) {int a = get<lessKP>(kp_max, sz_max);printf("%d\n", a);} else if (cmd == 3) {int a = get<greaterKP>(kp_min, sz_min);printf("%d\n", a);} else {}}return 0;}














他題目:
ble Queue
or/archive/2011/09/10/2173217.html
int k, p;scanf("%d%d", &k, &p);t.val = p;t.id = k;t.fix = fix_cnt;





POJ 3481 DouPOJ 1442 Black Box

1.4 樹狀數組
參考文獻:樹狀數組:
http://www.cnblogs.com/Creat
擴展閱讀:編寫:黃李龍 校核:黃李龍

1.4.1 本原理
一段區間內的和,你會怎么做?,最最簡單的方算,但是這需要
O(N)的時間復雜度,如這個需求非常的頻繁,那么這個的
CPU 時間,進一步想一想,你有可能會想到使用空間換取時間的方法,,盡 空間復雜度的提升。繁的更改怎么辦?使用上面的方案,我們需要大量的更新中間的很多更新的影響是重疊的,我們需要重復計





rray[4]值,需要更新區間[4,5],[4,5,6],在更新[4,5,6]需要這樣的更新帶來了非常多的重復計算,為了解決這一問題,樹狀數組繁的對數組元素進行修改

,同時又要頻繁的查詢數組內任一區間元素之和的時候,
是一種非常優雅的數據結構.先來看看一張樹狀結構的圖

在一個數組中。若你需要頻繁的計算法就是每次進行計操作 會占用大量 就把每一段區間的值一次記錄下來,然后存儲在內存中,將時間復雜度降低到


O1),的確,對於目前的這個需求來說,已經能夠滿足時間復雜度上的要求 管帶來了線性但若是我們的源數據需要頻我們保存到內存中的區間和,而且這算。 例如對於數組



array[10],更新了 a
又一次的計算[4,5],應運而生了。當要頻可以考慮使用樹狀數組


.樹狀數組片:

哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 37 -
圖中 C[1]的值等於 A[1]C[2]的值等於 C[1]+A[2]=A[1]+a[2],C[4]的值
=C[2]+C[3]=A[1]+A[2]+A[3]+A[4],假設我們現在需要更改元素 a[2],那么它將只影響到得 c
需要重新計算這幾個值即可, 減少了很多重復的操致的一個存貯示意圖。: 假設

a[1...N]為原數組,定義 c[1...N]為對應的樹狀數組:
- 2^k + 2] + ... + a[i](其中 k i 的二進制表示末尾 0 的個
a[i - 2^k + 1]...a[i]的計算公式保證數組的正確意義,至於證明過程,請讀者自己查找資料。數組中的元素有

c[2],c[4],c[8],我們只作。 這就是樹狀結構大下面看看它的定義


c[i] = a[i - 2^k + 1] + a[i
數)。下面枚舉出
i 1...5 的數據,可見正是因為上面的了我們
C
1.4.1.1 基本操作
對於 C[i]=a[i - 2^k + 1]...a[i]的定義中,比較難以逐磨的 k,他的值等於
0 的個數.4 的二進制表示 0100,此時 k 就等於 2,而實際上我們還會發現就是前一位的權值
,0100 ,2^2=4,剛好是前一位數 1 的權值.所以所以 2^ 可以表示為
n&(n^(n-1))或更簡單的 n&(-n), 例如:為了表示簡便, 假設現在一個
int 型為 4 ,最高位為符號位。 )
i 這個數的二進制表示末尾
2^kk

 i=3&(-3);
所以 0011&1101=1int j=4&(-4);
 此時 i=13 的二進制為 0011,-3 的二進制為 1101(負數存的是補碼) 此時 j=4,理由同上。所以計算 2^k 我們可以用如下代碼:
int lowbit(int x)//計算 lowbit

int
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 38 -
{return x&(-x); //
也可以寫成 return x & (x ^ (x – 1));}

這個操作的時間復雜度是 O(1)
1.4.1.2 求和操作
在上面的示意圖中,若我們需要求 sum[1..7]個元素的和,僅需要計算 c[7]+c[6]+c[4]的和即可,究竟時間復雜度怎么算呢?一共要進行多少次求和操作呢?求

sum[1..k],我們需查找 k 的二進制表示中 1 的個數次就能得到最終結果,具體為什么,請見代碼
i-=lowbit(i)注釋
int sum(int i)//求前 i 項和
{int s=0;{s += c[i];



1
剪去操作即可理解為依次找到所有的子節點。

0111,右邊第一個 1 出現在第 0 位上,也就是說要從 a[7]開始向為

0110,右邊第一個 1 出現在第 1 位上,也就是說要從
c[6];
后舍掉用過的 1,得到 4,二進制表示為 0100,右邊第一個 1 出現在第 2 位上,也就是說要從
(a[4],a[3],a[2],a[1]),c[4].
1.4.1 操作
影響到得 c 數組中的元素有
c[2],c[4],c[8] 的最壞的復雜度也不過
O(lo )
voi a ({i e( <=c[i]i}





時間 雜 是
while(i>0)i -= lowbit(i);}return s;}




時間復雜度是 。 O(logN)
代碼中“ i -= lowbit(i)”的解釋,這一步實際上等價於將 i 的二進制表示的最后一個,再向前數當前
1 的權個數(例子在下面),而 n 的二進制里最多有 log(n)1,所以查詢效率是
log(n),在示意圖上的以求
sum[1..7]為例,二進制為前數
1 個元素(只有 a[7]),c[7];
然后將這個 1 舍掉,得到 6,二進制表示
a[6]開始向前數 2 個元素(a[6],a[5]),即然

a[4]開始向前數 4 個元素所以
s[7]=c[7]+c[6]+c[4]
.3 給源數組加值
在上面的示意圖中,假設更改的元素是 a[2],那么它,我們只需一層一層往上修改就可以了
,這個過程
gN ;
d dd int i,int val)wh l i n){+= val;+= lowbit(i);}





復 度 O og (l N)
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 39 -
代碼 的二進制中最后一個 1 的權值,2^k,在示意圖上的操作即為提升一層,到上一層的節點,這個過程實際上也只是一個把末尾
1 后補 0 的過程

c[2],2 的二進制為 0010,末尾補 0 0100,c[4]4 的二進
000 c[8]。所以我們需要修改的有 c[2],c[4],c[8]。和三維數組數組等, 建議把二維樹狀數組掌握。

1.4.
1.4.2 tars
這些星星都在不同的位置, 每個星星有個坐標。如果一個星星的左下方(包含正左和正下
)k 顆星星,就說這顆星星是 k 級的.比如,在下面的例圖中,星星
5 3 級的( 124 在它左下)。個
0 級, 2 1 級, 1 2 級, 1 3 級的星。出各 級別的星星的個數。始題目 已經說明星星已經按照

Y 坐標排好序輸入了,所以可以不用排序了。利用了樹狀數組的知識, 並沒有把相應的
add sum 函數等寫

= 32000;N];+3];


中“ i += lowbit(i)”的解釋, i+i
( 例子在下 )。修
a 2]元素為例,需要修改面以 改

[
制為 0100,在末尾補 0 1
關於樹狀數組還有二維樹狀數組
2 樹狀數組例
52 S

.1 POJ 23
天空中有一些星星,星星
24 1 級的。例圖中有 1
求 個思路:算法有很多種

,最實用的是樹狀數組。每個星星的級別定義就是在它的左下角有多少顆星星,我們可以先對
Y 排序,然后就是查找每個坐標前面的坐標中
X 比他小的又多少個,這樣就能算出星星的級別了,這需要樹狀數組。從這里我們可以看出,樹狀數組存儲的是某一段范圍內有多少個點。原 中


C++代碼: ( 下面的代碼只是進去, 讀者可以自己修改)

#include <stdio.h>#include <string.h>const int MAXN = 15000+5;const int MAXXYint level[MAXint f[MAXXY*3





哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 40 -
t n;memset(level, 0, sizeof (level));eof(f));i < n; i++) {= MAXXY;&x, &y);while (l < r) {int mid = (r - l) / 2 + l;l = mid + 1;s += f[k];// leftr = mid;f[k]++;k <<= 1;}];or (int i = 0; i < n; i++) {%d\n",level[i]);}}



















1.4.2 h
意拉力賽,這種的比賽規模很大,涉及到很多國 的 這樣大規模的比賽,


XianGe 許最多 100000 人參加比有車輛的起始點可能不同,速度當然也會有差異。 想知道比賽中會出現多少次超車(如果兩輛車起點相



 同速  不 入
 接下來 n 行,每行輸入兩個數
Vi<1000000)數,每組輸出占一行。路:


inint main(){while (EOF != scanf("%d", &n)) {memset(f, 0, sizfor (int i = 0;int x, y, s = 0, k = 1, l = 0, rscanf("%d %d",if (mid < x) { // rightk = (k << 1) + 1;} else {}s += f[kf[k]++;level[s]++;}fprintf("return 0;}



















.2 rbustoj 1400 汽車比賽
題 :
XianGe 非常喜歡賽車比賽尤其是像達喀爾家 車隊 許多車手參賽。 的
XianGe 也夢想着自己能舉辦一個幻想着有許多人參賽,那是人山人海啊,不過
XianGe 只允賽。這么大規模的比賽應該有技術統計,在

XianGe 的比賽中所
XianGe
 度 輸
 同 :
 生一次超車)。 算發本題有多組測試數據,第一行一個整數 n,代表參賽人數,據,車輛起始位置
Xi 和速度 Vi0<Xi,
輸出:輸出比賽中超車的次思


哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 41 -
置靠前的速度慢的車肯定會被位置靠后速度快的車超過,如果我們按照車的位置從小到大排序,並按照這個順序不斷的用樹狀數組統計,對於當前位置為

i 肯定是會被位置比 i 小,速度快的車超過,我們用樹狀數組統計出車速從 1
到車 統計了多少量車已經知道,即位置為 i 的車就

v;(CAR*)a;-> == d->x) return d- v ->x - d->x;dt, int nMax)i <= nMax; i += lowbit(i)) {it[i] += dt;t getsum(int bit[], int pos)







我們可以這樣考慮,位的車,它

i 的速度 vi 之間有多少量車 s,因為當前是加入統計的第
i 量車,有 i-s 量車會超過車 i,統計后輸出這個結果即可。
C 代碼:
#include <stdio.h>#include <string.h>#include <stdlib.h>#define DATSIZ 100005#define lowbit(x) ((x)&(-(x)))typedef struct{int x,}CAR;CAR car[DATSIZ];int bit[DATSIZ*10];int cmp(const void* a, const void* b){CAR* c =CAR* d = (CAR*)b;if(c x > c->v;return c-}void update(int bit[], int pos, int{int i;for(i = pos;b}}in{int res = 0;while(pos > 0) {res += bit[pos];pos -= lowbit(pos);}return res;}int main(void){int n, i, nMax;long long sum;





































哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 42 -
while(scanf("%d", &n) != EOF) {, 0, sizeof(bit));;i <= n; i++) {scanf("%d%d", &car[i].x, &car[i].v);nMax = car[i].v;qsort(car+1, n, sizeof(car[0]), cmp);{update(bit, car[i].v, 1, nMax);sum += i - getsum(bit, car[i].v);lld\n", sum);










1.4.2
深火熱之中...yni
,獨自一人前往森林深處從靜竹手中奪回昏迷中的 Leyni。葉救出了
Leyni,但是靜竹為此極為惱怒,決定對他們發起最強烈的進攻。個叫做能量保護圈的道具,可以保護他們。


n 個小的小護盾圍成一圈,從 1 n 編號。當某一塊小護盾受到攻擊的時候,小護盾就會抵消掉這次攻擊,也就是說對這一塊小護盾的攻擊是無效攻擊,從而保護圈里的人,不過小護盾在遭到一次攻擊后,需要

t 秒進行冷卻,在冷卻期間受到的攻擊都是 到攻擊, 即假設
1 秒時受到攻擊並成功防御,到 1+t 秒時冷卻才結束並能進行防御,在
2 t 受到的都是有效攻擊。
i 昏迷,他們無法得知小護盾遭受的有效攻擊次數,他們需要長度,

q 表示攻擊的詢問的總次數, t
表示某范圍內的能量盾被攻擊的次數。

1 <= a <= na
b)總共受到了多少次有效攻擊。保證
1<=memset(bitnMax = 0for(i = 1;if(nMax < car[i].v)}sum = 0;for(i = 1; i <= n; i++)}printf("%}return 0;}












.3 hrbustoj 1161 leyni
題意:
Leyni 被人擄走,身在水小奈葉為了拯救
Le
歷經千辛萬苦,小奈不過小奈葉有一這個保護圈由有效攻擊


,此時他們就會遭現在小奈葉專心戰斗,
Leyn
你的幫助。輸入:第一行是一個整數 ,表示有多少組測試數據。

T
第一行是三個整數, n,q,tn 表示保護圈的能量盾的冷卻時間。接下來的

q 行,每行表示受到的攻擊或者她詢問攻擊:

Attack a
表示編號為 a 的小護盾受到一次攻擊, 保證詢問:

Query a b
表示詢問編號從 a b ( 到 的小護盾 包括
a,b<=n
k 次攻擊發生在第 k 秒,詢問不花費時間。
1 <= n,q <=1000001 <= t <= 50

哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 43 -
i 組測試數據,從 1 開始計數。的小護盾受到的有效攻擊次數,一個詢問一行。受了多少次的有效攻擊,即沒有保護罩的時候。因為每 統計護盾受到的攻擊次數;還要記錄每個護盾的最后開間,每次攻擊某快護盾的時候,先檢查這塊護盾的冷卻時間,如果冷卻時間已經過了,說明可以承受這次攻擊,如果冷卻時間沒過,則受到一次有效攻擊。






clude <stdio.h>nclude <string.h>= 100000;{nt init() {memset(c, 0, sizeof(c));int x, int v) {int getSum(int x) {int s = 0;a, b;scanf("%d", &T);










輸出:每一組測試數據,先輸出一行
"Case i:",i 表示第之 對 詢 ,輸出該范圍內 后 於每一個 問思路:題目要對每個查詢輸出某個護盾承個護盾有冷卻時間,冷卻時間內的攻擊都是有效攻擊,我們用一個樹狀數組始冷卻時間,也就是最后一次成功防御的時





C+in+

代碼:
##iconst int maxnconst int maxt = 50;int c[maxn+1];int n, q, t;int p[maxn+1];inline int lowbit(int x)return (x)&(-x);}i}void add(int i = x;for (; x <= n; x += lowbit(x)) {c[x] += v;}}for (; x > 0; x -= lowbit(x)) {s += c[x];}return s;}int main(){int Case, T;char cmd[16];int attack_cnt;int i,




























哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 44 -
Case %d:\n", Case);init();t = 0;(i = 0; i < q; ++i) {("%s", cmd);(cmd[0] == 'A') {attack_cnt++;scanf("%d", &a);if (p[a]+t <= attack_cnt) {p[a] = attack_cnt;} else {add(a, 1);}if (cmd[0] == 'Q'){("%d %d", &a, &b);(a > b) {int t = a;= b;= t;n", getSum(b) - getSum(a-1));return 0;




















1.4.3
個構造方法構造出答案,構造時會用到二分和樹狀數組找第 k 大的數。
es
用。維樹狀數組的應用

1.5
lysuccess.com/index.php/segment-tree-complete/
for (Case = 1; Case <= T; ++Case) {scanf("%d %d %d", &n, &q, &t);//printf("%d %d %d\n", n, q, t);for (i = 0; i <= n; ++i) {p[i] = -t;}printf("attack_cnforscanfif} elsescanfifab}printf("%d\}}}}





















其他推薦例題
POJ 2085 Inversion
此題要想出一
POJ 1195 Mobile phon
二維樹狀數組的應
hrbustoj 1451 Imagine

線段樹
擴展閱讀:胡浩的博客:
http://www.noton
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 45 -
寫: 盧俊達 校核:黃李龍


1.1.1 線段樹的介
線段樹是一種二叉搜索樹, 與區間樹相似,它將一個區間划分成一些單元區間, 每個單元子表示的區間為

[a,(a+b)/2],右兒子表示本的線段樹結構,但只有這些並不能做什么,就好比一個程序有輸入沒輸出,根本沒有任何用處。錄線段有否被覆蓋,並隨時查詢當前被覆蓋線段的總長度。 那么此時可以在結點結構中加入一個變量




int count;代表當前結點代表的子樹中被覆蓋的線段長度和。這樣就要在插入(刪除)當中維護這個
count 值,於是當前的覆蓋總值就是根節點的於查找一個有序數組給定區間里的最大值或者對該區間求和。屬於初級的線段樹應用,多數是較難的應用,例如對有序數組進行區間成段更新、區間成段加減等等的操作。這些較難應用一般需要利用到“延遲標記”。遲標記的作用是延遲對子區間的更新,優點是更新操作不必“進行到底”。例如將上圖中的有序數組的下標在區間





[1,3]上的數全部加 1,再全部減 7。利用延時標記思想則兩次更新均只需更新到第二層,延遲標記將記錄操作“減
6”,當查詢操作涉及的區間在[1,3]
內時
1.1.2
0int right;return tree[root].max=val[left];


區間對應線段樹中的一個葉結點。對於線段樹中的每一個非葉子節點
[a,b], 它的左兒的區間為
[(a+b)/2+1,b]。因此線段樹是平衡二叉樹,最后的子節點數目為 N,即整個線段區間的長度。上面介紹的是基最簡單的應用就是記



count 值了。另外也可以用上面兩種應用延 再執行更新操作。




線段樹模板代碼
#define MAXSIZE 20000int val[MAXSIZE+1];struct node{int max;int left;}tree[MAXSIZE*3];int max(int x,int y){return x>y?x:y;}int create(int root,int left,int right)//











root 為根節點建樹。
{tree[root].left=left;tree[root].right=right;if(left==right)int a,b,middle=(left+right)/2;




哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 46 -b=create(2*root+1,middle+1,right);[left,right]

中的最大值。
].right<left)ht<=right)return tree[root].max;;a=calculate(2*root,left,right);*root+1,left,right);s





的元素更新為 val
.max;tree[root].right==pos)ax=val;max(a,b);



1.1.3
1.1.3.1 HDU 1754 I Hate It
詢問區間[a,b]當中的元素的最大值是多少。
A
回區間最大值。本題意在考驗對線段樹的基本應用。
];a=create(2*root,left,middle);return tree[root].max=max(a,b);}int calculate(int root,int left,int right)//




root 為根節點的線段樹中,求取區間
{if(tree[root].left>right||tree[rootreturn 0;if(left<=tree[root].left&&tree[root].rigint a,bb=calculate(2return max(a,b);}int updata(int root,int pos,int val)//








root 為根節點的線段樹中,將位置 po{if(pos<tree[root].left||tree[root].right<pos)return tree[root]if(tree[root].left==pos&&return tree[root].mint a,b;a=updata(2*root,pos,val);b=updata(2*root+1,pos,val);return tree[root].max=}










經典題目
1.題目出處/來源
[HDU][1754][線段樹] I Hate It2
.題目描述給定一個序列和兩種操作:

Q 操作,表示這是一條詢問操作,
U 操作,表示這是一條更新操作,要求把元素 的值更改為 B
3.分析區間節點內封裝變量
max 用於記錄該節點所表示區間的最大值。更新和查詢操作均返
4.代碼(包含必要注釋,采用最適宜閱讀的 Courier New 字體,小五號,間距為固定值
12 磅)
#include<iostream>#include<stdio.h>using namespace std;#define MAXSIZE 200000int val[MAXSIZE+1];struct node{int max;int left;int right;}tree[MAXSIZE*3int max(int x,int y)











哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 47 -ght)root

為根節點建樹。
b=create(2*root+1,middle+1,right);ax(a,b);calculate(int root,int left,int right)


中,求取區間[left,right]中的最大值。
a=calculate(2*root,left,right);al)

新為 val
return tree[root].max=val;b=updata(2*root+1,pos,val);x(a,b);;i=1;i<=n;i++)create(1,1,n);for(int i=0;i<m;i++){char op;",&op,&a,&b);if(op=='Q')ulate(1,a,b));,a,b);{return x>y?x:y;}int create(int root,int left,int ri//

















{tree[root].left=left;tree[root].right=right;if(left==right)return tree[root].max=val[left];int a,b,middle=(left+right)/2;a=create(2*root,left,middle);return tree[root].max=m}int//









root 為根節點的線段樹
{if(tree[root].left>right||tree[root].right<left)return 0;if(left<=tree[root].left&&tree[root].right<=right)return tree[root].max;int a,b;b=calculate(2*root+1,left,right);return max(a,b);}int updata(int root,int pos,int v//









root 為根節點的線段樹中,將位置 pos 的元素更
{if(pos<tree[root].left||tree[root].right<pos)return tree[root].max;if(tree[root].left==pos&&tree[root].right==pos)int a,b;a=updata(2*root,pos,val);return tree[root].max=ma}int main(){int n,mwhile(~scanf("%d%d",&n,&m)){for(intscanf("%d",&val[i]);int a,b;scanf("\n%c%d%dprintf("%d\n",calcelseupdata(1}}}






















哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 48 -
北大培訓教材中做法。
1.1.3
/來源
DU][1698][線段樹] Just a Hook1
。接下來執行 Q 次更新操作,每次操出序列的總和。屬於對延遲標記的最基本應用。必 注 ,采用 宜閱讀的 字體,小五號,間距為固定




ace std;
標記節點建樹。

0;ht=right;.total=val[left];=create(2*root,left,middle)+create(2*root+1,middle+1,right);root



的子節點的延遲標記。
l=tree[root].mark*(tree[root].right-tree[root].left+1);ree[root*2+1].mark=tree[root].mark;}root


為根節點的線段樹中,求取區間[left,right]的元素和。
mark(root);root].left&&tree[root].right<=right)

5.思考與擴展:可以借鑒
.2 HDU 1698 Just a Hook
1.題目出處
[H2
.題目描述一個長度為
n 的序列,初始化序列中的元素為作將區間
[x,y]上的值更新為 z。所有操作結束后,求
3.分析考察隊線段樹的成段更新。本題需要用到延遲標記,

4.代碼(包含 要 釋 最適 Courier New
12 磅)
#include<iostream>#include<stdio.h>using namesp#define MAXSIZE 100000int val[MAXSIZE+1];struct node{int total;//







區間屬性,這里是 total,表示區間元素和。
int left;int right;int mark;//mark


表示延遲
}tree[MAXSIZE*3];int create(int root,int left,int right)//

root 為根
{tree[root].mark=tree[root].left=left;tree[root].rigif(left==right)return tree[root]int middle=(left+right)/2;return tree[root].total}void updata_mark(int root)//









更新
{if(tree[root].mark){tree[root].totaif(tree[root].left!=tree[root].right)tree[root*2].mark=ttree[root].mark=0;}int calculate(int root,int left,int right)//









{updata_if(tree[root].left>right||tree[root].right<left)return 0;if(left<=tree[




哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 49 -,right);t val)


節點的線段樹中,將區間[left,right]中的元素更新為 val
return tree[root].total;tree[root].right<=right)=val;ee[root].left+1);+1,left,right,val);int t;j=1;j<=n;j++)updata(1,x,y,z);}printf("Case %d: The total value of the hook is %d.\n",i,calculate(1,1,n));}










擴展:可以借鑒北大培訓教材中做法。
1.1.3 ith Integers
ple Problem with Integers
、…、 AN。你需要處理兩種操作。個數字加上一個給定的值。模板題,考研對延遲標記的應用能力。這里對延遲標記的操作與成段更新稍有不同。在更新延遲標記后,需要執行一次


updata_mark()操作,意在更新當前節點,因為更新操作需要返回當前節點的最新信息。的

Courier New 字體,小五號,間距為固定

pace std;efine MAXSIZE 100000t total_edge,val[MAXSIZE+1];return tree[root].total;return calculate(2*root,left,right)+calculate(2*root+1,left}int updata(int root,int left,int right,in//






root 為根
{updata_mark(root);if(tree[root].left>right||tree[root].right<left)if(left<=tree[root].left&&{tree[root].markreturn tree[root].total=val*(tree[root].right-tr}return tree[root].total=updata(2*root,left,right,val)+updata(2*root}int main(){scanf("%d",&t);for(int i=1,n,q;i<=t;i++){scanf("%d%d",&n,&q);for(intval[j]=1;create(1,1,n);for(int j=0,x,y,z;j<q;j++){scanf("%d%d%d",&x,&y,&z);}






















5.思考與
.3 POJ 3468 A Simple Problem w
1.題目出處/來源
[POJ][3468][線段樹] A Sim2
.題目描述你有
n 個整數, A1A2
將給定范圍內的每求一個給定范圍內的數字的總和。

3.分析線段樹成段增減的

4.代碼(包含必要注釋,采用最適宜閱讀值
12 磅)
#include<iostream>#include<stdio.h>using names#ind





哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 50 -al
,表示區間元素和。
;.mark=0;t].left=left;k)tal+=tree[root].mark*(tree[root].right-tree[root].left+1);=tree[root].right)tree[root*2].mark+=tree[root].mark;tree[root*2+1].mark+=tree[root].mark;e(int root,int left,int right)








區間[left,right]的元素和。
_mark(root);if(tree[root].left>right||tree[root].right<left);&tree[root].right<=right)[root].total;e(2*root,left,right)+calculate(2*root+1,left,right);l)






節點的線段樹中,將區間[left,right]中的元素加上 val
.right<left)if(left<=tree[root].left&&tree[root].right<=right)root);ot].total;root,left,right,val)+updata(2*root+1,left,right,val);struct node{long long total;//







區間屬性,這里是 totint left;int right;long long mark//mark



表示延遲標記
}tree[MAXSIZE*3];long long create(int root,int left,int right)//

root 為根節點建樹。
{tree[root]tree[rootree[root].right=right;if(left==right)return tree[root].total=val[left];int middle=(left+right)/2;return tree[root].total=create(2*root,left,middle)+create(2*root+1,middle+1,right);}void updata_mark(int root)//









更新 root 的子節點的延遲標記。
{if(tree[root].mar{tree[root].toif(tree[root].left!{}tree[root].mark=0;}}long long calculat//










root 為根節點的線段樹中,求取
{updatareturn 0if(left<=tree[root].left&return treereturn calculat}long long updata(int root,int left,int right,int va//







root 為根
{updata_mark(root);if(tree[root].left>right||tree[root]return tree[root].total;{tree[root].mark+=val;updata_mark(return tree[ro}return tree[root].total=updata(2*}










哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 51 -int n,q;te(1,1,n);op;scanf("\n%c",&op);%d",&a,&b);printf("%I64d\n",calculate(1,a,b));else,c;scanf("%d%d%d",&a,&b,&c);pdata(1,a,b,c);










可以借鑒北大培訓教材中做法。
1.1.3
/來源“回型”圖案, 每一個“回型”是由一個大矩形中間挖去一個小矩形構成,大小矩形的四邊都平行於坐標軸。”圖案,他們可能互相重疊,請求出被他們所覆蓋的平面的總面積。划分為四個矩形。將每個矩形的與




x 軸平行的邊按離 x 軸距離由近及遠順序插入線段樹中(底邊插入,頂邊刪除)。線段執行插入之前,先計算下當前線段與上一條線段之間的覆蓋面積。用


tree[ 間的距離(高度差), tree[1].len 表示圖形的寬。適宜閱讀的
Courier New 字體,小五號,間距為固定

;int main(){while(~scanf("%d%d",&n,&q)){for(int i=1;i<=n;i++)scanf("%d",&val[i]);creacharfor(int i=0;i<q;i++){if(op=='Q'){int a,b;scanf("%d}{int a,bu}}}}






















5.思考與擴展:
.4 POJ 3832 Posters
1.題目出處
[POJ][3832][線段樹] Posters2
.題目描述平面上有一些現在有

n 個不同大小的“回型
3.分析首先,將每個一個回型在對每條


1].len*當前線段與前一個線段之當遍歷完所有線段時,就會得出覆蓋面積。

4.代碼(包含必要注釋,采用最值
12 磅)
#include<iostream>#include<stdio.h>using namespace std;#define MAXSIZE 50000#define N 50000struct node{int total







哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 52 -//total
表示該節點所表示的區間被線段覆蓋的次數的長度。

ruct line
是否是矩形的上邊界。
e,n;total_edge
個線段, n 個回型。
id create(int root,int left,int right)tree[root].left=left;void updata(int root,int le


入或刪除。
al;{t,val);t,right,val);oot].left)tree[root].len=0;s,int left,int right,bool top)//






記錄一條線段的信息
{if(left>right)return;edge[total_edge].pos=pos;edge[total_edge].left=left;edge[total_edge].right=right;p(const void* m,const void* n)int left;int right;int len;//









該區間內,線段
}tree[MAXSIZE*3];st{int pos;//pos



表示 y 軸坐標
int left;int right;bool top;//


判斷該線段
}edge[N*8];int total_edg//

一共
vo//
建樹,明確每個節點對應的區間
{tree[root].right=right;if(left==right)return ;int middle=(left+right)/2;create(2*root,left,middle);create(2*root+1,middle+1,right);}ft,int right,int val)//








將線段插
{if(tree[root].left>right||tree[root].right<left)return ;else if(left<=tree[root].left&&tree[root].right<=right)tree[root].total+=velseupdata(2*root,left,righupdata(2*root+1,lef}if(tree[root].total>0)tree[root].len=tree[root].right-tree[root].left+1;else if(tree[root].right==tree[relsetree[root].len=tree[2*root].len+tree[2*root+1].len;}void add_edge(int poedge[total_edge].top=top;total_edge++;}int cm



















哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 53 -al_2,&y2,&x3,&y3,&x4,&y4);e);+1,x3+1,x4,false);add_edge(y3+1,x3+1,x4,true);add_edge(y4+1,x3+1,x4,false);1,x3+1,x4,true);1,x4+1,x2,false);eof(edge[0]),cmp);printf("%I64d\n",total_size);}











1.6 衡二叉查找樹
衡二叉查找樹 Treap 的分析與應用》
]http://dongxicheng.org/structure/treap/
//將線段按照 y 軸坐標由小到大排序。
{return (*(line*)m).pos-(*(line*)n).pos;}void init()//



初始化及輸入
{to edge=0; tfor(int i=1;i<MAXSIZE*3;i++){tree[i].len=0;tree[i].total=0;}for(int i=0;i<n;i++){int x1,y1,x2,y2,x3,y3,x4,y4;scanf("%d%d%d%d%d%d%d%d",&x1,&y1,&xadd_edge(y1+1,x1+1,x3,false);add_edge(y2+1,x1+1,x3,truadd_edge(y1add_edge(y2+add_edge(y1+add_edge(y2+1,x4+1,x2,true);}}int main(){create(1,1,MAXSIZE+1);while(scanf("%d",&n)&&n){init();qsort(edge,total_edge,sizlong long total_size=0;for(int i=0,pre=1;i<total_edge;i++){total_size+=(long long)tree[1].len*(edge[i].pos-pre);//





























這里注意, tree[1].len 必須轉化為 long long
updata(1,edge[i].left,edge[i].right,edge[i].top?-1:1);pre=edge[i].pos;}}



5.思考與擴展:可以借鑒北大培訓教材中做法。
隨機平
參考文獻:清華大學計算機科學與技術系·郭家寶《隨機平數據結構之

Treap[董的博客編寫:黃李龍 校核:黃李龍

哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 54 -
1.6.
優先 在以關鍵碼構成二叉搜索樹的同時, 還按優先級來滿足堆的性質。因而,
Trea 。這里需要注意的是, Treap 並不是二叉堆,二叉堆必須是完全二叉樹,而
Trea
1.6.
使 Treap 中的節點同時滿足 BST 性質和最小堆性質,不可避免地要對其結構進行式被稱為旋轉。 在維護
Treap 的過程中,只有兩種旋轉,分別是左旋轉(簡稱左旋它的根節點旋轉到根的左子樹位置, 同時根節點的右子節點成為子樹



1 概述
splay tree 一樣, treap 也是一個平衡二叉樹, 不過 Treap 會記錄一個額外的數據,即級。
Treapp=tree+heapp

可以並不一定是。
2 Treap 基本操作
為了調整, 調整方

)和右旋轉(簡稱右旋)。左旋一個子樹, 會把的根; 右旋一個子樹, 會把它的根節點旋轉到根的右子樹位置,同時根節點的左子節點成為子樹的根。



struct Treap_Node{Treap_Node *left,*right; //

節點的左右子樹的指針
int value,fix; //節點的值和優先級
};void Treap_Left_Rotate(Treap_Node *&a) //
左旋 節點指針一定要傳遞引用
{b=a->right;eft;


1.6.
ap 的基本操作有:查找,插入,刪除等。
1.6.3
Treap_Node *a->right=b->left;b->left=a;a=b;}void Treap_Right_Rotate(Treap_Node *&a) //




右旋 節點指針一定要傳遞引用
{Treap_Node *b=a->la->left=b->right;b->right=a;a=b;}





3 Treap 的操作
同其他樹形結構一樣, tre
查找同其他二叉樹一樣,
treap 的查找過程就是二分查找的過程,復雜度為 O(lg n)
.1 插入
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 55 -
法相似。 首先找到合適的插入位置, 然后建立 有一個優先級屬性, 該值可能會破壞堆序,因此我們要根據需要進行恰當的旋轉。具體方法如下:, 在當前節點的左子樹中插入, 插入后如果左子節點的優先級小於當前節點的優先級,對當前節點進行右旋;節點的右子樹中插入, 插入后如果右子節點的優先級小於當前節點的優先級,對當前節點進行左旋;果當前節點為空節點, 在此建立新的節點, 該節點的值為要插入的值, 左右子樹為空在








Treap 中插入元素,與在 BST 中插入方新的節點, 存儲元素。 但是要注意新的節點會

1. 從根節點開始插入;
2. 如果要插入的值小於等於當前節點的值
3. 如果要插入的值大於當前節點的值, 在當前
4. 如, 插入成功。

Treap_Node *root;void Treap_Insert(Treap_Node *&P,int value) //
節點指針一定要傳遞引用
{if (!P) //
找到位置,建立節點
P=new Treap_Node;else if (value <= P->value)>left->fix < P->fix)Treap_Right_Rotate(P);//


左子節點修正值小於當前節點修正值,右旋當前節點
}right,r);(P->right->fix < P->fix)


右子節點修正值小於當前節點修正值,左旋當前節點
1.6.3
{P->value=value;P->fix=rand();//

生成隨機的修正值
}{Treap_Insert(P->left,r);if (Pelse{Treap_Insert(P->ifTreap_Left_Rotate(P);//}}









.2 刪除
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 56 -
ap 中刪除元素要考慮多種情況。我們可以按照在 BST 中刪除元素同 的元素, 即用它的后繼
(或前驅)節點的值代替它,然后刪除它的

O(logN),但是這種方法並沒有充分利用 Treap 已有的隨機性 方法, 這種方法是基於 找到待刪除節點的位置, 然后分情況討論:則該節點是可以直接刪除的節點。若該節點有非空子節點,用非空子節點代替該節點的,否則用空節點代替該節點,然后刪除該節點。是通過旋轉,使該節點變為可以直接刪除 右旋該節點, 使該節點 , 左旋該節點, 使該節 這樣繼續下去, 直到變成可以直接刪除的節點。與









BST 一樣,在 Tre
樣的方法來刪除 Treap 中后繼
(或前驅)節點。上述方法期望時間復雜度為質, 而是重新得隨機選取代替節點。 我們給出一種更為通用的刪除旋轉調整的。 首先要在


Treap 樹中情況一,該節點為葉節點或鏈節點,情況二,該節點有兩個非空子節點。 我們的策略的節點。 如果該節點的左子節點的優先級小於右子節點的優先級,降為右子樹的根節點, 然后訪問右子樹的根節點, 繼續討論; 反之點降為左子樹的根節點, 然后訪問左子樹的根節點,





BST_Node * oot;void Treap_Delete(Treap_Node *&P,int *value) //
節點指針要傳遞引用
{r//

找到要刪除的節點 對其刪除
ht || !P->left) //情況一,該節點可以直接被刪除
*t=P;ht)//

用左子節點代替它
ght; //用右子節點代替它
//刪除該節點
>right->fix) //左子節點修正值較小,右旋
P);ht,r);

左旋
P);ete(P->left,r);}< P->value)if (value==P->value){if (!P->rig{Treap_Nodeif (!P->rigP=P->left;elseP=P->ridelete t;}else //














情況二
{if (P->left->fix < P-{Treap_Right_Rotate(Treap_Delete(P->rig}else //





左子節點修正值較小,
{Treap_Left_Rotate(Treap_Del}}else if (value





哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 57 -
//在左子樹查找要刪除的節點
e(P->right,r); //在右子樹查找要刪除的節點
1.7 re
reap 可以解決 splay tree 可以解決的所有問題,具體參見另一篇文章: 《數據結構之伸展樹
heng.org/structure/splay-tree/
以 體:
ight; //節點的左右子樹的指針
ze; //點的值,優先級,重復計數(記錄相同節點個數,節省空間

inline int lsize(){ return left ?left->size ?0; } //返回左子樹的節點個數

nl eturn right?right->size?0; } //返回右子樹的節點個數

1.8
eap 作為一種簡潔高效的有序數據結構,在計算機科學和技術應用中有着重要的地位。它可以用來實現集合、多重集合、字典等容器型數據結構,也可以用來設計動態統計數據 構上 很詳盡,非常推薦清華大學計算機科學與技術系· 家



p 的分析與應用》的論文,要學好 Treap 這篇論文非常 得

1.8 經 題目
1.8
操作前, i 初始為 0。1 3 33GET 3 -1000, -4, 1, 2, 3, 8 1



Treap_Delete(P->left,r);elseTreap_Delet}



T ap 應用
T
http://dongxic
可 這樣定義結構
struct Treap_Node{Treap_Node *left,*rint value,fix,weight,si



),子樹大小
i ine int rsize(){ r};

總結
Tr
結 。以 內容都是一些介紹,寫得不是郭 寶 隨機平衡二叉查找樹 《

Trea
值 一 。 看
.1
.1.1 POJ 1442 Black Box
題 :兩種操作:意我們有一個黑色盒子,有



ADD(X):把整數 X 放入黑色盒子中;
GET:先把 i 1,然后求第 i 小的數,在 GET
下面是操作的示范:1 ADD(3) 0 32 GET3 ADD(1) 1 1, 34 GET 2 1, 35 ADD(-4) 2 -4, 1, 36 ADD(2) 2 -4, 1, 2, 37 ADD(8) 2 -4, 1, 2, 3, 88 ADD(-1000) 2 -1000, -4, 1, 2, 3, 89









哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 58 -
21 4, 1, 2, 2, 3, 8一

M N。第二行是 M 的整數,表示按順序添加的 M 個數,即執行
AD 操 分 A2··· AM。第三行有 N 個數正整數,分別是 u1u2··· uN,表示當黑盒內有
GET 操作,要輸出第 i 小的數。操作,輸出其結果。析:求第

k 小的數,直接套 Treap 樹模板即可。注意判斷當前添加數后,是否能輸出一
GET 操作的結果即可。
io>ring>>ns 5;t]->sz : 0; }AXN = 100005>rea odt nt, pcnt;mem[MAXN], *pool[MAXN];eapNode * new_node(int val = 0) {pcnt--];else x = mem + mcnt++;/* c=0












左旋
 TreapNx->ch[y->ch[x = y;y = x->ch[c];y->sz = y->ch_sz(0) + y->ch_sz(1) + 1;ode * y = x->ch[!c];!c] = y->ch[c];c] = x;







z = x->ch_sz(0) + x->ch_sz(1) + 1;root = NULL;}


10 GET 4 -1000, -4, 1, 2, 3, 81 ADD(2) 4 -1000, -輸入第 行是兩個整數



D 作 別是 A1
ui 個數的時候,執行輸出對已每次

GET
分 個
C++代碼:
#include <cstd#include <cst#include <cstdlib#include <ctime>#include <algorithm>using namespace std;co t int MAXN = 3000struct TreapNode {TreapNode* ch[2];int sz, fix;in val;int ch_sz(int c) { return ch[c] ? ch[c};te la <c mp te onst int Mstruct Treap {T pN e *root;in mcTreapNodeTrTreapNode *x;if (pcnt >= 0) x = pool[x->ch[0] = x->ch[1] = NULL;x->sz = 1;x->fix = rand();x->val = val;return x;}


























c=1 右旋 */void rotate(TreapNode* &x, int c) {x->s}void init() {mcnt = 0;pcnt = -1;






哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 59 -
void insert(TreapNode* &x, int val) {x = new_node(val);} else if (val <= x->val) {insert(x->ch[0], val);x->sz++;x) rotate(x, 0);}&x, int val) {return false;l) {ch[1], val);> val) {val);0] || NULL == x->ch[1]) {x;/*













放入內存池進行刪除 */L != x->ch[0]) x = x->ch[0];= x->ch[1];true;) {ret = del(x->ch[1], val);lse {0);sz--;ur) {val, cur);k(x->ch[1], val, cur + x->ch_sz(0) + 1);ea h(TreapNode* x, int k) {x->ch_sz(0);if (k <= lsz) {, k - (lsz + 1));if (x == NULL) {if (x->ch[0]->fix < x->fix) rotate(x, 1);} else {insert(x->ch[1], val);x->sz++;if (x->ch[1]->fix < x->fi}void insert(int val) {insert(root, val);}bool del(TreapNode*if (NULL == x)bool ret;if (x->val < varet = del(x->} else if (x->valret = del(x->ch[0],} else {if (NULL == x->ch[pool[++pcnt] =if (NULelse xret =} else {if (x->ch[0]->fix < x->ch[1]->fixrotate(x, 1);} erotate(x,ret = del(x->ch[0], val);}}}if (ret) x->return ret;}int get_rank(TreapNode* x, int val, int cif (val == x->val) {return x->ch_sz(0) + cur + 1;} else if (val < x->val) {return get_rank(x->ch[0],} else {return get_ran}}T pNode* get r _ktint lsz =return get_kth(x->ch[0], k);} else if (k > lsz + 1) {return get_kth(x->ch[1]} else {return x;}}int get_kth(int k) {return get_kth(root, k)->val;}int size() {return root->sz;









































































哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 60 -
n() {&n)) {);sz == u[j]) {_kth(g));= n) break;





1.8
 意 在  辦理業務。每個客戶用一個正整數 K 標識, 因為銀行人手不多,客戶在辦理時需要等待其他用戶辦理。在客戶辦理業務前,銀行為用 戶提 一  先級來確定先處理哪個用戶的業務,並且有時候是優先級高的先服務,有時是優先級的先服務。銀行系統將對客戶的服務分為以下幾種請求:
 0  統的服務停止系
 1 K P  加入 K 用戶的業務請求,優先級為 P 2  先服務一個最高優先級的客戶 3  先服務一個最低優先級的客戶}};int a[MAXN], u[MAXN];Treap<MAXN> tr;in mai tint m, n;srand(time(0));wh e (EOF != scanf("%d%d", il &m,for (int i = 0; i < m; ++i) scanf("%d", &a[i]);for (int i = 0; i < n; ++i) scanf("%d", &u[i]);sort(u, u + n);tr.init(int j = 0, g = 0;for (int i = 0; i < m; ++i) {tr.insert(a[i]);int sz = tr.size();while (j < n &&++g;printf("%d\n", tr.get++j;}if (j =}}return 0;}

























.1.2 POJ 3481 Double Queue
題 : 一個銀行中,每天都會有客戶來供 個優先級
P,根據優輸入:個請求,並且最后一行的請求為

0,表示停止服務。輸入保證有類型為 1
的請求,每個客戶的標識都不一樣,並且沒有兩個請求的優先級是一樣的。客戶標識小於
100 P 小於 10000000,一個客戶可以有多次請求,每次請求的優先級都不同。於 個

3 的請求,輸出服務的客戶標識。如果沒有客戶在請求,則輸出路


reap 我們可以根據每個用戶的優先級 P 進行比較 構 也是根據
P 進行。剩下的工作就是查找和輸出了。每一行為一

0000,優先級輸出:對 每 類型為

2 或者
0。思 :

T 樹能查詢樹中數的最大值的和最小值,來 造
Treap 樹,插入和刪除節點直接套用
Treap 書模板就行了。
/** POJ 3481 Double Queue

哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 61 -
Treap*/c e <cstdio>string>stdlib>clude <ctime>#include <utility>pedef ir<int, int> PII;csz : 0; }}else x = mem + mcnt++;ch[0] = x->ch[1] = NULL;x->fix = rand();void rotate(TreapNode* &x, int c) {de* x->ch[y->ch[c] = x;h[cy-> + yx-> + x-}int c = x->val < val;insert(x->ch[c], val, id);e(x, !c);TreapNode * &x) {NULL == x->ch[1]) {*
























樹解
*#i lud n#include <c#include <c#inusing namespace std;ty paonst int MAXN = 1000006;struct TreapNode {TreapNode *ch[2];int sz, fix;int lsz() { return ch[0] ? ch[0]->sz : 0; }int rsz() { return ch[1] ? ch[1]->int val, id;;struct Treap {int mcnt, pcnt;TreapNode *root, mem[MAXN], *pool[MAXN];void init() {mcnt = 0;pcnt = -1;root = NULL;}TreapNode * new_node(int val, int id) {TreapNode* x;if (pcnt >= 0) x = pool[pcnt--];x->x->sz = 1;x->val = val;x->id = id;return x;}































 TreapNox->ch[!c] = y->ch[c];
 y =  !c]; x = y;y = x->c
 ]; y->sz =  lsz()  >rsz() + 1; x->sz =  lsz()  >rsz() + 1;void insert(TreapNode* &x, int val, int id) {if (x == NULL) {x = new_node(val, id);} else {x->sz++;if (x->fix > x->ch[c]->fix) rotat}}void insert(int val, int id) {insert(root, val, id);}PII del_max(PII ii;if (













哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 62 -
ii = make_pair(x->val, x->id);pool[++pcnt] = x;if (x->ch[0]) x = x->ch[0];else x = NULL;lse {ii = del_max(x->ch[1]);x->sz--;urn ii;_max() {(root) return del_max(root);TreapNode * &x) {II ii;f (NULL == x->ch[0]) {ii = make_pair(x->val, x->id);pool[++pcnt] = x;if (x->ch[1]) x = x->ch[1];else x = NULL;del_min(x->ch[0]);}T i} else {



















1.8 的出納員
型專業化軟件公司,有着數以萬計的員工。作為一名出納員,我的任務之一便是統計每位員工的工資。這本來是一份不錯的工作,但是令人郁悶的是,我

} e}ret}PII delifelse return make_pair(0, 0);}PII del_min(Pi} else {ii =x->sz--;}return ii;}PII del_min { ()if (root) return del_min(root);else return make_pair(0, 0);};reap tr;nt main() {int cmd;tr.init();while (EOF != scanf("%d", &cmd)) {if (cmd == 0) {tr.init();continue;}if (cmd == 1) {int k, p;scanf("%d%d", &k, &p);tr.insert(p, k);} else if (cmd == 2) {PII ii = tr.del_max();// printf("hig %d\n", ii.first);printf("%d\n", ii.second);} else if (cmd == 3) {PII ii = tr.del_min();// printf("low %d\n", ii.first);printf("%d\n", ii.second);}}return 0;}














































.1.3 NOI 2004 郁悶
題目:
OIER 公司是一家大
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 63 -
們的老板反復無常,經常調整員工的工資。如果他心情好,就可能把每位員工的工資加上一個 的工資扣除一個相同的量。我真不知道除員工反感,尤其是集體扣除工資的時候,一旦某位員工發現自己的工了。每位員工的工資下界都是統一規定的。每當一個人離開公司,我就要從電腦中把他的工資檔案刪去,同樣,每當公司招聘了一位新員工,我就得為他新建一個工資檔案。板 工的工資情況,而是問現在工資第 這時,我就不得不對數萬個員工進行一次漫長的排序, 后了 對我的工作了解不少了。正如你猜的那樣,我想請你編一個工資統計程序。怎么樣,不是很困難吧?一 數











n minn 表示下面有多少條命令, min 表示工資下界。命令可以是以下四種之一:

 名稱  格式  作用 I 命令  I_k  新建一個工資檔案,初始工資為 k。如果某員工的初始工資低於工資下界,他將立刻離開公司。
 A 命令  A_k  把每位員工的工資加上 k S 命令  S_k  把每位員工的工資扣除 k F 命令  F_k  查詢第 k 多的工資相同的量。反之,如果心情不好,就可能把他們了調工資他還做什么其它事情。工資的頻繁調整很讓資已經低於了合同規定的工資下界,他就會立刻氣憤地離開公司,並且再也不會回來老 經常到我這邊來詢問工資情況,他並不問具體某位員




k 多的員工拿多少工資。每當然 告訴他答案。好 ,現在你已經輸入第 行有兩個非負整接下來的




n 行,每行表示一條命令。
_( 划 下 線)表示一個空格, I 命令、 A 命令、 S k 是一個非負整數, F 命令中的 是一個正整數。出僅包含一個整數,為當前工資第


k 多的員工所拿 工 目,則輸出
-1。出 一個整數,為離開公司的員工的總數。

21

命令中的
k
在初始時,可以認為公司里一個員工也沒有。輸輸出文件的行數為

F 命令的條數加一。對於每條
F 命令,你的程序要輸出一行,的 資數,如果
k 大於目前員工的數輸 文件的最后一行包含樣例輸入


9 10I 60I 0 7S 50FI 30S 15A 5FF 2









樣例輸出
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 64 -
1
約 】
 100000

每次工資調整的調整量不超過 1000
工的工資不超過 100000I
命令的條數不超過 100000A
命令和 S 命令的總條數不超過
F 命令的條數不超過 100
新員 l l l ll



04
機平衡二叉查找樹 Treap 的分析與應用》)與一般的修改不同,這道題要求對所有人修改,如果一個一個進行的話,修改工資的時間復雜度高達
O(N)。如果我們反過來考慮,定義一個“基准值”,把所有人的工資看作“相對工資”,就是相對於基准值。這樣每次修改所有人工資僅僅需要修改基准值就行了。於是變成了一個動態統計問題,建立一個


Treap,存儲相對工資。為了方便考慮,定義基准值為 delta,相對工資 V
對應的實際工資為 F[V],則有 F[V]=V+deltaV=F[V]-delta。定義工資下限為
lo 個實際的下限,存儲相對下限就是 lowbound-delta。個新的工資記錄值
kk 為實際工資,對應的相對工資為 kde
於 准值 delta 增加 k。對於 S_k,將基准值 delta 減少 k,然后在
Treap (lowbound-delta)的元素。,如果我們是從小到大放置數據,求
(總數-k+1)小的工資即可。如果我們是從大到小放置數據,直接求排名為第
k 的數即可。
ru Tntp {(x) ((x).ch[0] ? (x).ch[0]->sz : 0)#define RSZ(x) ((x).ch[1] ? (x).ch[1]->sz : 0);apNode *r t, mem[MAXN], *pool[MAXN];x->sz = 1;x->fix = rand();








1020-2


【 定來 :
NOI 20
分析:(參考《隨
wbound 這是一對於 插
I_k 入一
lta,應把 k-delta 插入 Treap。對 ,
A_k 將基中刪除所有小由於我們總是查詢第

k 多的工資
C++代碼:
#include <cstdio>#include <cstring>#include <cstdlib>#include <ctime>const int MAXN = 100005;st ct reapNode {TreapNode *ch[2];i s fi nt z, x;i val;};struct Trea#define LSZint del_cnt, delta;int mcnt, pcntTre ooTreapNode *new_node(int val) {TreapNode* x;if (pcnt >= 0) x = pool[pcnt--];else x = mem + mcnt++;x->ch[0] = x->ch[1] = NULL;



















哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 65 -
[!c];[c];if (x == NULL) {val);}>ch[1] && x->fix > x->ch[1]->fix) {eapNode * find_kth(TreapNode*x, int k) {sz - 1);int find_kth(int k) {}x->val = val;return x;}void rotate(TreapNode* &x, int c) {TreapNode *y = x->chx->ch[!c] = y->ch[c];y->ch[c] = x;x = y;y = x->chy->sz = LSZ(*y) + RSZ(*y) + 1;x->sz = LSZ(*x) + RSZ(*x) + 1;}void insert(TreapNode* &x, int val) {x = new_node(val);} else {int c = x->val < val;insert(x->ch[c],x->sz++;if (x->fix > x->ch[c]->fix) rotate(x, !c);x->sz = LSZ(*x) + RSZ(*x) + 1;}}void insert(int val) {insert(root, val - delta);}void del_low(TreapNode* &x, int val) {if (x == NULL) return;del_low(x->ch[0], val);x->sz = LSZ(*x) + RSZ(*x) + 1;if (x->val < val) {del_low(x->ch[1], val);pool[++pcnt] = x;x = x->ch[1];del_cnt++;if (x) {if (xrotate(x, 0);}x->sz = LSZ(*x) + RSZ(*x) + 1;}}void del_low(int val) {del_low(root, val - delta);}Trif (x == NULL) return NULL;int lsz = LSZ(*x);if (k <= lsz) {return find_kth(x->ch[0], k);} else if (k == lsz + 1) {return x;} else {return find_kth(x->ch[1], k - l}}if (k < 1 || k > size()) return -1;TreapNode *x = find_kth(root, k);if (x) return x->val + delta;return -1;



































































哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 66 -
void init() {delta = 0;}};Treap tr;int main() {int n, low;srand(time(0));while (EOF != scanf("%d%d", &n, &low)) {int k;char s[4];tr.init();while (n--) {'I') {} else if (s[0] == 'A') {tr.delta += k;} else if (s[0] == 'S') {tr.size() - k + 1));}}



















1.8
ogs
因為區間是互不相交的,所以可以對區間進行排序,然后不斷的加樹解決。者


treap 樹找第 大的數即可。
1.9
ra 《 用思 《伸 樹

y-tree/
int size() {if (root) return root->sz;return 0;}mcnt = 0;pcnt = -1;root = NULL;del_cnt = 0;scanf("%s%d", s, &k);if (s[0] ==if (k >= low) {tr.insert(k);}tr.delta -= k;tr.del_low(low);} else if (s[0] == 'F') {printf("%d\n", tr.find_kth(} else {for(;;);}}printf("%d\n", tr.del_cnt);return 0;






















.2 其他例題
POJ 2761 Feed the d
可以用 Treap 樹解決,入的下一個區間的
dog,並處理請求。其實這題最好用划分

POJ 2085 Inversion
構造法求序列,用二分樹狀數組或 k
伸展樹(Splay Tree)
大部分內容來自 Crash 的論文。參考文獻:

C sh 運 伸展樹解決數列維護問題》楊 雨 展 的基本操作與應用》董的博客

-伸展樹: http://dongxicheng.org/structure/spla
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 67 -
1.9
叉 找樹 Bi ry Sort Tree)能夠支持多種動態集合操作,它可以用來表示有序集合、 建立索引等,因而在實際應用中,二叉排序樹是一種非常重要的數據結構。, 作用於二叉查找樹上的基本操作(如查找, 插入等)的時間復雜度與樹的高度成正比。對一個含)。它的主要特點是不會保證樹





1.9.2 基本操作
伸展樹的出發點是這樣的:考慮到局部性原理(剛被訪問的內容下次可能仍會被訪問,查找次數多的內容可能下一次會被訪問),為了使整個查找時間更小,被查頻率高的那些節點應當經常處於靠近樹根的位置。這樣,很容易得想到以下這個方案:每次查找節點之后對樹進行重構,把被查找的節點搬移到樹根,這種自調整形式的二叉查找樹就是伸展樹。每次對伸展樹進行操作后,它均會通過旋轉的方法把被訪問節點旋轉到樹根的位置。的父節點





Y 是根節點。這時,如果 X Y 的左孩子,我們進行一次右旋操作;編寫: 黃李龍 校核:黃李龍


.1 概述
二 查 ( na Search Tree,也叫二叉排序樹,即 Binary
從算法復雜度角度考慮,我們知道
n 個節點的完全二叉樹,這些操作的最壞情況運行時間為
O(log n)。但如果因為頻繁的刪除和插入操作,導致樹退化成一個 n 個節點的線性鏈(此時即為一個單鏈表),則這些操作的最壞情況運行時間為
O(n)。為了克服以上缺點,很多二叉查找樹的變形出現了,如紅黑樹、
AVL 樹, Treap 樹等。二叉查找樹的一種改進數據結構–伸展樹(
Splay Tree
一直是平衡的,但各種操作的平攤時間復雜度是 O(log n),因而,從平攤復雜度上看,二叉查找樹也是一種平衡二叉樹。另外,相比於其他樹狀數據結構( 如紅黑樹,
AVL 樹等),伸展樹的空間要求與編程復雜度要小得多。為了將當前被訪問節點旋轉到樹根,我們通常將節點自底向上旋轉,直至該節點成為樹根為止。 “ 旋轉” 的巧妙之處就是在不打亂數列中數據大小關系( 指中序遍歷結果是全序的)情況下,所有基本操作的平攤復雜度仍為



Olog n)。伸展樹主要有三種旋轉操作, 分別為單旋轉,一字形旋轉和之字形旋轉。 為了便於解釋,我們假設當前被訪問節點為

XX 的父親節點為 Y(如果 X 的父親節點存在), X 的祖父節點為
Z(如果 X 的祖父節點存在)。(
1)單旋轉節點
X
如果 X Y 的右孩子,則我們進行一次左旋操作。經過旋轉, X 成為二叉查找樹 T
的根節點,調整結束。上圖從左到右,表示的是右旋
X 節點操作,即將 X 節點順時針旋轉;從右到左,是左旋
Y 節點的操作,即將 Y 節點逆時針旋轉。
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 68 -
2)一字型旋轉時是各自父節點的左孩子 或者右右旋轉操作。 旋轉為一 旋轉。(節點




X 的父節點 Y 不是根節點, Y 的父節點為 Z,且 X Y 同或者同時是各自父節點的右孩子。 這時, 我們進行一次左左旋轉操作我們先旋轉

Y,再旋轉 X。 我們稱這種 字形(
3)之字形旋轉節點
X 的父節點 Y 不是根節點, Y 的父節點為 ZX Y 中一個是其父節點的左孩子而另一個是其父節點的右孩子。這時,我們進行一次左右旋轉操作或者右左旋轉操作。通常來說, 每進行一種操作后都會進行一次伸展 操作,這樣可以保證每次操作的平攤時間復雜度是 據結構與算法分析轉到根 那 也就可以把任意一個結點轉到其到根路徑上任何一






== y) y->pre->ch[0] = x; else y->pre->ch[1] = x;re = x;

操作,表示把結點 x 轉到結點 f 的下面
(Splay)O(log
2N)。 關於證明可以參見相關書籍和論文, 如《數
—C語言描述》美· Mark Allen Weiss。既然可以把任何一個結點 , 么個結點的下面( 特別地, 轉到根就是轉到空結點

Null 的下面)。 下面的利用伸展樹維護數列就要用到將一個結點轉到某個結點下面。最后附上

Splay 操作的代碼:
// node 為結點類型,其中 ch[0]表示左結點指針, ch[1]表示右結點指針
// pre 表示指向父親的指針
void Rotate(node *x, int c) // 旋轉操作, c=0 表示左旋, c=1 表示右旋
{node *y = x->pre;y->ch[! c] = x->ch[c];if (x->ch[c] != Null) x->ch[c]->pre = y;x->pre = y->pre;if (y->pre != Null)if (y->pre->ch[0]x->ch[c] = y, y->pif (y == root) root = x; // root







表示整棵樹的根結點
}void Splay(node *x, node *f) // Splay{


哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 69 -
, Rotate(x, 1); // 一字形旋轉
Rotate(x, 0), Rotate(x, 1); // 之字形旋轉
else
形旋轉轉

1.9.
怎展樹中表示任意一個區間。 比如我們要提取區間
[a,b],那么我們將 a 前面一個數對應的 的左子樹就對中的道理也是很簡單的, 將


a 前面一個數對應的結點轉到樹根后, a a 后面的數就在

for ( ; x->pre != f; )if (x->pre->pre == f) //
父結點的父親即為 f,執行單旋轉
if (x->pre->ch[0] == x) Rotate(x, 1); else Rotate(x, 0);else{node *y = x->pre, *z = y->pre;if (z->ch[0] == y)if (y->ch[0] == x)Rotate(y, 1)elseif (y->ch[1] == x)Rotate(y, 0), Rotate(x, 0); //








一字
elseRotate(x, 1), Rotate(x, 0); //
之字形旋
}}

3 在伸展樹中對區間進行操作
首先我們認為伸展樹的中序遍歷即為我們維護的數列, 那么很重要的一個操作就是么在伸結點轉到樹根, 將

b 后面一個結點對應的結點轉到樹根的右邊, 那么根右邊應了區間
[a,b]。其 根的右子樹上, 然后又將

b 后面一個結點對應的結點轉到樹根的右邊,那么[a,b]這個區間就是下圖中所示的
B 子樹。利用這個,我們就可以實現線段樹的一些功能,比如回答對區間的詢問。我們在每個結點上記錄關於以這個結點為根的子樹的信息, 然后詢問時先提取區間, 再直接讀取子樹的相關信息。還可以對區間進行整體修改,這也要用到和線段樹類似的延遲標記技術,就應地將標記向下傳遞。果我們要在




a 后面插入一些數,那么我們先把這些插入的數建成一棵伸展樹,我們可以利用分治法建立一棵完全平衡的二叉樹,就是說每次把最中間的作為當前區間的根,然后左右遞歸處理,返回的時候進行維護。接着將

a 轉到根,將 a 后面一個數對應的結點轉到根結點的右邊,最后將這棵新的子樹掛到根右子結點的左子結點上。個操作就 刪除一個區間

[a,b]內的數,像上面一樣,我們先提取區間,然后直接刪除那棵子樹,即可達到目的。最后還需注意的就是,每當進行一個對數列進行修改的是對於每個結點,再額外記錄一個或多個標記,表示以這個結點為根的子樹是否被進行了某種操作,並且這種操作影響其子結點的信息值。當然,既然記錄了標記,那么旋轉和其他一些操作中也就要相到目前為止,伸展樹只是實現了線段樹能夠實現的功能,下面兩個功能將是線段樹無法辦到的。如 還有一 是








哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 70 -
操作后,都要維護伸展樹,一種方法就是對影響到的結點從下往上執行 Update 操作。但還有一種方法,就是將修改的結點旋轉到根,因為
Splay 操作在旋轉的同時也會維護每個結點的值,因此可以達到對整個伸展樹維護的目的。最后還有一個小問題,因為數列中第一個數前面沒有數字了,並且最后一個數后面也沒有數字了,這樣提取區間時就會出一些問題。為了不進行過多的特殊判斷,我們在原數列最前面和最后面分別加上一個數,在伸展樹中就體現為結點,這樣提取區間的時候原來的第



k 個數就是現在的第 k ?1 個數。並且我們還要注意,這兩個結點維護的信息不能影響到正確的結果。序(能對結點信息進行維護):


ch[1]表示右結點指針旋轉操作,
c=0 表示左旋, c=1 表示右旋
x);
,再把 X 的標記向下傳遞
>[0] = x; els >pre->ch[1] = x;r f; ) //

一開始就將 X 的標記下傳
f,執行單旋轉
Rotate(x, 1); else Rotate(x, 0);otate(x, 1); //
之字形旋轉
elsech[1] == x)

一字形旋轉
otate(x, 0); // 之字形旋轉
行維護,而不對 X 結點進行維 , 是轉,在 過早地維護是多余的;而在一 ,但后面緊接着就是旋轉


X 結點,又會對 不少冗余的
Update 操作,能減小程序隱含的常數。最后我們看看怎么樣實現把數列中第
k 個數對應的結點轉到想要的位置。對於這個操作,我們要記錄每個以結點為根子樹的大小,即包含結點的個數,然下面看一下新的

Splay 操作的程
// node 為結點類型,其中 ch[0]表示左結點指針
// pre 表示指向父親的指針
void Rotate(node *x, int c) //{node *y = x->pre;Push_Down(y), Push_Down(/ Y /



先將 結點的標記向下傳遞( 因為 Y 在上面)
y- ch[! c] = x->ch[c];if (x->ch[c] != Null) x->ch[c]->pre = y;x->pre = y->pre;if (y->pre != Null)if (y->pre->ch[0] == y) y->pre->ch e yx->ch[c] = y, y->pre = x, Update(y); // Y



維護 結點
if (y == root) root = x; // root 表示整棵樹的根結點
}void Splay(node *x, node *f) // Splay
操作,表示把結點 x 轉到結點 f 的下面
{fo (Push_Down(x) ; x->pre !=if (x->pre->pre == f) //

父結點的父親即為
if (x->pre->ch[0] == x)else{node *y = x->pre, *z = y->pre;if (z->ch[0] == y)if (y->ch[0] == x)Rotate(y, 1), Rotate(x, 1); //





一字形旋轉
elseRotate(x, 0), Rif (y->Rotate(y, 0), Rotate(x, 0); //elseRotate(x, 1), R}Update(x); //






最后再維護 X 結點
}
可能有人會問,為什么在旋轉的時候只對 X 結點的父親進護 但
Splay 操作的最后卻又維護了 X 結點?原因很簡單。因為除了一字形旋
S lay 操形 轉

p 作里我們進行的旋轉都只對 X 結點進行,因此字 旋 中,好像在旋轉中沒有對
X 的父親進行維護
X 的父親進行維護,也是沒問題的。這樣可以節省
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 71 -
后從根開始,每次決定是向左走,還是向右走,具體見下面的代碼:
到結點 f 的下面
i Semp;/

由於要訪問 t 的子結點,將標記下傳
->size; // 得到 t 左子樹的大小
reak; // 得出 t 即為查找結點,退出循環
+ 1, t = t->ch[1];
執行旋轉
1.9 Sequence
目 持以下幾種操作:
 .  posi 0 tot 個數字。
tot 個數字統一修改為 c 。字開始的
tot 個數字,翻轉后放入原來的位  .
.  從 從
置。字開始連續
tot 個數字的和並輸出。和最大的一段子序列,並輸出最大和。

2 兩個操作,前面已經分析過了。而 34 兩個操作為修改操作,因此我們只需增加兩個標記:
same rev ,分別表示這棵子樹是否被置為一
  們給每個結點維護一個 sum,即可解決問們需要維護
4 個值: value MaxL MaxR  題。
MaxM及這個數列和最大的子序列。其中, MaxL MaxR MaxM 的值可以由 value 和這個結點子列中、全部左子樹的序列和這個結點以及橫跨左右子樹(左子樹全部包含)。

MaxR
 MaxM  有五種情況)。此每個結點還要維護一個
size 。最終得到每個結點、
value sumsize MaxL MaxR MaxM 。以及最大和子序列,因此上文中提及的額外增加的兩個

xM 應為一個很小的數(比如-100000),常的詢問結果。最后注意的是,每種修改操作
 要維 為 們 結點

(插入、刪除、修改和翻轉)過后,要對伸展樹的信息重新維護。按照前面說的,我們將修改的結點(即根結點右子結點的左子結點)
Splay 到根的位置。這樣就能保證新的伸展樹的要回收空間,不然會使用過多的空間(大概要

100MB)。並且
// 找到處在中序遍歷第 k 個結點,並將其旋轉
vo d lect(int k, node *f){int tnode *t;for (t = root; ; ) //



從根結點開始
{Push_Down(t); /tmp = t->ch[0]if (k == tmp + 1) bif (k <= tmp) //



k 個結點在 t 左邊,向左走
t = t->ch[0];else //
否則在右邊,而且在右子樹中,這個結點不再是第 k
k -= tmp}Splay(t, f); //}



.4 實例分析—NOI 2005 維護數列(
題 的 思很簡單:維護一個數列,支意

1 入: 當前數列第 posi 個數字后面插入 tot 個數字;若在數列首位插入,則。

2 除: 當前數列第 posi 個數字開始連續刪除
3 改: 當前數列第 posi 個數字開始連續
4. 翻轉:取出從當前數列第 posi 個數
5. 求和:計算從當前數列第 posi 個數
6. 求和最大子序列:求出當前數列中首先,對於
1、個 和是否被翻轉。對於第
5 個操作,我第
6 個操作的實現是最為復雜的,我,分別表示這個結點的值、這個子樹表示數列左起最大連續和、右起最大連續和以樹子樹的相關信息得到。以

MaxL 為例,我們要考慮三種情況:只在左子樹對應的序維護可以類比一下(注意
MaxM
然后由於要執行 Select 操作,因護
8 個標記或信息: same rev
因 我 問的是子序列和 要詢的
value MaxL MaxR Ma
sum 應該為 0,這樣才不會影響正值都是正確的。還有一點就是刪除操作


哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 72 -
由於
C 於操作,代碼結構也更良好。

*>ch[0])= x->val;*/->ch[0]->lmax, x->ch[0]->sum + x->lmax);x->rmax = max(x->val, x->ch[0]->rmax + x->val);>ch[0]->mmax, max(x->lmax, max(x->rmax,x->um;








更新完成 */x->ch[1]->lmax);->ch[1]->rmax, x->rmax + x->ch[1]->sum);ch[1]->mmax, max(x->lmax, x->rmax)));x->ch[0]==null_node)?0:x->ch[0]->rmax)+x->val+x->ch[1]->lmax);turn;






C++的指針回收操作過慢,因此我們人工壓一個棧回收結點指針。附
++代碼,封裝成結構體便
(測試數據在網上搜索能找到)
/* NOI2005 維護數列
* 以做模板用
* /#include <cstdio>#include <cstring>#include <algorithm>using namespace std;const int MAXN = 4000000 + 6;struct SplayTree {struct Node {Node *ch[2], *pre;int sz;int val, sum;bool rev, same;int mmax, lmax, rmax;};#define keyTree (root->ch[1]-N *root, *null_nod ode e;Node mem[MAXN];Node *que[MAXN], *pool[MAXN];int mem_cnt, top;void push_up(Node *x) {if (x == null_node) return;x->sz = 1;x->sum = x->lmax = x->rmaxint mmax = x->val;if (x->ch[0] != null_node) {push_down(x->ch[0]); /*
























有可能還沒更新完成
x->lmax = max(xmmax = max(mmax, max(xch[0]->rmax+x->val))));x->sz += x->ch[0]->sz;x->sum += x->ch[0]->s}if (x->ch[1] != null_node) {push_down(x->ch[1]); /*





有可能還沒
x->lmax = max(x->lmax, x->sum +x->rmax = max(xmmax = max(mmax, max(x->mmax = max(mmax, max(0, (x->sz += x->ch[1]->sz;x->sum += x->ch[1]->sum;}x->mmax = mmax;}vo d push_down(Node *x) { iif (x == null_node) reif (x->same) {x->same = 0;x->sum = x->val * x->sz;













哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 73 -
{= x->lmax = x->rmax = x->sum;>ch[0]->same = x->ch[1]->same = 1;l = x->ch[1]->val = x->val;if (x->rev) {->pre;down(y);]->pre = y;x->pre = y->pre;->pre->ch[ y == y->pre->ch[1] ] = x;









f null_node 時表 轉為根 */re, *z = y->pre;x;for (;;) {push_down(x); /*



需要先推下去,有可能這個區間會翻轉,這樣就不會弄錯左右子樹的

= x->ch[0]->sz;= sz) {= x->ch[0];if (x->val > 0)x->mmax} else {x->mmax = x->lmax = x->rmax = x->val;}xx->ch[0]->va}x->rev = 0;swap(x->ch[0], x->ch[1]);swap(x->lmax, x->rmax);x->ch[0]->rev ^= 1;x->ch[1]->rev ^= 1;}}/**
















旋轉操作, c=0 表示左旋,逆時針, c=1 表示右旋,順時針 */void rotate(Node *x, int c) {Node *y = xpush_push_down(x);y->ch[!c] = x->ch[c];if (x->ch[c] != null_node) x->ch[cif (y->pre != null_node) yx->ch[c] = y;y->pre = x;push_up(y);}/**











伸展的操作,將 x 節點轉到 f 下, f 要在 x 之上 示 xvoid splay(Node *x, Node *f) {push_down(x);while (x->pre != f) {if (x->pre->pre == f) {rotate(x, x->pre->ch[0] == x);} else {Node *y = x->pbool c = (z->ch[0] == y);if (y->ch[c] == x) {rotate(x, !c);rotate(x, c);} else {rotate(y, c);rotate(x, c);}}}push_up(x);if (f == null_node) root =}bool select(int k, Node *f) {Node *x = root;






















節點數了*/int szif (sz + 1 == k) break;if (k <x




哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 74 -
;ode) {pool[top--];de;x->rmax = val;{= a[m]);(x->ch[0], x, a, m);tree(x->ch[1], x, a+m+1, tot-m-1);push_up(x);d init(int n, int a[]) {null_node = mem;= null_node->ch[0] = null_node->ch[1] = null_node;h_up */t);














[l,r] 區間轉到 keyTree */2;= tmp_root;} else {k -= sz + 1;x = x->ch[1]}if (x == null_nreturn false;}}splay(x, f);return true;}int size() const {return root->sz - 2;}Node* new_node(int val) {Node *x;if (top >= 0) x =else x = mem + mem_cnt++;x->pre = x->ch[0] = x->ch[1] = null_nox->sz = 1;x->val = x->sum = x->mmax = x->lmax =x->rev = x->same = 0;return x;}void make_tree(Node *&x, Node *f, int a[], int tot)if (tot <= 0) return;int m = (tot - 1) >> 1;x new_node(x->pre = f;make_treemake_}voimemset(null_node, 0, sizeof(Node));null_node->premem_cnt = 1, top = -1;//






































開頭和末尾添加兩個節點,避免特判,方便取區間,但是節點數變為 n+2root = new_node(-1);root->ch[1] = new_node(-1);root->ch[1]->pre = root;root->sz = 2; /*



可能要用 pusmake_tree(keyTree, root->ch[1], a, n);push_up(root->ch[1]);push_up(roo}/*void ch_key_tree(int l, int r) {select(l, null_node);select(r+2, root);}void insert(int pos, int a[], int n) {Node *tmp_root = root, *tmp_rootmake_tree(root, null_node, a, n);tmp_root2 = root;rootch_key_tree(pos+1, pos);keyTree = tmp_root2;
















哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 75 -
null_node) {return;pre, **fr = que, **ta = que;] = x;>ch[1] != null_node) *ta++ = x->ch[1];x = null_node;_node) push_up(f);






歷輸出 */ull_node) return;< 2; ++i)= null_node && x->ch[i]->pre != x) {pre %d(%p)<-%d(%p)\n", x->val, x, x->ch[i]->val,x->>sz + x->ch[1]->sz) {x->ch[0]->sz+x->ch[1]->sz x, x-( pre_ord(x->ch[0]);









開始的 tot 個元素 */t-1);i l) {i int pos, int tot) {ch_key_tree(pos, pos+tot-1);t) {-1);keyTree->pre = root->ch[1]; /*






注意維護 pre */push_up(root->ch[1]);push_up(root);}void erase(Node * &x) {if (x ==}Node *f = x->*ta++ = x;while (fr < ta) {x = *fr++;pool[++topif (x->ch[0] != null_node) *ta++ = x->ch[0];if (x-}if (f != null}void pre_ord(Node *x) { /*
















中序遍
if (x == nfor (int i = 0; iif (x->ch[i] !printf("errorch ]); [0}if (x->sz != 1 + x->ch[0]-printf("error sz %d:%d %p %d\n", x->sz, 1+, >val);}if x->ch[0])printf("%d ", x->val);if (x->ch[1]) pre_ord(x->ch[1]);}void travel() { /*













輸出序列*/pre_ord(root);printf("\n");}void del(int pos, int tot) { /*



刪除 pos 位置
ch_key_tree(pos, pos+toerase(keyTree);push_up(root);}vo d make_same(int pos, int tot, int vach_key_tree(pos, pos+tot-1);keyTree->same = 1;keyTree->val = val;}v d reverse( okeyTree->rev ^= 1;}int get_sum(int pos, int toch_key_tree(pos, pos+totpush_down(keyTree);return keyTree->sum;}int max_sum() {ch_key_tree(1, size());


















哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 76 -
pl r spt;;%d", &n, &m)) {++i) scanf("%d", &a[i]);cmd);(cmd, "INSERT")) {pos, &tot);a, tot);f (0 == strcmp(cmd, "DELETE")) {scanf("%d%d", &pos, &tot);tot);trcmp(cmd, "MAKE-SAME")) {d%d%d", &pos, &tot, &c);make_same(pos, tot, c);trcmp(cmd, "REVERSE")) {&pos, &tot);t.reverse(pos, tot);} else if (0 == strcmp(cmd, "GET-SUM")) {, spt.get_sum(pos, tot));strcmp(cmd, "MAX-SUM")) {\n", spt.max_sum());, cmd);





















1.9
伸 以支持兩個線段樹無法支持的操作:在某個位置插入一些數和刪除一些連續的數。但是也帶了更大的常數和更大的代碼量。因此,在沒有必要使用伸展樹的時候,我們就不應該使用。不過有些問題看似線段樹無法解決,然而對模型進行轉化后,也能用線段樹解決,所以做題的時候不要急着出算法,還是要分析一下問題的本質,選擇最好最合適的方法解決。




1.9
1.9 P blem with Integers
push_down(keyTree);re ur keyTree->mmax; t n}};S ayT eeint a[MAXN];int main() {int n, mwhile (EOF != scanf("%dfor (int i = 0; i < n;spt.init(n, a);while (m--) {int pos, tot;char cmd[65];scanf("%s", cmd);// printf("%s\n",i 0 == strcmp f (scanf("%d%d", &for (int i = 0; i < tot; ++i) scanf("%d", &a[i]);spt.insert(pos,} else ispt.del(pos,} else if (0 == sint c;scanf("%spt.} else if (0 == sscanf("%d%d",spscanf("%d%d", &pos, &tot);printf("%d\n"} else if (0 ==printf("%d} else {printf("error cmd:%s\n"}}}return 0;}







































.5 線段樹的比較
用 展樹解決數列維護問題,可
.6 伸展樹例題
.6.1 OJ 3468 A Simple Pro
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 77 -
A1, A2, ... , AN(1N100000)。現在有Q個操作
(1
C +1, ... , Ab 分別加上 c-10000 c 10000。路:線段樹做,這里是為了練習伸展樹而用伸展樹解決此題。加一個


sum addsum 表示以這個節點為根的樹所有節點的和, add
表示這個子樹所有節點加的值。對於 C a b c 的操作,先把[a,b]區間提取出來,然后在這個
[a,b 的時候,再把這個節點的 add 值推向它的子節點 對 點的
sum 值即可。可以直接套用
I 題的代碼。
ode pr *ch[2];t ;dd;}#define keyTree (root->ch[1]->ch[0])val += x->add;Node *x) {r null_leaf");h[1]->sz;c=0








表示左旋,逆時針, c=1 表示右旋,順時針
PushDown(y);hDown(x);

題意:有N個整數的序列,值分別是
Q 100000) ≤ 操 , 作有兩種:
" a b c" 表示給 Aa, Aa"Q a b"
表示詢問 Aa, Aa+1, ... , Ab 的和。思最好的方法是用每個節點再添



]子樹的根節點的 add 加上 c 即可,在伸展; 於
Q a b 操作, 先提取[a,b]區間,然后返回這個節
NO 2005 維護數列一代碼如下:

#include <cstdio>#include <cstring>typedef long long int64;const int MAXN = 100005;st t de ruc No {N * e,in szint val, aint64 sum;;struct SplayTree {int cnt;Node mem[MAXN];Node *root, *null_leaf;void PushDown(Node *x) {if (x->add) {x->x->ch[0]->add += x->add;x->ch[1]->add += x->add;x->ch[0]->sum += (int64)(x->ch[0]->sz) * x->add;x->ch[1]->sum += (int64)(x->ch[1]->sz) * x->add;x->add = 0;}}void PushUp(if (x == null_leaf) puts("errox->sz = 1 + x->ch[0]->sz + x->cx->sum = x->add + x->val + x->ch[0]->sum + x->ch[1]->sum;}/*




























旋轉操作,
* */void Rotate(Node *x, int c) {Node *y = x->pre;Pusy->ch[!c] = x->ch[c];if (x->ch[c] != NULL) x->ch[c]->pre = y;x->pre = y->pre;if (y->pre != NULL) y->pre->ch[ y->pre->ch[1] == y ] = x;x->ch[c] = y;y->pre = x;PushUp(y);










哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 78 -
f 下,特別的 x->pre==NULL 表示的是根
if (x->pre->pre == f) {ate(x, x->pre->ch[0] == x);Node *y = x->pre, *z = y->pre;int c = (z->ch[0] == y);if (y->ch[c] == x) {Rotate(x, !c), Rotate(x, c); //




之字形旋轉
else {Rotate(y, c), Rotate(x, c); //
一字形旋轉
}(x);f == NULL) root = x;f == null_leaf) root = x;k



個節點,並轉到 f
*/ect(int k, Node *f) {*x = root;(x->ch[0]->sz + 1 != k) {shDown(x);if (k <= sz) {x = x->ch[0];k -= sz + 1;x->ch[1];Splay(x, f);e* newNode(int val) {Node *x = mem + cnt++;= x->ch[0] = x->ch[1] = null_leaf;re = NULL;x->sz = 1;x->add = 0;x->sum = x->val = val;return x;f;1, x, a);r, x, a);;L





















節點
+root = newNode(-1);] = newNode(-1);]->pre = root;}/*




x 轉到
* */void Splay(Node *x, Node *f) {PushDown(x);while (x->pre != f) {Rot} else {}}}PushUp//if (if (}/*












選擇第
*void SelNodewhilePuint sz = x->ch[0]->sz;} else {x =}}}Nodx->pre//x->p}void MakeTree(Node* &x, int l, int r, Node *f, int a[]) {if (l > r) return;int m = (l + r) >> 1;x = newNode(a[m]);x->pre =MakeTree(x->ch[0], l, mMakeTree(x->ch[1], m+1,PushUp(x)}void init(int n, int a[]) {// null_leaf























是特殊的葉節點,表示無,避免特判,因為有些不能有個 NULnull_leaf = mem;memset(null_leaf, 0, sizeof(*null_leaf));null_leaf->pre = null_leaf;cnt = 1;//




開頭和末尾添加兩個節點,避免特判,方便取區間,但是節點數變為 n 2root->ch[1root->ch[1


哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 79 -
ot->ch[1]);t64 Q ery(int a, int b) {t);>sum;, int c) {* root->ch[1]->ch[0]->sz;t q;nf("%d%d", &n, &q)) {; i < n; ++i) scanf("%d", &a[i]);"%s%d%d", cmd, &a, &b);if (cmd[0] == 'Q') {printf("%lld\n", spt.Query(a, b));%d", &c);c);













1.9 ence
ing the first in first out rule. Each time you can either push a numberinto the queue (+i), or pop a number out from the queue (-i). After a series of operation, you get asequence (e.g. +1 -1 +2 +4 -2 -4). eue sequence.ow e and asked to perform several operations:ositive number (e.g. i) that does not appear in the currentqueue sequence, then you are asked to insert the +i at position p (position starts from 0). For -i,insert it into the right most pos sequence (i.e. when encounteredwit lem t e should be exactly x).+4 -3 -4) would become (+1 +2 -1 +3 +4 -2 -3 -4) after operation'insert 1'uld become (+1 +2 -1 +4 -2 -4) after operation'rem











root->sz = 2;MakeTree(keyTree, 0, n-1, root->ch[1], a);PushUp(roPushUp(root);}in uSelect(a, null_leaf);Select(b+2, rooreturn keyTree-}void Add(int a, int bSelect(a, null_leaf);Select(b+2, root);keyTree->add += c;keyTree->sum += (int64) c}};SplayTree spt;int a[MAXN];int main() {i n, nwhile (EOF != scafor (int i = 0spt.init(n, a);while (q--) {char cmd[3];int a, b, c;scanf(} else {scanf("spt.Add(a, b,}}}return 0;}



































.6.2 HDU 4441 Queue SequProblem Description

There's a queue obeyWe call this sequence a quN you are given a queue sequenc1. insert pFirst you should find the smallest pition that result in a valid queueh en e -x, the front of the queuFor example, (+1 -1 +3.2. remove iRemove +i and -i from the sequence.For example, (+1 +2 -1 +3 +4 -2 -3 -4) woo 3 ve '.












哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 80 -
query iple, the result of query 1, query 2,query 4 in sequence (+1 +2 -1 +4 -2 -4) is 2, 3(obtained by -1 + 4), -2 correspond.g the number ofope io 100000). The following n lines with be 'insert p', 'remove i' or 'query i'(0




p nt sequence), 1 i, i is granted to be in the sequence).each case, the sequence is empty initially.

u
 ef e e ft ea B or ach case, pop


 ng the id of the test case.between +i and -i.
nseuemuese 0mse 1






am
as
及區間的動態刪除和詢問,可以想到伸展樹,也可以用其他的平衡樹做,比如
SBT(Size Balance Tree),這里用伸展樹,直接用了 NOI2005 維護數

3.Output the sum of elements between +i and -i. For exam

Input
There are less than 25 test cases. Each case begins with a number indicatinrat ns n (1
n
length (curreInThe input is terminated by EOF.


O tput
rint a line "Case #d:" indicatiA er ch eration, output the sum of elements

Sample Input
10insert 0insert 1query 1query 2i rt 2q ry 2re ove 1remove 2insert 2q ry 36in rti rt nse 0re ove 2query 1in rtquery 2

















S ple Output
C e #1:2-120Case #2:0-1







Source
2012 Asia Tianjin Regional Contest
思路:題目中的 remove query 涉列的模板。

哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 81 -
面 現。在 用一個樹狀數組去找了,二分找到最小的正整數

a,具體應用看代碼中的實現。然
a 之前有 x 個正整數,那么 們 數之后,要求
-a 盡量往右,那么我們找到第 x+1
個負 數 個位置 p_,因為可能沒有第 x+1 負整數,所以注意特判。

2.remove i
i -i 對應的節點指針,刪除時,先把節點用
 spla 3

操作  所在的序列位置,刪除即可。知道節點
。跟 remove 一樣,找到兩數的位置后用 splay 操作提取區間求和 可

ncns ntruti)) {;t






記錄每個值對應的節點,所有數都加上 offset 偏移,避免負數
ntid *x) {if (x == null_node) return;0;



下 分析各個操作的實
1.insert pp
位置插入一個沒在序列中的最小正整數,因為 n<=100000,可以后插入
a p 位置,還要插入-a,因為是隊列,所以如果我 的
-a 就應該放在開始的 x 個負整整 的位置
p_,然后插入到這把
i -i 移除。我用一個數組記

 y  旋轉到根,然后就能
. query ii -i 之間的數的和即 。

#include <cstdio>#i lude <cstring>#include <cassert>#include <algorithm>using namespace std;typedef long long int64;co t i MAXN = 200000 + 5;st ct TreeArray {in c[MAXN];#define lowbit(x) ((x)&(-x))void add(int i, int v) {for (; i < MAXN; i += lowbit(c[i] += v;}}int sum(int i) {int s = 0;for (; i > 0; i -= lowbit(i)) {s += c[i];}return s;}};struct SplayTree {s c od tru t N e {Node *ch[2], *pre;int sz, psz, nsz;int val;int64 sum;}#define keyTree (root->ch[1]->ch[0])Node *root, *null_node;Node mem[MAXN];Node *que[MAXN], *pool[MAXN];in mem_cnt, top;Node *vp[MAXN]; //i offset;v push_up(Node ox->sz = 1;if (x->val > 0) {x->psz = 1, x->nsz =








































哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 82 -
sz = 0;>s* c=1 */c) {de) x->ch[c]->pre = y;re->ch[ y == y->pre->ch[1] ] = x;





 * id/* spl


 f 要在 x 之上, f null_node 時表示 x 轉為根 */ x>pre->ch[0] == x);->pre, *z = y->pre;->ch[0] == y);if (y->ch[c] == x) {else {tate(y, c);olfor (;;) {int sz = x->ch[0]->sz;+ 1 == k) break;= sz) {= x->ch[0];{laturn true;const {return root->sz - 2;e(int val) {} else if (x->val < 0) {x->psz = 0, x->nsz = 1;} else {x->psz = x->n}x- um = x->val;for (int c = 0; c < 2; ++c) {if (x->ch[c] != null_node) {x->sz += x->ch[c]->sz;x->psz += x->ch[c]->psz;x->nsz += x->ch[c]->nsz;x->sum += x->ch[c]->sum;}}}/*
































旋轉操作, c=0 表示左旋,逆時針, 表示右旋,順時針
void rotate(Node *x, intNode *y = x->pre;y->ch[!c] = x->ch[c];if (x->ch[c] != null_nox->pre = y->pre;if (y->pre != null_node) y->px->ch[c] = y;y->pre = x;push_up(y);}









伸展的操作,將 節點轉到 f
vo ay(Node *x, Node *f) {while (x->pre != f) {if (x->pre->pre == f) {rotate(x, x-} else {Node *y = xbool c = (zrotate(x, !c);rotate(x, c);}rorotate(x, c);}}}push_up(x);push_up(f);if (f == null_node) root = x;}bo select(int k, Node *f) {Node *x = root;if (szif (k <x} else {k -= sz + 1;x = x->ch[1];}if (x == null_node)return false;}}sp y(x, f);re}int size()}Node* new_nod





































哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 83 -
ull_node;ll od ull_node->ch[1] = null_node;

開頭和末尾添加兩個節點,避免特判,方便取區間,但是節點數變為 n+2id h_ nt l, int r) {(Node * &x) {== null_node) {return;}x->pre, **fr = que, **ta = que;pool[++top] = x;if (x->ch[0] != null_node) *ta++ = x->ch[0];if (x->ch[1] != null_node) *ta++ = x->ch[1];}x = null_node;push_up(f);}void del(int pos, int tot) {ch_key_tree(pos, pos+tot-1);erase(keyTree);push_up(root);}int64 get_sum(int l, int r) {ch_key_tree(l, r);return keyTree->sum;}void pre_ord(Node *x) {if (x == null_node) return;for (int i = 0; i < 2; ++i)if (x->ch[i] != null_node && x->ch[i]->pre != x) {fprintf(stderr, "error pre %d(%p)<-%d(%p)\n", x->val, x, x->ch[i]->val,x->ch[0]);exit(-1);}if (x->sz != 1 + x->ch[0]->sz + x->ch[1]->sz) {fprintf(stderr, "error sz %d:%d %p %d\n", x->sz, 1+x->ch[0]->sz+x->ch[1]->sz, x, x->val);exit(-1);Node *x;if (top >= 0) x = pool[top--];else x = mem + mem_cnt++;memset(x, 0, sizeof(Node));x->pre = x->ch[0] = x->ch[1] = nvp[val+offset] = x;x->sz = 1;if (val > 0) x->psz = 1;else if (val < 0) x->nsz = 1;x al = x->sum = ->v val;return x;}void init(int n) {memset(vp, 0, sizeof(vp));offset = n;null_node = mem;memset(null_node, 0, sizeof(Node));nu _n e->pre = null_node->ch[0] = nmem_cnt = 1, top = -1;//root = new_node(0);root->ch[1] = new_node(-2*n-1);root->ch[1]->pre = roopush_up(root);t;}/ [l,r]o c y_tree(i






























































 * 將 實際
ke 區間轉到 keyTree */ v select(l, null_node);select(r+2, root);}void eraseif (xNode *f =*ta++ = x;while (fr < ta) {Node *x = *fr++;vp[x->val + offset] = NULL;









哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 84 -
}if (x->ch[0]) pre_ord(xprintf("%d ", x->val);//printf("%d(%p) ", x->val,if (x->ch[1]) pre_ord(x->ch[1]);");d %d\n", v, v + offset);= NULL) {l, int r) {return keyTree->sz;}keyTree->pre = root->ch[1];push_up(root->ch[1]);ot);int m = (l + r) >> 1;no) {}t_size(-1, 1, l) < no) ++l;}i+offset] == NULL) return -1;int l = get_sz(0, i), r = get_sz(0, -i);get_sum(l, r);>ch[0]);x);}void travel() {printf("travel() ");pre_ord(root);printf("\n}/******************** Problem Operation **********/int get_sz(int t, int v) {//printf("get_sz %if (vp[v+offset] =fprintf(stderr, "vp %d %d\n", v, v + offset);//while (1);assert(vp[v+offset]);}splay(vp[v+offset], null_node);if (t == 0) return root->ch[0]->sz;else if (t > 0) return root->ch[0]->psz + 1;else return root->ch[0]->nsz + 1;}int get_size(int t, intch_key_tree(l, r);if (t == 0)else if (t > 0) return keyTree->psz;else return keyTree->nsz;void ins(int p, int v) {//printf("ins %d %d\n", p, v);ch_key_tree(p, p-1);keyTree = new_node(v);push_up(ro}int loc_ne(int no) {int l = 1, r = size();while (l < r) {int s = get_size(-1, 1, m);if (s <l = m + 1;} else {r = m;}if (gereturn l;void rm(int v) {//if (vp[v+offset] == NULL) return;int l = get_sz(0, v);del(l, 1);}int64 query( t i) { inif (vp[i+offset] == NULL || vp[-//printf("query %d %d\n", l, r);return}};SplayTree spt;TreeArray ta;












































































哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 85 -
l + r) >> 1;}}int n, ncase = 0;e;d, &d);++d;t_minp();-1);spt.ins(d, a); // insert +a.get_sz(1, a);.loc_ne(as);//printf("loc %d => %d\n", as, p_);spt.ins(p_, -a);spt.rm(d);ta.add(d, 1);('q' == cmd[0]) { // query d("%lld\n", spt.query(d));spt.travel(); puts("");


















,可以直接使用雙線鏈表進行模擬,但是需要一些處理技巧,也可以用伸展樹模擬。

int get_minp() {int l = 1, r = MAXN - 1;while (l < r) {int m = (if (ta.sum(m) >= 1) {r = m;} else {l = m + 1;}return l;int main() {while (EOF != scanf("%d", &n)) {++ncasprintf("Case #%d:\n", ncase);memset(ta.c, 0, sizeof(ta.c));for (int i = 1; i <= n; ++i) ta.add(i, 1);spt.init(n);for (int i = 0; i < n; ++i) {char cmd[32];int d;scanf("%s%d", cm// fprintf(stderr, "%d:%s %d\n", i, cmd, d);if ('i' == cmd[0]) { // insert pint a = geta.add(a,int as = sptint p_ = spt} else if ('r' == cmd[0]) { // remove dspt.rm(-d);} else ifprintf} else {}//}}return 0;}





































其他例題:
HDU 4286 Data Handler 模擬題
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 86 -
2章 動態規划
寫:周 校核:黃李龍
2.1.
推的概念和基本思想:給定一個數的序列 H0,H1,,Hn,…若存在整數 n0,使當
n>n0
條件,利用特定關系得出中間推論,直至得到結果的算法。遞推算法分為順推和逆推兩種。,確認:能否容易的得到簡單情況的解?后,假設:規模為


N-1 的情況已經得到解決。當規模擴大到
N 時,如何枚舉出所有的情況,並且要確保對於每一種子

2.1.
所有的信都裝錯信封,共有多少種不同情況。

F(N-2)已經得到,重點分析下面的情況:可得,
f(n)=(n-1)*(f(n-1)+f(n-2))
int main(){

2.1 遞推
編 洲
1 遞推原理
遞 時
,可以用等號(或大於號、小於號)Hn 與其前面的某些項 Hi(0<i<n)聯系起來,這樣的式子就叫做遞推關系。遞推定義: 遞推算法是一種簡單的算法,即通過已知順推法: 從已知條件出發,逐步推算出要解決的問題的方法叫順推。 如斐波拉契數列,設它的函數為



f(n),已知 f(1)=1f(2)=1;f(n)=f(n-2)+f(n-1)(n>=3,nN)。則我們通過順推可以知道
,f(3)=f(1)+f(2)=2,f(4)=f(2)+f(3)=3……直至我們要求的解。逆推法: 從已知問題的結果出發,用迭代表達式逐步推算出問題的開始的條件,即順推法的逆過程,稱為逆推。


2.1.2 一般的思路
首先然最后,重點分析:情況都能用已經得到的數據解決。



3 經典題目
2.1.3.1 HDU 1465 不容易系列之一
1.題目出處/來源: HDU 1465 不容易系列之一
2.題目描述:某人寫了
n 封信和 n 個信封,如果所有的信都裝錯了信封。求
3.分析:
N=1 2 時,易得解,假設 F(N-1)和當有
N 封信的時候,前面 N-1 封信可以有 N-1 或者 N-2 封錯裝前者,對於每種錯裝,可從
N-1 封信中任意取一封和第 N 封錯裝,故=F(N-1)*(N-1)
后者簡單,只能是沒裝錯的那封和第 N 封交換信封,沒裝錯的那封可以是前面 N-1 封中的任意一個,故
= F(N-2) * (N-1)
由此
4.代碼:
#include<stdio.h>__int64 a[21]={0,0,1,2};int i,n;for(i=4;i<=20;i++)



哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 87 -
a[i]=(i-1)*(a[i-1]+a[i-2]);}

2.1.3
排成一行的n個方格,用紅(Red)、粉(Pink)、綠(Green)三色塗每個格子,每格塗一色,要求任何相鄰的方格不能同色,且首尾兩格也不同色.求全部的滿足要求的塗法
.
種填塗方法。個方格和
n-2 個方格填充得到。
F[n-1]種填塗方法。所以從 n-1 格擴充到
n 共有 F(n-1)種方法。時候有兩種填法。

n-1] + 2 * f[n-2];1] = 3;

#include <stdio.h>i{] = {0, 3, 6, 6};return}




2.2
寫:周洲 校核:黃李龍
while(scanf("%d",&n)!=EOF)printf("%I64d\n",a[n]);return 0;


.2 HDU 2045 不容易系列之(3)——LELE RPG 難題
1.題目出處/來源: HDU 2045 不容易系列之(3)——LELE RPG 難題
2.題目描述:有
3
.分析:
數組 F[i]保存 i 個方格有多少
n 個方格可以由 n-1
比如,在一塗好的 n-1 個格子里最后再插入一個格子,就得到了 n 個格子了。因為已經填好
n-1 的格子中,每兩個格子的顏色都不相同。所以只能插入一種顏色。而
n-1 個格子一共有格若前

n-1 不合法,而添加一個后變成合法,即前 n-2 個合法,而第 n-1 個與第 1 個相同。這所以



f[n] = f[f[f[2] = 6;f[3] = 64



.代碼:
#include <math.h>nt main()int i;__int64 d[51for (i = 4; i < 51; i++)d[i] = d[i-1] + 2*d[i-2];while (scanf("%d", &i) != EOF)printf("%I64d\n", d[i]);0;








背包問題
參考文獻:《背包九講》編


哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 88 -

V 的背包。第 i 件物品的費用是 c[i],價值是 w[i]。求解將哪些物品裝入背包可使價值總和最大。子問題定義狀態:即

f[i][v]表示前 i 件物品恰放入一個容量為 v 的背包可以獲得的最大價值。則其狀態轉移方程便是:

] ax v
它詳細解釋一下:“將前 i 件物品放入容量為 v 的背包中”這個子問題,若只考慮第
i 物 的策略(放或不放),那么就可以轉化為一個只牽扯前 i-1 件物品的問題。如果不放,此時能獲得的最大價值就是

f[i-1][v-c[i]]再加上通過放入第 i 件物品獲得的價值
w[i]
i=1..N,每次算出來二維數組
f[i][0..V]的所有值。那么,如果只用一個數組 f[0..V],能不能保證第 i 次循環結束后 f[v]
中表的

f[i][v]f[i][v-c[i]]推知,與本題意不符,但它卻是另一個重要的背包問題
P02 最簡捷的解決方案,故學習只用一維數組解 01 背包問題是十分必要的的物品過程,以后的代碼中直接調用不加說明。


01 背包中的物品,兩個參數 costweight 分別表明這件物品的費用和價值。

2.2.1 背包的入門和進階
一、 01 背包問有
N 件物品和一個容量為基本思路這是最基礎的背包問題,特點是:每種物品僅有一件,可以選擇放或不放。用



f[i][v =m {f[i-1][v],f[i-1][ -c[i]]+w[i]}
這個方程非常重要,基本上所有跟背包相關的問題的方程都是由它衍生出來的。所以有必要將件 品第


i 件物品,那么問題就轉化為“前 i-1 件物 品放入容量為 v 的背包中”,價值為 f[i-1][v]
;如果放第 i 件物品,那么問題就轉化為“前 i-1 件物品放入剩下的容量為 v-c[i]的背包中”優化空間復雜度以上方法的時間和空間復雜度均為


O(VN),其中時間復雜度應該已經不能再優化了,但空間復雜度卻可以優化到
O。先考慮上面講的基本思路如何實現,肯定是有一個主循環示的就是我們定義的狀態

f[i][v]呢? f[i][v]是由 f[i-1][v]f[i-1] [v-c[i]]兩個子問題遞推而來,能否保證在推
f[i][v]時(也即在第 i 次主循環中推 f[v]時)能夠得到 f[i-1][v]f[i-1][v-c[i]]
的值呢?事實上,這要求在每次主循環中我們以 v=V..0 的順序推 f[v],這樣才能保證推
f[v]f[v-c[i]]保存的是狀態 f[i-1][v-c[i]]的值。偽代碼如下:
for i=1..Nfor v=V..0f[v]=max{f[v],f[v-c[i]]+w[i]};


其中的 f[v]=max{f[v],f[v-c[i]]}一句恰就相當於我們的轉移方程 f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]}
,因為現在的 f[v-c[i]]就相當於原來的 f[i-1][v-c[i]]。如果將 v 循環順序從上面的逆序改成順序的話,那么則成了。事實上,使用一維數組解


01 背包的程序在后面會被多次用到,所以這里抽象出一個處理一件
01 背包中過程
ZeroOnePack,表示處理一件
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 89 -
eroOnePack(cost,weight)t

包問題題目中,事實上有兩種不太相同的問法。有的題目要求“恰好裝滿背包”時的最優解,有的題目則,那么在初始化時除了

f[0]0 其它 f[1..V]均設為 是一種恰好裝滿背包的最優解。果並沒有要求必須把背包裝滿,而是只希望價格盡量 ,初始化時應該將

f[0..V]全部設

f 數組事實上就是在沒有任何物品可以放入背包時的合法狀態。如果要求背包恰好裝滿,那么此時只有容量為
0 的背包可能被價值為 0
noth 有合法的解,屬於未定義的狀態,它們的值就都應該是
-∞了。如果背包並非必須被裝滿,那么 任何容量的背包都有一個合法解“什么都不 為
0,所以初始時狀態的值也就全部為 0 了。。個常數優化碼中有


for v=V..1,可以將這個循環的下限進行改進。
r i=1..N
以改成
r i=1..nm{w[i..n]},c[i]}

、完全背包問題有
N 種物品和一個容量為 V 的背包,每種物品都有無限件可用。第 i 種物品的費用是
procedure Zfor v=V..cosf[v]=max{f[v],f[v-cost]+weight}


注意這個過程里的處理與前面給出的偽代碼有所不同。前面的示例程序寫成 v=V..0 是為了在程序中體現每個狀態都按照方程求解了,避免不必要的思維復雜度。而這里既然已經抽象成看作黑箱的過程了,就可以加入優化。費用為

cost 的物品不會影響狀態 f[0..cost-1]
,這是顯然的。有了這個過程以后,
01 背包問題的偽代碼就可以這樣寫:
for i=1..NZeroOnePack(c[i],w[i]);

初始化的細節問題我們看到的求最優解的背並沒有要求必須把背包裝滿。一種區別這兩種問法的實現方法是在初始化的時候有所不同。如果是第一種問法,要求恰好裝滿背包




-∞,這樣就可以保證最終得到的 f[N]
如 大為
0。為什么呢?可以這樣理解:初始化的

ing“恰好裝滿”,其它容量的背包均沒裝”,這個解的價值這個小技巧完全可以推廣到其它類型的背包問題,后面也就不再對進行狀態轉移之前的初始化進行講解一前面的偽代由於只需要最后





f[v]的值,倒推前一個物品,其實只要知道 f[v-w[n]]即可。以此類推,對以第
j 個背包,其實只需要知道到 f[v-sum{w[j..n]}]即可,即代碼中的
fofor v=V..0


fobound=max{V-sufor v=V..bound


這對於 V 比較大時是有用的。二

哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 90 -
c[i],個問題非常類似於
01 背包問題,所 不同的是每種物品有無限件。也就是從每種物品的不是常數了,求解狀態


f[i][v]的時間是 O(v/c[i]),總的復雜度可以認為是 O(V*Σ (V/c[i])),是比較大的

01 背包問題的基本思路加以改進,得到了這樣一個清晰的方法。這說明 01 背包問題的單有效的優化,是這樣的:若兩件物品

ij 滿足 c[i]<=c[j]
w[i]& 慮。這個優化的正確性顯然:任何情況下都可將價值 成物美價廉的
i,得到至少不會更差的方案。對於 隨機生成的數據,這個方法往往會大大減少物品的件數,從而加快速度。然而這個並不能改善最壞情況的復雜度,因為有可能特別設計的數據可以一件物品也去不掉。的


O(N^2)地實現,一般都可以承受。另外,針對背包問題而言,比較不 掉,然后使用類似計數排序的做法,計算出程序。問題求解




01V/c[i]
件,於是可以把第 i 種物品轉 解這個
01 背包問題。這樣完全沒有改進基本 包、價值為

w[i]*2^k 的若干件物品,其中
i 種物品,總可以表示成若干個
2^k 件物品的和。這樣把每種物品拆成 O(log V/c[i])件物品,是一個更優的

O(VN)的算法。價值是
w[i]。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值總和最大。基本思路這角度考慮,與它相關的策略已並非取或不取兩種,而是有取



0 件、取 1 件、取 2
件……等很多種。如果仍然按照解 01 背包時的思路,令 f[i][v]表示前 i 種物品恰放入一個容量為
v 的背包的最大權值。仍然可以按照每種物品不同的策略寫出狀態轉移方程,像這樣:

f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v}
這跟 01 背包問題一樣有 O(VN)個狀態需要求解,但求解每個狀態的時間已經。 將方程的確是很重要,可以推及其它類型的背包問題。但我們還是試圖改進這個復雜度。一個簡單有效的優化完全背包問題有一個很簡





gt;=w[j],則將物品 j 去掉,不用考小費用高得
j 換這個優化可以簡單錯的一種方法是:首先將費用大於

V 的物品去費用相同的物品中價值最高的是哪個,可以
O(V+N)地完成這個優化。這個不太重要的過程就不給出偽代碼了,希望你能獨立思考寫 出偽代碼或轉化為

01 背包既然
01 背包問題是最基本的背包問題,那么我們可以考慮把完全背包問題轉化為背包問題來解。最簡單的想法是,考慮到第
i 種物品最多選化為
V/c[i]件費用及價值均不變的物品,然后求思路的時間復雜度,但這畢竟給了我們將完全背包問題轉化為
01 背 問題的思路:將一種物品拆成多件物品。更高效的轉化方法是:把第

i 種物品拆成費用為 c[i]*2^kk
滿足 c[i]*2^k<=V。這是二進制的思想,因為不管最優策略選幾件第很大的改進。但我們有


O(VN)的算法
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 91 -
使用一維數組,先看偽代碼:偽代碼與
P01 的偽代碼只有 v 的循環次序不同而已。 為什么這樣一改就可行呢?首先想想為什么
P01 中要按照 v=V..0 的逆序來循環。這是因為要保證第 i 次循環中的狀態
f[i][v]是由狀態 f[i-1] [v-c[i]]遞推而來。換句話說,這正是為了保證每件物品只選一次,保證在考慮“選入第
i 件物品”這件策略時,依據的是一個絕無已經選入第 i
件物 果 f[i-1][v-c[i]]。而現在完全背包的特點恰是每種物品可選無限件,所以在考慮 種物品”這種策略時,卻正需要一個可能已選入第
i 種物品的子結果
f[i] 以並且必須采用 v=0..V 的順序循環。這就是這個簡單的程序為何成立的 理

for 循環的次序可以顛倒。這個結論有可能會帶來算 時

f[i][v-c[i]]的狀態轉移方程顯式地寫出來,代入原方程中,會發現該方程可以等價地變形成這種形式:

][v]=m
用一維數組實現,便得到了上面的偽代碼。
N 種物品和一個容量為 V 的背包。第 i 種物品最多有 n[i]件可用,每件費用是
c[i]]。求解將哪些物品裝入背包可使這些物品的費用總和不超過背包容量,且價值包問題很類似。基本的方程只需將完全背包問題的方程略微一改即可, 物品有


n[i]+1 種策略:取 0 件,取 1 件……取 n[i]件。令 f[i][v]表示前
i 種物品恰放入一個容量為 v 的背包的最大權值,則有狀態轉移方程:
k*c[i]]+k*w[i]|0<=k<=n[i]}01
背包求解:把第 i 種物品換成 n[i]01 背包中的物品,則得到了物品數為Σ
n[i]01 背包問題,直接求解,復雜度仍然是 O(V*Σ
n[i]
這個算法
for i=1..Nfor v=0..Vf[v]=max{f[v],f[v-cost]+weight}


你會發現,這個品的 子結“加選一件第

i[v-c[i]]
,所以就可道 。值得一提的是,上面的偽代碼中兩層法 間常數上的優化。這個算法也可以以另外的思路得出。例如,將基本思路中求解




f[i ax{f[i-1][v],f[i][v-c[i]]+w[i]}
將這個方程最后抽象出處理一件完全背包類物品的過程偽代碼:

procedure CompletePack(cost,weight)for v=cost..Vf[v]=max{f[v],f[v-c[i]]+w[i]}


二、多重背包問題有價值是

w[i
總和最大。基本算法這題目和完全背因為對於第


i
f[i][v]=max{f[i-1][v-
復雜度是 O(V*Σ n[i])。轉化為
01 背包問題另一種好想好寫的基本方法是轉化為

)
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 92 -
將它轉化為 01 背包問題之后能夠像完全背包一樣降低復雜度。仍然考慮二進制的思想,我們考慮把第
i 種物品換成若干件物品,使得原問題中第 i 種物品可取的每種策略——取
0..n[i]件——均能等價於取若干件代換以后的物品。另外,取超過 n[i]件法是:將第
i 種物品分成若干件物品,其中每件物品有一個系數,這件物品的費用和價值均是原來的費用和價值乘以這個系數。使這些系數分別為
1,2,4,...,2^(k-1),n[i]-2^k+n[i]

件的第 i 種物品。另外這種方法也能保證對於
0..n[i]間的每一個整數,均可以用若干個系數的和表示,這個證明可以分
0. 望你自己思考嘗試一下。
i 種 物 品 分 成 了 O(log n[i]) 種 物 品 , 將 原 問 題 轉 化 為 了 復 雜 度 為
<m 01 背包問題,是很大的改進。重背包中物品的過程,其中
amount 表示物品的數

plePack(cost,weight,amount)cost,k*weight)amount=amount-knt*weight)



譯成程序代碼以后,單步執行幾次,或者頭腦加紙筆模擬一下,也許就會慢慢理解了。

2.2
2.2 one Collector
V 的石頭的袋子, 價值最大,求出最大價值。義,直接套用模版即可。但是我們期望的策略必不能出現。方




1
,且 k 是滿足 n[i]-2^k+1>0 的最大整數。例如,如果 n[i]13,就將這種 物品分成系數分別為
1,2,4,6 的四件物品。分成的這幾件物品的系數和為
n[i],表明不可能取多於
.2^k-1 2^k..n[i]兩段來分別討論得出,並不難,希這 樣 就 將 第

ath>O(V*Σ log n[i])的下面給出
O(log amount)時間處理一件多量:

procedure Multiif cost*amount>=VCompletePack(cost,weight)returninteger k=1while k<amountZeroOnePack(k*k=k*2ZeroOnePack(amount*cost,amou








希望你仔細體會這個偽代碼, 如果不太理解的話,不妨翻
.2 經典題目
.2.1 HDU 2602 B
1.題目出處/來源: HDU 2602 Bone Collector2
.題目描述: 知道了 N 塊石頭的體積和價值,有一個最多能裝體積為從
N 塊石頭里找出一些石頭放在袋子里使得總
3.分析:符合 01 背包的定
4.代碼:
#include <stdio.h>#include <string.h>struct A{int val;




哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 93 -
int b)a>b?a:b;}intscanf("%d",&T);("%d%d",&N,&V);=1;i<=N;i++)}







2.2.2
.題目出處/來源: HLG 1053 Warcraft III
的單位,每個單位都可以無限生產,但是生產都有一定的花費,每種單位都有自己的作戰價值,現在知道了總的金錢數,問做多可以生產出的單位作戰可。



# iint b[100000];int f[100000];i{while(t--){scanf("%d%d",&v,&m);scanf("%d%d",&a[i],&b[i]);for(i=0;i<m;i++)f[j]=max(f[j],f[j-b[i]]+a[i]);printf("%d\n",f[v]);int v;}E[1011];int max(int a,{returnint main(){int T;int dp[1011];int i,l;N,V;while(T--){scanffor(iscanf("%d",&E[i].val);for(i=1;i<=N;i++)scanf("%d",&E[i].v);memset(dp,0,sizeof(dp));for(i=1;i<=N;i++)for(l=V;l>=E[i].v;l--)dp[l]=max(dp[l],dp[l-E[i].v]+E[i].val);printf("%d\n",dp[V]);}return 0;



































.2 HLG 1053 Warcraft III
12
.題目描述:有 N 種作戰價值之和是多少。

3.分析:從完全背包的定義能夠看出該題屬於完全背包的題型,直接套用模版即
4.代碼:
#include<stdio.h>#include<string.h>define max(a,b)(a)>(b)?(a):(b)nt a[100000];nt main()int i,m,v,k,j,t;scanf("%d",&t);memset(f,0,sizeof(f));for(i=0;i<m;i++)for(j=b[i];j<=v;j++)









哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 94 -
return 0;}

2.2.2.3 HD 2191 悼念 512
目出處/來源: HD 2191 悼念 512
.題目描述:災區同胞的生命,心系災區同胞的你准備自己采購一些糧食支援災區,現在假設限的資金最多能采購多少公斤糧食呢?



#i{00], d[200],s[200];intfori++)f[i] = 0;oif (d[i]*s[i]>=v) //






使用完全背包
{for(j=d[i];j<=v;j++)if(f[j]<f[j-d[i]]+p[i])f[j]=f[j-d[i]]+p[i];con ue;0; j--)[i])d;}k=k*2;for (j=v;j-s[i]*d[i]>=0;j--) //01









背包
if (f[j]<f[j-s[i]*d[i]]+s[i]*p[i])}}


1.題
2
為了挽救你一共有資金
n 元,而市場有 m 種大米,每種大米都是袋裝產品,其價格不等,並且只能整袋購買。請問:你用有


3.分析:多重背包,注意每種背包對應的情況。
4.代碼:
include<stdio.h>nt main()int f[200], p[2t, n, q, v;int i, j, k;scanf("%d",&t);while (t--){scanf("%d %d",&v,&n);(i=0; i<n; i++)scanf("%d %d %d",&d[i],&p[i],&s[i]);for (i=0; i<200;f r (i=0;i<n;i++){{else f[j]=f[j];}tin}k=1;while (k<s[i]) // 01



















背包的變形,注意優化
{for (j=v; j-k*d[i]>={if (f[j]<f[j-k*d[i]]+k*pf[j]=f[j-k* [i]]+k*p[i];else f[j]=f[j]s[i]=s[i]-k;}{f[j]=f[j-s[i]*d[i]]+s[i]*p[i];else f[j]=f[j];










哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 95 -
printf("%d\n",f[v]);
2.3
《深入分析區間型動態規划》寫 黃 校核:黃李龍

2.3 引子
間 態 法,而是一種分類。某些動態規划的方程的狀態是以區間作為狀態,並在區間之間進行狀態轉移,獲得最去理 特別的要注意題目


2.3.2 N Ip
Hrbustoj 1212
ingjun/archives/2010/51274.html
}}return 0;}



區間動態規划
參考文獻:劉汝佳、黃亮《算法藝術與信息學競賽》鄭州市第九中學 張旭祥編 李龍 :



.1
區 動 規划不是一種方優解,所以區間動態規划需要以具體的題目解 的動態規划題,需要自己多做 這種類別 題,總結這類題的特點,中最優子結構的證明,明白為什么這么做是正確。下面以




NOIp2000 乘積最大
POJ 1141 Brackets SequenceN Ip 06 O 20
能量項鏈
P 1 OJ 191 棋 分 盤 割作為例題對區間動態規划做介紹。

O 2000 乘積最大
題目鏈接:參考解答:
http://blog.wledu.org/user1/yingq
題 : 意一個長度
N 的數字串,要求選手使用 K 個乘號將它分成 K+1 個部分,找出一種分法,使得這
K+1 個部分的乘積能夠為最大。題意,主持人還舉了如下的一個例子:一個數字串

: 312,當 N=3K=1 時會有以下兩種分法:一行共有
2 個自然數 N, K (6<=n<=401<=k<=6, K < N)
於每組測試數據,輸出一行,為所求得的最大乘積(一個自然數)。樣例
:
設有同時,為了幫助選手能夠正確理解有

1
3*12=362
31*2=62
這時,符合題目要求的結果是: 31*2=62
現在,請你設計一個程序,求得正確的答案。輸入有多組測試數據,對於每組測試數據:第第二行是一個長度為



N 的數字串。對

哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 96 -
2
的乘法,當
k=2 的時候,用窮舉的方法

K=1 ,這樣就得到 n-1 個子串的乘積:值為

F[n-1][1]*an
個數作為被乘數:
1a2 ··· *a n-2*a n-1an , a1a2 …*a n-3 a n-2* an-1 an , a1*a2 ··· an-3 a n-2* an-1an
n-2 個數中插入一個乘號的最大值,則的最大值為 F[n-2][1]*an-1an

數:入一個乘號的最大值
,則的最大值為 F[n-3][1]*an-2anF[2][ 1]*a3

an-2an-1an
為:,
F[n-3][1]*an-2an-1an,···, F[2][1]*a3
anj 個乘號的最大值, val[i][j]表示從 ai aj 的子串作為一個數字的值,則可得到動規方程:

i], F[i-2][j-1]*val[i-1][i], F[i-3][j-1]*val[i-2][i],··· , F[j][j-1]* k)

數列本身)題是在子串中插入
j-1,j-2……1,0 個乘號,因此乘號個數作為階段的划分(
j
變化划分狀態。:在每個階段的每種狀態中做出決策。轉換。需要使用大整數乘法,也可以使用


Java BigInteger
輸入
4 21231

輸出
6
分析:設字符串長度為
n,乘號數為 k,如果 n=50k=1 時,有(n-1)=49 種不同時,有
C(250-1)=1176 種乘法,即 C(k,n-1)種乘法,當 nk 更大就不行了。設數字字符串為

a1a2an
時:一個乘號可以插在 a1a2an 中的 n-1 個位置
a1*a2an, a1a2*a3an, , a1a2a n-1*an ( 這相當於是窮舉的方法)此時,最大值
= max{a1*a2an, a1a2*a3an, , a1a2a n-1*an }K=2
時,二個乘號可以插在 a1a2an n-1 個位置的任兩個地方, 把這些乘積分個類,便於觀察規律:①最后一個數作為被乘數:


a1a2··· *a n-1*an , a1a2 ··· *a n-2 a n-1*an , a1*a2 ··· a n-3 a n-2 a n-1*an
設符號 F[n-1][1]為在前 n-1 個數中插入一個乘號的最大值,則的最大②最后
2a

設符號 F[n-2][1]為在③最后
3 個數作為被乘設符號
F[n-3][1]為在前 n-3 個數中插
-1an
······
a3~an 作為被乘數:此時的最大乘積

F[n][k]=max{F[n-1][1]*anF[n-2][1]*an-1an2an-1an}

F[i][j]表示在 i 個數中插入
F[i][ j] = max{F[i-1][j-1]*val[i][val[j+1][i} (1<=i<=n, 1<=i<=

邊界: F[i][0] = val[1][i] (階段:子問個階段)狀態策:每個階段隨着被乘數數列的決注意問題:(







1)輸入的字符需要進行數值(
2)由於乘積可能很大, C\C++
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 97 -
大整
tream(System.in));{asNext()) {n, k;nt();for(int i = 0; i < n; i++) {val[i][i] = BigInteger.valueOf(s.charAt(i)-'0');for(int j = i+1; j < n; j++) {= val[i][j-(10)).add(BigInteger.valueOf(s.charAt(j)-'0'));nt i = 0; i < n; ++i) {}











法的括號序列:是一個合法的括號序列。

A B 是分別是合法的括號序列,那么 AB 也是一個合法的括號序列。
'(', ')', '[', ']'字符的括號序列,要你添加盡量少的括號,使得整個序列成為合法的括號序列。數類。


Java 代碼:
import java.math.*;import java.util.*;import java.io.*;public class Main {Scanner cin = new Scanner(new BufferedInputSBigInteger f[][] = new BigInteger[41][7];BigInteger val[][] = new BigInteger[41][41];public void solve()while(cin.hintString s;n = cin.nextInt();k = cin.nextIs = cin.next();val[i][j]1].multiply(BigInteger.valueOf}}for(if[i][0] = val[0][i];for(int j = 1; j <= i && j <= k; ++j) {f[i][j] = BigInteger.ZERO;for(int l = j-1; l < i; ++l) {f[i][j] = f[i][j].max(f[l][j-1].multiply(val[l+1][i]));}}}System.out.println(f[n-1][k]);}public static void main(String[] args) {Main test = new Main();test.solve();}}

































2.3.3 POJ 1141 Brackets Sequence
題意:我們定義一個合
1.空序列是一個合法的括號序列。
2.如果 S 是一個合法的括號序列,那么(S)活着[S]
3.如果現在給出一個僅包含輸入有多組測試數據,每組測試數據一行,為一個僅包含

'(', ')', '[', ']'字符的括號序列。對於每組測試數據,輸出添加括號后最短的合法括號序列。如果有多個答案,輸出任意一個即可。樣例:




哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 98 -

)[()]
以采用遞歸的方法來解決問題。設序列 SiSi+1···· Sj 最少需要添加 dp[i,j]個括號,根據不同情況,可以采用不同的方式轉化成子問題。

(S`)或者[S`],此時只要把 S`變成合法序列, S 就是合法序列。如
(S`,先把 S`變成合法的序列,再在最后添加一個)即可。形如
[S`S`)S`],和上一種情況一樣。只要序列的長度大於
1,都可以把 S 分成兩部分 Si··· SkSk+1··· Sj,分別變。示。如配的括號,如果不是則


pos[i,j]記錄的是從哪個位置把
S
N], pos[MAXN][MAXN];nt_br(int i, int j)el= -1){br[i]);pr t_br(i+1, j-1);, br[j]);}}, k, mid, t;while (NULL != gets(br)){len_br = strlen(br);












([(]
輸出
(
分析:可
z S 形如
z S
z S
z
成合法的規則序列后連在一起就變成了合法序列。請思考一下,為什么這里的最優子結構能夠成立,並獲得最優解?如果直接采用遞歸的方式解決,將會有很多重復的計算,可以用一個數組把這些已經計算過的子問題保存下來,這種方法叫做記憶化。也可以改成非遞歸的,此時就要先把子問題求出來,那么就是最小的問題一直推到大的問題,下面的代碼是非遞歸形式實現的題目還要求把答案輸出,還要記錄




dp[i,j]在獲得最優值時的決策,我們用 pos[i,j]表果
pos[i,j]=-1,則表示 Si Sj 是一堆匹
i··· Sj 分成兩部分,輸出答案的時候采用遞歸輸出。最簡單直接的做法是直接記錄
dp[i,j]獲得最優值時形成的序列,當然這種方法空間和時間花費更多。代碼:

#include <stdio.h>#include <string.h>const int MAXN = 256;char br[MAXN];int dp[MAXN][MAXint len_br;void pri{if (i > j)return;if (i == j){if (br[i]=='(' || br[i]==')')printf("()");seprintf("[]");}else if (pos[i][j] =printf("%c",inprintf("%c"else {print_br(i, pos[i][j]);print_br(pos[i][j]+1, j);}int main(){int i, j


























哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 99 -
dp));i++)dp[i][i] = 1;j = i + k;dp[i][j] = 0x7fffffff;if1][j])){dp[i][j] = t, pos[i][j] mid;}}printf("\n");}return 0;












2.3. Ip2006 能量項鏈
目鏈接: Hrbustoj 1376
子,這些標記對應着某個正整數。並且,對於相鄰的兩顆珠子,前一顆珠子的尾標記一定等於后一顆珠子的頭標記。因為只有這樣,通過吸盤(吸 人吸收能量的一種器官)的作用,這兩顆珠子才能聚合成一顆珠子,同時釋放出可以被吸盤吸收的能量。如果前一顆能量珠的頭標記為


m,尾標記為 r,后一顆能量珠
*r*nMars 單位),新產生的珠子的頭標記為
m,尾標記為 n。,
Mars 人就用吸盤夾住相鄰的兩顆珠子,通過聚合得到能量,直到項鏈上只剩下一顆珠子為止。顯然,不同的聚合順序得到的總能量是不同的,請你設計一個聚合順序,使一串項鏈釋放出的總能量最大。


4 顆珠子的頭標記與尾標記依次為(23) (35) (510) (102)。我們用記號⊕表示兩顆珠子的聚合操作,
(jk)表示第 jk 兩顆珠子聚合后所釋放的能量。則第
4 合后釋放的能量為:
=60。得到最優值的一個聚合順序所釋放的總能量為)

=10*2*3+10*3*5+10*5*10=710。測試數據。組測試數據,輸入的第一行是一個正整數

N4N100),表示項鏈上珠子的個數。第二行是
N 個用空格隔開的正整數,所有的數均不超過 1000。第 i 個數為第 i 顆珠子的 ≤
iN),當 i<N 時,第 i 顆珠子的尾標記應該等於第 i+1 顆珠子的頭標記。 珠子的尾標記應該等於第
1 顆珠子的頭標記。於珠子的順序,你可以這樣確定:將項鏈放到桌面上,不要出現交叉,隨意指定第一顆


memset(dp, 0, sizeof(for (i = 0; i < len_br;for (k = 1; k < len_br; k++){for (i = 0; i + k < len_br; i++){(('('==br[i]&&br[j]==')') || ('['==br[i]&&br[j]==']')){dp[i][j] = dp[i+1][j-1], pos[i][j] = -1;}for (mid = i; mid < j; mid++){if (dp[i][j] > (t=dp[i][mid]+dp[mid+=}}print_br(0, len_br-1);}













4 NO
題題目描述:在

Mars 星球上,每個 Mars 人都隨身佩帶着一串能量項鏈。在項鏈上有 N 顆能量珠。能量珠是一顆有頭標記與尾標記的珠盤是

Mars
的頭標記為 r,尾標記為 n,則聚合后釋放的能量為 m
需要時例如:設
N=4,、
1 兩顆珠子聚
(41)=10*2*3
這一串項鏈可以
((41)2)3
輸入有多組對於每頭標記(


1
N 顆至珠子,然后按順時針方向確定其他珠子的順序。處理到文件結束。



哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 100 -
輸出對於每組測試數據,輸出只有一行,是一個正整數
EE2.1*109),為一個最優聚合順序所釋放的總能量。

3 5 10
a[

k]為首標記的能量珠開始順時針數到以
b[j]為尾標記的能量珠為止的串,然后將這兩顆合並后的能量珠合並,放出的能量為
 f[i,k] *b[
 *b[k]*b[j]。也就是說,狀態轉移方程為 f[i,j] = Max{f[i,k]+f[k,j]+a[i]j<=k<=i)
。到含有能量珠數目比它少的串在數組中的值,所以可以以串的能

i (i+l) mod n=j 的順序進行規划。 量珠數目 >nclude <cstring>)) {n; ++i) scanf("%d", &a[i]);(f));{+i) {k = (k + 1) % n) {(f[i][j], f[i][k] + f[(k + 1) % n][j] + a[i] *a[(% n]) {% n];











樣例輸入:

42

輸出:
710
分析:記 a[i]為第 i 顆能量珠的首標記, b[i]為第 i 顆能量珠的尾標記。記錄 f[i,j]為從
i]為首標記的能量珠開始順時針數到以 b[j]為尾標記的能量珠為止所有能量珠組成的串合並后放出的最大能量。那么對於
f[i,j],若先合並從以 a[i]為首標記的能量珠開始順時數到以
b[k]為尾標記的能量珠為止的串,再合並以 a[+f[k,j]+a[i]k]*b[j]} (i<=k<=j

或者計算
f[i,j]時,只會
x 為順序規划,即按照從用對於

b[i],有 b[i] = a[i+1],所以可以只記錄每顆能量珠的首標記即可。
C++代碼:
#include <cstdio#i#include <algorithm>using namespace std;const int NN = 105;int f[NN][NN];int a[NN];int main(){int n;while (EOF != scanf("%d", &nfor (int i = 0; i <memset(f, 0, sizeoffor (int l = 1; l < n; ++l)for (int i = 0; i < n; +int j = (i + l) % n;for (int k = i; k != j;f[i][j] = maxk+1)%n] * a[(j+1)%n]);}}}int ans = 0;for (int i = 0; i < n; ++i) {if (ans < f[i][(i-1 + n)ans = f[i][(i-1 + n)}}printf("%d\n", ans);}return 0;}































哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 101 -
2.3 棋盤分割
鏈接: POJ 1191 棋盤分割 http://poj.org/problem?id=1191
.5 NOI 2001
題目題目描述:將一個

8*8 的棋盤進行如下分割: 將原棋盤割下一塊矩形棋盤並使剩下部分也是矩形,再將剩下的部分繼續如此分割,這樣割了
(n-1)次后,連同最后剩下的矩形棋盤共有 n
塊矩形棋盤。 (每次切割都只能沿着棋盤格子的邊進行)
原棋盤上每一格有一個分值, 一塊矩形棋盤的總分為其所含各格分值之和。現在需要把棋盤按上述規則分割成
n 塊矩形棋盤,並使各矩形棋盤總分的均方差最小。均方差 ,其中平均值 ,
xi 為第 i 塊矩形棋盤的總分。盤及

n,求出 O'的最小值。
0 的非負整數,表示棋盤上相應格子的分值。每行相鄰兩數之間用一個空格分隔。四舍五入精確到小數點后三位)。請編程對給出的棋輸入第




1 行為一個整數 n(1 < n < 15)。第
2 行至第 9 行每行為 8 個小於 10
輸出僅一個數,為
O'(樣例:輸入


31 1 1 1 1 1 1 31 1 1 1 1 1 1 11 1 1 1 1 1 1 11 1 1 1 1 1 1 11 1 1 1 1 1 1 11 1 1 1 1 1 1 11 1 1 1 1 1 1 01 1 1 1 1 1 0 3








哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 102 -
輸出
1.633
題目來源
Noi 99
分析:參考《算法藝術與信息學競賽》第 116 頁。均方差的公式比較復雜,先將其變形為

2
 1
n
ii

= 1
ii

=  1
ii

=  n22
( ( ) 2 2 2 ) x (x)
n x x x x
= + - = -
σ
由於平均值是一定的(它等於所有方格里的數的和除以 n),所以只需要讓每個矩形的總分的平方和盡量小。考慮左上角坐標為

1 n n 1 n
(x1, y1),右下角坐標為(x2,y2)的棋盤,設它的總和為
s[x1, y1, x2, y2]切割 k 次以后得到的 k+1 塊矩形的總分的平方和最小值為 d[k, x1, y1,x2, y2]
, 則它可以沿着橫線切, 也可以沿着豎線切( 這里用到了遞歸! )。 故狀態轉移方程為:

d[k, x1, y1, x2, y2] = min {min{d[k-1,x1,y1,a,y2]+s[a+1,y1,x2,y2],d[k-1,a+1,y1,x2,y2]+s[x1,y1,a,y2]}(x1<=a<x2)

min{d[k-1,x1,y1,x2,b]+s[x1,b+1,x2,y2],d[k-1,x1,b+1,x2,y2]+s[x1,y1,x2,b]}(y1<=b<y2)
}
m為棋盤的邊長,則狀態數目為m4n,決策數目為O(m)。預處理先用O(m2)時間算出左上

il ix2][y2];for (y1 = 0; y1 <= y2; y1++){for (x1 = 0; x1 <= x2; x1++){s[x1][y1][x2][y2] = sl - sl2;




角為(1,1)的所有矩陣元素和,這樣狀態轉移時間就是O(1),故總的時間復雜度為
O(m5n)。由於m=8n<=15,這個方法還是夠快的。
C 代碼:
#include <stdio.h>#include <string.h>#include <math.h>nt n;ong dp[20][8][8][8][8];long s[8][8][8][8];nt map[8][8];long l_min(long a, long b){return a<b?a:b;}long l_sqr(long x){return x*x;}double d_sqr(double x){return x*x;}void init_status(){memset(s, 0, sizeof(s));long sl, sl2;int x1, y1, x2, y2;for (x2 = 0; x2 < 8; x2++){sl = 0;for (y2 = 0; y2 < 8; y2++){sl += map[sl2 = 0;



















哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 103 -
> 0)x1][y1][x2][y2] += s[x1][y1][x2-1][y2];sl20; y1 < 8; y1++){2 = x1; x2 < 8; x2++){for (y2 = y1; y2 < 8; y2++){s[x1][y1][x2][y2] = l_sqr(s[x1][y1][x2][y2]);dp[0][x1][y1][x2][y2] = s[x1][y1][x2][y2];} lfor (x1 = 0; x1 < 8; x1++){for (y1 = 0; y1 < 8; y1++){for (x2 = x1; x2 < 8; x2++){){k-1][x1][y1][x2][a]+s[x1][a+1][x2][y2]);*p = l_min(*p, dp[k-1][x1][a+1][x2][y2]+s[x1][y1][x2][a]);int i, j, x_;longmap[i][j]);_n = 1.0 / n;sqrt(_n*ret - d_sqr((_n*x_))));if (x2s[}+= map[x2][y1];}}}for (x1 = 0; x1 < 8; x1++){for (y1 =for (x}}}}ong solve(){int x1, x2, y1, y2, k, a;long *p;for (k = 1; k < n; k++){for (y2 = y1; y2 < 8; y2++){p = &dp[k][x1][y1][x2][y2];*p = 9999999;for (a = x1; a < x2; a++*p = l_min(*p, dp[k-1][x1][y1][a][y2]+s[a+1][y1][x2][y2]);*p = l_min(*p, dp[k-1][a+1][y1][x2][y2]+s[x1][y1][a][y2]);}for (a = y1; a < y2; a++){*p = l_min(*p, dp[}}}}}}return dp[n-1][0][0][7][7];}int main(){double _n;ret;scanf("%d", &n);x_ = 0;for (i = 0 8; i++){ ; i <for (j = 0; j < 8; j++){scanf("%d", &x_ += map[i][j];}}init_status();ret = solve();printf("%.3f\n",return 0;







































































哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 104 -
2.3
代價字母樹 [OIBH 模擬賽]u.cn/acmhome/problemdetail.do?method=showdetail&id=1086h emdetail.do?method=showdetail&id=1007Are the Onelding




搜索引擎搜索 “區間 DP”。
2.4 規划
2.4
狀態壓縮 DP 基本都可以轉換成普通的 DP,但是用狀態壓縮的目的,是為了減小內存消耗,因為利用狀態壓縮之后,所有的狀態都可以用一個
32
 位甚  (以 16 位居多,因為 32 位狀態太多)表示出來,也可以利用位運 算極快的運算速度來提高程序的速度,所以需要對幾種位運算有一定的了解: 的或類似,按位或運算滿足只有兩個二進制位都為 0
,其他情況皆為 1,舉例來說 0000 0001|0000 0010 結果即為 0000 0011,按位 時結 或運算在狀態 DP 中,常作為把一個狀態集合加到另一個狀態集合的方法,並且保證不會重復(因為重復得到的還是
1)。運算符),同樣和邏輯中的與運算類似, 按位與運算滿足兩個二進制位

 都為 算在
 00 1010&0000 1101 結果為 0000 1000,按位與運
DP 中常作為判斷狀態集合中是否含有某種狀態集合。),按位異或運算符的運算規則是只有兩個二進制位相反時結

1010^0000 1101 結果為 0000 0111,,‘^’運算在狀態 DP 中常 果為 作為去掉某種狀態集合的運算符,可先利用&判斷是否含有這種狀態,然后可以利用^運算從狀態集合中去掉這種狀態(這種方法在記憶化搜索中基本上是必須用到的,要熟練掌握)是位運算中唯一一個單目運算符,這個運算符在狀態


DP 中用的並不多,但是也要掌握,
~是對相應的操作數按照二進制位按位取反, ~0000 0011 結果為一 數的二 制往左移位,相當於是這個數乘

2,對於
 有符 越界的問題,
 1 補齊,所以在用的時候要注意有可能會造成
0001 0100<<’運算符在狀態 DP 中經常要和上面幾 種運算符結合起來使用實現狀態的轉換。 相反,右移意味着這個數除 2,這個在二分里面用的比里的用處不多,
0001 0100>>3 結果為 0000 0010。用到的一 基礎的位運算的知識。狀態壓縮
DP 還有一個況下,狀態壓縮的數據范圍都在
20 以內,要保證總 較多,但是在狀態  很重 狀態數最多在百萬的數量級的范圍內才能保證 1 秒內能得出答案,一般會給出 16 以內的數據范圍,這個時候就可以考慮利用狀態壓縮
DP 或者是狀態枚舉來解決了。有的時候題
}
.6 其他題目
NUAA 1086 最小
http://acm.nuaa.edNUAA 1007
加分二叉樹[NOIp2003]http://acm.nuaa.edu.cn/acm ome/problHDU 4283 YouPOJ 2176 Fo



更多題目可以用
狀態壓縮動態
編寫:曹振海 校核:黃李龍
.1 狀態壓縮的原理
狀態壓縮 DP,其實所有的至是
16 位的整數‘
|’ (按位或運算符),和邏輯中果才為
0
&’ (按位與
1 時結果為 1,其他情況皆為 000
狀態‘
^’ (按位異或運算符
1,相同則為 0,如 0000
。‘
~’ (按位取反運算)這
1111 1100
 <<’ ( 按位左移運算),是把 個 號數來說,正數低位用
0 補齊,負數用 0000 0101<<2 結果為‘
>>’(按位右移運算),和左移
DP
 上面是狀態壓縮 DP 里面要 要的特點就是數據范圍,一般情
 
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 105 -
目給出的數據范圍並不一定滿足條件,但是可以通過一定的轉化把數據范圍縮小到這樣的范圍,那么也是可以考慮利用狀態
DP 的。直接的一種就動態規划一樣, 狀態


DP 一樣可以分為兩種,自頂向下的記憶化搜索和自下向上的遞推式求解。在自頂向下的記憶化搜索中,經常要利用
’^’運算去掉一些狀態得到比較小的狀態集合,而在自下向上的遞推式求解中,又要經常利用
’|’運算逐步加上一些狀態以

2.4
, 就應該嘗試去利用狀態 DP 解決了,或者可以通過一定的方式把數據范圍減小到這個范圍以內,也可以考慮狀態
DP,想要用狀態壓縮 DP 解決一道題時,首先要搞明白,每個狀態應該表示的意義是什么(或者說每一位的
1 或者 0
表示 示的意義推導出狀態轉移方程, 最后是確定如何用位運

2.4
2.4
打算在 N*M 的網格地圖上部署他們的炮兵部隊。一個 N*M 的地圖由
N 成, 地圖的每一格可能是山地( 用"H" 表示),也可能是平原(用"P"表示),如下圖。在每一格平原地形上最多可以布置一支炮兵部隊(山地上不能夠部署炮兵部隊) 擊范圍 圖中黑色區域所示:另外,狀態中的每一個二進制位的


0 或者是 1 可以表示的意義有很多種, 最是取不取, 或者表示是不是全取。和最基本的達到最終的狀態。



.2 一般的解題思路
當題中出現數據范圍為 20 以內時的意義分別是什么), 然后根據表算進行優化。


.3 經典題目
.3.1 POJ1185-炮兵陣地
1.題目出處/來源
POJ1185-炮兵陣地
2.題目描述司令部的將軍們行

M 列組; 一支炮兵部隊在地圖上的攻 如如果在地圖中的灰色所標識的平原上部署一支炮兵部隊,則圖中的黑色的網格表示它能夠攻擊影響。現在,將軍們規划如何部署炮兵部隊,在防止誤傷的前提下(保證任何兩支炮兵部隊之間不能一行能放的合法狀態由其上面兩行的狀態共同決定,所以我們確定






f[i][j][k]表示的意義為到的區域: 沿橫向左右各兩格, 沿縱向上下各兩格。圖上其它白色網格均攻擊不到。從圖上可見炮兵的攻擊范圍不受地形的互相攻擊, 即任何一支炮兵部隊都不在其他支炮兵部隊的攻擊范圍內),在整個地圖區域內最多能夠擺放多少我軍的炮兵部隊。




3.分析這道題是最經典的一道狀態
DP 的題,其中 N<=100,M<=10,從 M 的數據范圍中可以看出這道題可以用狀態
DP 來解決,我們分別先對每一行進行處理,每一行的狀態范圍為
0~(1<<M)-1,從這些狀態中先得到一些在行上是合法的狀態(即不會互相攻擊),另外每
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 106 -
#ing.h>

超過 61 種合法的狀態圖態對應的大炮的數量


/記錄每一行的山地狀態
;false;t 1

的數量,即為狀態所表示的大炮的數量
i n{if(x&1);d int n)//



對於單行來說,得到合法的狀態
{w e anf("%d%d",&r,&c)!=EOF){sc ]);



i-1 行的狀態為 k i 行的狀態為 j 時所能得到的最優解,在不會與上兩行的狀態矛盾的情況下
f[i][j][k]=max(f[i][j][k],f[i-1][k][l]+sum[j]),其中 sum[j]表示 j 狀態所能放的大炮的數量。

4.代碼(包含必要注釋,采用最適宜閱讀的 Courier New 字體,小五號,間距為固定值
12 磅)
include<stdio.h>#include<strint st[61];//

最多不會
char a[110][15];//
int sum[61];//每個狀
int surface[101];/int f[101][61][61]int cnt;//

合法的狀態的個數
int r,c;int max(int a,int b){return a>b?a:b;}bool can(int x)//




判斷狀態是否合法
{if(x&(x<<1))returnif(x&(x<<2))return false;return true;}in getsum(int x)//






求狀態中
{nt um=0;while(x)num++x/=2;}return num;}voi getst({int i;for(i=0;i<(1<<n);i++)if(can(i)){st[cnt]=i;sum[cnt++]=getsum(i);}}}int main(){n(sci t i,j,k;hilcnt=0;for(i=0;i<r;i++)anf("%s",a[i



























哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 107 -
ace));
能出現大炮的位置所表示的狀態
urface[i]+=1<<j;//這里+也可以改為|+)//
預處理第一行
;st[i])

地矛盾,不能與對應的第 1 行的狀態矛盾
f[1][i][k]=max(f[1][i][k],f[0][k][0]+sum[i]);
解后面的狀態
for(j=0;j<cnt;j++)if(surface[i]&st[j])continue;ontinue;for(int(st[l st[j]))j





不與上兩行的狀態矛盾,不與本行的山地矛盾
}ts ge t(c);memset(surface,0,sizeof(surfmemset(f,0,sizeof(f));for(i=0;i<r;i++)//



得到每一行的不
for(j=0;j<c;j++){if(a[i][j]=='H')s}for(i=0;i<cnt;i+{if(surface[0]&st[i])continuef[0][i][0]=sum[i];}for(i=0;i<cnt;i++)//










預處理第二行
{if(surface[1]&st[i])continue;for(k=0;k<cnt;k++){if(surface[0]&st[k])continue;if(st[k]&continue;//







保證狀態 i 不與第二行山
}}for(i=2;i<r;i++)//


{{for(k=0;k<cnt;k++){if(surface[i-1]&st[k])continue;if(st[k]&st[j])cl=0;l<cnt;l++){if(st[l]&surface[i-2])continue;if((st[l]&st[k])|| ]&continue;//












保證狀態
f[i][j][k]=max(f[i][j][k],f[i-1][k][l]+sum[j]);}}}int max=0;for(i=0;i<cnt;i++)//




求解最優值
for(j=0;j<cnt;j++)if(f[r-1][i][j]>max)max=f[r-1][i][j];printf("%d\n",max);}return 0;





哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 108 -
以借鑒北大培訓教材中做法。
2.4
ACM-ICPC 亞洲區預選賽長春站 C -Math MagicN
以及這 K 個數的最小公倍數 M1 ≤ N, M ≤ 1,000, 1 ≤ K ≤ 100,要求出這 多少種不同的組合。的,但是我們看到有

K 個數的
 最小公倍數為  K 個數的最小公倍數為 M,暨代表着在這 K 個數種每一個 質因子所對應的最高的次數和 M 的質因子分解對應的次數是一樣的,所以我們可以先對 M 2*3

內,  以 內 的 數 最 多 有 四 個 不 同 的 質 因 子 ( 因 為 最 小 的 五 個 質 數
1000),這樣就把這道題的數據范圍縮小到了可以承受的范圍之 子分解 的某一個質因子也達到了這個次數,才把這個因子所對應的狀態中表示這一個質因子的二 進制位置為 1.然后, DP[i][j][k  個數,狀態為 i,和為 k 所對應的所有情況的數量。因為這道題中每個狀態有可能多次出現,所以最好采用遞推式的方法用’|’運算從下至上的運算,采用記憶化搜索自頂向下運算時,因為
’^’運算本身的性質,會使得處理多次出現變

er New 字體,小五號,間距為固定 )

ring>10050std;TE];//g



存儲的是每一種狀態所對應的因子都有哪些, num 存儲每種狀態對應的因子

e[i]){cnt++]=i;i*i;j<=1000;j+=i)se;}i{init();int n,m,k,i,j,temp,tn,pn;while(scanf("%d%d%d",&m,&n,&pn)!=EOF)//







這里 m 表示和, n 表示最小公倍數, pn 表示數的個數
}
5.思考與擴展:可
.3.2 Math Magic
1.題目出處/來源
ZOJ3662-2012 年第 372
.題目描述給出
K 個數的和
K 個數共有
3.分析從這道題所給出的數據范圍我們是無法用狀態
DP 來做
M,我們知道,質 因 子 分 解 ,
1000*5*7*11
已經超過了記錄
M 的每一個質因子對應的次數,枚舉 M 所有的因子,只有當因子的質因
]表示前 i
的很麻煩。
4.代碼(包含必要注釋,采用最適宜閱讀的 Couri
12
#include<iostream>#include<cstdio>#include<cst#define SUM#define STATE 2#define N 105#define MOD 1000000007using namespaceint dp[N][STATE][SUM];bool isprime[SUM];int prime[200],cnt;int dn,res[4];int g[STATE][100],num[STA












的數量
void init()//打素數表
{int i,j;memset(isprime,true,sizeof(isprime));cnt=0;for(i=2;i<=1000;i++){if(isprimprime[for(j=isprime[j]=fal}}nt main()












哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 109 -
n 進行質因子分解
n/=prime[i];for(i=1;i<=tn;i++)//
枚舉 tn 的每一個因子,並確定所對應的狀態
i==0);j<dn;j++)[j]==0)sta|=(1<<j);//i


tn 的因子並且 i 達到了某一個質因子的最大的次數
0;i<pn;i++)(dp [j][k{if(i+1<=pn&&k+g[l][t]<=m){dp[i+1][j|l][k+g[l][t]]+=dp[i][j][k];dp[i+1][j|l][k+g[l][t]]%=MOD;}turn 0;








2.4 主的遺產
源的遺產教主 日上午

10:48,坐上前往北京的火車,從此開始了高富帥的生活。年
ACM 生涯中,他用事實告訴我們,要想在比賽拿獎,除了平時的刻苦努力外,很大一部分還要依賴比賽時是

{tn=n;dn=0;memset(dp,0,sizeof(dp));for(i=0;i<cnt;i++)//{if(n%prime[i]==0){temp=n;while(n%prime[i]==0){}res[dn]=temp/n;//res











存儲的是每一個質因子對應的次數所表示因子
dn++;if(n==1)break;}}int sta;memset(num,0,sizeof(num));{if(tn%{sta=0;for(j=0{if(i%res}g[sta][num[sta]++]=i;}}dp[0][0][0]=1;for(i=r(j=0;j<(1<<dn);j++) fofor(k=0;k<m;k++){if [i] ])//






















當前面狀態存在時,才進行下面的處理
{for(int l=0;l<(1<<dn);l++)//
這兩層循環相當於是枚舉每一個因子
for(int t=0;t<num[l];t++)}}}printf("%d\n",dp[pn][(1<<dn)-1][m]);}re}







.3.3 HLG1473-
1.題目出處/
HLG1473-教主
2.題目描述恭送教主!在

2012 7 19
在教主的的大學四
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 110 -
策略,簡單來講就是做題順序,唯有想把能過的都過掉,然后再過難題,這樣才能在題順序量化表示,即

AC 系數, AC 系數越大,拿獎可能性就越大。題,不同做題順序會有不同的
AC 系數,假如 A 先做, B 后做的話,
A AC 系數為 4, 反過來 B 先做, A 后做的話, A B AC 系數為 5,說明先做 的
AC 系數。題做完后才做獲得的
AC 系數,有三道題 a, b, c,做題順序為
bac Sum = Sab + Scb + Sca
AC 系數和最大。幾里得旅行商問題,做題有一個先后的順序,根據題目的數據范圍應該很容易想到用狀態

DP 去做, DP[i]表述狀態 i 所對應的最優值,這道題因為每道題只能選一次,所以可以用記憶化搜索的方法去做,而且最好先對原始的每兩個題應該做的順序做一個預處理,可以減少記憶化搜索的層數,實現更好的時間效率。


Courier New 字體,小五號,間距為固定

(dp));n
表示的是題數每一個二進制位都進行枚舉,如果包含在狀態里便去掉且計算最優值

(state^(1<<i),n);{f(s&(1<<j))temp+=map[i][j];}}i{for(i=0;i<n;i++)for(j=0;j<n;j++)







做題比賽中拿獎。我們將用這個做在比賽中會給出


n
B
B 后做 A 將得到更高設
Sij 表示第 i 題在第 j
,則系數和為求一個做題順序,使得

3.分析這道題其實有點類似於歐

4.代碼(包含必要注釋,采用最適宜閱讀的值
12 磅)
#include<iostream>#include<cstdio>#include<cstring>#define N 16using namespace std;int dp[1<<N];int map[N][N];//





不同做題順尋產生的 AC 系數的矩陣
int max(int a,int b){return a>b?a:b;}void init(){memset(dp,-1,sizeof}int dfs(int state,int n)//







記憶化搜索的部分,
{if(dp[state]!=-1)return dp[state];int i,j,temp,s;for(i=0;i<n;i++){if(state&(1<<i))//






{temp=dfss=state^(1<<i);for(j=0;j<n;j++)i}dp[state]=max(dp[state],temp);//





得到最優值
}return dp[state];nt main()int n,i,j;while(scanf("%d",&n)!=EOF){init();






哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 111 -
("%d\n",dfs((1<<n)-1,n));
2.4
fersordStack

2.5 規划
: 黃李龍
2.5
划, 是在一個樹形結構上進行的決策選擇,因為是樹形結構,所以決策就會受到邊的連接關系的影響,樹形動態規划能夠處理的題目很多,也很繁雜,很多時候要和圖論相互結合着用,不過很多樹形動態規划的基本原理和一些背包問題很類似。


2.5
難點, 為能處理的題型太多, 所以很多時候不知道怎么用樹形動態規划,狀態轉移方程也很不好寫,不過有的時候也可以從題面中給出的一些數據中判斷,一般用得到樹形動態規划的題,所給的樹的節點的數量都是

10^5 這樣的數量級,因為這種數量級用最優值這樣的話,就可以試一下樹形動態規划的思路。


2.5
2.5 egic game
但是因為游戲有時候很難,不知道怎么玩兒會讓他覺得不高興,現在有一個問題,在一個城市中,街道的構成是樹形的,他想知道最少要在多少個節點上駐守士兵能夠監視到所有的道路。動態規划, 和


01 背包特別像,因為每個點都有選和不選
 兩種 點
 [0]表示以 i 為根節點的子樹不選 i 點的最優值, dp[i][1]表示選 iscanf("%d",&map[i][j]);if(n==1){printf("0\n");continue;}for(i=0;i<n-1;i++)for(j=i+1;j<n;j++)dp[1<<i|1<<j]=max(map[i][j],map[j][i]);//







對只有兩個二進制為 1 的狀態預處理
printf}return 0;}


.4
擴展變型
POJ 1170 Shopping Of
六進制的狀
OJ 2817 W
DPP
樹形動態

編寫:曹振海 校核
.1 樹形動態規划介紹
樹形動態規
.2 解題思
樹形動態規划屬於一個 因
n^2 時間復雜度的算法就已經難以解決了,或者說題中有明顯的出現求
.3 經典題目
.3.1 POJ1463-Strat
1.題目出處/來源
POJ1463-Strategic game2
. 題目描述
Bob 很喜歡玩兒游戲,
3.分析這道題是很簡單的一個樹形情況, 所以定義

dp[i]
的 最 優 值 , dp[i][0]= dp[j ][1] , 其 中 j i 的 子 節 點 ,
dp[ dp j (j i 的子節點)。含必要注釋, 采用最適宜閱讀的
Courier New 字體,小五號,間距為固定

i][1]=min(dp[j ][0], [ ][1])
4. 代碼( 包值
12 磅)
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 112 -
ild[MAX];]
表示以 i 為根節點的子樹選擇 i 的最優值, dp[i][0]為不選的最優值
int dp0=0;dp1=0;{if(pre[i]==r)//


對每一個子孫節點進行 DP}dp1+=min(dp[i][1],dp[i][0]);//

狀態轉移方程
dp0+=dp[i][1];}}nroot,root,m,cld;memset(pre,-1,sizeof(pre));memset(child,-1,sizeof(child));memset(dp,0,sizeof(dp));memset(used,0,sizeof(used));#include<stdio.h>#include<string.h>#define MAX 1501int pre[MAX],chint dp[MAX][2];//dp[i][1int min(int a,int b){return a<b?a:b;}int n;int used[MAX];void fun(int r){used[r]=1;int i;if(child[r]==0){dp[r][1]=1;dp[r][0]=0;return;}int m=child[r];intfor(i=0;i<n;i++){if(!used[i]){fun(i);}dp[r][1]=dp1+1;dp[r][0]=dp0;int main(){int i,while(scanf("%d",&n)!=EOF){for(i=0;i<n;i++){scanf("%d:(%d)",&nroot,&m);if(!i)root=nroot;child[nroot]=m;while(m--){scanf("%d",&cld);pre[cld]=nroot;if(cld==root)






















































哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 113 -
oot=nroot;d\n",min(dp[root][1],dp[root][0]));//
最后也要在根節點選和不選中選最優值
教材中做法。
2.5.3.2 P
.. 目道 是說 節點有自己的權值,求去掉一條邊,使得分割成的兩棵樹的



0000, 1 ≤ M ≤ 1000000,但是很明顯 M 不會達到那么大,因為 n 個點的樹邊數是確定的,這道題中,樹形搜索只是一部分,現在很多題的樹形
DP 都會和圖論結合起來,成為題里的一部分,這就使得想到用樹形
DP 更難。要用搜索先得到以每一個點為根節點的樹的所有節點的權值和
dp[i],那么去掉從根節點到 i 點的路徑上直接連接 i 點的那條邊所得到的兩棵樹的權值分別為
sum-dp[i]dp[i]差值為 abs(sum-2*dp[i])
#include<cstdio>#include<cstring>#define N 100005005e std;is{int}int t;long long v[N];//long long dp[N];//









i 為根節點的子樹的權值的和
bvoid init(){memset(head,-1,sizeof(head));memset(used,0,sizeof(used));t=0;}void add(int x,int y)//






加邊操作
{e[t].v=y;head[x]=t++;void dfs(int u){used[u]=true;r}}fun(root);printf("%}return 0;}













5. 思考與擴展:可以借鑒北大培訓
OJ3140-Contestants Division
1 題 出處 目 /來源
POJ3140-Contestants Division2
題 描述這 題 給定一棵樹,每個權值之差最小


3. 析 分這道題的
N ≤ 104
.代碼(包含必要注釋,采用最適宜閱讀的 Courier New 字體,小五號,間距為固定值
12 磅)
#include<iostream>#define M 1000using namespacnt head[N];//


鄰接表存儲
truct edgeint v;next;;edge e[M];




點的權值
ool used[N];memset(dp,0,sizeof(dp));e[t].v=x;e[t].next=head[y];head[y]=t++;e[t].next=head[x];}






哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 114 -
dp[u]=v[u];int i;for(i=head[u];i>=0;i=e[i].next)} }i{while(scanf("%d%d",&n,&m)&&(m||n))init();{l+=v[i];}{scanf("%d%d",&j,&k);add(j,k);dfs(1);printf("Case %d: ",icase++);ans=abs(all-2*dp[1]);for(i=2;i<=n;i++)return 0;


















文 《用單調性優化動態規划》
/index.php/dp_optimize/
{if(!used[e[i].v]){dfs(e[i].v);dp[u]+=dp[e[i].v];}}long long abs(long long x){return x>=0?x:-x;}long long min(long long a,long long b){return a<b?a:b;nt main()int n,m;int i,j,k;int icase=1;{long long all=0;long long ans;for(i=1;i<=n;i++)scanf("%lld",&v[i]);alwhile(m--)}ans=min(ans,abs(all-2*dp[i]));printf("%lld\n",ans);}}





























2.6 利用單調性質優化動態規
參考文獻:
JSOI2009 集訓隊論
IOI2004 國家集訓隊論文 周源 《淺談數形結合思想在信息學競賽中的應用》單調隊列
+斜率優化DPhttp://www.notonlysuccess.com
編寫:黃李龍 校核:黃李龍
2.6.1 利用單調性優化最長上升子序列
POJ 3903 Stock Exchange
L 天的股票價格,分別是 p1, p2··· pL,現在要求一個最長的上升子序列
pi1 <
題意:已
pi1 < ··· <pik,並且有 i1<i2<··· <ik,輸出其長度。
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 115 -
入:測試數據,每組測試數據的第一行為一個整數
LL<=100000)。第二行為 L 個整數,為連續
L 天的股票價格。:描述的最長的長度。的最長上升子序列問題,


DP 方程 F[i] = MAX{F[j] + 1} (1<=j< ip[j] <p[i]) F[i] = 1
。求解的關鍵在於能否優化查找最大的 F[j],且滿足 p[j] < p[i]。如 的最優解為
F[j]+1,那么有p[j] < p[i],且F[j] < F[i]。再考慮另外一種情況,當
a<b]=F[b]時,有p[a] > p[b],因為如果p[a]<p[b],則F[b]=F[a]+1 會得到最優值,但是
F[a]=F[b],所以有p[a] > p[b]。假設當前處理到第i天的股票,我們用H[c]表示,序列長度為
c時 的的股票價格最小是多少,可以看出H[]數組是單調遞增的。並且,我們求得當前 證明可以根據之前的提示得出)。那么我們可以對

H[]數組進行二分查找的操作, 尋找一個最大的長度l,且滿足H[l]<p[i]。時間復雜度為

#include <cstdio>int h[100005];int main() {int n, a;while (EOF != scanf("%d", &n)) {scanf("%d", &a);int ans = 1;h[1] = a;for (int i = 1; i < n; ++i) {%d", &a);}










似的題目有 POJ 1631 Bridging signals Hrbustoj 1427 Leyni 的情人節,其中 Leyni
的情
2.6.
就是一個元素單調的隊列,那么就能保證隊首的元素是最小(最大)的,從而滿足動態規划的最優性問題的需求。名雙端隊列。雙端隊列,就是說它不同於一般的隊列只能在隊首刪除、隊尾插入,它能夠在隊首、隊尾同時進行刪除。輸有多組輸出如題中分,析:很經典否則假











F[i]
F[a
,第c
F[i]的最優解時,有H[F[i]] = p[i],且為最小(
O(Nlog2N)
C++代碼:
scanf("int l = 1, r = ans;while (l <= r) {int m = (l + r) / 2;if (h[m] < a) {l = m + 1;} else {r = m - 1;}}h[l] = a;if (ans < l) ans = l;}printf("%d\n", ans);}return 0;















類人節這題需要做一些轉換才能利用最長下降子序列的方法做。

2 單調隊列
什么是單調(雙端)隊列單調隊列,顧名思義,單調隊列,又


哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 116 -
, 單調隊列中每個元素一般存儲的是兩個值:中的狀態值一個問題: 一個含有

n 項的數列(n<=2000000),求出每一項前面的第 m
個數到它這個區間內的最小值。
st 算法之類的 RMQ 問題的解法。但龐大的數據范圍讓這些對數級的算法沒有生存的空間。我們先嘗試用動態規划的方法。用 代表第 個 , 很容易寫出狀態轉移方程:個 度是


O(nm)的,甚至比線段樹還差。這時候,單調隊列就發揮 他們 護 域

{position,value},分別代表他在原隊 中 位 兩個域都單調遞增。計 不斷刪除, 直到隊首的

position 大於等於單調隊列的性質一般, 在動態規划的過程中在原數列中的位置(下標)他在動態規划而單調隊列則保證這兩個值同時單調。單調隊列有什么用我們來看這樣這道題目, 我們很容易想到線段樹、 或者








f (i)
i 數對應的答案, a[i] 表示第i 個數
( ) ( [ ])
1
f i Min a j
ij
=i-m+
=
這 方程, 直接求解的復雜了 的作用:我 維 這樣一個隊列: 隊列中的每個元素有兩個列 的 置和


a[i] , 我們隨時保持這個隊列中的元素那 算
f (i) 的時候, 只要在隊首 i - m +1,那此時 首 不二人選, 因為隊列是單調的!樣將 插入到隊列中供別人決策:首先,要保證

position 單調遞增,由於我們動態規划的過程總是由小到大(反之亦然),所以肯定在隊尾插入。又因為要保證素不斷刪除, 直到隊尾元素小於 。、 進隊一次, 所以時間復雜度是


O(n)。用單調隊列完美的解決了這一題。隊尾插入: 為什么前面那些比 大的數就這樣無情的被槍斃了?我們來反問自己:他們活着有什么意義?!由於隊 的



value 必定是 f (i) 的我們看看怎
a[i]
隊列的 value 單調遞增, 所以將隊尾元 a[i]
時間效率分析很明顯的一點, 由於每個元素最多出隊一次為什么要這么做我們來分析為什么要這樣在


a[i]
i - m +1是隨着 單調遞增的,所以對 以
j
們再來分析為什么能夠在隊首不斷刪除, 一句話:
i
j < i,a[ j] > a[i],在計算任意一個狀態 f (x), x >= i 的時候, j 都不會比i 優, 所被槍斃是“罪有應得”。我

i - m +1是隨着 單調遞增的!單調隊列來解決:

[ ]
 f x
( )  opt const i
i=bound x
( [ ])
x-1=
x 單調不降,而 const[i]則是可以根據 i 在常數時間內確定的唯一的常數。這類問題,一般用單調隊列在很優美的時間內解決。

i
小結對於這樣一類動態規划問題, 我們可以運用其中

bound[x]
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 117 -
2.6.
,An),從中找出一段連續的長度不超過 m
的子多組測試數據 不超過
20 組測試數據。表示這個序列的長度, 第二行為
n 個數,每個數的范圍為[-1000, 1000]。輸出

31001

分析:
F(i)為以 Ai 結尾長度不超過 M 的最大子序和對於每個
F(i),從 1 m 枚舉 k 的值,完成 Aj 的累加和取最大值。該算法的時間復雜度為
O(N2)
簡化方程單調隊列優化在算法中,考慮用隊列來維護決策值

S(i-k)。每次只需要在隊首刪掉 S(i-m-1),在隊尾添加
S(i-1) 。但是取最小值操作還是需要 O(n)時間復雜度的掃描。考察在添加
S(i-1)的時候,設現在隊尾的元素是 S(k),由於 k<i-1,所以 S(k)必然比
S(i-1)先出隊。若此時 S(i-1)<=S(k),則 S(k)這個決策永遠不會在以后用到,可以將 S(k)
從隊尾刪除掉(此時隊列的尾部形成了一個類似棧的結構)
3 直接利用單調隊列解題
2.6.3.1 Hrbustoj 1522 子序列的和
題目描述:輸入一個長度為n的整數序列(
A1,A2,……序列, 使得這個子序列的和最大。輸入:有 ,對於每組測試的第一行,包含兩個整數



n m(n,m<=10^5),表示有 n 個數,子序列長度限制為
m,輸出:對於每組測試數據,輸出最大的子序列和,並換行。樣例輸入:




3 11 2 33 2-1000 1000 1




= - +
 =
F i
( ) max{  =
A j k m
1
| 1.. }ij i k

=
=
ij 1

令S(i) Aj

= - +
= =
ij i k

F i A j k m
1
( ) max{ | 1.. }( ) min{ ( )| 1.. }max{ ( ) ( )| 1.. }


S i S i k k mS i S i k k m

= - - == - - =

哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 118 -
同理,若隊列中兩個元素 S(i)S(j),若 i<j S(i)>=S(j),則我們可以刪掉 S(i)
(因為 S(i)永遠不會被用到)。此時的隊列中的元素構成了一個單調遞增的序列,即:
S1<S2<S3<……<Sk
我們來整理在求 F(i)的時候,用隊列維護 S(i-k)所需要的操作:☆若當前隊首元素
S(x),有 x<i-m,則 S(x)出隊;直到隊首元素 S(x)x>=i-m 為止。☆若當前隊尾元素

S(k)>=S(i-1),則 S(k)出隊;直到 S(k)<S(i-1)為止。☆在隊尾插入
S(i-1)。☆取出隊列中的最小值,即隊首元素。由於對於求每個

F(i)的時候,進隊和出隊的元素不止一個。但是我們可以通過分攤分析得知,每一個元素
S(i)只進隊一次、出隊一次,所以隊列維護的時間復雜度是
O(n)。而每次求 F(i)的時候取最小值操作的復雜度是 O(1),所以這一步的總復雜度也是
O(n)。綜上所述,該算法的總復雜度是
O(n)C++
代碼:
#include <cstdio>#include <cstring>const int INF = 999999999;const int MaxN = 1000005;struct Node {int i, s;Node() {}Node(int ti, int ts) : i(ti), s(ts) {}} que[MaxN];int s[MaxN];int main() {int n, m;while (EOF != scanf("%d%d", &n, &m)) {s[0] = 0;int ans = -INF;for (int i = 1; i <= n; ++i) {scanf("%d", &s[i]);s[i] += s[i-1];}Node *fr = que, *ta = que;que[0] = Node(0, 0);ans = s[1];for (int i = 2; i <= n; ++i) {while (fr <= ta && fr->i < i - m) ++fr;while (fr <= ta && ta->s >= s[i-1]) --ta;*++ta = Node(i-1, s[i-1]);int t = s[i] - fr->s;if (ans < t) ans = t;}printf("%d\n", ans);}return 0;}
































2.6.3.2 其他直接應用單調隊列的題目
FZU 1894 志願者選拔
HDU 3415 Max Sum of Max-K-sub-sequencePOJ 2823 Sliding Window

哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 119 -
2.6.4 單調隊列優化動態規划
2.6.4.1 HDU 3530 Subsequence
題意:給一個長度為
n 的數列,要求一個子區間,使得區間的最大值與最小值的差 s 滿足
,m<=s<=k,求滿足條件的最長子區間的長度。輸入:有多組測試數據,對於每組測試數據:第一行是


n m k,分別表示題目中的變量。第二行有
n 個整數,每個整數的的范圍是[0, 1000000]。輸出:最長的長度。分析:先考慮朴素的算法,枚舉區間的右邊界


i,再枚舉左邊界 j,然后找出區間[j,i]
的最大和最小值,然后判斷是否滿足,更新答案。很明顯會超時。考慮如何優化。如果某次枚舉的左邊界
j 使得區間[j,i]滿足條件,那么就不用繼續枚舉
j 了,因為再枚舉也不會比當前更優。那么問題就轉化成對於每一個右邊界 i 找到一個最小的左邊界
j 使得區間[j,i]滿足最大最小值之差在給定范圍內。一個貪心的想法就是維護
[1,i]這個區間的最值,因為 1 最小,但是這個區間可能並不滿足,也就是說,
j 還可能從 1 繼續往右枚舉。看一個結論,如果對於一個右邊界
i,其最優區間的左邊界是 j,那么對於以后所有的
i`>i j'>=j。這個結論用反證法證明。有如下調不遞減,記錄最小值,一個單調不遞增,記錄最大值;到兩個單調隊列的隊尾,注意維護各自隊列的單調性;素,如果兩個元素之差大於題目所給出的上界




k 則將隊首元素序號較小的那個隊列的隊首元素出隊,並且更新左邊界
j。 (出隊是為了使兩個隊首元素之

q1[MAXN], q2[MAXN];int main() {int n, low, up;while (EOF != scanf("%d%d%d", &n, &low, &up)) {int ans = 0, h1 = 0, t1 = 0, h2 = 0, t2 = 0, j = 0;for (int i = 0; i < n; ++i) {scanf("%d", &a[i]);






都有 i`的最優區間左邊界枚舉
i 的時候, j 都是單調不遞減的,這提示我們可以使用單調隊列來維護了,於是方法:維護兩個單調隊列,一個單每次枚舉一個


i,先將 i 加入檢查兩個單調隊列的隊首元差在規定范圍內。想一想,為何要將序號較小的出隊?)如果兩個隊首元素之差大於等於題目所給出的下界


m 則更新答案。注意左邊界
j 初始化為 0
C++代碼:
#include <cstdio>#include <cstring>#include <algorithm>using namespace std;const int MAXN = 100005;int a[MAXN];i t n






哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 120 -
while (h1 < t1 && a[q1[t1-1]] < a[i]) --t1;q1[t1++] = i;while (h2 < t2 && a[q2[t2-1]] > a[i]) --t2;q2[t2++] = i;while (a[q1[h1]] - a[q2[h2]] > up) {if (q1[h1] < q2[h2]) {j = q1[h1] + 1;++h1;} else {j = q2[h2] + 1;++h2;}}int t = a[q1[h1]] - a[q2[h2]];if (low <= t && t <= up) {t = i - j + 1;if (ans < t) ans = t;}}printf("%d\n", ans);}return 0;}






















2.6.4.2 HDU 3401 Trade
題意:給出
T 天股票的信息,每天的信息分別可以表示為: APi , BPi , ASi , BSi,分別表示第
i 天時股票的買入單價、賣出單價、第 i 天最多可以買入的股票數、最多可以賣出的股票數,並且還有一個約束條件, 那就是兩個交易日相距的天數要大於W天,即如果是在第


i 天交易了,下次的交易時間只能從 i+W+1 開始,每天最多能持有 MaxP 的股票數量。求最大獲利。輸入:有多組測試數據,對於每組測試數據:第一行為



T MaxP W,含義如題目描述。接下來是
T 行,第 i+1 行為第 i 天的信息: APi BPi ASi BSi,含義如題目描述。輸出:最大獲利。分析:參考:




http://blog.csdn.net/ivan_zjj/article/details/7559985
這題的 DP 方程很容易列,用 f[i][j]表示第 i 天持有 j 股的最大擁有的錢數,轉移方程為:不做交易:


f[i][j] = MAX( f[i][j] , f[i-1][j] )
買入:
f[i][j] = MAX( f[ pre ][ k] - (j - k) * AP[i] )
我們把不變的量 j * AP[i] 提出來,則:
f[i][j] + j * AP[i] = MAX( f[pre][k] + k *AP[i] )
Fa(k) = f[ pre ][k] + k * AP[i] ,則: f[i][j] = MAX( FF(k) ) - j * AP[i]
這樣就轉化為經典的單調隊列優化的 DP 了,我們只需要用一個單調隊列來存放決策點
k 就可以了。
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 121 -
賣出:
f[i][j] = MAX( f[pre][k] + (k - j )*BP[i] )
我們把不變的量 j * AP[i] 提出來,則:
f[i][j] + j * AP[i] = MAX( f[pre][k] + k *BP[i] )
Fb(k) = f[ pre ][k] + k * BP[i] ,則: f[i][j] = MAX( Fb(k) ) - j *AP[i]
情況和上面的類似。這樣每次求
f[i][j] 的時候,我們可以得出以下的一個重要結論:在第 i 天的時候,最優的
pre
應該是: pre = i - W - 1。其實證明這個結論很簡單, 假設 pre1 < pre2,我們假設
f[pre1][k] >f[pre2][k]
,但是這種情況是不可能出現的,因為我們可以這樣考慮,我現在從 pre1
天到
pre2 天這中間都不交易,那么我就可以得到 f[pre2][k] >= f[pre1][k],所以說上面的假設是不可能成立的,因此我們每次只要將

pre 賦值為 i - W - 1 即可。剩下的工作就是每次求
f[i][j] 的時候分別用兩個隊列分別維護買入和賣出就可以了。
C++代碼:
#include <cstdio>#include <cstring>#include <algorithm>using namespace std;const int MAXN = 2005, INF = 0x3fffffff;int ap[MAXN], bp[MAXN], as[MAXN], bs[MAXN];int f[MAXN][MAXN];int q[MAXN];template<class T>void check_max(T &a, const T &b) {if (a < b) a = b;}int main() {int runs;scanf("%d", &runs);while (runs--) {int n, mp, w;scanf("%d%d%d", &n, &mp, &w);for (int i = 1; i <= n; ++i) {scanf("%d%d%d%d", &ap[i], &bp[i], &as[i], &bs[i]);}for (int i = 1; i <= n; ++i) {for (int j = 0; j <= as[i]; ++j) f[i][j] = -j * ap[i];for (int j = as[i] + 1; j <= mp; ++j) f[i][j] = -INF;}for (int i = 2; i <= n; ++i) {for (int j = 0; j <= mp; ++j) check_max(f[i][j], f[i-1][j]);if (i - w - 1 <= 0) continue;int fr = 0, ta = 1, pr = i - w - 1;q[0] = 0;for (int j = 1; j <= mp; ++j) {while (fr < ta && j - q[fr] > as[i]) ++fr;if (fr < ta) check_max(f[i][j], f[pr][q[fr]] - ap[i] * (j - q[fr]));while (fr < ta && f[pr][q[ta-1]] + ap[i] * q[ta-1] <= f[pr][j] + ap[i]* j) --ta;q[ta++] = j;}




































哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 122 -
fr = 0, ta = 1;q[0] = mp;for (int j = mp-1; j >= 0; --j) {while (fr < ta && q[fr] - j > bs[i]) ++fr;if (fr < ta) check_max(f[i][j], f[pr][q[fr]] + bp[i] * (q[fr] - j));while (fr < ta && f[pr][q[ta-1]] + bp[i] * q[ta-1] <= f[pr][j] + bp[i]* j) --ta;q[ta++] = j;}}int ans = -INF;for (int j = 0; j <= mp; ++j) {if (ans < f[n][j]) ans = f[n][j];}printf("%d\n", ans);}return 0;}

















2.6.4.3 POJ 3017 Cut the Sequence
以下分析來着: JSOI2009 集訓隊論文 《用單調性優化動態規划》 。問題描述給定一個有

n 個非負整數的數列 a,要求將其划分為若干個部分,使得每部分的和不超過給定的常數
m,並且所有部分的最大值的和最小。其中 n<=105。例:
n=8, m=178 個數分別為 2 2 2 | 8 1 8 |1 2,答案為 12,分割方案如圖所示。• 解法分析剛開始拿到這道題目,首先要讀好題:最大值的和最小。首先設計出一個動態規划的方法:,其中 代表把前



i 個數分割開來的最小代價。
= , 可以用二分查找來實現。直接求解復雜度最壞情況下(
M 超大)是 的,優化勢在必行。通過仔細觀察,可以發現以下幾點性質:在計算狀態

f(x)的時候,如果一個決策 k 作為該狀態的決策,那么可以發現第 k 個元素和第
x 個元素是不分在一組的。
b[x]隨着 x 單調不降的,用這一點,可以想到什么?可以想到前面單調隊列的一個限制條件。來看一個最重要的性質:如果一個決策

k 能夠成為狀態 f(x)的最優決策,當且僅當。為什么呢?其實證明非常非常容易(用到性質
1),交給讀者自己考慮。到此為止,我們可以這樣做:由於性質三,每計算一個狀態

f(x),它的有效決策集肯定是一個元素值單調遞減的序列,我們可以像單調隊列那樣每次在隊首刪除元素,直到隊首在數列中的位置小於等於 ,然后將

a[x]插入隊尾,保持隊列的元素單調性。這時候問題來了,隊首元素一定是最佳決策點嗎?我們只保證了他的元素值最大……如果掃一遍隊列,只是常數上的優化,一個遞減序足以將它否決。我們觀察整個操作,將隊列不斷插入、不斷刪除。對於除了隊尾的元素之外,每個隊列中的元素供當前要計算的狀態的“值”是





( ) ( [ ] [ 1, ])
1[ ]

f i Max f j Maxnumber j i
ij b x

= + +
-=
f (i)
b[i] Min( j | sum[ j +1,i] <= m) b[i]
O(n 2 )
a[k] > ∀a[ j], j [k +1, x]
b[x]
f (q[x].position) + a[q[x +1].position] ,其中,q[x]
代表第 x 個隊列元素, position 這代表他在原來數組中的位置,我們不妨把這個值記為 t。那每一次在隊首、隊尾的刪除就相當於刪除
t,每一次刪除完畢之后又要插入一個新的 t
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 123 -
然后需要求出隊列中的 t 的最小值。我們發現,完成上述一系列工作的最佳選擇就是平衡樹,這樣每個元素都插入、刪除、查找各一遍,復雜度為

O(logn),最后的時間復雜度是 。有一個細節: 這一個單獨的決策點是不能夠被省掉的(仍然留給讀者思考),而上述隊列的方法有可能將其刪除,所以要通過特判來完成。以上就是《用單調性優化動態規划》中的分析內容, 。因為此題所給的數據量小,分析中所說的求數隊列中的



t 的最小值可以直接枚舉得到。當然,也可以使用 C++ STL 中的
multiset 去維護當前最小值。下面給出兩份代碼,分別是直接枚舉獲得隊列最小值和用
multiset 維護隊列最小值的兩種方法。
// 直接枚舉獲得隊列最小值的方法
#include <cstdio>#include <string>typedef long long int64;const int MAXN = 100005;int64 a[MAXN];int64 f[MAXN];int d[MAXN];int q[MAXN];template <class T>void check_min(T &a, const T b) {if (a > b) a = b;}int main() {int n;int64 m;while (EOF != scanf("%d%lld", &n, &m)) {a[0] = 0;bool can = true;for (int i = 1, t; i <= n; ++i) {scanf("%d", &t);a[i] = t;if (t > m) can = false;}if (!can) {puts("-1");continue;}int fr = 0, re = 0, low = 0;f[0] = 0;int64 sum = 0;for (int i = 1; i <= n; ++i) {sum += a[i];while (sum > m) {sum -= a[++low];}while (fr < re && q[fr] < low) {++fr;}while (fr < re && a[q[re-1]] <= a[i]) {--re;}if (fr < re) {d[re] = q[re-1];} else {d[re] = low;}













































O(nlog n)
b[x]
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 124 -
q[re++] = i;if (d[fr] < low) {d[fr] = low;}f[i] = f[d[fr]] + a[q[fr]];for (int j = fr + 1; j < re; ++j) {check_min(f[i], f[d[j]] + a[q[j]]);}}printf("%lld\n", f[n]);}return 0;}//












multiset 維護隊列最小值
#include <cstdio>#include <string>#include <set>using namespace std;typedef long long int64;const int MAXN = 100005;int64 a[MAXN];int64 f[MAXN];int d[MAXN];int q[MAXN];template <class T>void check_min(T &a, const T b) {if (a > b) a = b;}int main() {int n;int64 m;while (EOF != scanf("%d%lld", &n, &m)) {a[0] = 0;bool can = true;for (int i = 1, t; i <= n; ++i) {scanf("%d", &t);a[i] = t;if (t > m) can = false;}if (!can) {puts("-1");continue;}int fr = 0, re = 0, low = 0;f[0] = 0;int64 sum = 0;multiset<int64> ms;for (int i = 1; i <= n; ++i) {sum += a[i];while (sum > m) {sum -= a[++low];}while (fr < re && q[fr] < low) {ms.erase(f[d[fr]] + a[q[fr]]);++fr;}while (fr < re && a[q[re-1]] <= a[i]) {ms.erase(f[d[re-1]] + a[q[re-1]]);--re;}if (fr < re) {d[re] = q[re-1];} else {
















































哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 125 -
d[re] = low;}q[re++] = i;ms.insert(f[d[re-1]] + a[q[re-1]]);if (d[fr] < low) {ms.erase(f[d[fr]] + a[q[fr]]);d[fr] = low;ms.insert(f[d[fr]] + a[q[fr]]);}/*f[i] = f[d[fr]] + a[q[fr]];for (int j = erasefr + 1; j < re; ++j) {check_min(f[i], f[d[j]] + a[q[j]]);}*/f[i] = *ms.begin();}printf("%lld\n", f[n]);}return 0;}




















2.6.4.4 其他應用單調隊列或者單調性題目
POJ 3245 Sequence PartitioningPOJ 1742 Coins
單調隊列優化多重背包
HDU 3474 Necklace
2.6.5 利用斜率的單調性
基礎入門: MAX Average Problem
HDU 2993 MAX Average Problem
題目描述:給一個長度為 N,僅包含正整數的序列,序列為 a1,a2··· an,還給出一個不超過
N 的正整數 K,定義 AVE(i,j)ai··· aj 的平均值,求出最大的 AVE(i,j),並且
1<=i<=j-K+1<=N。輸入:有多組測試數據,對於每組測試數據:第一行為


N K。第二行為
N 個整數, a1 a2 ··· an。變量含義在題目描述中有說明。輸出:最大的平均值,精確到小數點后兩位。分析: (一下分析來自


IOI2004 國家集訓隊論文 周源 《淺談數形結合思想在信息學競賽中的應用》)簡單的枚舉算法可以這樣描述:每次枚舉一對滿足條件的

(a, b),即 ab-F+1,檢查
ave(a, b),並更新當前最大值。然而這題中
N 很大, N2 的枚舉算法顯然不能使用,但是能不能優化一下這個效率不高的算法呢?答案是肯定的。目標圖形化首先一定會設序列


ai 的部分和: Si=a1+a2++ai, ,特別的定義 S0=0。這樣可以很簡潔的表示出目標函數:

哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 126 -
如果將 S 函數繪在平面直角坐標系內,這就是過點 Sj 和點 Si-1 直線的斜率!於是問題轉化為:平面上已知
N+1 個點, Pi(i, Si)0iN,求橫向距離大於等於 F 的任意兩點連線的最大斜率。構造下凸折線有序化一下,規定對


i<j,只檢查 Pj Pi 的連線,對 Pi 不檢查與 Pj 的連線。也就是說對任意一點,僅檢查該點與在其前方的點的斜率。於是我們定義點
Pi 的檢查集合為
Gi = {Pj, 0ji-F}
特別的,當 i<F 時, Gi 為空集。其明確的物理意義為:在平方級算法中,若要檢查
ave(a, b),那么一定有 PaGb;因此平方級的算法也可以這樣描述,首先依次枚舉
Pb 點,再枚舉 PaGb,同時檢查
k(PaPb)。若將
Pi Gi 同時列出,則不妨稱 Pi 為檢查點, Gi 中的元素都是 Pi 的被檢查點。當我們考察一個點
Pt 時,朴素的平方級算法依次選取 Gt 中的每一個被檢查點 p,考察直線
pPt 的斜率。但仔細觀察,若集合內存在三個點 Pi, Pj, Pk,且 i<j<k,三個點形成如下圖所示的的關系,即
Pj 點在直線 PiPk 的上凸部分: k(Pi, Pj)>k(Pj,Pk),就很容易可以證明
Pj 點是多余的。以上就是論文中的分析,我認為寫得很好,寫得很直觀, 把題目轉換一下,利用了斜率的單調性去解決,推薦好好看一遍這篇論文。


C++代碼:
#include <cstdio>typedef long long int64;const int MAXN = 100005;int q[MAXN];int64 s[MAXN];int64 cross(int64 x1, int64 y1, int64 x2, int64 y2) {return x1 * y2 - x2 * y1;}int main() {int n, k;while (EOF != scanf("%d%d", &n, &k)) {s[0] = 0;for (int i = 1; i <= n; ++i) {int t;scanf("%d", &t);s[i] = s[i-1] + t;}int fr = 0, ta = 0;double ans = 0.0;for (int i = k; i <= n; ++i) { //


















單調隊列里維護的應該是可以選擇的決策點, 雖然不一定是最優點

int j = i - k;while (fr<ta-1 && cross(q[ta-1]-q[ta-2], s[q[ta-1]]-s[q[ta-2]], j-q[ta-2],s[j]-s[q[ta-2]]) <= 0) {--ta;}q[ta++] = j;while (fr+1<ta && (s[i]-s[q[fr+1]])*(i-q[fr]) >= (s[i]-s[q[fr]])*(iq[fr+1])) {++fr;







哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 127 -
}double t = (double)(s[i] - s[q[fr]]) / (i - q[fr]);if (ans < t) ans = t;}printf("%.2lf\n", ans);}return 0;}







k(Pt, Pj) > k(Pt, Pi),那么可以看出, Pt 點一定要在直線 PiPj 的上方,即陰影所示的
1 號區域。同理若 k(Pt, Pj) > k(Pt, Pk),那么 Pt 點一定要在直線 PjPk 的下方,即陰影所示的
2 號區域。綜合上述兩種情況,若
PtPj 的斜率同時大於 PtPi PtPk 的, Pt 點一定要落在兩陰影的重疊部分,但這部分顯然不滿足開始時
t>j 的假設。於是, Pt 落在任何一個合法的位置時,
PtPj 的斜率要么小於 PtPi,要么小於 PtPk,即不可能成為最大值,因此 Pj 點多余,完全可以從檢查集合中刪去。這個結論告訴我們,任何一個點

Pt 的檢查集合中,不可能存在一個對最優結果有貢獻的上凸點,因此我們可以刪去每一個上凸點, 剩下的則是一個下凸折線。最后需要在這個下凸折線上找一點與

Pt 點構成的直線斜率最大——顯然這條直線是在與折線相切時斜率最大,如下圖所示。

哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 128 -
維護下凸折線這一小節中,我們的目標是:用盡可能少的時間得到每一個檢查點的下凸折線。算法首先從

PF開始執行:它是檢查集合非空的最左邊的一個點,集合內僅有一個元素P0,而這顯然滿足下凸折線的要求,接着向右不停的檢查新的點:
PBF+1B,PBF+2B, , PBNB。檢查的過程中,維護這個下凸折線:每檢查一個新的點
Pt,就可以向折線最右端加入一個新的點
Pt-F,同時新點的加入可能會導致折線右端的一些點變成上凸點,我們用一個類似於構造凸包的過程依次刪去這些上凸點,從而保證折線的下凸性。由於每個點僅被加入和刪除一次,所以每次維護下凸折線的平攤復雜度為

O(1),即我們用 O(N)的時間得到了每個檢查集合的下凸折線。最后的優化:利用圖形的單調性最后一個問題就是如何求過


Pt 點,且與折線相切的直線了。一種直接的方法就是二分,每次查找的復雜度是
O(log2N)。但是從圖形的性質上很容易得到另一種更簡便更迅速的方法:由於折線上過每一個點切線的斜率都是一定的
3,而且根據下凸函數斜率的單調性,如果在檢查點
Pt 時找到了折線上的已知一個切點 A,那么 A 以前的所有點都可以刪除了:過這些點的切線斜率一定小於已知最優解,不會做出更大的貢獻了。於是另外保留一個指針不回溯的向后移動以尋找切線斜率即可,平攤復雜度為為


O(1)。至此,此題算法時空復雜度均為
O(N),得到了圓滿的解決。小結回顧本題的解題過程,一開始就確立了以平面幾何為思考工具的正確路線,很快就發現了檢查集合中對最優解有貢獻的點構成一個下凸函數這個重要結論,之后借助計算幾何中求凸包的方法維護一個下凸折線,最后還利用下凸函數斜率的單調性發現了找切線簡單方法。題解圍繞平面幾何這個中心,以斜率為主線,整個解題過程一氣呵成,又避免了令人頭暈的代數式變換,堪稱以形助數的經典例題。這里僅僅對斜率優化的方法做一些入門的介紹,可以參看其他利用斜率優化的題目:







HDU 2829 Lawrence
哈爾濱理工大學 ACM-ICPC 培訓資料匯編
- 129 -
HDU 3507 Print Article
2.6.6 擴展推薦
利用單調棧解題: POJ2082 POJ2559 POJ 2796。四邊形不等式優化
:JSOI2009 集訓隊論文 《用單調性優化動態規划》和 IOI2004 國家集訓隊論文 周源 《淺談數形結合思想在信息學競賽中的應用》都有介紹,推薦閱讀。



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM