2019清北學堂學習筆記

暴力求解法

迭代加深搜

適用於搜索樹深度不肯定的時候,可使用迭代加深搜。算法

步驟:

1.枚舉maxd表示最深枚舉深度;
2.假設當前深度爲g(n),樂觀估計至少要h(n)層才能到達葉子節點,那麼g(n)+h(n)>maxd時,就應該剪枝。

在我理解看來,樂觀估計的意思是說不去管全部的限制,而後去計算當前點到終點的距離或者所須要的操做數量,即還須要h(n)層,這時候的h(n)纔是最好的,即最小(大)的,能夠利用來剪枝。A*就是把狀態樂觀估計還要h(n)層纔到達的想法使用到bfs上面。數組

雙向搜索

對普通搜索的改進,使用的是BFS,大致流程就是從起點開始向終點搜,終點開始向起點搜,而後就直到第一個搜索碰到第二個或者是第二個碰到了第一個,而後答案就是兩個搜索步數的和再減去1。比較典型的例題有魔板,然而那個題目一共3個變換方式,還能夠不用雙向搜索。數據結構

分治算法

數列上的分治

顧名思義,數列上的分治就是平常用的分治,最經典的例題就是求逆序對數量。使用的算法是歸併排序,比較兩個指針所對應的數字,若是左邊的大於右邊的,那麼就說明,當前左邊的指針到mid的全部數字都是大於右邊指針所指的數字,那麼答案就應該加上mid-l+1。less

CDQ分治

能夠代替數據結構的利器,能夠解決的問題是區間加和單點修改。
\(\color{red}{流程:}\)
1. 按全部的按全部的操做等分分紅先後兩部分
2. 處理前面部分的、後面部分各自的修改和查詢
3. 處理前面部分修改對後面部分查詢的貢獻(動態查詢變成靜態查詢)
聽老師說很好打,並且用處也很大,這個CDQ分治處理的問題須要知足離線操做和修改與修改之間互不影響。由於是把動態詢問變成了靜態詢問,因此還能夠經過分治把某些排序能夠作好,更好地進行靜態查詢。函數

樹分治

樹的重心是指以該點爲根時,最大子樹節點數最小。子樹節點數都<=n/2,層數也是有logn層,每一層均可以在O(n)的時間內處理問題。學習

貪心算法題目選講

貪心,個人理解就是就當前狀態來看,所選取的最優決策,可是不必定會是全局最優決策,因此不少題目看似是貪心,可是貪心並不正確,只能看直覺,畢竟證實很麻煩。典型例題是鋪設道路(積木大賽),就是在當前點時,對比上一個點,若是比上一點大,那麼加上多出的一部分,反之,就是continue,而後更新last爲當前點的大小,last初值爲0.優化

簡單數學

歐幾里得算法

能夠求出變量a,b的最大公約數。spa

\(gcd\)代碼:

int gcd(int a, int b){
return b == 0? a : gcd(b, a % b);
}

擴展歐幾里得算法

能夠求解相似於\(ax+by=c\)的不定方程,有整數解的條件是\(gcd(a,b)|c\),便是在說\(gcd(a,b)\)必須是\(c\)的因子。設計

\(exgcd\)代碼:

void gcd(int a, int b, int &g, int &x, int &y){
if (!b) {g = a; x = 1; y = 0;}
else{gcd(b, a % b, d, y, x); y -= x * (a / b);}
}

式子推導以下:
\(ax+by=gcd(a,b)\)
\(b \times x+(a- a/b \times b) \times y=gcd\)
\(y \times a+(x-y\times a/b)\times b = gcd\)指針

線性篩素數

學習過三種篩法,第一種,也是最耗費時間的就是,從2開始向n-1枚舉,對其稍加優化就是枚舉到 ,可是根本不會知足需求。因而就有了埃式篩,時間複雜度能夠作到O(nlognlogn),能夠說是很接近線性了,可是又不是真正的線性篩,因此還須要優化。歐拉篩成功的作到了線性篩的要求,在O(n)的時間內篩出素數。

