基礎演算法

Outline

  • STL
  • 雙指針
  • 二分搜
  • 三分搜
  • 單調隊列

 

STL (標準模板庫)

只簡單帶過幾個比較常用的 STL
可以去參考 STL教學

用法也可以去 cplusplus reference

STL

  • pair
  • vector
  • list
  • stack
  • queue
  • deque
  • priority_queue
  • map
  • unordered_map
  • set 
  • multiset

pair

  • 兩個元素組成
  • 預設比較順序是先比第一個,相同在比第二個
pair<type1,type2> p;
p.first; // 第一個
p.second; // 第二個
p=make_pair(a,b) / p={a,b}; // 設定 p 為 (a,b)

vector

  • 動態陣列
  • 可以在尾端加入一個數字、刪除一個數字
  • 可以向陣列一樣拿第 i 個 (\(O(1)\))
  • 刪除和插入的時間複雜度是 \(O(n)\), 在尾部則是 \(O(1)\)
vector<type> v(length); // 宣告 長度不設就會是0
v.assign(l,val); // 設定陣列長度 l , 初始值皆為 val
v.push_back(x) / v.emplace_back(x) ; // 在尾端加入 x
v.pop_back(); // 刪除最後面的值
v[i]; // v 中第 i 個元素
v.begin(); // 第一個位置 
v.end(); // 尾部的下一個位置

v.insert(pos,val); // 在位置 pos 插入 val
v.erase(pos); // 移除位置 pos 的 
// p.s. pos 是 iterator , 因此表示位置 i pos=v.begin()+i;

list

  • 和 vector 蠻像,但不能快速拿第 i 個
    • iterator 必須從頭開始每次移動一個
  • 能 \(O(1)\) 刪除、插入
  • \(O(n)\) 取得位置 i
list<type> v(length); // 宣告 長度不設就會是0
v.assign(l,val); // 設定陣列長度 l , 初始值皆為 val
v.push_back(x) / v.emplace_back(x) ; // 在尾端加入 x
v.push_front(x) / v.emplace_front(x);// 在首端加入 x
v.pop_back(); // 刪除最後面的值
v.pop_front(); // 刪除最前面的值
v.begin(); // 第一個位置 
v.end(); // 尾部的下一個位置
v.insert(pos,val); // 在位置 pos 插入 val
v.erase(pos); // 移除位置 pos 的 
// p.s. pos 是 iterator , 但在list 無法隨意訪問位置 i

stack

  • 只能在尾端插入、拿尾端的元素
  • 時間複雜度皆是 \(O(1)\)
stack<type> st; // 初始stack st
st.push(x); // 在尾端插入一個元素 x
st.top();  // 拿尾端的元素
st.pop(); // 移除尾端元素

queue

  • 只能在首端插入、拿尾端的元素
  • 時間複雜度皆是 \(O(1)\)
queue<type> st; // 初始queue st
st.push(x); // 在尾端插入一個元素 x
st.front();  // 拿尾端的元素
st.pop(); // 移除尾端元素

deque

  • queue+stack 綜合體
  • 能拿頭尾、刪除頭尾
  • 也可以拿第 i 個
  • 都是 \(O(1)\)
deque<type> dq;
dq.push_front(x);
dq.push_back(x);
dq.front();
dq.back();
dq.pop_front();
dq.pop_back();
dq[i];

priority queue

  • 內建的 heap 資料結構
  • 拿最大/最小的元素
  • 刪除最大/最小元素、插入元素
  • 單次時間複雜度都是 \(O(\log{n})\)
priority_queue<type> pq; // 初始priority_queue pq (預設為最大)
priority_queue<type,vector<type>,cmp> pq; // 自定義大小
struct cmp{
	bool operator () (pair<int,int> a,pair<int,int> b){ 
    	//pair 自定義函式寫法
    	if(a.S!=b.S) return a.S>b.S;
        return a.F>b.F;
    }
};
pq.push(x); // 在尾端插入一個元素 x
pq.top();  // 拿尾端的元素
pq.pop(); // 移除尾端元素

map

  • 由 key 值一一對應 val 的二元平衡樹(紅黑樹)
  • 可以當作陣列操作
  • 插入、移除時間複雜度皆是 \(O(\log{n})\)
  • 當 key 值太大(超過1e7 (?) 會變異常慢)

map

map<type1,type2> m; // key(type1) , val(type2)
m[key]=val; , m[key]+=val,m[key]-=val,... // 和陣列操作類似
m.insert({key,val});
m.erase({key,val});

m.find(key); // 確認是否有key 值,如果有回傳其 iterator , 否則回傳 m.end();

