数据结构整理中。。。
目錄
- 棧
- 隊列
- 鏈表
- 單向鏈表
- 雙向鏈表
- 向鏈表中插入(寫入)數據
- 單向鏈表
- 單向循環鏈表
- 雙向循環鏈表
- 從鏈表中刪除數據
- 單向(循環)鏈表
- 雙向循環鏈表
- 哈希表
- 哈希函數
- 沖突
- 拉鏈法
- 閉散列法
- 并查集
- 啟發式合并(按秩合并)
- 帶權并查集
- 其他應用
- 堆
- 二叉堆
- 插入操作
- 刪除操作
- 減小某個點的權值
- 實現
- 建堆
- 方法一:使用 decreasekey(即,向上調整)
- 方法二:使用向下調整
- 對頂堆
- 左偏樹
- dist 的定義和性質
- 左偏樹的定義和性質
- 核心操作:合并(merge)
- 左偏樹的其它操作
- 整個堆加上/減去一個值、乘上一個正數
- 隨機合并
- 塊狀數據結構
- 分塊思想
- 塊狀數組
- 塊狀鏈表
- 樹分塊
- Sqrt Tree
- 單調棧
- 樹狀數組
- 用法及操作
- 區間加 & 區間求和
- Tricks O(n)O(n)O(n)建樹:
- 線段樹
棧
| 定義棧 | Stack?char?s\left \langle char \right \rangle s?char?s | 定義一個char型的棧 |
| 元素訪問: | s.top() | 返回棧頂 |
| 容量: | s.empty() | 返回是否為空 |
| s.size() | 返回堆的大小 | |
| 修改: | s.push() | 插入傳入的參數到棧頂 |
| s.pop() | 彈出棧頂 |
括號匹配+Java棧
隊列
| 定義棧 | queue?int?q\left \langle int \right \rangle q?int?q | 定義一個int型的隊列 |
| 元素訪問: | [優先隊列]q.top()/q.front() | 訪問隊首 |
| 容量: | q.empty() | 返回是否為空 |
| q.size() | 返回堆的大小 | |
| 修改: | q.push() | 插入元素在隊尾 |
| q.pop() | 刪除隊首元素 | |
| q.clear() | 清空隊列 |
優先隊列和堆一樣有兩種形式:最大優先隊列和最小優先隊列。
1.如果直接定義一個優先隊列,系統默認的為降序優先隊列。
3.如果隊列元素為某結構體,可以通過重載<符號來進行自定義優先級,這里必須要注意的是只能是<并且在重載函數還需要加上const!
#include <iostream> #include<queue> #include<ctime> #include<cstdlib> #include<vector> using namespace std;struct node {int data;bool operator<(const node&a)const{return data>a.data; //此時是升序,如果是<則是降序} };int main() {srand(time(NULL));priority_queue<node>q;for(int i=10;i>0;i--){node t;t.data=rand();q.push(t);}while(!q.empty()){cout<<q.top().data<<endl;q.pop();}return 0; }4.結構體定義優先級還有種方式,可以不加const,但需要定義為友元。
struct node {int data;friend bool operator <(const node a,const node b) //!!!{return a.data>b.data; //此時是升序,如果是<則是降序} };5.對于結構體的定義,當在使用優先級定義時,根據其是堆的特性,還可以有如下定義優先級的方式。
#include <iostream> #include<queue> #include<ctime> #include<cstdlib> #include<vector> using namespace std;struct node {int data; }; struct nodeCmp {bool operator()(const node &a,const node &b)//注意寫法{return a.data>b.data; //此時為升序,<為降序} };int main() {srand(time(NULL));priority_queue<node,vector<node>,nodeCmp>q; //此處定義與之前方式不同for(int i=10;i>0;i--){node t;t.data=rand();q.push(t);}while(!q.empty()){cout<<q.top().data<<endl;q.pop();}return 0; }鏈表
天梯賽模擬 鏈表去重 (25 分)
單向鏈表
單向鏈表中包含數據域和指針域,其中數據域用于存放數據,指針域用來連接當前結點和下一節點。
struct Node {int value;Node *next; };雙向鏈表
雙向鏈表中同樣有數據域和指針域,不同之處在于指針域有左右(或上一個、下一個)之分,用來連接上一個節點、當前結點、下一個結點。
struct Node {int value;Node *left;Node *right; };向鏈表中插入(寫入)數據
單向鏈表
void insertNode(int i, Node *p) {Node *node = new Node;node->value = i;node->next = p->next;p->next = node; }單向循環鏈表
上面介紹了簡單的單向鏈表的插入數據,有時我們會將鏈表的頭尾連接起來將鏈表變為循環鏈表
void insertNode(int i, Node *p) {Node *node = new Node;node->value = i;node->next = NULL;if (p == NULL) {p = node;node->next = node;} else {node->next = p->next;p->next = node;} }由于是循環的鏈表,我們在插入數據時需要判斷原鏈表是否為空,為空則自身循環,不為空則正常插入數據循環。具體過程可參考下面這張圖。
雙向循環鏈表
void insertNode(int i, Node *p) {Node *node = new Node;node->value = i;if (p == NULL) {p = node;node->left = node;node->right = node;} else {node->left = p;node->right = p->right;p->right->left = node;p->right = node;} }從鏈表中刪除數據
單向(循環)鏈表
void deleteNode(Node *p) {p->value = p->next->value;Node *t = p->next;p->next = p->next->next;delete t; }從鏈表中刪除某個結點時,將 p 的下一個結點 (p->next) 的值覆蓋給 p 即可,與此同時更新 p 的下下個結點。具體過程可參考下面這張圖。
雙向循環鏈表
void deleteNode(Node *&p) {p->left->right = p->right;p->right->left = p->left;Node *t = p;p = p->right;delete t; }哈希表
哈希表是又稱散列表,一種以 “key-value” 形式存儲數據的數據結構。所謂以 “key-value” 形式存儲數據,是指任意的 key 都唯一對應到內存中的某個位置。只需要輸入查找的值 key,就可以快速地找到其對應的 value。可以把哈希表理解為一種高級的數組,這種數組的下標可以是很大的整數,浮點數,字符串甚至結構體。
哈希函數
要讓 key 對應到內存中的位置,就要為 key 計算索引,也就是計算這個數據應該放到哪里。這個根據 key 計算索引的函數就叫做哈希函數,也稱散列函數。舉個例子,比如 key 是一個人的身份證號碼,哈希函數就可以是號碼的后四位,當然也可以是號碼的前四位。生活中常用的“手機尾號”也是一種哈希函數。在實際的應用中,key 可能是更復雜的東西,比如浮點數、字符串、結構體等,這時候就要根據具體情況設計合適的哈希函數。哈希函數應當易于計算,并且盡量使計算出來的索引均勻分布。
在 OI 中,最常見的情況應該是 key 為整數的情況。當 key 的范圍比較小的時候,可以直接把 key 作為數組的下標,但當 key 的范圍比較大,比如以 10910^{9}109范圍內的整數作為 key 的時候,就需要用到哈希表。一般把 key 模一個較大的質數作為索引,也就是取 f(x)=xf(x)=xf(x)=x modmodmod MMM 作為哈希函數。另一種比較常見的情況是 key 為字符串的情況,在 OI 中,一般不直接把字符串作為 key,而是先算出字符串的哈希值,再把其哈希值作為 key 插入到哈希表里。
能為 key 計算索引之后,我們就可以知道每個 value 應該放在哪里了。假設我們用數組 a 存放數據,哈希函數是 f,那鍵值對 (key,value) 就應該放在 a[f(key)]上。不論 key 是什么類型,范圍有多大,f(key) 都是在可接受范圍內的整數,可以作為數組的下標。
沖突
如果對于任意的 key,哈希函數計算出來的索引都不相同,那只用根據索引把 (key,value) 放到對應的位置就行了。但實際上,常常會出現兩個不同的 key,他們用哈希函數計算出來的索引是相同的。這時候就需要一些方法來處理沖突。在 OI 中,最常用的方法是拉鏈法。
拉鏈法
拉鏈法也稱開散列法(open hashing)。
拉鏈法是在每個存放數據的地方開一個鏈表,如果有多個 key 索引到同一個地方,只用把他們都放到那個位置的鏈表里就行了。查詢的時候需要把對應位置的鏈表整個掃一遍,對其中的每個數據比較其 key 與查詢的 key 是否一致。如果索引的范圍是 1~M,哈希表的大小為 N,那么一次插入/查詢需要進行期望 O(NM)O(\frac{N}{M})O(MN?)次比較。
這邊再為大家提供一個封裝過的模板,可以像 map 一樣用,并且較短
struct hash_map // 哈希表模板 {struct data{long long u;int v, nex;}; // 前向星結構data e[SZ << 1]; // SZ 是 const int 表示大小int h[SZ], cnt;int hash(long long u){return u % SZ;}int& operator[](long long u){int hu = hash(u); // 獲取頭指針for (int i = h[hu]; i; i = e[i].nex)if (e[i].u == u)return e[i].v;return e[++cnt] = (data) {u, -1, h[hu]}, h[hu] = cnt, e[cnt].v;}hash_map(){cnt = 0;memset(h, 0, sizeof(h));} };解釋一下,hash 函數是針對 key 的類型設計的,并且返回一個鏈表頭指針用于查詢。在這個模板中我們寫了一個(long long ,int) 式的 hash 表,并且當某個 key 不存在的時侯初始化對應的 val 成 -1。hash_map() 函數是在定義的時侯初始化用的。
閉散列法
閉散列方法把所有記錄直接存儲在散列表中,如果發生沖突則根據某種方式繼續進行探查。
比如線性探查法:如果在 d 處發生沖突,就依次檢查 d + 1,d+2……
并查集
并查集是一種樹形的數據結構,顧名思義,它用于處理一些不交集的 合并 及 查詢 問題。 它支持兩種操作:
- 查找(Find):確定某個元素處于哪個子集(優化在路徑壓縮);
- 合并(Union):將兩個子集合并成一個集合。
(1)Friendly Group Gym - 102769F 2020(并查集)ccpc秦皇島分站賽
(2)UVA10129 Play on Words (并查集判連通+歐拉回路)
(3)Ice_cream’s world I HDU - 2120(并查集判環)
啟發式合并(按秩合并)
一個祖先突然抖了個機靈:「你們家族人比較少,搬家到我們家族里比較方便,我們要是搬過去的話太費事了。」
由于需要我們支持的只有集合的合并、查詢操作,當我們需要將兩個集合合二為一時,無論將哪一個集合連接到另一個集合的下面,都能得到正確的結果。但不同的連接方法存在時間復雜度的差異。具體來說,如果我們將一棵點數與深度都較小的集合樹連接到一棵更大的集合樹下,顯然相比于另一種連接方案,接下來執行查找操作的用時更小(也會帶來更優的最壞時間復雜度)。
當然,我們不總能遇到恰好如上所述的集合點數與深度都更小。鑒于點數與深度這兩個特征都很容易維護,我們常常從中擇一,作為估價函數。而無論選擇哪一個,時間復雜度都為O(max(m,n))O(max(m,n))O(max(m,n)) 。
在算法競賽的實際代碼中,即便不使用啟發式合并,代碼也往往能夠在規定時間內完成任務。
如果只使用啟發式合并,而不使用路徑壓縮,時間復雜度為O(mO(mO(m logloglog n)n)n) 。由于路徑壓縮單次合并可能造成大量修改,有時路徑壓縮并不適合使用。例如,在可持久化并查集、線段樹分治 + 并查集中,一般使用只啟發式合并的并查集。
此處給出一種 C++ 的參考實現,其選擇點數作為估價函數:
std::vector<int> size(N, 1); // 記錄并初始化子樹的大小為 1 void unionSet(int x, int y) {int xx = find(x), yy = find(y);if (xx == yy) return;if (size[xx] > size[yy]) // 保證小的合到大的里swap(xx, yy);fa[xx] = yy;size[yy] += size[xx]; }帶權并查集
(1)How Many Answers Are Wrong HDU - 3038(帶權并查集)
(2)Rochambeau POJ - 2912 (枚舉和加權并查集+路徑壓縮)找唯一裁判
我們還可以在并查集的邊上定義某種權值、以及這種權值在路徑壓縮時產生的運算,從而解決更多的問題。比如對于經典的「NOI2001」食物鏈,我們可以在邊權上維護模 3 意義下的加法群。
其他應用
種類并查集:
Find them, Catch them POJ - 1703(種類并查集)
最小生成樹算法 中的 Kruskal 和 最近公共祖先 中的 Tarjan 算法是基于并查集的算法。
堆
堆是一棵樹,其每個節點都有一個鍵值,且每個節點的鍵值都大于等于/小于等于其父親的鍵值。
每個節點的鍵值都大于等于其父親鍵值的堆叫做小根堆,否則叫做大根堆。STL 中的 priority_queue 其實就是一個大根堆。
(小根)堆主要支持的操作有:插入一個數、查詢最小值、刪除最小值、合并兩個堆、減小一個元素的值。
習慣上,不加限定提到“堆”時往往都指二叉堆。
二叉堆
從二叉堆的結構說起,它是一棵二叉樹,并且是完全二叉樹,每個結點中存有一個元素(或者說,有個權值)。
堆性質:父親的權值不小于兒子的權值(大根堆)。同樣的,我們可以定義小根堆。本文以大根堆為例。
由堆性質,樹根存的是最大值(getmax 操作就解決了)。
插入操作
插入操作是指向二叉堆中插入一個元素,要保證插入后也是一棵完全二叉樹。
最簡單的方法就是,最下一層最右邊的葉子之后插入。
如果最下一層已滿,就新增一層。
- 插入之后可能會不滿足堆性質?
向上調整:如果這個結點的權值大于它父親的權值,就交換,重復此過程直到不滿足或者到根。插入之后向上調整后,沒有其他結點會不滿足堆性質。
向上調整的時間復雜度是O(logO(logO(log n)n)n) 的。
刪除操作
刪除操作指刪除堆中最大的元素,即刪除根結點。
但是如果直接刪除,則變成了兩個堆,難以處理。
所以不妨考慮插入操作的逆過程,設法將根結點移到最后一個結點,然后直接刪掉。
然而實際上不好做,我們通常采用的方法是,把根結點和最后一個結點直接交換。
于是直接刪掉(在最后一個結點處的)根結點,但是新的根結點可能不滿足堆性質……
向下調整:在該結點的兒子中,找一個最大的,與該結點交換,重復此過程直到底層。刪除并向下調整后,沒有其他結點不滿足堆性質。
時間復雜度 O(logO(logO(log n)n)n) 。
減小某個點的權值
很顯然,直接修改后,向上調整一次即可,時間復雜度為 。
實現
我們發現,上面介紹的幾種操作主要依賴于兩個核心:向上調整和向下調整。
考慮使用一個序列 h 來表示堆hih_{i}hi?。 的兩個兒子分別是h2ih_{2i}h2i? 和 h2i+1h_{2i+1}h2i+1?, 1是根結點:
參考代碼:
建堆
考慮這么一個問題,從一個空的堆開始,插入n 個元素,不在乎順序。
直接一個一個插入需要 O(logO(logO(log n)n)n) 的時間,有沒有更好的方法?
方法一:使用 decreasekey(即,向上調整)
從根開始,按 BFS 序進行。
void build_heap_1() {for (i = 1; i <= n; i++) up(i); }方法二:使用向下調整
這時換一種思路,從葉子開始,逐個向下調整
void build_heap_2() {for (i = n; i >= 1; i--) down(i); }對頂堆
維護一個序列,支持兩種操作:
1.向序列中插入一個元素
2.輸出并刪除當前序列的中位數(若序列長度為偶數,則輸出較小的中位數)
這個問題可以被進一步抽象成:動態維護一個序列上第 k大的數,k 值可能會發生變化。
對于此類問題,我們可以使用 對頂堆 這一技巧予以解決(可以避免寫權值線段樹或 BST 帶來的繁瑣)。
對頂堆由一個大根堆與一個小根堆組成,小根堆維護大值即前 k大的值(包含第 k 個),大根堆維護小值即比第k 大數小的其他數。
這兩個堆構成的數據結構支持以下操作:
- 維護:當小根堆的大小小于k 時,不斷將大根堆堆頂元素取出并插入小根堆,直到小根堆的大小等于k ;當小根堆的大小大于 k時,不斷將小根堆堆頂元素取出并插入大根堆,直到小根堆的大小等于 k;
- 插入元素:若插入的元素大于等于小根堆堆頂元素,則將其插入小根堆,否則將其插入大根堆,然后維護對頂堆;
- 查詢第 k 大元素:小根堆堆頂元素即為所求;
- 刪除第 k 大元素:刪除小根堆堆頂元素,然后維護對頂堆;
- k 值+1/-1 :根據新的 k值直接維護對頂堆。
顯然,查詢第 k 大元素的時間復雜度是(1)(1)(1) 的。由于插入、刪除或調整 值后,小根堆的大小與期望的 k 值最多相差 1,故每次維護最多只需對大根堆與小根堆中的元素進行一次調整,因此,這些操作的時間復雜度都是 O(logO(logO(log n)n)n) 的。
#include <cstdio> #include <iostream> #include <queue> using namespace std; int t, x; int main() {scanf("%d", &t);while (t--) {// 大根堆,維護前一半元素(存小值)priority_queue<int, vector<int>, less<int> > a;// 小根堆,維護后一半元素(存大值)priority_queue<int, vector<int>, greater<int> > b;while (scanf("%d", &x) && x) {// 若為查詢并刪除操作,輸出并刪除大根堆堆頂元素// 因為這題要求輸出中位數中較小者(偶數個數字會存在兩個中位數候選)// 這個和上面的第k大講解有稍許出入,但如果理解了上面的,這個稍微變通下便可理清if (x == -1) {printf("%d\n", a.top());a.pop();}// 若為插入操作,根據大根堆堆頂的元素值,選擇合適的堆進行插入else {if (a.empty() || x <= a.top())a.push(x);elseb.push(x);}// 對堆頂堆進行調整if (a.size() > (a.size() + b.size() + 1) / 2) {b.push(a.top());a.pop();} else if (a.size() < (a.size() + b.size() + 1) / 2) {a.push(b.top());b.pop();}}}return 0; }左偏樹
左偏樹是一種 可并堆,具有堆的性質,并且可以快速合并。
dist 的定義和性質
對于一棵二叉樹,我們定義 外節點 為左兒子或右兒子為空的節點,定義一個外節點的 dist 為 1,一個不是外節點的節點dist 為其到子樹中最近的外節點的距離加一。空節點的 dist為0 。
注:很多其它教程中定義的 dist都是本文中的 dist 減去1 ,本文這樣定義是因為代碼寫起來方便。
一棵有n 個節點的二叉樹,根的 dist不超過 [log(n+1)][log(n+1)][log(n+1)],因為一棵根的 dist為x 的二叉樹至少有x-1層是滿二叉樹,那么就至少有2x?12^{x}-12x?1 個節點。注意這個性質是所有二叉樹都具有的,并不是左偏樹所特有的。
左偏樹的定義和性質
左偏樹是一棵二叉樹,它不僅具有堆的性質,并且是「左偏」的:每個節點左兒子的 都大于等于右兒子的dist 。
因此,左偏樹每個節點的dist 都等于其右兒子的 dist加一。
需要注意的是, dist不是深度,左偏樹的深度沒有保證,一條向左的鏈也是左偏樹。
核心操作:合并(merge)
合并兩個堆時,由于要滿足堆性質,先取值較小(為了方便,本文討論小根堆)的那個根作為合并后堆的根節點,然后將這個根的左兒子作為合并后堆的左兒子,遞歸地合并其右兒子與另一個堆,作為合并后的堆的右兒子。為了滿足左偏性質,合并后若左兒子的 dist小于右兒子的dist ,就交換兩個兒子。
參考代碼:
由于左偏性質,每遞歸一層,其中一個堆根節點的 dist 就會減小1 ,而“一棵有n 個節點的二叉樹,根的 dist 不超過[log(n+1)][log(n+1)][log(n+1)] ”,所以合并兩個大小分別為n 和 m 的堆復雜度是O(logO(logO(log n+logn+logn+log m)m)m) 。
左偏樹還有一種無需交換左右兒子的寫法:將 較大的兒子視作左兒子, 較小的兒子視作右兒子:
int& rs(int x) { return t[x].ch[t[t[x].ch[1]].d < t[t[x].ch[0]].d]; } int merge(int x, int y) {if (!x || !y) return x | y;if (t[x].val < t[y].val) swap(x, y);rs(x) = merge(rs(x), y);t[x].d = t[rs(x)].d + 1;return x; }左偏樹的其它操作
- 插入節點
單個節點也可以視為一個堆,合并即可。 - 刪除根
合并根的左右兒子即可。 - 刪除任意節點
做法
先將左右兒子合并,然后自底向上更新 dist、不滿足左偏性質時交換左右兒子,當 dist無需更新時結束遞歸:
整個堆加上/減去一個值、乘上一個正數
其實可以打標記且不改變相對大小的操作都可以。
在根打上標記,刪除根/合并堆(訪問兒子)時下傳標記即可:
int merge(int x, int y) {if (!x || !y) return x | y;if (t[x].val > t[y].val) swap(x, y);pushdown(x);t[x].rs = merge(t[x].rs, y);if (t[t[x].rs].d > t[t[x].ls].d) swap(t[x].ls, t[x].rs);t[x].d = t[t[x].rs].d + 1;return x; } int pop(int x) {pushdown(x);return merge(t[x].ls, t[x].rs); }隨機合并
int merge(int x, int y) {if (!x || !y) return x | y;if (t[y].val < t[x].val) swap(x, y);if (rand() & 1) //隨機選擇是否交換左右子節點swap(t[x].ls, t[x].rs);t[x].ls = merge(t[x].ls, t[y]);return x; }可以看到該實現方法唯一不同之處便是采用了隨機數來實現合并,這樣一來便可以省去 dist的相關計算。且平均時間復雜度亦為O(logO(logO(log n)n)n) ,
塊狀數據結構
分塊思想
塊狀數組
塊狀鏈表
樹分塊
Sqrt Tree
單調棧
單調棧中存放的數據應該是有序的,所以單調棧也分為單調遞增棧和單調遞減棧
- 單調遞增棧:單調遞增棧就是從棧底到棧頂數據是從大到小
- 單調遞減棧:單調遞減棧就是從棧底到棧頂數據是從小到大
單調隊列
//在“尾部”添加元素x while (l != r && mq[r] <= x) r--; mq[++r] = x;//查詢隊首元素 if (l != r) printf("%d\n", mq[l+1]); else printf("-1\n");//彈出隊首元素 if (l != r) l++;樹狀數組
(1)藍橋杯2014屆試題9題 小朋友排隊(樹狀數組+類逆序對)
(2)A Simple Problem with Integers POJ - 3468(線段樹+區間查詢+區間修改+建樹+懶惰標記模板)+(樹狀數組)
(3)Minimum Inversion Number HDU - 1394(求一個數字環的逆序對+多種解法)
樹狀數組的代碼要比線段樹短得多,思維也更清晰,在解決一些單點修改的問題時,樹狀數組是不二之選。
如果要具體了解樹狀數組的工作原理,請看下面這張圖:
這個結構的思想和線段樹有些類似:用一個大節點表示一些小節點的信息,進行查詢的時候只需要查詢一些大節點而不是更多的小節點。
最下面的八個方塊就代表存入a中的八個數,現在都是十進制。
他們上面的參差不齊的剩下的方塊就代表 的上級——c 數組。
很顯然看出:
c2c_{2}c2?管理的是 a1a_{1}a1?&a2a_{2}a2?;
c4c_{4}c4?管理的是 a1a_{1}a1?&a2a_{2}a2?&a3a_{3}a3?&a4a_{4}a4?;
c6c_{6}c6?管理的是 a5a_{5}a5?&a6a_{6}a6?;
c8c_{8}c8?則管理全部 8個數。
所以,如果你要算區間和的話,比如說要算 ~ 的區間和,暴力算當然可以,那上百萬的數,那就 TLE 嘍。
那么這種類似于跳一跳的連續跳到中心點而分值不斷變大的原理是一樣的(倍增)。
你從 開始往前跳,發現 ( 我也不確定是多少,算起來太麻煩,就意思一下)只管 這個點,那么你就會找 ,發現 管的是 &;那么你就會直接跳到 , 就會管 ~ 這些數,下次查詢從 往前找,以此類推。
用法及操作
那么問題來了,你是怎么知道 管的 的個數分別是多少呢?你那個 1 個, 2個, 8個……是怎么來的呢? 這時,我們引入一個函數——lowbit:
int lowbit(int x) {// 算出x二進制的從右往左出現第一個1以及這個1之后的那些0組成數的二進制對應的十進制的數return x & -x; }lowbit 的意思注釋說明了,咱們就用這個說法來證明一下a[88] :
88(10)=1011000(2)88_{(10)}=1011000_{(2)}88(10)?=1011000(2)?
發現第一個1 以及他后面的 0組成的二進制是 1000(2)=8(10)1000_{(2)}=8_{(10)}1000(2)?=8(10)?
1000對應的十進制是8 ,所以 c 一共管理 8個a 。
這就是 lowbit 的用處,僅此而已(但也相當有用)。
您可能又問了:x & -x 是什么意思啊?
在一般情況下,對于 int 型的正數,最高位是 0,接下來是其二進制表示;而對于負數 (-x),表示方法是把 x 按位取反之后再加上 1。
例如 :
那么對于 單點修改 就更輕松了:
每次只要在他的上級那里更新就行,自己就可以不用管了。
int getsum(int x) { // a[1]……a[x]的和int ans = 0;while (x >= 1) {ans = ans + c[x];x = x - lowbit(x);}return ans; }區間加 & 區間求和
若維護序列a的差分數組 b,此時我們對a 的一個前綴r 求和,即∑i=1rai∑^{r}_{i=1}a_{i}∑i=1r?ai? ,由差分數組定義得 ai=a_{i}=ai?=∑j=1ibj∑^{i}_{j=1}b_{j}∑j=1i?bj?
進行推導
區間和可以用兩個前綴和相減得到,因此只需要用兩個樹狀數組分別維護∑bi∑b_{i}∑bi? 和∑i?bi∑i*b_{i}∑i?bi? ,就能實現區間求和。
代碼如下
int t1[MAXN], t2[MAXN], n;inline int lowbit(int x) { return x & (-x); }void add(int k, int v) {int v1 = k * v;while (k <= n) {t1[k] += v, t2[k] += v1;k += lowbit(k);} }int getsum(int *t, int k) {int ret = 0;while (k) {ret += t[k];k -= lowbit(k);}return ret; }void add1(int l, int r, int v) {add(l, v), add(r + 1, -v); // 將區間加差分為兩個前綴加 }long long getsum1(int l, int r) {return (r + 1ll) * getsum(t1, r) - 1ll * l * getsum(t1, l - 1) -(getsum(t2, r) - getsum(t2, l - 1)); }Tricks O(n)O(n)O(n)建樹:
每一個節點的值是由所有與自己直接相連的兒子的值求和得到的。因此可以倒著考慮貢獻,即每次確定完兒子的值后,用自己的值更新自己的直接父親。
// O(n)建樹 void init() {for (int i = 1; i <= n; ++i) {t[i] += a[i];int j = i + lowbit(i);if (j <= n) t[j] += t[i];} }O(logO(logO(log n)n)n)查詢第k 小/大元素。在此處只討論第k 小,第 k 大問題可以通過簡單計算轉化為第k 小問題。
將所有數字看成一個可重集合,即定義數組 a表示值為 i 的元素在整個序列重出現了 aia_{i}ai?次。找第 k 大就是找到最小的x 恰好滿足 ∑i=1x,ai>=k∑^{x}_{i=1},a_{i}>=k∑i=1x?,ai?>=k
因此可以想到算法:如果已經找到 x 滿足 ∑i=1x,ai>=k∑^{x}_{i=1},a_{i}>=k∑i=1x?,ai?>=k,考慮能不能讓 x 繼續增加,使其仍然滿足這個條件。找到最大的x 后,x+1 就是所要的值。 在樹狀數組中,節點是根據 2 的冪劃分的,每次可以擴大 2 的冪的長度。令sum 表示當前的 x所代表的前綴和,有如下算法找到最大的 x:
將 depth減 1,回到步驟 2,直至 depth 為 0
時間戳優化:
對付多組數據很常見的技巧。如果每次輸入新數據時,都暴力清空樹狀數組,就可能會造成超時。因此使用tag 標記,存儲當前節點上次使用時間(即最近一次是被第幾組數據使用)。每次操作時判斷這個位置tag 中的時間和當前時間是否相同,就可以判斷這個位置應該是 0 還是數組內的值。
//時間戳優化 int tag[MAXN], t[MAXN], Tag; void reset() { ++Tag; } void add(int k, int v) {while (k <= n) {if (tag[k] != Tag) t[k] = 0;t[k] += v, tag[k] = Tag;k += lowbit(k);} } int getsum(int k) {int ret = 0;while (k) {if (tag[k] == Tag) ret += t[k];k -= lowbit(k);}return ret; }線段樹
(1)Mayor’s posters POJ - 2528 (離散化+線段樹)
(2)Balanced Lineup POJ - 3264(線段樹模板+查詢比大小+建樹)
(3)Just a Hook HDU - 1698(查詢區間求和+最基礎模板)
(4)Assign the task HDU - 3974(線段樹+dfs建樹+單點查詢+區間修改)
李超線段樹
區間最值操作 & 區間歷史最值
劃分樹
二叉搜索樹 & 平衡樹
跳表
可持久化數據結構
樹套樹
K-D Tree
珂朵莉樹
動態樹
析合樹
總結
以上是生活随笔為你收集整理的数据结构整理中。。。的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微信两种拍照方法
- 下一篇: 创建型模式——单例模式