---
tags: 嘉中資訊科培訓講義
---
# 嘉中資訊科培訓講義(elementary level)
作者:[vincent97198](https://vincent97198.blogspot.com/)、[エミリア](https://oemiliatano.github.io/)
---
> [TOC]
>
> [這些都學過了,讓我離開新手村!](https://hackmd.io/x1u_TyWNSuGL9Wg_mTKlSw)
---
## 零、概論
我相信會用到這個題單的人,已經解完基礎的題單了><
所以接下來的內容,會省略掉我認為你們應該會的東西,然後多做一些補充。
這是一份花了幾天、犧牲考試成績等價交換出來的講義,好好珍惜。
> 練習題有時候會需要已經教過但非該章的知識,請自行思考。
---
### Online Judge
[UVa](https://onlinejudge.org/), [Atcoder](https://atcoder.jp/), [codeforces](https://codeforces.com/), [TIOJ](https://tioj.ck.tp.edu.tw/), [POJ](http://poj.org/), [Luogu](https://www.luogu.com.cn/)
---
### 主要競賽
* 請多打線上比賽,友cf、友at、最重要的,~~友CYSH Online judge~~。
如果你想進1!或更高,請拿出毅力打cf,at就不用了;它都8點開始。
> codeforce 的出題方向和TOI不太一樣,不要練到走火入魔了
> 等你大學打ICPC的時候就可以練cf練到走火入魔了(?
---
### 複雜度
* 時間複雜度
* 常見的複雜度
__dfs, bfs__ is $O(E + V)$
__many data structrue__, __build__ is $O(n)$, __require__ is $O(logn)$, __modify__ is $O(logn)$
__sort__ is $O(nlogn)$
__lower/upper_bound__ is $O(logn)$
__floyd warshall__ is $O(V^3)$
__Dijkstra__ is $O((V+E)logV)$
__set.count()__ is $O(log n)$
__multiset.count()__ => Logarithmic in size and linear in the number of matches.
* 空間複雜度
同時間複雜度去分析
_i.e._ 常數空間, 線性空間...
* coding複雜度
程式碼很長、容易出bug的就是高coding複雜度
一般來說,數學題的coding複雜度最低,而資料結構題的coding複雜度最高。
* 均攤分析
假設有一連串的操作,大部分代價小,而少部分代價大,那他們的代價可以均攤到每筆操作上,如果這樣整體代價仍然合理,那就可以敲code了。
實際上,這裡面有很深的學問,在此不展開講解。
i.e 並查集, 單調隊列
* $10^8$法則
把數據範圍代進複雜度裡,除以$10^8$後的數字,就代表會跑幾秒。
這是經驗法則。
且會隨著時間、judge有所不同。但是不失為一個判斷能否AC的好方法。
---
### 前國手們的建議
心法分享: (不知道會不會有智慧財產權的問題就先撤掉了)
解題技巧:
## 一、基礎演算法與技巧整理
### Greedy
多數情況下,Greedy策略是假解,請務必小心使用。
~~(不過在IOI賽制下還是可以拿來喇分)~~
* __如何看出Greedy__
通常整題都考Greedy的題目,n都不小。
其他就是要多觀察題目的性質
* __如何證明Greedy是對的?__
怕自己Greedy假解的好方法就是:
把Greedy的想法寫成DP式的樣子,然後檢查DP轉移有沒有漏考慮了什麼。
或是直接做,靠通靈。
---
### 二分搜
* 上界/下界值其實可以開很大。反正頂多搜128次而已。
請小心設定退出條件。
* 二分搜用途及廣,他可以搜很多東西,最稀疏平常的就是搜答案
有許多問題都喜歡叫你求「滿足條件的最小值」。
這類問題若能滿足以下條件,那或許可以考慮對答案二分搜。
* 任何大於答案(滿足條件的最小值)的數,都能滿足條件
* 判斷一數能否滿足條件的時間複雜度是好的,記得複雜度會帶個$O(logn)$
練習題: [poj 3685](http://poj.org/problem?id=3685), [zj c904](https://zerojudge.tw/ShowProblem?problemid=c904), [zj c575](https://zerojudge.tw/ShowProblem?problemid=c575)
---
### 三分搜
* __用法__
三分搜尋法和我們熟悉的二分搜尋法在實做層面上,算是差不多的東西,唯一不同的地方在於
* 二分搜尋法用於 __尋找單調函數的某個值__
* 三分搜尋法則是用於 __尋找n次函數的某個值__
> 須確定搜索的範圍內函數的形狀是U或∩才行
* __原理__
假定要拿來解釋的這個是要找二次函數的極小值,且極值的位置在左右界內僅靠一個參考點無法得知,該點的左右何者較接近極小值,但若有2個參考點(也就是三等份)就能得知較小的點必定比另一點更靠近極小值(二次函數性質),就能因此鬆弛解的範圍
__實作__ (U型函數)
```cpp=
while(fabs(l - r) > s)
{
m1 = (l + l + r) / 3
m2 = (l + r + r) / 3;
if (f(m1) > f(m2))
l = m1;
else
r = m2;
}
```
* __複雜度__ $O(log n)$
練習題: [AtCoder Beginner Contest 130 pF](https://atcoder.jp/contests/abc130/tasks/abc130_f)
---
### 打表
有的時候題目的形質非常神秘,不易觀察(尤其是數學題(~~玄學題~~))
我建議可以本地打表,觀察性質,可能會有助於思考(吧
練習題: [zj e320](https://zerojudge.tw/ShowProblem?problemid=e320), [cf 1342C](https://codeforces.com/problemset/problem/1342/C)
---
### 本地爆搜
本地算完答案或一些關鍵數字,藉此壓縮複雜度。
(面對難題或許有奇效 ~~通常沒有~~)
練習題: [zj e299](https://zerojudge.tw/ShowProblem?problemid=e299)
---
### 時間剪枝
快要TLE時,不管答案是否最優,直接輸出答案。
或是搜什麼東西,結果時間快到了還搜不到,直接輸出不存在。
(喇分還是挺有效的。)
---
## 二、資料結構
> 工欲善其事,必先利其器
### 線段樹
大概是資料結構的新手技能吧。 ~沒有人不會的。~
生來就是解決 __區間__ 問題。
> 給定一長度為n的數列,求區間$l, r$的最大/小值、和。
* __原理__
將一段數列,分治處理,利用分治的技巧,整個線段最多被切割$logn$次。藉此可以很快的維護線段的查詢、修改。
* __lazy tag__
> 現在增加一操作: 在區間$l,r$加上$v$
>
當原本的線段樹遇到區間加/減值時,單點修改會TLE,lazy tag即是利用 __不使用就不更新__ 的想法,在某個區間上打上tag,代表這裡的子節點應修改,然後不管他直到查詢需要用到才把tag推到子節點。
> 類似硬碟的刪除標記(?
>
* __差分__
> 給定原陣列$A$,現有兩操作
> 1. 在區間$l, r$內加上一首項為$k$、公差為$d$的等差數列
> 2. 詢問索引$p$的數。
>
如果我們現在在一般數列$b_i$存入$A_i$與$A_{i-1}$的差,而首項為$A_1$,我們可以知道這樣的陣列有一些性質
1. 前綴和$S_i=a_i$
2. 區間$l,r$加值相當於$b_l+v, b_{r+1}-v$
於是我們如果維護這樣一個數列,對於操作1,我們可以很簡單的先$b_l+k,b_{r+1}-(k+(r-l)\times d)$,而後整個區間$l+1,r$加上$d$。
明顯的,我們可以用線段樹維護上述數列,查詢前綴和即可。
練習題: [zj d801](https://zerojudge.tw/ShowProblem?problemid=d801), [luogu P1438](https://www.luogu.com.cn/problem/P1438)
---
### Sparse Table
又稱稀疏表。很少用到。
只能用在RMQ(Range Minimum/Maximum Query),不能算區間和。
* __原理__
與許多資料結構的想法一樣,先處理小部分,在合併成整體。
定義`Table[i][j]`為,在索引值i處,長度為$2^j$的數列最大/小或和,甚至乘。
__build__: $O(nlogn)$
__query__: $O(1)$
__modify__: __it can't be modified__
* __使用時機__
不需要修改,大量尋找區間極值時。
但是很少會用到,但是一旦用到絕對比刻一顆線段樹還快很多。
* __實做__
__build__:
先預處理長度為1的段。
```cpp=
for (int32_t i = 0; i < n; ++i)
for (int32_t j = 1; i - 1 + (1 << j) < n; ++j)
Table[i][j] = maintain(Table[i][j - 1], Table[i + (1 << (j - 1))][j - 1]);
```
__query__:
```cpp=
int32_t k = log2(r - l + 1); // int len = __lg(r - l + 1);
auto ans = maintain(Table[l][k], Table[r - (1 << k) + 1][k]);
```
> 跟倍增法好像哦
---
### BIT ~這是エミリア最喜歡的資料結構之一。~
~~學好BIT說不定就可以看見她的笑容~~
![](https://i.imgur.com/78ybbyh.jpg =800x)
不是你惠惠。
* __原理__
首先要先知道`lowbit(x)`在幹嘛,此函數會回傳x在二進制下最低的1是多少
_i.e._ `lowbit(3 = 0b11) = 1`, `lowbit(6 = 0b110) = 2`
而實作上可以用 `x&(-x)` 來取得。
定義`bit[x]`維護了$(x-lowbit(x), x]$的區間和。
(如果你還是不懂,可以畫圖看看)
那 __單點修改__ 就是
```cpp=
for (int32_t i = pos; i <= maxn; i += lowbit(i))
bit[i] += value;
```
__前綴查詢__:
```cpp=
for (int32_t i = pos; i; i -= lowbit(i))
res += bit[i];
```
如果將元素的值當作下標;且在下標處+1的話,可以擴展使用下面的功能
__第k小查詢__:
錯誤範例複雜度:$O(log^2n)$
```cpp=
//錯誤範例
int32_t l = 0, r = maxn;
while(r > l)
{
int32_t mid = (r + l) / 2;
if(k > sum(mid))
l = mid + 1;
else
r = mid;
}
res = mid;
```
正確解法複雜度:$O(logn)$
```cpp=
//正確範例
res = 0;
for (int32_t i = (1<< floor(log2(maxn))); i; i >>= 1)
if (res + i <= maxn && bit[res + i] < k)
k -= bit[res += i];
++res;
```
* 複雜度
__modify__: $O(logn)$
__query__: $O(logn)$
* 使用頻率
不算很低,也不算很高,但是很方便,算是輕便型segment tree
* 優點
常數很小、coding複雜度小
> 練習題: [tioj 1282](https://tioj.ck.tp.edu.tw/problems/1282), [zj b577](https://zerojudge.tw/ShowProblem?problemid=b577), [zj d847](https://zerojudge.tw/ShowProblem?problemid=d847)
---
### treap ~同樣是エミリア最喜歡的資料結構之一~
__!這裡只討論split-merge treap__
屬於二元平衡樹的一種,因為 __易編寫__ __速度快__ __靈活性高__ 的特性而在競程占有一席之地。
他可以解決 segment tree 的問題,也可以解決splay tree的問題,同樣也可以解決大部分二元平衡樹的問題,學一個treap抵過學一堆樹。
* __基本原理__
Treap = tree + heap。
亦即同時擁有BST與heap性質的資料結構。
> heap: 父節點的```pri```值大於子結點
> BST: 左子樹```key```值均小於等於父節點,右子樹則大於。
>
> 一般二元平衡樹為了避免退化,會利用 __旋轉__ 操作去維持深度。
> 而treap因為擁有BST與heap的性質,所以既能擁有BST的查找功能,又能像heap一樣維持深度,
- __treap最重要的兩個操作:__
- merge(a, b):
合併兩顆treap,注意此函式必須滿足 __a的所有key值小於b的所有key__
- split(t, k):
將treap t分成兩顆treap, __一顆裡的key均小於等於k,另一顆的均大於__。
兩種操作都因為BST的特性所以實作難度降低許多。
* __基本架構__
* __node:__
```cpp=
struct Treap
{
Treap *l, *r;
int32_t key, pri;
Treap(int32_t _key)
{
l = r = nullptr;
key = _key;
pri = rand();
}
}
```
* __merge:__
```cpp=
Treap* merge(Treap* a, Treap* b)
{
if (a == nullptr || b == nullptr) return a ? a : b;
if (a->pri > b->pri)
{
a->r = merge(a->r, b);
return a;
}
else
{
b->l = merge(a, b->l);
return b;
}
}
```
* __split:__
```cpp=
void split(Treap* t, int32_t k, Treap* &a, Treap* &b)
{
if (t == nullptr)
{
a = b = nullptr;
return;
}
if (t->key <= k)
{
a = t;
split(t->r, k, a->r, b);
}
else
{
b = t;
split(t->l, k, a, b->l);
}
}
```
只要有上面兩種操作,基本就寫完了。
* __insert:__
```cpp=
void insert(Treap *t, int32_t k)
{
Treap *lt, *rt;
split(t, k, lt, rt);
merge(merge(lt, new Treap(k)), tr);
}
```
* __remove:__
```cpp=
void remove(Treap *t, int32_t k)
{
Treap *lt, *rt;
split(t, k - 1, lt, t);
split(t, k, t, rt);
t = merge(lt, rt);
}
```
__treap,就是這麼簡單。__
在平衡樹的問題中,常常遇見尋找第k小元素的要求,就跟BST一樣,treap一樣是用節點size去判斷,所以我們現在需要維護treap上節點的size,這樣可以跟BST一樣找kth了。
```cpp=
int32_t Size(Treap *t)
{
return t == nullptr ? 0 : t->sz;
}
void pull(Treap *t)
{
t->sz = 1 + Size(t->l) + Size(t->r);
}
```
* __真正實作__
__node:__
```cpp=
struct Treap
{
static Treap mem[MAXN], *ptr;
Treap *l, *r;
int32_t pri, key, siz;
Treap() = default;
Treap(int32_t _key)
{
l = r = nullptr;
pri = rand(); //可以用其他隨機方法,保證更好的隨機性
key = _key;
siz = 1;
}
}Treap::mem[MAXN], *Treap::ptr = Treap::mem;
```
__split:__
```cpp=
//與上面唯一有差別的只有當某顆treap的結構改變時,需要呼叫pull()去更新資訊
//例如 呼叫完split()後
```
__merge:__
```cpp=
//與上面唯一有差別的只有當某顆treap的結構改變時,需要呼叫pull()去更新資訊
//例如 呼叫完merge()後
```
__kth:__ (第k小)
```cpp=
int32_t kth(Treap *t, int32_t k)
{
int32_t lsz = sz(t->l) + 1;
if (lsz < k) return kth(t->r, k - lsz);
else if(lsz == k) return t->key;
else return kth(t->l, k);
}
```
__以上是treap當平衡樹的版本__
---
* __treap區間維護__
上面提過,treap不只可以解決平衡樹問題,也可以解決序列操作問題。
我們只需要在node裡多加一個```val```當作序列上的值、```key```當作序列上的索引值、```mx/mn/sum```當作要維護的值即可。
__treap,就是這麼簡單。__
但是當我們遇到區間加值怎麼辦?
線段樹巧妙的用 __lazy tag__ 解決了,同樣的treap也可以!
只要用好好維護我們的tag即可。
```cpp=
void push(Treap *t)
{
if (t == nullptr) return;
t->val += t->lazy;
t->mx += t->lazy;
if (t->l != nullptr)
t->l->lazy += t->lazy;
if (t->r != nullptr)
t->r->lazy += t->lazy;
t->lazy = 0;
}
void pull(Treap *t)
{
t->sz = 1 + Size(t->l) + Size(t->r);
}
```
__node:__ (改)
```cpp=
struct Treap
{
static Treap mem[MAXN], *ptr;
Treap *l, *r;
int32_t pri, key, siz, lazy;
int32_t val; //新增這兩個
int32_t mx;
Treap() = default;
Treap(int32_t _key, int32_t _val)
{
l = r = nullptr;
pri = rand();
key = _key;
siz = 1;
val = mx = _val;
}
}Treap::mem[MAXN], *Treap::ptr = Treap::mem;
```
__build:__
```cpp=
Treap *t = nullptr;
for (int32_t i = 1; i <= n; ++i)
t = merge(t, new (Treap::ptr++) node(i, a[i]));
```
上面用到了placement new的技巧,通過先開記憶池再去new一個node,可以降低系統分配記憶體的開銷。
__區間加值:__
```cpp=
void add_range(Treap *t, int32_t l, int32_t r, int32_t val)
{
Treap *lt, *rt;
split(t, l - 1, lt, t);
split(t, r, t, rt);
t->lazy += val;
merge(merge(lt, t), rt);
}
```
---
* __翻轉吧! treap__
有沒有觀察到我們剛剛 __build__ 時,```key```直接放1, 2, 3...,仔細想想這樣```key```的意義不就是 __在treap中有幾個比他小__。
那只要維護好```size```就可以不用管```key```了。
所以```split(t, k)```的意義也就變成了:__在t中切開前$k$個節點與後$n - k$個節點__。
* 只用```size```的好處
不必再被```key```綁手綁腳的,當我們遇到什麼區間翻轉、區間剪下貼上,就真的直接剪下去、或轉下去(正常還是會打標)。
__treap,就是這麼簡單__
__node:__ (改)
```cpp=
struct Treap
{
static Treap mem[MAXN], *ptr;
Treap *l, *r;
int32_t pri, siz, lazy;
int32_t val;
int32_t mx;
bool rev; //翻轉標記
Treap() = default;
Treap(int32_t _val)
{
l = r = nullptr;
pri = rand();
siz = 1;
val = mx = _val;
rev = false;
}
}Treap::mem[MAXN], *Treap::ptr = Treap::mem;
```
__split:__ (改)
```cpp=
void split(Treap *t, int32_t k, Treap *&a, Treap *&b)
{
if (!t) { a = b = nullptr; return; }
push(t);
//此節點的左子樹數量大於等於k
if (Size(t->l) >= k)
{
b = t;
push(b);
//將b指向整個樹,向左子樹處理。
split(t->l, k, a, b->l);
pull(b);
}
else
{
a = t;
push(a);
split(t->r, k - Size(t->l) - 1, a->r, b);
pull(a);
}
}
```
這部分的```split()```比較難理解,可以畫圖看看或直接貼程式,輸出中間過程。
!翻轉其實就是左右子樹交換
練習題: [luogu P3391](https://www.luogu.com.cn/problem/P3391)(模板), [cf 702F](http://codeforces.com/problemset/problem/702/F)
---
## 三、圖論
>
資訊的圖是由 __點(vertex)__ __邊(edge)__ 構成的。
圖有下面幾種:
1. 有向圖 : 邊有方向性
2. 無向圖 : 邊無方向性
3. 簡單圖 : 無重邊、無自環
4. 完全圖 : 所有點彼此互連
5. 二分圖 : 把圖分成兩部分,同一個集合裡的點 __彼此不相連__,且圖上不存在奇環
---
### 專業術語
1. $DAG$ : 有向無環圖
2. $|V|$ : 點的個數
3. $|E|$ : 邊的個數
---
### 樹
圖論中的樹,指的是有$n$個點、$n-1$條邊,每個點都相連通的圖。
#### 樹直徑
> 要求兩端點$i$, $j$使得$dis(i, j)$為樹中最大,求$i$, $j$, $dis(i, j)$,
有DP、兩種dfs作法,這裡只介紹dfs。
1. __兩次dfs作法__
隨機一頂點找最遠點v,再由v去找離v最遠的點u,則(u,v)為樹直徑兩端,直覺無比皆大歡喜。
```cpp=
// 沒有code啦,難道dfs也不會?
```
2. __一次dfs作法__
樹dp
```cpp=
int32_t ans = 0;
int32_t dfs(int32_t now, int32_t pa)
{
int32_t MAX = 0, MAX2 = 0; //目前節點第一,二長路徑
int32_t nexDep;
for (auto next : G[now])
{
if (next == pa) continue;
nexDep = dfs(next, now);
if (nexDep > MAX)
MAX2 = MAX, MAX = nexDep;
else if (nexDep > MAX2)
MAX2 = nexDep;
}
ans = max(MAX + MAX2, ans);
return MAX + 1; //只需要回傳這個節點下最大的路徑長就好
}
```
---
#### 最近共同祖先(LCA)
> 給定$root, a, b$,有$Q$個詢問求$a, b$的共同祖先$c$,且$c$距離兩者皆最小。
- __naive解__
dfs一遍,確定每個點的父親以及深度,然後從$a, b$往上爬到同樣的節點即可。
- 複雜度: $O(Qn)$
- __倍增法__
從剛剛的naive解我們會發現我們每次都只是往上走 __1__ 而已,何不走大步一點呢?
如果每次我們都走2的冪次步的話,可以走到任意祖先。 (二進制的性質)
那我們就預處理每個點往上走$2^j$步會到誰就好了。
當詢問時,就開始跳,直到相同的點。
```cpp=
void dfs(int32_t now, int32_t fa = -1)
{
for (int32_t i = 1; i < 32; ++i)
{
//maintain
MX[now][i] = MX[Father[now][i - 1]][i - 1];
Father[now][i] = Father[Father[now][i - 1]][i - 1];
}
for (auto node : G[now])
{
int32_t to = node.first;
int32_t wi = node.second;
if (to == fa) continue;
//maintain
MX[to][0] = Val[now][to];
Father[to][0] = now;
depth[to] = depth[now] + 1;
dfs(to, now);
}
}
```
```cpp=
int32_t lca(int32_t u, int32_t v)
{
if (_depth[u] > _depth[v]) swap(u, v);
int res = INT_MIN;
int tmp = _depth[v] - _depth[u];
for (int32_t i = 0; tmp; ++i, tmp >>= 1)
if (tmp & 1)
{
//maintain
res = max(res, MX[v][i]);
v = _Father[v][i];
}
if (u == v) return u;
for (int32_t j = 31; j >= 0 && u != v; --j)
if (_Father[u][j] != _Father[v][j])
{
//maintain
res = max({res, MX[u][j], MX[v][j]});
u = _Father[u][j];
v = _Father[v][j];
}
//return _Father[u][0];
return max(res, MX[u][0]);
}
```
__複雜度__: build: $O(N)$, query: $O(logN)$
---
#### dfs序
> 給定一棵樹及根,上面有$n$個節點,每個節點都有一顏色$c_i$,總共有$m$個操作,有兩種操作:
> 1. 將$v$的子樹染色成$k$
> 2. 詢問$v$的子樹裡包含幾種顏色
>
> $n, m\leq 4\times 10^5$ , $c_i \leq 60$
首先考慮這棵樹是一條直鍊的情況。
用位元去表示顏色,拿個資料結構實現可以查找區間OR的功能就好了。
那當這棵樹不只一條鍊的話怎麼辦?
如果點集合$S$是$v$的子樹,那點集$S$必然比$v$還要晚被dfs到,也就是說假設我們現在有一棵樹,dfs下去,那每個點都有他進去的順序。
_i.e_ root = 1, root's son1 = 2, root's son2 = 3 ...etc.
而你會發現如果將這序號排好,$v$的子樹,就是其中的一段區間。
而$l$就是$v$的序號,$r$則是$v$子樹中最大的序號(或是$l + size - 1$)。
至此,我們只需要一次dfs找序號,用一支援區間OR的資料結構即可解決問題。
一些樹上問題可以透過dfs序解決,當然也需要點巧思才能將問題轉化。
例如
> 給定一棵樹一個根,總共$n$個點,有$q$個查詢,問$i$是不是$j$的 __祖先__
>
如果只單看進入時間根本看不出來,一個不夠? 那就看兩個。
連同 __離開時間__ 一起看。
細節自己想想,這邊不多贅述。
---
[本章](###樹)練習題: [USACO 2019.12 Platinum Bessie's Snow Cow](https://loj.ac/problem/3227), [cf 620E](https://codeforces.com/problemset/problem/620/E)
---
### 一般圖的連通性
#### dfs邊分類
- 樹邊: 真正在dfs樹上的邊,從父親連往小孩
- 回邊: 從子孫連回祖先的邊
- 交錯邊: 連向非直系血親的邊
#### 無向圖的連通性
> 定義一連通圖$G$為 __點-$k$-連通__,滿足刪去$k-1$個 __節點__ 能使$G$變得不連通。
>
> 定義一連通圖$G$為 __邊-$k$-連通__,滿足刪去$k-1$個 __邊__ 能使$G$變得不連通。
>
競賽中最常見的$k=2$,分別被稱為 __點雙連通__ 及 __邊雙連通__
- __邊雙連通__
- 性質: 對於點雙圖中的點$u, v$,至少存在兩條路徑從$u$到$v$,且不共邊。
- Robbin定理: 對於一無向圖$G$,$G$是邊雙連通若且唯若存在一種將G的邊定向的方式使任兩節點可以互相到達。
定義 __橋__(bridge): 若$e$是橋,則移除後會使圖$G$不連通。
所以只要能夠找到橋就可以判斷圖$G$是否為邊雙連通。
- __Tarjan's Algorithm__
定義 __low(v)__:對於無向圖的節點$v$,$low(v)$被定義為在不透過父邊,能夠到達的最低祖先的深度
![](https://i.imgur.com/WaJ3nSQ.png)
!上圖的節點4可以走到2,所以$low(4)=1$;節點7可以走到1,所以$low(7)=0$,而3可以通過4走到2,所以$low(3)=low(4)=1$,其餘相同。
可以發現當$low(v)$與$dep(v)$一樣時,$e=\{v, fa(v)\}$為橋。
- __實作__:
```cpp=
void dfs(int32_t now, int32_t fa)
{
vis[now] = true;
low[now] = dep[now] = dep[fa] + 1;
for (auto nex : G[now])
{
if (nex == fa)
continue;
if (vis[nex])
low[now] = min(low[now], dep[nex]);
else
{
++childcnt;
dfs(nex, now);
low[now] = min(low[now], low[nex]);
}
}
if (low[now] == dep[now])
{
// edge{now, pa} is bridge unless now is root
}
}
```
複雜度:$O(E+V)$
- __邊雙連通分量__
雙連通分量指的是圖$G$裡的 __雙連通子圖__。
一般是指極大的連通子圖,也就是說無法在該子圖中添加新節點,使得新加入的子圖仍滿足雙連通。
如何找到邊雙連通分量?
- __naive解__
把所有橋都移除掉,剩餘的就是邊雙連通分量。
複雜度: $O(2(V+E))$
- __tarjan解__
在遞迴過程中維護一個stack,在每次進入一個節點的時候把它推入stack中,當我們確定節點$v$的父邊為橋時,在stack裡由上而下直到節點$v$就都是邊雙分量。
```cpp=
void dfs(int32_t now, int32_t fa)
{
vis[now] = true;
low[now] = dep[now] = dep[fa] + 1;
st.push(now);
for (auto nex : G[now])
{
if (nex == fa)
continue;
if (vis[nex])
low[now] = min(low[now], dep[nex]);
else
{
dfs(nex, now);
low[now] = min(low[now], low[nex]);
}
}
if (low[now] == dep[now])
{
// edge{now, pa} is bridge unless now is root
while(!st.empty())
{
int32_t x = st.top(); st.pop();
bcc[x] = Nbcc;
if (x == now) break;
}
++Nbcc;
}
}
```
複雜度: $O(V+E)$
當我們找到雙連通分量後,就可以根據編號而縮點,縮完點後的圖 __不會存在環__,這是個好的性質,這樣的圖常常存在複雜度好的算法。
---
- __點雙連通__
- 性質: 對於兩個圖中節點$u, v$,都存在兩條從$u$到$v$的路徑,且無共用點。
定義 __割點__ 或 __關節點__: 若$v$是割點,則移除後圖$G$不連通
- __tarjan's algorithm__
沿用上面的low()定義,有一個節點$v$以及子節點$u$,若有一個$u$的$depth(v)\leq low(u)$,則節點$v$必為割點。
!但是root需要特判
```cpp=
void dfs(int32_t now, int32_t fa)
{
int childcnt = 0;
vis[now] = true;
low[now] = dep[now] = dep[fa] + 1;
for (auto nex : G[now])
{
if (nex == fa)
continue;
if (vis[nex])
low[now] = min(low[now], dep[nex]);
else
{
++childcnt;
dfs(nex, now);
low[now] = min(low[now], low[nex]);
if (low[nex] >= dep[now])
cut[now] = true;
}
}
if (now == root && childcnt <= 1)
cut[now] = false;
}
```
- __點雙連通分量__
與上面的邊雙連通分量一樣,我們一般討論極大點雙連通子圖。
但是會有一個問題:
![](https://i.imgur.com/B4qsTet.png)
可以發現一個點可能會被分在不同的分量中,而這情況只發生在割點,因此若是我們使用上面的方法:存點,可能會把割點pop掉,但是實際上還有其他分量包含著,因此找點雙連通分量時,我們必須存邊
- __tarjan解__
```cpp=
void dfs(int32_t now, int32_t fa)
{
int childcnt = 0;
vis[now] = true;
low[now] = dep[now] = dep[fa] + 1;
for (auto nex : G[now])
{
if (nex.node == fa)
continue;
if (!inStack[nex.edgeNum])
{
inStack[nex.node] = true;
st.push(nex.edgeNum);
}
if (!vis[nex.node])
low[now] = min(low[now], dep[nex.node])
else
{
++childcnt;
dfs(nex.node, now);
low[now] = min(low[now], low[nex.node]);
if (low[nex.node] >= dep[now])
{
cnt[now] = true;
while(!st.empty())
{
int e = st.top(); st.pop();
bcc[e] = Nbcc;
if (e == nex.edgeNum) break;
}
++Nbcc;
}
}
}
if (now == root && childcnt == 1)
cut[now] = false;
}
```
點雙連通分量可以建成一種特殊的樹,__圓方樹__,對於割點$v$如果把它獨立看做一點,而點雙分量也看成獨立一點的話,就會變成
![](https://i.imgur.com/vSqH1WM.png)
割點為圓點,而方點為連通分量。
---
#### 有向圖的連通性
> 定義一有向圖為 __弱連通__,若圖上所有邊都換為無向邊後圖連通。
> 定義一有向圖為 __強連通__,對於兩個節點$u,v$都存在一條從$u$走到$v$以及一條從$u$到$v$的路徑。
>
- __強連通分量__
同無向圖一樣,我們都只討論極大子圖。
- __Kosaraju algorithm__
```cpp=
void reDfs(int32_t now)
{
vis[now] = true;
for (auto nex : rG[now]) //rG為G的反圖
if (!vis[nex])
reDfs(nex);
order.push_back(now);
}
void dfs(int32_t now, int32_t s)
{
scc[now] = s;
for (auto nex : G[now])
if (scc[nex] == -1)
dfs(nex, s);
}
void Kosaraju()
{
for (int32_t i = 0; i < n; ++i)
{
if (!vis[i])
reDfs(i);
}
int32_t NScc = 0;
for (int32_t i = n - 1; i >= 0; --i)
{
int32_t now = order[i];
if (scc[now] == -1)
dfs(now, NScc++);
}
}
```
---
> [本章](###一般圖的連通性)練習題 :[tioj 1981](https://tioj.ck.tp.edu.tw/contests/71/problems/1981), [cf 1220E](https://codeforces.com/problemset/problem/1220/E)
---
## 四、DP ~這是エミリア最討厭的東西~
### 背包
算是DP入門的新手技,但是有許多變體。
---
#### 完全背包
> 給定n個物品,每種物品都有重量與價值,__且每種物品有無限個__,你最多可以拿M單位重物品,求最大價值
* __code:__
```cpp=
for (int32_t i = 1; i <= n; ++i)
for (int32_t j = w[i]; j <= M; ++j)
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
```
其實就只是01背包的 __枚舉順序相反__ 而已
練習題: [luogu P1616](https://www.luogu.org/problemnew/show/P1616)
---
#### 多重背包
> 給定n個物品,每種物品都有重量與價值,__且每種物品有$c_i$個__,你最多可以拿M單位重物品,求最大價值
>
* 用01背包的方式去做
每 __件__ 物品都去跑01背包,則複雜度: $O(M\sum c_i)$
相信這應該不是好的複雜度。
* 二進制優化
在上面的例子中,我們的時間都浪費在重複選取上面了。
例如: 同時選第1種物品的第1, 2個 跟 第1, 3個是相同的。
那麼優化拆分方式就成了關鍵。
這裡要介紹的即是 二進制分組:
> 把同種類的$c_i$個物品不斷包裝成價值$v_i \times 2^j$、重量$w_i \times 2^j$
可以發現因為二進制的關係,雖然那些單個單個的物品消失了,但是同樣可以選擇同樣的數量!
_i.e._ $v_i \times 2^0 + v_i \times 2^1 = v_i \times 3$
但是最後記得補上沒辦法裝成2冪次的物品集,$v_i \times (c_i - 2 ^ {log_2 (c_i+1) - 1})$, $w_i \times (c_i - 2 ^ {log_2 (c_i+1) - 1})$。
複雜度: $O(W\sum log_2 c_i)$
```cpp=
for (int32_t i = 1; i <= n; ++i)
{
int32_t j = 0;
cin >> w >> v >> c;
while (c >= (1 << j))
{
V[idx] = v * (1 << j);
W[idx] = w * (1 << j);
c -= (1 << j);
++j;
++idx;
}
V[idx] = v * c;
W[idx] = w * c;
++idx;
}
```
練習題: [luogu P1776](https://www.luogu.org/problemnew/show/P1776)
---
#### 二維背包
> 給定n個物品,每種物品都有重量與價值,你最多可以拿 __M單位重的物品__ 以及 __L單位價值物品__,求最多可以選幾件。
>
其實基本思想一樣,只要再多開一維去轉移兩者就好。
```cpp=
for (int32_t i = 1; i <= n; ++i)
for (int32_t j = L; j >= v[i]; --j)
for (int32_t k = M; k >= w[i]; --k)
dp[j][k] = max(dp[j][k], dp[j - v[i]][k - w[i]] + 1);
```
練習題: [luogu P1855](https://www.luogu.com.cn/problem/P1855)
---
#### 超大背包
> 給定n個物品,每種物品都有重量與價值,你最多可以拿M單位重物品,求最大價值
> $M \leq 10^{15}, n \leq 40$
>
__!這題跟dp沒甚麼關係__
還記得一般背包的複雜度嗎: $O(nM)$
明顯的這裡根本不行用一般背包解。
我們應該利用$n$比較小的特性。
假設我們將物品分成兩組,那最優解一定是在第一組總共拿$v1, w1$、第二組則是$v2, w2$,且$w1 + w2 <= M$
枚舉拿第一組東西的所有情況成一集合$S$,然後再去枚舉第二組的情況,這樣看起來複雜度是$O(2^{\frac{n}{2} + 1})$,不算太差。
但是我們枚舉完第一組東西後,正在枚舉第二組時,還需要找到$S$中最大的$v$,且滿足$w\leq M-w_{now}$。
我們得先把第一組枚舉情況排序好,然後剔除掉$S_{vi} < S_{vj}$且$S_{wi} > S_{wj}$的$i$,在枚舉第二組時,二分搜$max\{S_{vi} ,S_{wi}\leq M-w_{now}\}$
__真正的複雜度:__ $O(2^{\frac{n}{2}}+2^{\frac{n}{2}} \times \frac{n}{2})$
練習題:
---
### 狀態壓縮DP
> 旅行員推銷問題(TSP):
> 給定n個城市以及m個道路,保證所有城市都連通,問從起點尋遍所有城市再回到起點的最短路徑。
>
如果你想dfs直接硬爆找解,複雜度是$O(n!)$
當$n=10$就已經很勉強了。
一般會使用位元dp去壓縮複雜度。
* 把每個點的狀態區分為0跟1,就能用一個整數的位元去表達一群狀態的集合
* 可以發現這樣就能簡單的儲存各狀態下的解了,因為一群狀態其實就是一個二進位的數,可以換成十進位的數當作dp的index
> 其實不一定要是0或1,也有3進位的狀態壓縮喔
$S$為走過的城市集合,而$v$是目前的位置
定義```dp[S][v]```為走過的集合且目前在v的位置,走完剩餘點,回到起點的最小路徑。
則轉移式為 $dp[s|a << u][u] = min\{dis(v, u) + dp[s][v], u \notin s\}$
```cpp=
for (int32_t i = 0; i < (1 << n); ++i)
fill(dp[i], dp[i] + n, INF);
dp[(1 << n) - 1][0] = 0;
for (int32_t S = (1 << n) - 2; S >= 0; --S)
{
for (int32_t v = 0; v < n; ++v)
for (int32_t u = 0; u < n; ++u)
if ((S & (1 << u)) == 0)
dp[S][v] = min(dp[S][v], dis(v, u) + dp[S | (1 << u)][u]);
}
ans = dp[0][0];
```
練習題: [tioj 1468](https://tioj.ck.tp.edu.tw/problems/1468)(三進位狀壓)
---
## 五、字串
### 術語解說
- substring
- subsequence
舉例:
"__Emiliatan__"是"__Emiliatan__ Maji Tenshi"的 __substring也是subsequence__
"__EMT__"是"__E__ miliatan __M__ aji __T__ enshi"的 __subsequence但不是substring__
### 當hash遇見string
> 有沒有可能用一個整數表達一個由小寫字母組成的字串呢?
不難想到就是把每個字符對照轉換,然後用26進位法做處理。
我們稱這種方法叫 __hash__,又稱 __哈希__、__雜湊__
但是我們的整數最大就是64位元,是不可能任意的轉換。
有什麼方法可以解決這樣的問題?
同餘可以處理這樣的問題。
但是在同餘下,每個數字就不會只代表一個字串。
不同的字串在hash後,代表同一數字我們稱為 __碰撞__
> 計算hash跟直接比較字串不是要花一樣的複雜度嗎?
- __rolling hash__
假設已知 $hash(Emiliata)=p$。
則$hash(Emiliatan)$顯然就是$p\times 26+'n'-'a'\pmod M$。
這種利用其他已知字串hash值求得新字串hash值的方法就是rolling hash。
(已知tan或sintan都可以用來求Emiliatan,請自己想)
- __處理碰撞__
當你遇到兩個hash值相同的字串時,有可能他們是同一字串,也可能不是,所以要逐一比對。
但是當兩字串hash值不同,那顯然不是同字串
- __減少非相同字串碰撞的機會__
- __選擇一個好的$M$__
在數論那一章節你會知道,當$M$是質數的時候,可以盡可能的「互質」。因此可以可以盡可能的讓每一個字串均勻的分佈到0~MOD-1上
- __做兩次hash__
一次不夠就兩次,選擇不同的MOD可以大大的降低碰撞的機率(這很顯然吧w)
- __複雜度分析__
因為rolling hash的緣故,求hash的複雜度可以被均攤,但是做模運算的常數很大(如果可以,用減法代替),也有碰撞的可能發生,因此分析碰撞的機率非常重要。
- __碰撞的機率分析__
如果出來的hash值分布是均勻的,且$M$是一個非常大的質數,差不多$10^{15}$,那麼相撞的機率是$10^{-15}$,機率小的幾乎不會發生。
> 但還是有方法可以構造出卡hash的測資......請小心
- hash在codeforce上的注意事項
在codeforce上,程式碼都會被看光光,所以當你要用hash時就要有被同房的hack爛的心裡準備。
他可以看你使用的MOD值輕鬆做出會不斷碰撞的字串卡死你。
hack別人的練習題:[zj c706](https://zerojudge.tw/ShowProblem?problemid=c706)
---
### 字串比對
> 給定一字串$A$,詢問字串$B$是否為$A$的substring。
>
- __Ctrl + F__
我開玩笑的。
- 複雜度: ~~看自己手速~~
- __暴力法__
一個個字元去比對$A$,失敗就挪動$B$再去比。
- 複雜度: $O(|A||B|)$
- __KMP Algorithm__
全名Knuth-Morris-Pratt Algorithm,學習這個演算法之前得先了解一些東西
- __prefix-suffix__
前綴等於後綴,稱作「相同前綴後綴」。一個字串通常有許多個「相同前綴後綴」。
_i.e._
abc -> {_null_, abc}
abcaa -> {_null_, a, abcaa}
abcabc -> {_null_, abc, abcabc}
ababa -> {_null_, a, aba, aba}
aaaaa -> {_null_, a, aa, aaa, aaaa, aaaaa}
abaabaa -> {_null_, a, abaa, abaabaa}
abzab -> {_null_, ab, abzab}
- __longest proper prefix-suffix__
一個字串的「最長的相同前綴後綴」就是原字串,「最短的相同前綴後綴」就是空字串,「次長的相同前綴後綴」就是第二長的相同前綴後綴。
_i.e._
abc -> _null_
abcaa -> a
abcabc -> abc
ababa -> aba
aaaaa -> aaaa
abaabaa -> abaa
abzab -> ab
- __failure function__
窮舉法的過程當中,當前比對成功的字串片段,是 P 的前綴。因為我們無法預測是 P 的哪幾個前綴,所以我們可以預先計算 P 的每一個前綴的「次長的相同前綴後綴」,以備不時之需。
failure function 是一個字串函數:輸入字串的其中一個前綴,則輸出該前綴的「次長的相同前綴後綴」。
input: __abzabc__
prefix | border |failure function| implementation
-------|--------|----------------|----------------
Ø | Ø | f(Ø) = Ø |
a | Ø | f(a) = Ø | failure[0] = -1
ab | Ø | f(ab) = Ø | failure[1] = -1
abz | Ø | f(abz) = Ø | failure[2] = -1
abza | a | f(abza) = a | failure[3] = 0
abzab | ab | f(abzab) = ab | failure[4] = 1
abzabc | Ø | f(abzabc) = Ø | failure[5] = -1
- A[0...i] 的「次長的相同前綴後綴」是 A[0...failure[i]]。
- failure[] 值為 -1 時,代表「次長的相同前綴後綴」為空字串。
- __code__
```cpp=
void failure_fuc(string &A)
{
for (int32_t i = 1, j = failure[0] = -1; i < A.size(); ++i)
{
while (j >= 0 && A[i] != A[j + 1])
j = failure[j];
if (A[j + 1] == A[i])
++j;
failure[i] = j;
}
}
```
__了解failure function最好的方法就是自己多試幾遍,試著在紙上寫幾個字串看看。__
- __KMP Algorithm__
終於可以講KMP的主體了。
有了failure function的幫助,我們可以很輕鬆的比對字串。
比對字串與構造failure幾乎一樣,只要遇見不符合的就一直移動。
```cpp=
void string_search(string &A, string &B)
{
failure_fuc(B);
for (int i = 0, j = -1; i < A.size(); ++i)
{
while (j >= 0 && A[i] != B[j + 1])
j = failure[j];
if (B[j + 1] == A[i])
++j;
if (j == B.size() - 1)
{
cout << "B starts at pos" << i - B.size() + 1 << endl;
j = failure[j];
}
}
}
```
下面練習題(cf 1326D2)的一點提示: 想想看failure function的定義、還有迴文顛倒後會有什麼性質?
> 練習題: [cf 1326D2](https://codeforces.com/contest/1326/problem/D2)
>
---
## 六、數論
### 同餘理論
- __模__
$a \mod b = q$, $q$滿足$a = b\times t+q$且$q < b$
_i.e_: $9 \mod 2=1$
- __同餘__
給定一正整數$m$,如果兩整數$a, b$滿足$a - b$可被$m$整除,則稱$a$與$b$對模$m$同餘。
- __模運算性質__
- 反身性: $a\equiv a \pmod m$
- 對稱性: $a\equiv b \pmod m$, 則$b\equiv a \pmod m$
- 傳遞性: 若$a\equiv b \pmod m$, $b\equiv c \pmod m$, 則$a\equiv c \pmod m$
- 同餘式相加/減/乘: 如同一般方程式
- 上述推導得: $a^k\equiv b^k \pmod m$
- 同餘式相除: 唯有定義下列運算,同餘式相除才有意義
$\frac{a}{b} \equiv x \pmod m$, $x$滿足$a \equiv bx \pmod m$
則$x\equiv ab^{-1} \pmod m$
$b^{-1}$為模$m$意義下的 __乘法反元素(inversion)__,或稱 __模逆元__
- $a\equiv b \pmod m$, 假設有一數$c$滿足$c|m$,則$a\equiv b \pmod c$
練習題: [zj c686](https://zerojudge.tw/ShowProblem?problemid=c686)(用到最後一個性質)
---
### 費馬小定理
> 任意質數$p$皆滿足 $a^{p-1}\equiv 1 \pmod p$
---
### 歐拉公式
~此公式為費馬小定理的推廣~
> 若$gcd(a,n)=1$ 則滿足$a^{\varphi(n)} \equiv 1 \pmod n$
!當$gcd(a,n)\neq1$時,另外有公式可以化簡指數(詳情請見intermediate level)
---
### 盧卡斯定理
> 設$n=sp+q, m=tp+r$,而$q,r\leq p$且$p$是質數。
> 有$C^{n}_{m} = C^{sp+q}_{tp+r} \equiv C^{s}_{t}C^{q}_{r} \pmod p$
>
---
### 擴展歐幾里得算法
英文稱exgcd,這大概是資訊中你唯一一個國小就會的東西,本質上就是輾轉相除法。
exgcd可以在求gcd(a, b)的同時求出滿足貝祖等式的x, y
> 貝祖等式: 對於$a, b, m$,$ax + by = m$
> 斐蜀定理: 對於$a, b, m$,$ax + by = m$有解,若且唯若$m=t \times gcd(a, b)$。
>
* 解法
![](https://i.imgur.com/frRrAm9.jpg)
* 實作
```cpp=
int32_t exgcd(int32_t a, int32_t b, int32_t& X, int32_t& Y)
{
if (b == 0)
{
X = 1, Y = 0;
return a;
}
int32_t r = exgcd(b, a % b, Y, X);
Y -= a / b * X;
return r;
}
```
練習題: [zj e320](https://zerojudge.tw/ShowProblem?problemid=e320)(斐蜀定理)
---
### 模逆元
> 已知$a, b$,求$ax \equiv 1 \pmod b$之$x$。
__!模逆元存在的條件是$gcd(a, b) = 1$__
__!只要模逆元存在,即有無限多個__
~可以用貝祖等式證證看~
* __exgcd求模逆元__
* 改寫: $ax + by = 1$
是不是跟上面的甚麼東西一樣阿?
現在你會求模逆元囉。
* 有時候題目會要求正整數模逆元,只要先求出$x$,再寫這行就好:
```cpp=
x = (x % b + b) %b;
```
* __歐拉定理求模逆元__
* 改寫 $a^{\varphi(b)} \equiv 1 \pmod b$ 成 $a \times a^{\varphi(b) - 1} \equiv 1 \pmod b$
$a$之模逆元即為$a^{\varphi(b) - 1}$
可用快速冪實做。
__!求單個模逆元可以用上面兩個方法__
* __遞推式模輸出對於1~p的所有模逆元。 $p \leq 10^6$__
* 遞推式: $i^{-1} = (b - \lfloor\dfrac{b}{i}\rfloor) \times (b \pmod i)^{-1} \pmod b$
* 推導過程:
設$t = \lfloor\dfrac{b}{i}\rfloor$, $k = b \pmod i$
則$t \times i + k \equiv 0 \pmod b$
使$k \equiv -t \times i \pmod b$
兩邊同除$i\times k$
得$i^{-1} \equiv -t\times k^{-1} \pmod b$
成$i^{-1} \equiv (b- \lfloor\dfrac{b}{i}\rfloor)\times (b \pmod i)^{-1} \pmod b$
- __逆元積性:__
> 對於$a, b$模$p$的逆元$a^{-1}, b^{-1}$,有$(ab)^{-1}\equiv a^{-1}b^{-1} \pmod p$
>
練習題: [zj a289](https://zerojudge.tw/ShowProblem?problemid=a289)(模板題),[zj e902](https://zerojudge.tw/ShowProblem?problemid=e902), [cf 696C](https://codeforces.com/contest/696/problem/C)
---
### 中國剩餘定理
> 韓信點兵: 有$n$種數法,每次數$b_i$個人,會剩下$r_i$個人。求最少有幾個人
* __遞推法__
$n$種數法的問題看起來很難,不妨先思考$n=2$的情況。
我們有以下算式
$\left\{ \begin{array}{rcl}
answer=b_1\times x_1+r_1 \\
answer=b_2\times x_2+r_2
\end{array}\right.$
可以整合成
$b_1\times x_1+r_1=b_2\times x_2+r_2$
$=> b_1\times x_1 - b_2 \times x_2 =r_2 - r_1$
這樣就能利用exgcd求出$x_1$和$x_2$進而還原出$answer$
至於$n$種數法就是從$2$一路推廣到$n$就好了,非常簡單
__複雜度__:$O(nlogC)$ ~C是值域~
* __構造法__
設$T = \prod b_i$, $T_i = \dfrac{T}{b_i}$。
設$M_i=$模$b_i$意義下$T_i$的模逆元。
答案即是$\sum M_i*T_i * r_i$
__複雜度__: $O(nlogC)$
__!此作法容易超出整數範圍。__
練習題: [zj d791](https://zerojudge.tw/ShowProblem?problemid=d791), [tioj 1459](https://tioj.ck.tp.edu.tw/problems/1459)
---
## 七、離線算法
> IOI的題目都會強制在線,不過等你當上國手再來擔心吧w
每當寫資料結構題時,有時候會需要寫一顆很複雜的結構,有沒有辦法避免呢?
實際上,操作可以離線做,每次詢問都不需要立即輸出答案,可以先把所有操作全部讀入,再來一次處理輸出答案。
這種想法可以大大避免極為複雜的資料結構。
下面將介紹幾種離線算法。
### 莫隊算法
> 給定一長度為$n$的數列,有$q$個詢問,問區間$l,r$內眾數出現幾次? 有幾個眾數?
> !可以離線
這題目乍看下是要刻一個比較特殊的線段樹,但是真的需要嗎?
首先,我們把問題reduce成
> 給定一長度為$n$的數列,有兩個索引$l,r$,有$q$個操作,使$l$變大或變小1、使$r$變大或變小1,問每次操作
區間$l,r$內眾數出現幾次? 有幾個眾數?
>
這個問題看起來就簡單多了。
設```app[x]```, ```cnt[c]```, ```maxcur```為$x$出現幾次、出現$c$次的有幾種數字、目前的眾數是出現幾次。
每次移動雙指標時,都去維護```app[x], cnt[c], maxcur```,
```cpp=
void sub(int x)
{
--cnt[app[x]];
--app[x];
++cnt[app[x]];
if (cnt[maxcur] == 0)
--maxcur;
}
void add(int x)
{
--cnt[app[x]];
++app[x];
++cnt[app[x]];
maxcur = max(maxcur, app[x]);
}
//~~
while(q--)
{
//~~
if (op == "L l")
add(arr[--l]); //左指標左移
else if (op == "L r")
sub(arr[l++]); //左指標右移
else if (op == "R l")
sub(arr[r--]); //右指標左移
else if (op == "R r")
add(arr[++r]); //右指標右移
cout << maxcur << ' ' << cnt[maxcur] << '\n';
}
```
這樣的複雜度是$O(q)$。完美解決。
但是這個問題跟原始問題比起來,差在一個 __移動是連續的__;一個是 __左界右界亂跳__。
那我們不妨讓原始題目的詢問左右界排序成連續的樣子。
依照左界大小將詢問排序,再依照剛剛的做法去做,但是這樣會有一個問題
> 右界怎麼辦?
如果有很多筆操作,左界都相同,但是右界差距極大,這樣複雜度最差是$O(n^2)$
有沒有一種辦法去同時把左界排序又能維護好右界遞增呢?
有,但是左右界都需要犧牲一點單調性。
利用分塊的思想,將$n$個元素切成$K$塊,接著將所有詢問按照$L_i$所在的區塊排序,如果在同一個區塊,就再對
$R_i$做排序。
- __複雜度分析__:
因為分塊的特性,在同一塊的左界不會移動超過$\frac{N}{K}$次,而左界屬於不同塊的操作,移動次數最多也不會超過N次。所以左界移動複雜度是$O(\frac{NQ}{K})$。
對於右界因為$R_i$是遞增,所以對於同一塊複雜度最多$O(N)$,對不同塊因為交界最多$K$次,每次最多也$O(N)$,所以維護右界複雜度為$O(NK)$。
取$K=\sqrt N$,複雜度為 $O(Q\sqrt N +N \sqrt N)$
- __奇偶優化__:
每一塊的$R_i$都是遞增,因此在遇到交界處時會有一個"大斷層",導致右界必須移動許多次,但是如果反過來變成奇數塊遞增,而偶數塊遞減,就會讓右界的移動接近連續
實做判斷區塊是否為偶數進行遞增或遞減排序。
- __code:__
因為有點長,所以貼連結: [Mo's alogrithm](https://pastebin.com/sLbTNxmP)
練習題: [zj b417](https://zerojudge.tw/ShowProblem?problemid=b417)(模板題),[tioj 1699](https://tioj.ck.tp.edu.tw/problems/1699)
## 八、常數優化
> premature optimization is the root of all evil
* __編譯器優化__
```cpp
#pragma GCC optimize("Ofast,unroll-loops,no-stack-protector,fast-math")
#pragma GCC optimize("O3")
```
---
* __bitset__
1. all 測試此 bitset 中的所有位,以 __判斷它們是否全部都設為true__
2. any 判斷 __任何位元是否設為1__
3. count 回傳位元序列中 __位元1數量_
4. flip 反轉 bitset 中的所有位元值,或在指定位置反轉單一位元
5. none 判斷 __是否沒有已設為 1 的位元__
6. reset 將 bitset 中的所有位元重設為 0,或將指定位置的位元重設為 0
7. set 將 bitset 中的所有位元設為 1,或將指定位置的位元設為 1。
8. size 傳回 bitset 物件中的位元數
9. test 判斷指定位置的 __位元是否為 1__
10. to_string 將 bitset 物件轉換為字串表示
> 選訓時如果我會bitset就能多拿好多分數了......
## 九、計算幾何
計算幾何是競賽中很少出現,但是一旦出現高機率是難題。
通常牽涉到很多數學,_i.e._ 向量
### 基本數學知識
請至少具備高中的數學知識而且擁有把圖形座標化的技巧
外積、斜率、內積、行列式等等...
### 極角排序
![](https://i.imgur.com/5tB3LAp.png)
- __實作:__
- atan2()
```cpp=
bool cmp(point& a,point& b)
{
return atan2(p1.y, p1.x) < atan2(p2.y, p2.x);
}
```
- 判斷象限後用叉積判斷順序,極角相同再用長度判斷(視需求而定)
```cpp=
double cross(Point& a, Point& b)
{
return a.x * b.y - a.y * b.x;
}
bool cmp(Point& a, Point& b)
{
if (a.y == 0 && b.y == 0 && a.x * b.x <= 0) return a.x > b.x;
if (a.y == 0 && a.x >= 0 && b.y != 0) return true;
if (b.y == 0 && b.x >= 0 && a.y != 0) return false;
if (a.y * b.y < 0) return a.y > b.y;
double c = cross(a, b);
return c > 0 || c == 0 && fabs(a.x) < fabs(b.x);
}
```
atan2()有精度問題,cross版寫起來又繁瑣。
### 鞋帶公式
> 給定一多邊形的頂點座標$(x_1, y_1), (x_2, y_2)...$,求此多邊形面積?
一個很簡單的公式: $S = \frac{1}{2}|\sum_{i=1}^{n} x_iy_{i+1}-y_ix_{i+1}|$
其中$(x_{n+1}, y_{n+1})=(x_1,y_1)$
![](https://pic2.zhimg.com/80/v2-d9161fbb47bf548647468cce479ab6a1_720w.jpg =200x200)
!注意這個公式必須先讓頂點座標呈 __順時針或逆時針順序__,且無論凸的多邊形或是凹的多邊形都可以用這個公式。
### 凸包
> 給定點集$S$,求能夠包覆住所有點的最小凸多邊形。
> 在這裡「凸」的定義為,多邊形內任意兩點連線不經過圖形外部。
>
- __Andrew's Monotone Chain__
如果我們觀察下凸包會發現他的斜率是遞增的,所以我們可以先將點集$S$按$x$排序,$y$為第二比較。
最左邊必然為凸包上一點,所以可以先將它加入一個容器。
我們現在要維護這個容器裡的點之斜率單調性,也就是說$slope(a_i,a_{i+1})<slope(a_{i+1},a_{i+2})$。
每次加入一個點,若剛剛的點不滿足上面的條件,就一直pop這個容器的head,
__直到上述條件滿足為止__。
看圖比較好理解。
__初始狀態__
![初始狀態](https://i.imgur.com/xAt91Xs.png =300x)
__檢查第三點的正確性__: $slope(1,2)<slope(2,3)$
![](https://i.imgur.com/QU8f1Ac.png =300x)
__檢查第四點__: 發現$slope(2,3)>slope(3,4)$,不符合維護的性質,將第三點pop掉。
![](https://i.imgur.com/o9EN6vq.png =300x)
__檢查第五點__: $slope(2,4)<slope(4,5)$
![](https://i.imgur.com/aRoxqo5.png =300x)
__檢查第六點__: 發現$slope(4,5)>slope(5,6)$,將第五點pop掉。同樣,$slope(2,4)>slope(4,6)$,將第四點pop掉。
![](https://i.imgur.com/xXi4eOn.png =300x)
這樣我們就找到了下凸包。
上凸包一樣可以用此策略去判斷,但須稍做修改,例如斜率單調的判斷,細節留給你們思考。
> 確定template有沒有寫對時,可以構造一個四個點的正方形當簡單的測資
練習題: [luogu P2742](https://www.luogu.com.cn/problem/P2742)
### 最近點對
> 給定點集$S$,求$S$中最近的兩個點
- 暴力法
任意的窮舉兩點
- 複雜度: $O(n^2)$
- 再優化
先讓點依照x軸排序,這樣就可以剪枝
- 最差複雜度: $O(n^2)$
在點是隨機的情況下,我們可以相信,每一次枚舉第二點,期望上都可以排除$\dfrac{n}{2}$個第一點的選擇。
因此我們有 __平均複雜度:$O(nlogn)$__
實際比賽上,這種作法通常會有特殊測資讓你TLE,不過我們可以先random旋轉整張圖片,讓這個作法也能AC。
- 分治法 (更詳細可以參考[演算法筆記](http://www.csie.ntnu.edu.tw/~u91029/Point2.html#3))
我們可以畫一條把整張圖切成兩半,然後把答案分成「最近點對在左側」、「最近點對在右側」、「最近點對橫跨兩側」三種情形。先將左側與右側分別遞迴求解之後,再利用左側與右側的答案,快速算出橫跨兩側的答案。
- __Preprocessing__:
一、排序所有點,以$X$座標為主,$Y$座標無所謂。$O(NlogN)$
二、檢查是否有共點。如果有,就找出所有共點,演算法結束。$O(N)$
- __Divide__:
把所有點分為左右兩側,左側、右側的點數盡量一樣多。
- __Conquer__:
左側、右側分別遞迴求解。
- __Combine__:
利用左側、右側的最佳解$d$,求出橫跨兩側的解:
一、首先找出靠近中線的點,與中線的距離$\leq d$。$O(N)$
(小於d、不包含d,則只找出其中一組最近點對。)
二、排序這些點,Y座標為主,X座標為輔。O(NlogN)
三、每一個點都找其上方鄰近的點,看看能不能形成最近點對。
只需檢查至後三點。O(N⋅3) = O(N)
答案為左側、右側、橫跨兩側,三者當中的最佳解。
至於為何是檢查後三點呢?就留給讀者自行思考
### 最遠點對
> 給定點集$S$,求$S$中最遠的兩個點
- 旋轉卡尺
很顯然的,最遠點對一定在凸包上,然後呢?
![](https://i.imgur.com/QHYqV5d.png)
如果距離A點最遠的點是D,那顯而易見的,距離B最遠的點就會是D,E或F。
你可以把最遠點對的選擇想像成一個two pointers然後,他們會順時針或逆時針的轉。
那我們就可以枚舉其中一個pointer,然後另一個pointer只要跟著轉就可以在O(n)下找到凸包的最遠點對。
> 另一個pointer要不要轉?
最簡單的作法就是直接算出距離來決定要不要轉。但這樣常數有點大。
所以可以用向量的作法
這很好想,如果D到A的距離大於E到A,那依照底乘以高這個公式可以觀察到$\Delta ABD$的面積大於$\Delta ABE$。
因此只要用向量算兩三角形的面積就能決定要不要轉了。
---
## 寫完心得
在這裡感謝ioic的講義,以及所有我問過的大神、找過資料的blog、OI wiki、還有一堆題目的出題者。
一開始僅有的第一行我還記得一清二楚,"嘉中資訊講義",一直到這裡已經增加到1370行了。(4.14.20更新: 1534行) (4.15.20更新: 1688行)
希望後人們都可以靠這份講義上1!、2!、甚至國手,__looking forward to the day.__