// 遍歷 map (按照 key 由小到大)
for(auto [key,val]:m) ...
for(auto it=m.begin();it!=m.end();it++) ... 
// (it 為iterator, 實際應為 map<type1,type2>::iterator it; 太長改用 auto 代替)

unordered_map

  • 一個神奇的東西,和 map 類似
  • 實作上把 map 改成 unordered_map 就好
    • 遍歷時不會是由小到大
  • 想像成一個key不設限的陣列
  • 內部是用雜湊函數,因此並非每種資料型別都可 (除非你要自己刻)
    • int , string 都可
    • 其他可能不行 (?

unorered_map

unordered_map<type1,type2> m; // key(type1) , val(type2)
m[key]=val; , m[key]+=val,m[key]-=val,... // 和陣列操作類似
m.insert({key,val});
m.erase({key,val});

m.find(key); // 確認是否有key 值,如果有回傳其 iterator , 否則回傳 m.end();

// 遍歷 map 
for(auto [key,val]:m) ...
for(auto it=m.begin();it!=m.end();it++) ... 
// (it 為iterator, 實際應為 unordered_map<type1,type2>::iterator it; 太長改用 auto 代替)

set

  • 排序好的集合,也是紅黑樹
  • 支援插入、刪除 \(O(\log{n})\)
  • 同一個元素只會有一個
set<type> st;
st.insert(x);
st.erase(x); // 刪除元素x
st.erase(pos); // 刪除位置 pos 
st.find(x); // 有的話回傳 iterator , 沒有的話回傳 st.end();

multiset

  • 和 set 差別是同一元素可有很多個
multiset<type> st;
st.insert(x);
st.erase(x); // 刪除所有元素 x
st.erase(pos); // 刪除位置 pos 
st.find(x); // 有的話回傳 iterator , 沒有的話回傳 st.end();

雙指針

  • 其實只是一個很簡單的概念,就是利用兩個指針
  • 大部分時候兩指針表示是 區間 [l,r]
  • 當滿足 r 越大 , l 會呈現單調性
  • 那就可以用雙指針 \(O(n)\) 而非 \(O(N^2)\) 枚舉
  • 詳細可以參考 CF EDU教學
  • 有兩個已經由小到大排序好的陣列,
  • 利用 \(O(N)\) 把兩陣列合併且保持由小到大
  • 設定兩個指針 l1,l2 代表兩陣列目前合併到的位置
  • 初始皆為 0
  • 之後依序比較兩指針的值,移動值比較小的指針
while(i<n||j<m){
	if(i<n&&(j>=m||a[i]<b[j]) cout<<a[i++]<<" ";
	else cout<<b[j++]<<" ";
}cout<<"\n";
  • 給定陣列 \(a\), 求有幾組點對 \([l,r] \land \sum_{l}^{r} a_k \leq s \)
  • \(1 \leq a_i \leq 10^9\)
  • \(1\leq n\leq 10^5\)
  • \(1\leq s \leq 10^{18}\)
  • 可以注意到因為陣列元素都是正的,
  • 因此當右界固定時,左界越往右會越小
  • 符合雙指針單調性
  • 設指針分別代表區間 \(l,r\)
  • 當目前總和是滿足的話,代表左界任意往右移都可
  • 反之則持續移動左界直到滿足前面條件
for(int i=0;i<n;i++){
	x+=v[i];
	while(l<i&&x>s){
		l++;
		x-=v[l];
	}
	if(x<=s){
		len=max(len,i-l);
	}
}cout<<len<<"\n";

二分搜

二分搜

  • 當題目符合單調性質
  • ex:
    • 若 \(x\) 可以則 \(>x\) 一定也可以
    • 若 \(x\) 可以則 \(<x\) 一定也可以
  • 一樣有 CF edu 教學

STL lower bound/ upper bound

  • 在已由小到大的陣列可以\(\log{n}\)找到比 \(x\)大的第一個位置*(是記憶體位置,所以要轉乘元素index 需要 - l/v.begin()
  • 第一個 \(\geq\) 位置
    • lower_bound(l,l+n,x)
    • lower_bound(v.begin(),v.end(),x)
  • 第一個 \(>\)位置
    • upper_bound(l,l+n,x)
    • upper_bound(v.begin(),v.end(),x)

二分搜 

  • 我是習慣寫左閉右開,當然也可以左閉右閉
  • 通常二分搜的 ok 函數是會暴力跑過整個陣列,整題時間複雜度會是 \(O(n\log{n})\)

 

int l=0,r=n;
while(l+1<r){
	int mid=(l+r)>>1;
    if(ok(mid)) r=mid;
    else l=mid;
}
cout<<r<<"\n";

浮點數二分搜 

  • 浮點數也可以,通常題目會說要和答案不超過 \(10^{-6}\)
  • 有一種寫法是更改執行條件
  • 另一種寫法是直接改成跑30~40次之類的

 

double l=0,r=n;
for(int cnt=0;cnt<30;cnt++){
	int mid=(l+r)>>1;
    if(ok(mid)) r=mid;
    else l=mid;
}
cout<<r<<"\n";
  • 給定陣列 \(a,b\) 會把所有 \(a_i+b_j , \forall(1\leq i \leq n, 1\leq j \leq n\) 寫出陣列 \(c\) 
  • 之後由小到大排好,求第 \(k\) 小數字
  • \(1\leq n \leq 10^5\)
  • \(1 \leq k \leq n^2\)
  • \(1 \leq a_i,b_j\)
  • 那這題如果直接暴力做,\(O(n^2)\) 建出 \(c\) 再排序後輸出第 \(k\) 大的
  • 時間複雜度 \(O(n^2\log{n})\)
  • 既然答案要問第 \(k\) 大的數字,那就二分搜答案
  • 但是恰第 \(k\) 大沒有單調性
  • 恰 \(k\) 沒有單調性,但只要 \(<k\) 有。
  • 假設詢問答案是 \(x\) , 要怎麼在 \(O(n)\) 判斷是否是第 \(k\) 大
  • 先把兩個陣列排序
  • 既然有兩個陣列,直接枚舉其中一個,另一個有單調性質
  • 雙指針
  • 因為時限不緊,用 lower_bound 實作更容易
bool ok(int x){
	int sum=0;
	for(int i:a){
		int p=lower_bound(b.begin(),b.end(),x-i)-b.begin();
		sum+=p;
	}
	return sum<k;
}
int main(){
	sort(a.begin(),a.end());
	sort(b.begin(),b.end());
	int l=a[0]+b[0]-1;
	int r=a[n-1]+b[n-1]+1;
	while(l+1<r){
		int middle=(l+r)>>1;
		if(ok(middle)) l=middle;
		else r=middle;
	}

}

三分搜

三分搜

  • 和二分搜不同處是搜二次函數
  • 有時候亂砸三分搜可以唬爛一些分數
  • 三分搜每次會少 \(\frac{1}{3} / \frac{1}{4}\)  
  • 看寫法,但是因為每次會變成前面的 \(k \ | \ 0< k <1 \) 倍
  • 因此一樣只要 \(O(\log{n})\) 次就會收斂

三分搜

三分搜

  • eps 判斷一樣可以改成跑 100 次
  • 如果是整數三分搜可能邊界有點問題,
  • 比較理想作法是搜到極小距離( 10) 之後暴力搜索
while(r-l>10){
    int l2=(l*2+r)/3;
    int r2=(l+r*2)/3;
    if(cal(l2)<cal(r2)) l=l2;
    else r=r2;
}
for(int i=l;i<=r;i++){
    ans=max(ans,cal(i));
}
double l,r;
for(int t=1;t<=100;t++){
    double l2=(l*2+r)/3;
    double r2=(l+r*2)/3;
    if(cal(l2)<cal(r2)) l=l2;
    else r=r2;
}

單調隊列

單調隊列

  • 通常是在滑動窗口
  • 當一個東西有一時間離開後,就必定不會在用到
  • 使用 deque 實作 
    • 有時候只有單方向,叫做單調棧 用 stack
      • 也可以用deque

單調隊列

  • 通常是在滑動窗口
  • 當一個東西有一時間離開後,就必定不會在用到
  • 使用 deque 實作 
    • 有時候只有單方向,叫做單調棧 用 stack
      • 也可以用deque
  • 給定陣列 \(a\) , \(k\)
  • 請輸出所有相鄰長度為 \(k\) 陣列的極大值
  • \(1\leq n,k \leq 10^6\)
  • 可以注意到若存在 \(a_i\leq a_j , i<j\) \(a_i\)
  • 在 \(j\) 出現後就會消失
  • 因此可行的答案序列必定是遞減
  • 利用 deque 去維護滑動窗口,保持序列值是持續遞減
  • 順便維護進來時間,從頭開始距離超過 \(k\) 移除
deque<pii> dq;
for(int i=0;i<n;i++){
	cin>>a[i];
	while(dq.size()&&(dq.back().F<=a[i])) dq.pop_back();
	if(dq.size()&&i-dq.front().S>=k) dq.pop_front();
	dq.push_back(mp(a[i],i));
	if(i>=k-1) cout<<dq.front().F<<"\n";
}

單調隊列 dp 優化

  • 有時候當你 dp 式寫完會出現需要滑動窗口極值
  • 那就可以考慮用單調隊列優化
  • 單調隊列(斜率優化) dp 題

基礎算法

By yuhung94

基礎算法

  • 197