Codeforces Round #579 (Div. 3) D2. Remove the Substring (hard version) (思維,貪心)
樹狀陣列
一、適用範圍
- 樹狀陣列是一個查詢和修改複雜度都為 \(log(n)\) 的資料結構,常常用於查詢任意區間的所有元素之和。
- 與字首和的區別是支援動態修改, \(log(n)\) 的時間進行修改,\(log(n)\) 查詢。
- 支援如下操作:
- 單點修改區間查詢
- 區間修改單點查詢
- 區間修改區間查詢
二、演算法原理
- 樹狀陣列較好的利用了二進位制。它的每個節點的值代表的是自己和前面一些連續元素的和。至於到底是前面哪些元素,這就由這個節點的下標決定。
- 設節點的編號為 \(i\) ,那麼:
-
即可以推匯出:
C[1] = A[1] # lowbit(1)個元素之和 C[2] = C[1] + A[2] = A[1] + A[2] # lowbit(2)個元素之和 C[3] = A[3] # lowbit(3)個元素之和 C[4] = C[2] + C[3] +A[4] = A[1] + A[2] + A[3] + A[4] # lowbit(4)個元素之和 C[5] = A[5] C[6] = C[5] + A[6] = A[5] + A[6] C[7] = A[7] C[8] = C[4] + C[6] + C[7] + A[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8]
-
顯然一個節點並不一定是代表自己前面所有元素的和。只有滿足 \(2^n\) 這樣的數才代表自己前面所有元素的和。
-
理解 \(lowbit\) 函式
-
原碼:如果機器字長為 \(n\),那麼一個數的原碼就是用一個 \(n\) 位的二進位制數,其中最高位為符號位:正數為 \(0\),負數為 \(1\)。剩下的 \(n-1\) 位表示該數的絕對值。
-
反碼:知道了原碼,那麼你只需要具備區分 \(0\) 跟 \(1\) 的能力就可以輕鬆求出反碼,為什麼呢?因為反碼就是在原碼的基礎上,符號位不變其他位按位取反(就是 \(0\) 變 \(1\),\(1\) 變 \(0\))就可以了。
-
補碼也非常的簡單,就是在反碼的基礎上按照正常的加法運算加 \(1\)
-
$lowbit(x)=x&-x $ :表示擷取 \(x\) 二進位制最右邊的 \(1\) 所表示的值,可以寫成函式或巨集定義
-
注意巨集定義是括號,因為巨集名只是起到一個替代作用,不加括號在運算時優先順序會出問題
//1. 巨集定義,注意括號,不建議這樣寫,容易產生歧義 #define lowbit(x) ((x) & -(x)) //2. 函式寫法,推薦寫法: int lowbit(int x){return x & -x;}
-
三、 樹狀陣列的操作
-
\(update\) 更新操作
-
因為樹狀陣列 \(c[x]\) 維護的是一個或若干個連續數之和,當我們修改了 \(a[x]\) 之後,\(x\sim n\) 字首和均發生了變化,所以除了\(c[x]\) 需要修改之外 \(x\) 的祖先節點也必須修改而 \(x\) 的父親節點為 \(x+lowbit(x)\),我們叫向上更新。
-
把序列中第 \(i\) 個數增加 \(x\),\(sum[i]\sim sum[n]\) 均增加了 \(x\) ,所以我們只需把這個增量往上更新即可。如果,把 \(a[i]\) 修改成 \(x\),則我們向上更新 \(a[i]\) 的增量:\(x-a[i]\)。
//1. a[id] 增加 x while寫法 void updata(int id,int x){ while(id<=n){//向上更新,更新到n為止 c[id]+=x; id+=lowbit(id); } } //2. a[id] 修改成 x for寫法 void updata(int id,int x){//或者傳遞引數是x=x-a[id],此時跟第一種寫法一樣 for(int i=id;i<=n;i+=lowbit(i)) c[i]+=x-a[id]; }
-
-
\(getsum\) 查詢操作
-
因為樹狀陣列維護的是一個能夠動態修改的字首和,所以可以在 \(log(n)\) 的效率下求出前 \(n\) 項和\(sum[i]\) 。
-
如果 \(i=2^j (j=0,1,..n)\), 此時最簡單,顯然有:\(sum[i]=c[i]\) ,如果 \(i\) 是其他的情況呢?
- \(sum[5]=c[5]+c[4]\ (4=5-lowbit(5))\)
- \(sum[15]=c[15]+c[14]+c[12]+c[8]\ (14=15-lowbit(15),12=14-lowbit(14),...)\)
-
顯然,想要求出前 \(i\) 項字首和 \(sum[i]\) ,只需沿著當前節點向下累加直到節點編號為 \(2^j\) 為止。我們叫向下求和。
int getsum(int id){ int tot=0; for(int i=id;i>0;i-=lowbit(i)) tot+=c[i]; return tot; }
-