埃式篩代碼:

for(int i=2;i<=t;i++)
if(prime[i])
for(int j=2 * i;j<MAXN , j += i)
        prime[j]=false;

歐拉篩代碼:

for(int i=2; i<=n; i++){
    if(!vis[i]) prime[cnt++]=i; 
    for(int j = 0; j < cnt && i * prime[j] <= n; j++){ 
        vis[i * prime[j]]=prime[j]; 
        if(i % prime[j] == 0) break;
    } 
}

逆元

我理解的逆元就是一個數在模p的意義下的倒數。如今也有了多種求解逆元的方法,好比費馬小定理,遞推式求逆元,擴展歐幾里得求逆元,固然用的最多的莫過於遞推式,由於很快的就能求出。
組合數
有一個公式 ,而後是就是圍繞着它進行推導,好比 等等。

數據結構

堆有兩種,一個是大根堆,另外一個是小根堆;一個是less,另外一個是greater。
頭文件是

#include <queue>
priority_queue<int>q;

支持的操做有:
插入,時間複雜度爲O(logn);
查詢,時間複雜度爲O(1);
刪除,時間複雜度爲O(logn).
若是卡常數的話可使用make_heap之類的。

二叉搜索樹

樹高指望 \(logn\)
操做:插入,刪除,查詢
例如STL中的set,map

線段樹

線段樹能夠維護區間信息的數據結構,每個節點都對應着一個序列的區間。
能夠支持單點修改或者是查詢。

動態開點

合併 複雜度就是兩顆線段樹重合線段樹重合節點個數

tree *merge(int l, int r, tree *A, tree *B){
if(A == NULL) return B; 
if(B == NULL) return A; 
if(l == r) return new tree(NULL, NULL, A -> data + B -> data); 
int mid = (l + r) >> 1; 
return new tree(merge(l, mid, A -> ls, B -> ls),merge(mid + 1, r, A -> rs, B -> rs), A -> data + B -> data); 
}

樹狀數組

一個十分好用的數據結構,其重點就是lowbit函數。只能維護知足可減性的量,例如和,異或和,而與,或不能用樹狀數組維護。線段樹包括了樹狀數組,可是樹狀數組常數小。

二維樹狀數組

與一維樹狀數組相似,只是多了一層循環。令s[x][y]表示x-lowbit(x)+1<=i<=x, y-lowbit(y)+1<=j<=y的a[i][j]的和

並查集

按秩合併+路徑壓縮能夠證實複雜度是對的,可是也能夠只寫路徑壓縮,是卡不掉的。

例題就是洛谷P1525關押罪犯。第一個作法是二分+二分圖匹配,第二個作法是並查集。

Trie

代碼:

//trie樹,字母集
int rt=1,cnt=1;
int ch[N][26];
void insert(strint s) {
        int o=rt;
for(re int i = 0 ; i <s.size;++i) {
int k=s[i]-’a’;
if(ch[o][k]) o=ch[o][k];
else ++cnt,o=ch[o][k]=cnt;
}
sz[o]++;
}
int query(string s) {
int o=rt;
for(re int i = 0 ; i <s.size ; ++ i ) {
int k=s[i]-’a’;
if(ch[o][k]) o=ch[o][k];
else return 0;
}
return size[o];
}

Trie樹仍是蠻重要的,數也能夠上trie樹,可是須要變成二進制,這樣的話在一個序列中尋求兩個數的異或最大值就變得很容易求出來。好比說是異或最大值,咱們讓每個數字都拆分爲二進制,而後上trie樹,老是選取1,若是沒有1再去選取0,這樣最後出來的數字就是最大的異或值。原理就是1000,老是會比0100好,更加優,因此一直選取1,沒有1再取0.

Hash

19260817是一個經常使用的mod數,素數。

Hash碰撞

原本不相同的兩個數能夠在某一個數的時候會相同,被稱爲hash碰撞。爲了不可使用map映射或者雙模數。還有一個方法就是天然溢出,天然溢出加上雙hash還會更好,不容易被卡。

