在C語言,我們可以透過下語法宣告一個大小固定的陣列
int arr[100];
然而這樣做缺點很多...
int sz = 100;
int arr[sz];
// VLA(Variable-length array) Not Allow In C++
1. C++中,大小不能是變數
然而這樣做缺點很多...
int a[100], b[100];
//a = b; // CE!
memcpy(a, b, sizeof(a)); //OK
for(int i=0;i<100;++i)
a[i]=b[i]; //OK
2. 不能簡單複製
然而這樣做缺點很多...
void f()
{
int a[600000];
//Maybe Stack Overflow
}
3. 宣告於函數內會占用有限的Stack空間
Linux / Windows 預設僅提供 8 MB的 Stack 空間
透過 calloc / malloc 函數,可以動態建立陣列
int a = 100;
int *ptr = (int*)malloc( sizeof(int)*100 );
缺點
int a = 100;
int *ptr = (int*)malloc( sizeof(int)*100 );
1.語法上有點累贅
缺點
int a = 100;
int *ptr = (int*)malloc( sizeof(int)*100 );
// free(ptr);
2. 忘記 free 可能會導致memory leak,浪費資源
缺點
int a = 100;
int *ptr = (int*)malloc( sizeof(int)*100 );
free(ptr);
free(ptr); // Double Free Issue
3. free 2 次會導致你的程式有嚴重的安全性 Bug
#include <vector>
#include <cstdio>
using namespace std; //簡化C++的一些語法
int main()
{
vector<int> arr;
return 0;
}
#include <vector>
#include <cstdio>
using namespace std; //簡化C++的一些語法
int main()
{
vector<int> arr;
int n = 100;
arr.resize(n);
a[0]=a[99]=123; //當作可以當作 int[100] 的陣列用
}
#include <vector>
#include <cstdio>
using namespace std; //簡化C++的一些語法
int main()
{
vector<int> arr, tmp;
arr.resize(2);
arr[0] = arr[1] = 777;
tmp = arr; //It is OK~
}
void f(vector<int> data) // Pass Value By COPY!
{
data[0] = 12;
}
int main()
{
vector<int> arr;
arr.resize(1);
arr[1] = 0;
f(arr);
printf("%d", arr[0]); // Output: 0
}
void f(vector<int> &data) // Pass Value By REFERENCE! (C++)
{
data[0] = 12;
}
int main()
{
vector<int> arr;
arr.resize(1);
arr[1] = 0;
f(arr);
printf("%d", arr[0]); // Output: 12
}
使用Stack的小題目
Stack 像一個箱子,可以把東西放進去,拿出來
如果火車頭只有向前開的動力
你知道如何設計軌道讓火車掉頭嗎?
Car 1
Car 2
一般而言,火車兩端都有一個火車頭可以拉動整台車
目標是要把所有的車廂順序也顛倒過來
Car 1
Car 2
C1
C2
C3
C1
C2
C3
C1
C2
C3
Stack:
最後進來的,最先出去
C1
C2
C3
Stack:
最後進來的,最先出去
C1
C2
C3
Stack:
最後進來的,最先出去
C1
C2
C3
Stack:
最後進來的,最先出去
C1
C2
C3
Stack:
最後進來的,最先出去
C1
C2
C3
上次介紹vector的一些用法
說明 | |
---|---|
v[id] | 存取第id個元素 |
v.resize(N) | 將大小重設為 N |
v.clear() | 清除所有元素 |
vector<int> v;
v.resize(3);
printf("%zu", v.size()); // 3
vector<int> v; // v = [ ]
v.emplace_back(1); // v = [ 1 ]
v.emplace_back(2); // v = [ 1 , 2 ]
v.emplace_back(3); // v = [ 1 , 2 , 3 ]
printf("%zu", v.size()); // 3
vector<int> v; // v = [ ]
v.emplace_back(7122); // v = [ 7122 ]
printf("%zu", v.back()); // 7122
vector<int> u;
// u.back(); ERROR! 沒東西不可以存取
vector<int> v;
v.resize(3);
v[0]=8; v[1]=7; // v = [ 8 , 7 ]
v.pop_back(); // v = [ 8 ]
v.pop_back(); // v = [ ]
// v.pop_back(); ERROR! 不可以對空vector刪除元素
今天有一台火車,車廂編號是 \(1,2,3\dots N\) 放在一個 Y 形軌道上
請問是否透過 Y 形軌道,使車廂重新排列程指定的順序
車廂可以分離,獨立移動,只要在軌道上就好,要遵守方向行駛
\(1,2,3,4\)
\(1,2,3,4\)
\(1,2,3,4\)
\(3,4,2,1\)
\(1,2,3,4\)
\(3,4,2,1\)
能做的事有哪些?
1. 把一節車廂從A放進Station
2. 把一節車廂從Station放進B
\(1,2,3,4\)
\(3,4,2,1\)
考慮第一台車 \(3\)
要如何把 \(3\)從移動過來?
\(4\)
\(3\)
Algorithm
檢查 Station 最前面的車廂是不是 3,如果不是,就從A 拿車廂到Station,直到滿足條件為止
\(2,1\)
\(4\)
\(3\)
考慮第二台車 \(4\)
要如何把 \(4\)移動過來?
\(2,1\)
\(-\)
\(3,4\)
Algorithm
檢查 Station 最前面的車廂是不是 4,如果不是,就從A 拿車廂到Station,直到滿足條件為止
\(2,1\)
\(-\)
\(3,4,2,1\)
Algorithm
對於每一個編號\(x_i\)依序檢查
今天 \(Sylveon\) 得到了一個只由小/中括號組合成的字串
好奇的他想知道這些括號是不是合法配對的
(), [](), ([]) 都是合法的配對
)(, (] , ([)] 是不合法的配對
自由練習題
今天 \(Sylveon\) 得到了一個只由數字,加減乘除組成的算式
好奇的他想這個算式計算完後的結果
\(2+3*5-6\)
自由練習題 / 程式設計二期中 Project
更多二分搜尋法的技巧 @
int BinarySearch(int *a, int N, int x) {
int L = 0;
int R = N-1;
while( L<=R ) {
int M = (L+R)/2;
if( a[M] == x ) return M;
if( x < a[M] ) R = M - 1;
else L = M + 1;
}
return -1; // Not Found
}
小於 x | 等於 x | 大於 x |
---|
第一個小於\(x\)
第一個等於\(x\)
第一個大於\(x\)
最後一個等於\(x\)
小於 \(x\)
大於等於 \(x\)
L
R
定義 位置 L 總是在小於\(x\)的位置上
定義 位置 L 總是在大於\(x\)的位置上
小於 \(x\)
大於等於 \(x\)
L
R
透過一些方法,使得 L, R 收斂在交界處
再根據需求選取要的答案
L
R
小於 \(x\)
大於等於 \(x\)
L
R
與二分搜尋法很像
每次選一個 M,檢查 M 在哪一邊
M
小於 \(x\)
大於等於 \(x\)
L
R
與二分搜尋法很像
每次選一個 M,檢查 M 在哪一邊
小於 \(x\)
大於等於 \(x\)
特別注意特判這些狀況:
全部都大於等於 \(x\)
全部都小於 \(x\)
int LowerBound(int *a, int N, int x)
{
int L = 0;
int R = N-1;
if( a[L] >= x ) return L;
if( a[R] < x ) return -1;// Not Found
while( L+1<R )
{
int M = (L+R)/2;
if( x <= a[M] ) R = M;
else L = M;
}
return L;
}
\(f(x) < 7122\)
\( f(x) \geq 7122 \)
\(x=0\)
\(x=100\)
\(f(x) = 7100 + x\)
今天黎明卿想讓 \(n\) 位小朋友排隊搭電梯上樓,體重依序是 \(w_1, w_2, \cdots , w_n\),電梯有限重 \(S\),如果黎明卿希望用小於 \(K\) 趟電梯讓小朋友們全數搭乘上樓,\(S\) 最小值是多少?
類似:TIOJ 1432
今天黎明卿想讓 \(n\) 位小朋友排隊搭電梯上樓,體重依序是 \(w_1, w_2, \cdots , w_n\),電梯有限重 \(S\),如果黎明卿希望用小於等於 \(K\) 趟電梯讓小朋友們全數搭乘上樓,\(S\) 最小值是多少?
類似:TIOJ 1432
Example 1.
\(\{w_i\}=1,5,2,3,4\)
\(K = 3\)
Example 2.
\(\{w_i\}=1,2,2,3,1\)
\(K = 3\)
今天黎明卿想讓 \(n\) 位小朋友排隊搭電梯上樓,體重依序是 \(w_1, w_2, \cdots , w_n\),電梯有限重 \(S\),如果黎明卿希望用小於等於 \(K\) 趟電梯讓小朋友們全數搭乘上樓,\(S\) 最小值是多少?
類似:TIOJ 1432
觀察會發現
( \(0\sim \sum w_i\) 之間 )
今天黎明卿想讓 \(n\) 位小朋友排隊搭電梯上樓,體重依序是 \(w_1, w_2, \cdots , w_n\),電梯有限重 \(S\),如果黎明卿希望用小於等於 \(K\) 趟電梯讓小朋友們全數搭乘上樓,\(S\) 最小值是多少?
類似:TIOJ 1432
設 \(f(x)\) 為:
類似:TIOJ 1432
設 \(f(x)\) 為:
從經驗可以發現,如果 \(x\) 越大,載運次數就越少
類似:TIOJ 1432
設 \(f(x)\) 為:
\(f(x) > K\)
\( f(x) \leq K \)
\(x=0\)
\(x=\sum w\)
從經驗可以發現,如果 \(x\)越大,載運次數就越少
類似:TIOJ 1432
設 \(f(x)\) 為:
\(f(x) > K\)
\( f(x) \leq K \)
\(x=0\)
\(x=\sum w\)
如果直接算答案很困難
不仿用二分搜尋法把答案給搜尋出來
類似:TIOJ 1432
設 \(f(x)\) 為:
\(f(x) > K\)
\( f(x) \leq K \)
\(x=0\)
\(x=\sum w\)
如果直接算答案很困難
不仿用二分搜尋法把答案給搜尋出來
int LowerBound(int W, int K, int x)
{
int L = 0;
int R = W;
while( L<=R )
{
int M = (L+R)/2;
if( f(x)<= K ) R = M;
else L = M;
}
return R;
}
今天黎明卿想讓 \(n\) 位小朋友排隊搭電梯上樓,體重依序是 \(w_1, w_2, \cdots , w_n\),電梯有限重 \(S\),如果黎明卿希望用小於等於 \(K\) 趟電梯讓小朋友們全數搭乘上樓,\(S\) 最小值是多少?
類似:TIOJ 1432
使用模擬的方法計算\(f(x)\)要 \(O(n)\) 的時間
二分搜尋法的範圍是 \(0\sim \sum w\),故需要 \(\log{\sum w}\) 次計算
因此總共花費了 \(O(n\log{\sum w})\)
就能用二分搜尋法的技巧,把答案給搜尋出來 !
並查集 / 互斥集
有一些遊戲,會要求每一個角色都要隸屬於一個唯一的公會
到處吃飯團
飯糰
可口羅
背骨仔
失智老人
國小聯盟
8歲小朋友
9歲小朋友
10歲小朋友
幼稚園
園長
廢物女僕
會說話的熊
大鈴鐺
有一些遊戲,會要求每一個角色都要隸屬於一個唯一的公會
到處吃飯團
飯糰
可口羅
背骨仔
失智老人
國小聯盟
8歲小朋友
9歲小朋友
10歲小朋友
幼稚園
園長
廢物女僕
會說話的熊
大鈴鐺
每一個公會都有唯一的公會會長,每個會員都知道
有一些遊戲,會要求每一個角色都要隸屬於一個唯一的公會
飯糰
可口羅
背骨仔
失智老人
8歲小朋友
9歲小朋友
10歲小朋友
園長
廢物女僕
會說話的熊
大鈴鐺
每一個公會都有唯一的公會會長,每個會員都知道
每一個公會的公會名稱隨時都會被修改
所以沒人知道自己確切的公會名是什麼
有一些遊戲,會要求每一個角色都要隸屬於一個唯一的公會
飯糰
可口羅
背骨仔
失智老人
8歲小朋友
9歲小朋友
10歲小朋友
園長
廢物女僕
會說話的熊
大鈴鐺
每一個公會都有唯一的公會會長,每個會員都知道
所以沒人知道自己確切的公會名是什麼
Q:有什麼方法,可以判斷任兩人是否隸屬在同一個公會中?
飯糰
可口羅
背骨仔
失智老人
8歲小朋友
9歲小朋友
10歲小朋友
園長
廢物女僕
會說話的熊
大鈴鐺
Q:有什麼方法,可以判斷任兩人是否隸屬在同一個公會中?
A : 判斷兩人的公會長是否為同一人
飯糰
可口羅
背骨仔
失智老人
8歲小朋友
9歲小朋友
10歲小朋友
任兩個公會可以合併成一個新公會,僅需要會長同意即可
新公會的會長為其中一方原公會會長
合併公會
飯糰
可口羅
背骨仔
失智老人
8歲小朋友
9歲小朋友
10歲小朋友
任兩個公會可以合併成一個新公會,僅需要會長同意即可
新公會的會長為其中一方原公會會長
Q:有什麼方法,可以判斷任兩人是否隸屬在同一個公會中?
Note : 公會成員可能不知道被合併的資訊,但是會長一定知道
任兩個公會可以合併成一個新公會,僅需要會長同意即可
新公會的會長為其中一方原公會會長
Q:有什麼方法,可以判斷任兩人是否隸屬在同一個公會中?
詢問自己知道的會長,目前的會長是誰
如果就是會長自己,就找到答案了
如果不是,就問新知道的會長一樣的問題。
A : 判斷兩人的公會長是否為同一人
A : 詢問自己知道的會長,目前的會長是誰
如果就是會長自己,就找到答案了
如果不是,就問新知道的會長一樣的問題。
int find(int x)
{
if( F[x] == x )
return x;
return find(F[x]);
}
int find(int x)
{
if( F[x] == x )
return x;
return F[x] = find(F[x]);
}
如果知道了真正的會長,就記住新的會長是誰,減少調查的次數
bool same(int A, int B)
{
return find(A) == find(B);
}
void same(int A, int B)
{
return find(A) == find(B);
}
任兩個公會可以合併成一個新公會,僅需要會長同意即可
新公會的會長為其中一方原公會會長
A
B
void U(int A, int B)
{
F[find(A)] = find(B);
}
int find(int x)
{
if( F[x] == x )
return x;
return F[x] = find(F[x]);
}
void U(int A, int B){
{
F[find(A)] = find(B);
}
void same(int A, int B)
{
return find(A) == find(B);
}
int find(int x)
{
if( F[x] == x )
return x;
return F[x] = find(F[x]);
}
int find(int x)
{
if( F[x] == x )
return x;
return F[x] = find(F[x]);
}
有路徑壓縮的 Disjoint Set,find 的平均複雜度是\(O(\log{n})\)
更快的方法及詳細的證明請自行搜尋
或好好修演算法 @
枕頭書:n 元素 f 次操作複雜度為\(\Theta(n+f\cdot(1+\log_{2+f/n}n))\)
前綴和 1
加入、修改、刪除...一個元素
加入、修改、刪除...很多個元素
問某個元素數值是多少...
問一些元素經過運算數值是多少...
很多問題經過化簡或變形後,可以變成序列問題,故多了解何種序列問題能被解決有助於擴展解題方向
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
while( i <= j )
ans += a[ i++ ];
如果現在修程設一功課這樣寫OJ也會TLE
迴圈最差會跑 \(O(n)\) 次,故複雜度為 \(O(Qn)\)
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
高一數學 - 數列與級數
一個數列 \(\{a\}\)的級數 \(S\) 定義如下
\(S_i=a_1+a_2+a_3+\dots+a_i=\sum\limits_{x=1}^{i}{a_x}\)
所以 \(a_i+a_2+\cdots+a_j = S_j-S_{i-1}\)
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
因為 \(a_i+a_2+\cdots+a_j = S_j-S_{i-1}\)
如果能預先計算 \(S\) ,就能很快的求出答案
for(int i=1; i<=n; ++i)
for(int j=1;j<=i;++j)
s[i] += a[j];
如何有效率計算 \(S_i\)
s[0]=0;
for(int i=1; i<=n; ++i)
s[i] = a[i] + s[i-1];
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
透過計算 \(S\) ,就可以在很快的時間計算區間總和
預處理:\(O(n)\)
算一次答案:\(O(1)\)
總計:\(O(n+Q)\)
s[0]=0;
for(int i=1; i<=n; ++i)
s[i] += s[i-1];
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
操作結束後,詢問數列的數值分別是多少
練習:TOJ239
\(a=\{0,0,0,0,0\}\)
操作
\(\{2,4\}, K=2\)
\(\{1,5\}, K=1\)
\(a=\{1,2,3,4,5\}\)
操作
\(\{3,4\}, K=2\)
\(\{1,3\}, K=1\)
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
操作結束後,詢問數列的數值分別是多少
練習:TOJ239
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
---|
\(\{a\}\)
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
---|
\(S\)
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
操作結束後,詢問數列的數值分別是多少
練習:TOJ239
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
---|
\(\{a\}\)
0 | 0 |
---|
\(S\)
2
2
2
2
2
2
2
2
2
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
操作結束後,詢問數列的數值分別是多少
練習:TOJ239
0 | 0 | 0 | 0 | 0 | 0 | 0 |
---|
\(\{a\}\)
0 | 0 | 0 | 0 | 0 | 0 |
---|
\(S\)
2
2
2
2
2
-2
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
操作結束後,詢問數列的數值分別是多少
練習:TOJ239
0 | 0 | 0 | 0 | 0 | 0 | 0 |
---|
\(\{a\}\)
0 | 0 | 0 | 0 | 0 | 0 |
---|
\(S\)
2
2
2
2
2
-2
透過前綴和陣列的轉換,可以很快的做到區間加 \(K\) 的動作
複雜度為\(O(Q+n)\)
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
可以運用計算 \(S\) 的方法解決嗎?
有更好的方法嗎? 可以比暴力 \(O(nQ)\) 快嗎?
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
三個解決此問題的基本方法
其中兩個會在進階班教
前綴和 2
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
用前綴和 \(S\) 的技巧處理第二個動作要怎麼做?
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
用前綴和 \(S\) 的技巧處理第二個動作要怎麼做?
因為 \(S_i, S_{i+1}\cdots S_{n}\)都包含\(a_i\)
所以 \(a_i\) 改變的話,這些都要重新計算
暴力 | 前綴和 | |
---|---|---|
算 ai+...+aj | O(n) | O(1) |
改 ai | O(1) | O(n) |
因為 \(S_i, S_{i+1}\cdots S_{n}\)都包含\(a_i\)
所以 \(a_i\) 改變的話,這些都要重新計算
所以如果一直修改 \(a_1\) 會導致 \(S\)不停地重新計算
我們需要一些方法來平衡兩動作花費的時間
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
分塊方法
如果一個問題太大
可以試圖把問題切割一些的獨立的小區塊
使得討論小問題簡單
並且能從小區塊還原原來的問題的答案
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
分塊方法
如果一個問題太大
可以試圖把問題切割一些的獨立的小區塊
不同的切割/組織會有不同特色
沒有唯一的方法,要動得變通活用
\(a+b+c+d=(a+b)+(c+d)\)
2. 加法可以逆向操作 (減法)
\(c+d=(a+b+c+d)-(a+b)\)
還有哪些運算有這些特性,那些沒有?
\({14}_{10}={1110}_{2}\)
\({1110_2}\)
數學上意思是
\(8+4+2\)
\({13}_{10}=?\)
\({1101_2}\)
數學上意思是
\(8+4+1\)
\(a_1+a_2+a_3+\cdots+a_{13}=\)
1
4
8
\(a_{13}+\)
\(a_{12}+a_{11}+a_{10}+a_{9}+\)
\(a_8+a_7+a_6+a_5+a_4+a_3+a_2+a_1\)
注意分解的方向
\(a_1+a_2+a_3+\cdots+a_{11}=\)
1
2
8
\(a_{11}+\)
\(a_{10}+a_9+\)
\(a_8+a_7+a_6+a_5+a_4+a_3+a_2+a_1\)
\(a_1+a_2+a_3+\cdots+a_{11}=\)
1
\(a_{11}+\)
\(a_1+a_2+a_3+\cdots+a_{10}=\)
2
\(a_{10}+a_9\)
\(11=1011_2\)
\(10=1010_2\)
\(a_1+a_2+a_3+\cdots+a_{8}=\)
8
\(a_8+a_7+a_6+a_5+a_4+a_3+a_2+a_1\)
\(8=1000_2\)
\(a_1+a_2+a_3+\cdots+a_{11}=\)
1
\(a_{11}+\)
\(a_1+a_2+a_3+\cdots+a_{10}=\)
2
\(a_{10}+a_9\)
\(11=1011_2\)
\(10=1010_2\)
\(a_1+a_2+a_3+\cdots+a_{8}=\)
8
\(a_8+a_7+a_6+a_5+a_4+a_3+a_2+a_1\)
\(8=1000_2\)
\(15 = 1111_2\), \(\operatorname{lowbit}(15)=?\)
\(12 = 1100_2\), \(\operatorname{lowbit}(12)=0100_2=4\)
\(6 = 0110_2\), \(\operatorname{lowbit}(6)=?\)
1
2
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
如果已經計算完 \(B[x]\)
則 \(a_1+a_2+\dots a_x=\)
\(B[x]+\)
\(a_x+a_{x-1}+\dots+a_{x-\operatorname{lowbit(x)+1}}\)
\(a_{x-\operatorname{lowbit(x)}}+a_{x-\operatorname{lowbit(x)}-1}+\dots+a_1\)
用一樣方法計算剩餘的部分
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
\(B[x]+\)
\(a_k+a_{k-1}+\dots+a_{k-\operatorname{lowbit(k)+1}}\)
\(a_{x-\operatorname{lowbit(x)}}+a_{x-\operatorname{lowbit(x)}-1}+\dots+a_1\)
用一樣方法計算剩餘的部分
因為一個數字會根據二進位分解成幾組
一個數字 \(x\) 最多只有 \(\log{x}\) 組
所以可以在 \(O(\log x)\) 的時間計算前綴和
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
int B[1000];
int sum(int x) { // a1+a2+...ax
int ans = 0;
for( int i = x ; i != 0 ; i -= lowbit(i) )
ans += B[i];
return ans;
}
lowbit 怎麼算?
C/C++ HACK
在C/C++裡面,有一個技巧可以快速地計算 lowbit
lowbit(x) = x&-x
Why? Hint: 邏輯設計課有教現今電腦的負數怎麼儲存的
前頁可改寫為 i -= i&-i;
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
怎麼把 \(a_i\) 加上 \(K\)?
把\(B[i]\) 裡面有包含 \(a_i\) 的都加上\(K\)就好了
怎麼找?
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
怎麼把 \(a_i\) 加上 \(K\)?
第一個有儲存 \(a_i\) 的是 \(B[i]\)
在 \(B[x]\) 出現過的元素
在 \(B[x+\operatorname{lowbit}(x)]\) 也會出現
在 \(B[x]\) 出現過的元素
在 \(B[x+\operatorname{lowbit}(x)]\) 也會出現
因為 \(Y=x+\operatorname{lowbit}(x)\) 會讓 lowbit 的地方進位
\(11 = 1011_2\)
\(\operatorname{lowbit}(11)=0001_2\)
\(11+\operatorname{lowbit}(11)=1100_2\)
所以 \(\operatorname{lowbit}(Y)\geq 2\times\operatorname{lowbit}(x)\)
\(\operatorname{lowbit}(11+\operatorname{lowbit}(11))=0100_2\)
\(\operatorname{lowbit}(11)=0001_2\)
因為 \(Y=x+\operatorname{lowbit}(x)\) 會讓 lowbit 的地方進位
所以 \(\operatorname{lowbit}(Y)\geq 2\times\operatorname{lowbit}(x)\)
但是 \(\operatorname{lowbit}(x)\leq x\)
所以 \(B[Y]\) 必定涵蓋 \(B[x]\) 的所有元素
B[x]
B[Y]
X
X+lowbit(X)
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
int B[1000];
void add(int x, int k) {
for( int i = x ; i < MAX_N ; i += i & -i )
B[i] += k;
}
BIT 用二進位的分塊,使得可以在:
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
暴力 | 前綴和 | BIT | |
---|---|---|---|
算 ai+...+aj | O(n) | O(1) | O(log n) |
改 ai | O(1) | O(n) | O(log n) |
利用 BIT ,就能在 \(O(Q\log n)\) 的時間解決本問題
暴力 | 前綴和 | BIT | |
---|---|---|---|
算 ai+...+aj | O(n) | O(1) | O(log n) |
改 ai | O(1) | O(n) | O(log n) |
利用 BIT,非常的簡單好寫 (一個功能兩行)
可以延伸成 2D 的版本
能技巧性的解決不少跟更新有關的問題
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
BIT 的弱點...
如何解決此問題呢?
方法未列入基礎範圍中
不過大家想聽之後也能教
不過Code會開始難寫起來
最大值 1
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
比起前綴和的加法,\(\max\) 函數沒有扣除的性質
只能利用合併的方法好好處理
\(\max(a_1, a_2, \cdots, a_n)=\max(\max(a_1,\cdots a_{k-1}), \max(a_k, \cdots, a_n))\)
區間分割成許多片段,要使用時再組合起來
因此線段樹能處理的問題需要能透過「組合」答案完成
區間加法:\( (a_i+\cdots a_m)+(a_{m+1}+\cdots+a_j)\)
區間最大值:\( \max(a_i,\cdots ,a_m),\max(a_{m+1},\cdots a_j)\)
線段樹是一個僅依靠合併性質求取答案的資料結構
如果知道 \(i\sim k\) 以及 \(k+1\sim j\) 的答案
能快速算出 \(i\sim j\)的答案,就能使用線段樹
\(\max(a_1, a_2, \cdots, a_n)=\max(\max(a_1,\cdots a_{k-1}), \max(a_k, \cdots, a_n))\)
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
\( 1\sim 2 \)
\( 3\sim 4 \)
\( 7\sim 8 \)
\( 5\sim 6 \)
\( 1 \)
\( 2 \)
\( 3 \)
\( 4 \)
\( 5 \)
\( 6 \)
\( 7 \)
\( 8 \)
線段樹是一個僅依靠合併性質求取答案的資料結構
如果很大一段不會算,就切成一半
線段樹是一個二元樹,每一個節點代表區間\([L,R]\)的資訊(答案)
若非葉節點,左右子樹分別為左半區間及右半區間的資訊
$$[L,R]$$
$$[L,M]$$
$$[M+1,R]$$
區間有分成開區間以及閉區間
$$M=(L+R)/2$$
\([L,R]\)的左右節點是\([L,M],[M+1,R]\),葉子是\([L,L]\)
\([L,R)\)的左右節點是\([L,M),[M,R)\),葉子是\([L,L+1)\)
範例實作都用前者
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
線段樹每一個點,都儲存一個區間的答案
struct node{
int max;
}a[4 * MAXN];
根據需求,會在線段樹上記錄許多不同的資訊,通常要記錄的資訊就是題目要求的資料
\( \max=\max(6,8)=8 \)
\( \max=6 \)
\( \max=8 \)
線段樹任兩個小區間,要能合併成大區間
node pull(node a, node b)
{
node c;
c.max = max(a.max, b.max);
return c;
}
線段樹的關鍵就是要如何透過合併節點算答案
區間最大值:\(\max([L,R])=\max(\max([L,M]),\max([M+1,R]))\)
node pull(const node &x,const node &y)
{
node tmp;
tmp.max = max( x.max , y.max );
return tmp;
}
我們可以在\(O(N)\)的時間初始化線段樹
#define IL(X) ((X)*2+1)
#define IR(X) ((X)*2+2)
void build(int L,int R,int id)
{
if(L==R)
{
arr[id].max = a[L];
return ;
}
int M = (L+R)/2;
build(L ,M,IL(id));
build(M+1,R,IR(id));
arr[id] = pull( arr[IL(id)] ,
arr[IR(id)] );
}
如果當前區間只有一個
直接算答案
否則遞迴左右
再合併答案
假設已經有了一個完整的線段樹,怎麼分割區間?
如果答案在左邊,就往左邊找
如果答案在左邊,就往右邊找
如果答案在兩邊,就分別找再合併起來
在線段樹的操作,通常會需要三個變數:L,R,id記錄節點資訊
L,R:表示點id的區間範圍
如果定義Root ID = 0,那左右子樹的ID
$$\text{Left ID}= x\times 2+1$$
$$\text{Right ID}= x\times 2+2$$
我們可以在\(O(\log N)\)的時間查詢任意區間
node Query(int l,int r,int L,int R,int id)
{
if( l==L && r==R ) return arr[id];
int M = (L+R)/2;
if( r <= M )
return Query(l,r,L ,M,IL(id));
if( M < l )
return Query(l,r,M+1,R,IR(id));
return pull(
Query(l ,M,L ,M,IL(id)),
Query(M+1,r,M+1,R,IR(id))
);
}
如果要查的區塊與現在一樣
直接丟答案
否則看看在哪一邊
跨區間要合併答案
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
\( 1\sim 2 \)
\( 3\sim 4 \)
\( 7\sim 8 \)
\( 5\sim 6 \)
\( 1 \)
\( 2 \)
\( 3 \)
\( 4 \)
\( 5 \)
\( 6 \)
\( 7 \)
\( 8 \)
找 \(2\sim 5\) 的答案
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
\( 1\sim 2 \)
\( 3\sim 4 \)
\( 7\sim 8 \)
\( 5\sim 6 \)
\( 1 \)
\( 2 \)
\( 3 \)
\( 4 \)
\( 5 \)
\( 6 \)
\( 7 \)
\( 8 \)
找 \(2\sim 5\) 的答案
左右都有,分別找
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
\( 1\sim 2 \)
\( 3\sim 4 \)
\( 7\sim 8 \)
\( 5\sim 6 \)
\( 1 \)
\( 2 \)
\( 3 \)
\( 4 \)
\( 5 \)
\( 6 \)
\( 7 \)
\( 8 \)
找 \(2\sim 5\) 的答案
找2~4
找5~5
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
\( 1\sim 2 \)
\( 3\sim 4 \)
\( 7\sim 8 \)
\( 5\sim 6 \)
\( 1 \)
\( 2 \)
\( 3 \)
\( 4 \)
\( 5 \)
\( 6 \)
\( 7 \)
\( 8 \)
找 \(2\sim 5\) 的答案
找2~4
找5~5
左右都有
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
\( 1\sim 2 \)
\( 3\sim 4 \)
\( 7\sim 8 \)
\( 5\sim 6 \)
\( 1 \)
\( 2 \)
\( 3 \)
\( 4 \)
\( 5 \)
\( 6 \)
\( 7 \)
\( 8 \)
找 \(2\sim 5\) 的答案
找2~4
找5~5
找 2~2
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
\( 1\sim 2 \)
\( 3\sim 4 \)
\( 7\sim 8 \)
\( 5\sim 6 \)
\( 1 \)
\( 2 \)
\( 3 \)
\( 4 \)
\( 5 \)
\( 6 \)
\( 7 \)
\( 8 \)
找 \(2\sim 5\) 的答案
找2~4
找5~5
找 2~2
全在右邊
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
\( 1\sim 2 \)
\( 3\sim 4 \)
\( 7\sim 8 \)
\( 5\sim 6 \)
\( 1 \)
\( 2 \)
\( 3 \)
\( 4 \)
\( 5 \)
\( 6 \)
\( 7 \)
\( 8 \)
找 \(2\sim 5\) 的答案
找2~4
找5~5
找 2~2
全在右邊
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
\( 1\sim 2 \)
\( 3\sim 4 \)
\( 7\sim 8 \)
\( 5\sim 6 \)
\( 1 \)
\( 2 \)
\( 3 \)
\( 4 \)
\( 5 \)
\( 6 \)
\( 7 \)
\( 8 \)
找 \(2\sim 5\) 的答案
找2~4
找5~5
找 3~4
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
\( 1\sim 2 \)
\( 3\sim 4 \)
\( 7\sim 8 \)
\( 5\sim 6 \)
\( 1 \)
\( 2 \)
\( 3 \)
\( 4 \)
\( 5 \)
\( 6 \)
\( 7 \)
\( 8 \)
找 \(2\sim 5\) 的答案
找2~4
找5~5
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
\( 1\sim 2 \)
\( 3\sim 4 \)
\( 7\sim 8 \)
\( 5\sim 6 \)
\( 1 \)
\( 2 \)
\( 3 \)
\( 4 \)
\( 5 \)
\( 6 \)
\( 7 \)
\( 8 \)
找 \(2\sim 5\) 的答案
找2~4
找5~5
全在左
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
\( 1\sim 2 \)
\( 3\sim 4 \)
\( 7\sim 8 \)
\( 5\sim 6 \)
\( 1 \)
\( 2 \)
\( 3 \)
\( 4 \)
\( 5 \)
\( 6 \)
\( 7 \)
\( 8 \)
找 \(2\sim 5\) 的答案
找2~4
找5~5
全在左
全在左
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
\( 1\sim 2 \)
\( 3\sim 4 \)
\( 7\sim 8 \)
\( 5\sim 6 \)
\( 1 \)
\( 2 \)
\( 3 \)
\( 4 \)
\( 5 \)
\( 6 \)
\( 7 \)
\( 8 \)
找 \(2\sim 5\) 的答案
找2~4
找5~5
全在左
全在左
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
\( 1\sim 2 \)
\( 3\sim 4 \)
\( 7\sim 8 \)
\( 5\sim 6 \)
\( 1 \)
\( 2 \)
\( 3 \)
\( 4 \)
\( 5 \)
\( 6 \)
\( 7 \)
\( 8 \)
找 \(2\sim 5\) 的答案
找2~4
node query(int l, int r, int L, int R, int id)
{
if( l==L && r==R ) // 要查的與線段樹存的一樣
return arr[id];
int M = (L+R)/2; // 算分割點
if( r <= M) //全左邊
return query( l, r, L , M, id*2+1 );
if( mid< l ) // 全右邊
return query( l, r, M+1, R, id*2+2 );
return pull( //左右都有
query( l , M , L M , id*2+1 ),
query( M+1 , r , M+1, R , id*2+1 );
);
}
\( 1\sim 8 \)
\( 1\sim 4 \)
\( 5\sim 8 \)
\( 1\sim 2 \)
\( 3\sim 4 \)
\( 7\sim 8 \)
\( 5\sim 6 \)
\( 1 \)
\( 2 \)
\( 3 \)
\( 4 \)
\( 5 \)
\( 6 \)
\( 7 \)
\( 8 \)
我們可以在\(O(\log N)\)的時間修改一個點的資料
void Modify(int i,int v,int L,int R,int id)
{
if(L==R){//==i
arr[id].max = v;
return ;
}
int M = (L+R)/2;
if( i<=M )Modify(i,v,L ,M,IL(i));
else Modify(i,v,M+1,M,IR(i));
arr[id] = pull( arr[IL(id)] ,
arr[IR(id)] );
}
找到位置就直接改
不然就看看在哪邊
改完要pull重算答案
node update(int i, int v, int L, int R, int id)
{
if( L==R ){ // 找到要改的點 L==R==i
arr[id].max = v;
}
int M = (L+R)/2; // 算分割點
if( r <= M ) //全左邊
return query( l, r, L , M, id*2+1 );
if( mid< l ) // 全右邊
return query( l, r, M+1, R, id*2+2 );
return pull( //左右都有
query( l , M , L , M , id*2+1 ),
query( M+1, r , M+1 , R , id*2+1 );
);
}
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做一些事