Competitive Programming
std::vector
C Array
在C語言,我們可以透過下語法宣告一個大小固定的陣列
int arr[100];
C Array
然而這樣做缺點很多...
int sz = 100;
int arr[sz];
// VLA(Variable-length array) Not Allow In C++
1. C++中,大小不能是變數
C Array
然而這樣做缺點很多...
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. 不能簡單複製
C Array
然而這樣做缺點很多...
void f()
{
int a[600000];
//Maybe Stack Overflow
}
3. 宣告於函數內會占用有限的Stack空間
Linux / Windows 預設僅提供 8 MB的 Stack 空間
C Dynamic Array
透過 calloc / malloc 函數,可以動態建立陣列
int a = 100;
int *ptr = (int*)malloc( sizeof(int)*100 );
C Dynamic Array
缺點
int a = 100;
int *ptr = (int*)malloc( sizeof(int)*100 );
1.語法上有點累贅
C Dynamic Array
缺點
int a = 100;
int *ptr = (int*)malloc( sizeof(int)*100 );
// free(ptr);
2. 忘記 free 可能會導致memory leak,浪費資源
C Dynamic Array
缺點
int a = 100;
int *ptr = (int*)malloc( sizeof(int)*100 );
free(ptr);
free(ptr); // Double Free Issue
3. free 2 次會導致你的程式有嚴重的安全性 Bug
std::vector
解決上述問題
使用std::vector
#include <vector>
#include <cstdio>
using namespace std; //簡化C++的一些語法
int main()
{
vector<int> arr;
return 0;
}
std::vector作為陣列
#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] 的陣列用
}
std::vector 複製
#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~
}
std::vector 函數呼叫
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
}
std::vector 函數呼叫
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
}
Problems with Stack
使用Stack的小題目
What is Stack? (堆疊)
Stack 像一個箱子,可以把東西放進去,拿出來
火車掉頭問題
如果火車頭只有向前開的動力
你知道如何設計軌道讓火車掉頭嗎?
一般的列車設計
Car 1
Car 2
一般而言,火車兩端都有一個火車頭可以拉動整台車
目標是要把所有的車廂順序也顛倒過來
Car 1
Car 2
Y 形軌道
Y 形軌道
C1
C2
C3
Y 形軌道
C1
C2
C3
Y 形軌道
C1
C2
C3
Stack:
最後進來的,最先出去
Y 形軌道
C1
C2
C3
Stack:
最後進來的,最先出去
Y 形軌道
C1
C2
C3
Stack:
最後進來的,最先出去
Y 形軌道
C1
C2
C3
Stack:
最後進來的,最先出去
Y 形軌道
C1
C2
C3
Stack:
最後進來的,最先出去
C1
C2
C3
用Vector來完成Stack
- Stack 有下面的操作
- \(\operatorname{push}~x~\) :把x放到stack的最上面
- \(\operatorname{pop}~~~~~~~~\):把stack最上面的元素刪除
- \(\operatorname{top}~~~~~~~~\):看看最上面的元素是什麼
- \(\operatorname{isEmpty}\):調查Stack是不是空的
- \(\operatorname{size}~~~~~~~\):調查Stack有幾個元素
vector 的更多功能
上次介紹vector的一些用法
說明 | |
---|---|
v[id] | 存取第id個元素 |
v.resize(N) | 將大小重設為 N |
v.clear() | 清除所有元素 |
vector 大小
- 用 size() 可以回傳vector當前的元素數量
vector<int> v;
v.resize(3);
printf("%zu", v.size()); // 3
vector 插入元素至最後
- 用 emplace_back(x) 可以將 x 新增到 vector 的所有元素後
- 新增完後大小 + 1
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
- 舊版本 C++ 用的是 push_back(x)
vector 看最後一個元素
- 用 back() 可以回傳 vector 的最後一個元素
vector<int> v; // v = [ ]
v.emplace_back(7122); // v = [ 7122 ]
printf("%zu", v.back()); // 7122
vector<int> u;
// u.back(); ERROR! 沒東西不可以存取
vector 刪除最後的元素
- 用 pop_back(x) 可以將vector最後面的元素刪除
- 刪除完後,大小 -1
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刪除元素
Q514: Rails
今天有一台火車,車廂編號是 \(1,2,3\dots N\) 放在一個 Y 形軌道上
請問是否透過 Y 形軌道,使車廂重新排列程指定的順序
車廂可以分離,獨立移動,只要在軌道上就好,要遵守方向行駛
Q514: Rails
\(1,2,3,4\)
\(1,2,3,4\)
Q514: Rails
\(1,2,3,4\)
\(3,4,2,1\)
Q514: Rails
\(1,2,3,4\)
\(3,4,2,1\)
能做的事有哪些?
1. 把一節車廂從A放進Station
2. 把一節車廂從Station放進B
Q514: Rails
\(1,2,3,4\)
\(3,4,2,1\)
考慮第一台車 \(3\)
要如何把 \(3\)從移動過來?
Q514: Rails
\(4\)
\(3\)
Algorithm
檢查 Station 最前面的車廂是不是 3,如果不是,就從A 拿車廂到Station,直到滿足條件為止
\(2,1\)
Q514: Rails
\(4\)
\(3\)
考慮第二台車 \(4\)
要如何把 \(4\)移動過來?
\(2,1\)
Q514: Rails
\(-\)
\(3,4\)
Algorithm
檢查 Station 最前面的車廂是不是 4,如果不是,就從A 拿車廂到Station,直到滿足條件為止
\(2,1\)
Q514: Rails
\(-\)
\(3,4,2,1\)
Algorithm
對於每一個編號\(x_i\)依序檢查
- 檢查 Station 最前面的車廂是不是 \(x_i\),如果不是,就從A 拿車廂到Station ,直到滿足條件為止
- 如果都不能拿到需要的車廂,則無法完成要求 (Why?)
括號匹配問題
今天 \(Sylveon\) 得到了一個只由小/中括號組合成的字串
好奇的他想知道這些括號是不是合法配對的
-
(), [](), ([]) 都是合法的配對
-
)(, (] , ([)] 是不合法的配對
自由練習題
運算問題
今天 \(Sylveon\) 得到了一個只由數字,加減乘除組成的算式
好奇的他想這個算式計算完後的結果
-
\(2+3*5-6\)
自由練習題 / 程式設計二期中 Project
Advanced Binary Search
更多二分搜尋法的技巧 @
二分搜尋法
- 對於一個有 \(n\) 個元素已排序的陣列
- 二分搜尋法可以在 \(O(\log{n})\) 的時間找出指定的資料
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\)
Lower Bound
- 在一個已序陣列中找出第一個 大於等於 \(x\) 的數字
Lower Bound
小於 \(x\)
大於等於 \(x\)
L
R
定義 位置 L 總是在小於\(x\)的位置上
定義 位置 L 總是在大於\(x\)的位置上
Lower Bound
小於 \(x\)
大於等於 \(x\)
L
R
透過一些方法,使得 L, R 收斂在交界處
再根據需求選取要的答案
L
R
Lower Bound
小於 \(x\)
大於等於 \(x\)
L
R
與二分搜尋法很像
每次選一個 M,檢查 M 在哪一邊
M
Lower Bound
小於 \(x\)
大於等於 \(x\)
L
R
與二分搜尋法很像
每次選一個 M,檢查 M 在哪一邊
Lower Bound
小於 \(x\)
大於等於 \(x\)
特別注意特判這些狀況:
全部都大於等於 \(x\)
全部都小於 \(x\)
Lower Bound
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
觀察會發現
- 直接求 \(S\) 很困難 (答案很明顯不是平均之類的數字)
- \(S\) 一定在一個範圍內
- 如果答案是 \(S_{a}\),選擇的限重 \(S \geq S_a\) ,就能在 \(K\) 趟內載完小朋友
- 如果答案是 \(S_{a}\),選擇的限重 \(S < S_a\) ,就不能在 \(K\) 趟內載完小朋友
- 如果知道限重,求載運幾趟很簡單 (直接模擬)
( \(0\sim \sum w_i\) 之間 )
電梯向上
今天黎明卿想讓 \(n\) 位小朋友排隊搭電梯上樓,體重依序是 \(w_1, w_2, \cdots , w_n\),電梯有限重 \(S\),如果黎明卿希望用小於等於 \(K\) 趟電梯讓小朋友們全數搭乘上樓,\(S\) 最小值是多少?
類似:TIOJ 1432
設 \(f(x)\) 為:
- 如果限重是 \(x\) ,要載運幾趟才能把所有人載運完畢
電梯向上
類似:TIOJ 1432
設 \(f(x)\) 為:
- 如果限重是 \(x\) ,要載運幾趟才能把所有人載運完畢
從經驗可以發現,如果 \(x\) 越大,載運次數就越少
電梯向上
類似:TIOJ 1432
設 \(f(x)\) 為:
- 如果限重是 \(x\) ,要載運幾趟才能把所有人載運完畢
\(f(x) > K\)
\( f(x) \leq K \)
\(x=0\)
\(x=\sum w\)
從經驗可以發現,如果 \(x\)越大,載運次數就越少
電梯向上
類似:TIOJ 1432
設 \(f(x)\) 為:
- 如果限重是 \(x\) ,要載運幾趟才能把所有人載運完畢
\(f(x) > K\)
\( f(x) \leq K \)
\(x=0\)
\(x=\sum w\)
如果直接算答案很困難
不仿用二分搜尋法把答案給搜尋出來
電梯向上
類似:TIOJ 1432
設 \(f(x)\) 為:
- 如果限重是 \(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})\)
二分搜答案
- 直接求答案很困難
- 知道答案在一個範圍內
- 可以用一個簡單方法 \(f(x)\) 知道 \(x\) 比答案大還是小
就能用二分搜尋法的技巧,把答案給搜尋出來 !
無情工商 = =
Disjoint Sets
並查集 / 互斥集
公會
有一些遊戲,會要求每一個角色都要隸屬於一個唯一的公會
到處吃飯團
飯糰
可口羅
背骨仔
失智老人
國小聯盟
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 : 判斷兩人的公會長是否為同一人
Disjoint Set
- 每個人都屬於一個公會裡面
- 公會與公會可以合併
- 可以調查 A 跟 B 是不是在同一個公會中
Disjoint Set
- Disjoint Set 只需要一個陣列 F[x] ,用於紀錄 x 知道的公會會長是誰。
- 最初的狀況,我們假設大家都是自己公會的會長
Disjoint Set - find(x)
- 如何找 x 真正的會長是誰?
A : 詢問自己知道的會長,目前的會長是誰
如果就是會長自己,就找到答案了
如果不是,就問新知道的會長一樣的問題。
int find(int x)
{
if( F[x] == x )
return x;
return find(F[x]);
}
Disjoint Set - find(x)
- 如何找 x 真正的會長是誰?
int find(int x)
{
if( F[x] == x )
return x;
return F[x] = find(F[x]);
}
如果知道了真正的會長,就記住新的會長是誰,減少調查的次數
Disjoint Set - same(A,B)
- 如何調查 A 跟 B 是不是在同一個公會中?
bool same(int A, int B)
{
return find(A) == find(B);
}
Disjoint Set - U(A,B)
- 如何把 A 跟 B 的公會合併起來?
void same(int A, int B)
{
return find(A) == find(B);
}
任兩個公會可以合併成一個新公會,僅需要會長同意即可
新公會的會長為其中一方原公會會長
- 讓其中一邊的會長,認為另一個人是會長即可
Disjoint Set - U(A,B)
- 如何把 A 跟 B 的公會合併起來?
- 讓其中一邊的會長,認為另一個人是會長即可
A
B
Disjoint Set - U(A,B)
- 如何把 A 跟 B 的公會合併起來?
- 讓其中一邊的會長,認為另一個人是會長即可
void U(int A, int B)
{
F[find(A)] = find(B);
}
Disjoint Set
- 每個人都屬於一個公會裡面
-
公會與公會可以合併
- 可以調查 A 跟 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);
}
Disjoint Set 的效率
- Disjoint 的所有動作都與 find 有關係
- find 的執行次數,與有幾層會長相關
int find(int x)
{
if( F[x] == x )
return x;
return F[x] = find(F[x]);
}
Disjoint Set 的效率
- 路徑壓縮:把自己的會長直接接到正確的會長上
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,\dots a_n\) 想要你做很多奇怪的事情
加入、修改、刪除...一個元素
加入、修改、刪除...很多個元素
問某個元素數值是多少...
問一些元素經過運算數值是多少...
序列問題
- 給定一個序列 \(a_1,a_2,a_3,\dots a_n\) 想要你做很多奇怪的事情
很多問題經過化簡或變形後,可以變成序列問題,故多了解何種序列問題能被解決有助於擴展解題方向
前綴和問題
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
- 求\(a_i+a_2+\cdots+a_j\)
while( i <= j )
ans += a[ i++ ];
如果現在修程設一功課這樣寫OJ也會TLE
迴圈最差會跑 \(O(n)\) 次,故複雜度為 \(O(Qn)\)
前綴和問題
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
- 求\(a_i+a_2+\cdots+a_j\)
高一數學 - 數列與級數
一個數列 \(\{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\)
因為 \(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\) 件事
- 求\(a_i+a_2+\cdots+a_j\)
透過計算 \(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\) 件事
- 把 \(a_i,a_2,\cdots,a_j\) 都加上\(K\)
操作結束後,詢問數列的數值分別是多少
練習: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\) 件事
- 把 \(a_i,a_2,\cdots,a_j\) 都加上\(K\)
操作結束後,詢問數列的數值分別是多少
練習: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\) 件事
- 把 \(a_i,a_2,\cdots,a_j\) 都加上\(K\)
操作結束後,詢問數列的數值分別是多少
練習: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\) 件事
- 把 \(a_i,a_2,\cdots,a_j\) 都加上\(K\)
操作結束後,詢問數列的數值分別是多少
練習: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\) 件事
- 把 \(a_i,a_2,\cdots,a_j\) 都加上\(K\)
操作結束後,詢問數列的數值分別是多少
練習: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\) 件事
- 求\(a_i+a_2+\cdots+a_j\)
- 把\(a_i\) 加上 \(K\)
可以運用計算 \(S\) 的方法解決嗎?
有更好的方法嗎? 可以比暴力 \(O(nQ)\) 快嗎?
前綴和問題
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
- 求\(a_i+a_2+\cdots+a_j\)
- 把\(a_i\) 加上 \(K\)
三個解決此問題的基本方法
其中兩個會在進階班教
序列問題
前綴和 2
前綴和問題
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
- 求\(a_i+a_{i+1}+\cdots+a_j\)
- 把\(a_i\) 加上 \(K\)
用前綴和 \(S\) 的技巧處理第二個動作要怎麼做?
前綴和問題
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
- 求\(a_i+a_{i+1}+\cdots+a_j\)
- 把\(a_i\) 加上 \(K\)
用前綴和 \(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_i+a_{i+1}+\cdots+a_j\)
- 把\(a_i\) 加上 \(K\)
分塊方法
如果一個問題太大
可以試圖把問題切割一些的獨立的小區塊
使得討論小問題簡單
並且能從小區塊還原原來的問題的答案
前綴和問題
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
- 求\(a_i+a_{i+1}+\cdots+a_j\)
- 把\(a_i\) 加上 \(K\)
分塊方法
如果一個問題太大
可以試圖把問題切割一些的獨立的小區塊
不同的切割/組織會有不同特色
沒有唯一的方法,要動得變通活用
加法的特色
- 在分塊中,加法是最簡單容易的,因為
- 加法可以分段
\(a+b+c+d=(a+b)+(c+d)\)
2. 加法可以逆向操作 (減法)
\(c+d=(a+b+c+d)-(a+b)\)
還有哪些運算有這些特性,那些沒有?
分塊 - 二進位分解
- 一個任意的數字\(K\),考慮其二進位的寫法
\({14}_{10}={1110}_{2}\)
\({1110_2}\)
數學上意思是
\(8+4+2\)
分塊 - 二進位分解
- 一個任意的數字\(K\),考慮其二進位的寫法
\({13}_{10}=?\)
\({1101_2}\)
數學上意思是
\(8+4+1\)
分塊 - 二進位分解
- 如果今天有 \(K\) 個數字要相加,可以根據其二進位表示法的形式再重組
\(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\)
注意分解的方向
分塊 - 二進位分解
- 如果今天有 \(K\) 個數字要相加,可以根據其二進位表示法的形式再重組
\(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+\cdots+a_k\) 的第一個分解存起來 \(B[k]\)
- 就能計算前綴和了!
\(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\)
求分解第一塊的大小
- 簡單來說,就是求一個數字 \(x\) 在二進位下最右邊的 \(1\) 表示的數字
- 我們稱之為 \(\operatorname{lowbit}\)
\(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\) 件事
- 求\(a_i+a_{i+1}+\cdots+a_j\)
- 把\(a_i\) 加上 \(K\)
如果已經計算完 \(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\) 件事
- 求\(a_i+a_{i+1}+\cdots+a_j\)
- 把\(a_i\) 加上 \(K\)
\(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\) 件事
- 求\(a_i+a_{i+1}+\cdots+a_j\)
- 把\(a_i\) 加上 \(K\)
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+a_{i+1}+\cdots+a_j\)
- 把\(a_i\) 加上 \(K\)
怎麼把 \(a_i\) 加上 \(K\)?
把\(B[i]\) 裡面有包含 \(a_i\) 的都加上\(K\)就好了
怎麼找?
前綴和問題
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
- 求\(a_i+a_{i+1}+\cdots+a_j\)
- 把\(a_i\) 加上 \(K\)
怎麼把 \(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\) 件事
- 求\(a_i+a_{i+1}+\cdots+a_j\)
- 把\(a_i\) 加上 \(K\)
前綴和問題
int B[1000];
void add(int x, int k) {
for( int i = x ; i < MAX_N ; i += i & -i )
B[i] += k;
}
Binary Indexed Tree
- 又稱 Fenwick Tree、樹狀數組、二元索引樹
BIT 用二進位的分塊,使得可以在:
- \(O(\log n) \) 的時間計算 \(a_1+a_2+\cdots+a_j\)
- \(O(\log n) \) 的時間把把 \(a_i\) 加上 \(K\)
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
- 求\(a_i+a_{i+1}+\cdots+a_j\)
- 把\(a_i\) 加上 \(K\)
前綴和問題
暴力 | 前綴和 | 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\) 件事
- 求 \(\max(a_i,a_{i+1},\cdots,a_j)\)
- 把 \(a_i\) 改成 \(K\)
最大值問題
BIT 的弱點...
如何解決此問題呢?
方法未列入基礎範圍中
不過大家想聽之後也能教
不過Code會開始難寫起來
序列問題
最大值 1
有數列\(a_1,a_2,a_3,\cdots,a_n\),想要做 \(Q\) 件事
- 求 \(\max(a_i,a_{i+1},\cdots,a_j)\)
- 把 \(a_i\) 改成 \(K\)
最大值問題
比起前綴和的加法,\(\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\),想要做一些事
- 求區間連續最大和\(a_i+a_2+\cdots+a_j,L\leq i\leq j \leq R\)
- 求區間兩兩乘積和 (只有一個數字值為0 )
- 把\(a_i\)改成\(v\)
競技程式設計
By sylveon
競技程式設計
- 2,469