RMQ問題

對於長度爲n的數列A,回答若干詢問RMQ(A,i,j)(i,j<=n),返回數列A中下標在[i,j]裏的最小(大)值,也就是說,RMQ問題是指求區間最值的問題。
St表
令f[i][j]表示從i開始,2^j個數的最小值
f[i][j]總共有nlogn個狀態
能夠由j從小到大遞推求出
靜態查詢最好使用ST表,這樣的複雜度是最優的。
遞推式爲

f[i][j]=min(f[i][j-1],f[i+(1<<(j-1))][j-1]);

求LCA

1.倍增求LCA

\(\color{red}{步驟:}\)

1.先使兩個結點跳到同一深度。
2.若是兩個結點相等了,那直接返回這個結點
3.不然兩個結點一塊兒跳到LCA的子結點位置
4.返回這個位置的父親,即爲LCA

2. 轉換爲RMQ問題

\(\color{red}{步驟:}\)

1.求出樹的dfs序(每一個結點進入和每次回溯到的時候都記錄)
2.記錄每一個點第一次進入時在dfs序列中的位置pos[u]
3.兩個點u,v的LCA即爲dfs序中[pos[u],pos[v]](不妨設pos[u]<=pos[v])中dep最小的結點

3. Tarjan求LCA

\(\color{red}{步驟:}\)
1.記錄每一個點的詢問
2.dfs回溯時用並查集合並(將子結點集合併到父節點集合,以父節點爲根)
3.查詢lca(u,v)時假設此時u已訪問,v剛訪問到,那麼u的並查集的根即爲答案

最小生成樹

Kruskal

貪心方法:將邊按照邊權排序,每一次都只是拿取邊權最小的邊,看它鏈接的兩個點是否在一個連通塊中,這裏會用到並查集來維護,若是已經聯通了,那麼就說明會有更小的邊已經連了;反之就加入,而且把兩個點聯通起來。

最短路徑

Dijkstra算法

支持單源最短路,可使用堆優化,複雜度爲O(mlogm),可是不能夠去判負環。

SPFA算法

該算法能夠判負環,可是平常仍是不要用,由於複雜度是錯誤的。

判負環的方法:

1.用len[u]表示源點到u的最短路中有幾條邊。
2.在更新最短路長度的時候順便求出。
3.顯然若是沒有負環,len [u]<n
4.每一次頂點入隊時都要進行check,若是len[u]>=n,則負環存在

Floyed算法

適用於多源最短路,直接三重循環枚舉,利用中轉站來使得兩個點的距離變小。因爲比較暴力並且複雜度是O(n^3)因此通常也不會用到。可是該算法也是一種思想,也會用到其餘的題目。

建圖技巧

經過添加虛擬點等手段將問題轉化,從而達到減小邊數等目的。

拓撲排序

給定一張有向圖,要求輸出一個序列,輸出是要知足:若是圖中u到v有一張有一條有向邊,那麼序列v在u的後面。若是該序列存在的話,那麼就說明必定沒有環,由於若是有環的話就不會有入度爲0的邊。

連通份量

在圖中:頂點之間有路徑必定互相到達。能夠用並查集維護一個連通份量。
可使用Tarjan算法在O(n+m)的時間內求出。

Tarjan算法

記錄兩個數組,一個表示沒個點被訪問的時間,另外一個記錄搜索樹子樹的點能訪問的點DFN的最小值。

縮點

在圖中,因爲有強連通份量,而且在強連通份量中的點也均可以很容易的到達,因此咱們能夠把強連通份量看做爲一個點,這樣會比較好處理。

差分約束

直觀的看,差分約束就是幾個不等式,經過不斷的相加來得到一個最終的形如x+y<=a的形式,a是一個常數,來得到最大值。咱們想要求的最大值,能夠經過圖論跑最短路來求的。實際上就是x到y的最短路。
\(\color{red}{步驟:}\)
1.將形式轉化爲xi-xj<=ci的形式
2.對於每個不等式都是從xj向xi來那一條長爲ci的邊
3.求出s到t的最短路
若是圖中有負環,那麼就不存在解;
S到t沒有約束,即不能到達,因此是無限大。

環套樹/基環樹

其實就是樹上加上了一條邊。
步驟:
1.隨意找一個點開始當成樹進行dfs,並記錄每一個結點訪問的時間戳dfn
2.dfs的過程當中必定會有一個點往dfn比本身小的點連了邊,那麼這條邊能夠當作加上的那條。記錄下這條邊(u,v)
3.暴力讓u和v往上爬到根,記錄他們分別通過的點。
4.深度最大的他們都通過的點爲他們的lca,u->lca之間的全部點+v->lca之間的全部點即構成環。

歐拉圖

經過圖中全部邊一次且僅一次行遍全部頂點的通路稱爲歐拉通路。
經過圖中全部邊一次且僅一次行遍全部頂點的迴路稱爲歐拉回路。
具備歐拉回路的圖稱爲歐拉圖。
具備歐拉通路的圖稱爲半歐拉圖。

圈套圈算法

dfs搜索,不能再往下走(不能重複使用一條邊,但能夠重複通過一個點)便回溯,回溯時記錄路徑,回溯時不清除對邊的標記,最後求出來的路徑就是歐拉回路。

DP

狀態與記憶化搜索

狀態的本質就是問題的子問題。不管是搜索仍是DP都須要設計問題的狀態,設計出了狀態,就能夠找出狀態與狀態之間的關係,而後再進行推導,狀態之間的轉移就是狀態之間的聯繫,通常地DP是遞推式的形式。狀態須要保證無後效性和最優子結構。
動態規劃和記憶化搜索都作到了每個狀態只會計算一遍,去掉了冗餘計算。記憶化搜索也是搜索的一個有效的剪枝方法,複雜度多半是多項式級別的。記憶化搜索的代碼比DP的代碼更好理解,代碼實現也容易。

線性DP

最基礎的DP就是線性DP了吧,通常的話都會是一個for循環求出答案,複雜度通常都是O(n)左右的。學習好線性DP有助於對DP的理解,仍是蠻重要的。

多維DP

樹形動態規劃

樹形動態規劃是在樹上,根據樹的拓撲性進行動態規劃。咱們依舊可使用原來的思路來解決。

揹包動態規劃

揹包DP主要包含三種問題,01揹包,徹底揹包,多重揹包。01揹包是每個物品只能選擇一件,而且是倒着枚舉;而徹底揹包是能夠選擇無限件,是正着枚舉的;多重揹包是每個物品有多件,能夠隨便選擇多件。

P4394 選舉

按照席位數量從大到小排序,而後再去維護一個01揹包。由於題目要知足任意政黨退出時,其餘政黨的席位不得超過一半,因此須要枚舉j>sum/2,j-a[i]<=sum/2。

數位DP

數位DP能夠解決區間內求知足必定條件的數的個數。咱們這裏運用了前綴和的思想,在詢問[l,r]這個區間時,若是直接枚舉是會超時的,因此咱們能夠用[0,r]-[0,l-1]來得到[l,r]區間的知足條件的數的個數。

狀壓DP

狀壓DP,在題目中,咱們定義某些狀態是很繁瑣的,因此須要進行簡單化,咱們能夠用幾個簡單的數字來進行表示狀態,最形象的莫過於二進制數字表示狀態。例如在八數碼這個題中,咱們可使用到這個思想,來使得狀態好表示,原本是一個3*3的矩陣,咱們能夠用字符串來表示狀態,來進行變換。

DP的優化

關於DP的優化,咱們最常使用的就是單調隊列優化,單調隊列能夠支持插入一個元素和查詢當前隊列中的最大值。單調隊列優化應用的DP方程多半是f[i]=max(f[l]...f[r])+a[i].咱們就能夠預處理出f[l]~f[r]中的最大值。

記錄:n(1+1/2+1/3+...+1/n)=ln(n)

相關文章
相關標籤/搜索