編年史:OI算法總結

目錄(按字典序)

A

——A*

D

——DFS找環

J

——基環樹

S

——數位動規

——樹形動規

###Tnode

——Tarjan(e-DCC)

——Tarjan(LCA)

——Tarjan(SCC)

——Tarjan(v-DCC)


A*

用處

當你在作搜索題時,發現各類剪枝的效果都不怎麼好,那也就意味着你在搜索時將遇到一棵龐大的搜索樹。根據廣度優先搜索的性質,當第一次搜索到答案時就一定是最優解,因此在求解最優解一類的問題時咱們惟一的策略就是讓程序快點搜到答案,也就是儘量往靠近答案的地方搜索。這裏就要用到A*

思想

"將來預估"。先得出隊列中全部狀態的預估值,也就是離答案還有多遠。而後用優先隊列維護出從起始狀態到當前狀態的值+預估值最小的狀態,優先從它開始拓展。

算法流程

設計出預估函數。預估函數的要求是:預估值≤實際值。而後廣度優先搜索,當算出當前狀態的代價時,計算出它的預估值,把代價+預估值的值加入優先隊列。反覆拓展直到搜出答案。

核心代碼

inline int f(......){ ......; return; }//本身設計的預估函數

priority_queue< node > q;//本身設計出存儲狀態和優先級的結構體
inline int bfs(){
    q.push(初始state);
    while(q.size()){
        node now=q.front(); q.pop();
        if(now==ans) return ......;
        if(vis[now.(......)]) continue;
        for(......){
            ......(得出新狀態);
            q.push(node(新狀態,新狀態的代價+預估值));
        }
    }
}

時間複雜度爲O(搜索分叉數^搜索樹規模),但實際遠遠達不到這個程度


DFS找環

用處

當你作基環樹的題時好不容易有了思路,卻發現不會找環就很尷尬。這時候能夠用到DFS找環的方法

思想

有向圖

相似於Tarjan求強連通份量的方法,只不過不須要用到時間戳和追溯值之類的高級東西。

無向圖

相似於Tarjan求點雙連通份量的方法,要用到時間戳,但不須要追溯值。

算法流程

有向圖

維護一個棧,把遍歷到的點入棧。當發現下一個點被遍歷過且這個點在棧中,那麼找到了環,不斷彈出棧頂直到下一個點出棧爲止,出棧的點共同構成了一個環。不然往下一個點遍歷。最後回溯時把當前點出棧。

無向圖

當遍歷到一個點u時,給這個點打上時間戳。遍歷過程當中記錄下每一個點在搜索樹上的父親。當發現下一個點v被遍歷過,而且dfn[u]<dfn[v],那麼先把v加入環中,而後不斷把fa[v]加入環中並讓v成爲fa[v],直到u也被加入環中。

核心代碼

有向圖

int stack[maxn],top;
bool vis[maxn],instack[maxn];
void dfs(int u){
    vis[u]=instack[u]=true,stack[++top]=u;
    for(register int i=head[u];~i;i=e[i].next){
        int v=e[i].to;
        if(!vis[v]) dfs(v);
        else if(instack[v]){
            int w,t=top;
            do{
                w=stack[t--],instack[w]=false;
                ......(有關環的操做);
            }while(w!=v);
        }
    }
    instack[u]=false,top--;
}

無向圖

void dfs(int u){
    dfn[u]=++tot;
    for(register int i=head[u];~i;i=e[i].next){
        int v=e[i].to;
        if(v==fa[u]) continue;
        if(!dfn[v]) fa[v]=u,dfs(v);
        else if(dfn[u]<dfn[v]){
            ......(環中關於v的操做)
            do{
                ......(環中關於fa[v]的操做);
                v=fa[v];
            }while(v!=u);
        }
    }
}

時間複雜度都是O(N)


基環樹

用處

若是你把樹上的一些問題作得很熟練了,請不要狂妄。由於若是給樹加上一條邊,題目的算法並無變,但難度確噌噌上去了。此時就要用到基環樹的一些作法和性質來求解

思想

作基環樹的題通常會先求解斷開環上全部邊以後每棵子樹的答案,再加上環上部分。

核心代碼

基環樹的題很靈活,隨機應變吧~

時間複雜度爲O(N(找環) + 處理子樹的複雜度 + 處理環的複雜度)


數位動規

用處

解決關於數的統計類的問題。通常狀況下題目中有「求'一段區間內'知足'某個性質'的數的個數」時,就能夠用數位動規來解。

思想

遞推或者記憶化搜索,經過低位來更新高位。

核心代碼:

int bit[];//原數
dfs(int len,state,bool havelim){//分別爲:當前在第幾位,前幾位的狀態,當前是否有限制
    if(len==0) return (是否知足條件);
    if(!havelim&&dp[len][state]) return dp[len][state];//記憶化
    int lim=havelim?bit[len]:9; long long cnt=0;//多少個知足條件的數
    for(register int i=0;i<=lim;i++){
        if(不知足條件) continue;
        cnt+=dfs(len-1,next state,havelim&&i==lim);
    }
    return havelim?cnt:dp[len][state]=cnt;//記憶化
}

時間複雜度爲O(狀態數 * 轉移複雜度)


樹形動規

用處

當一棵樹上不存在會本身改變的變量時,這棵樹上一般會存在最優值的傳遞性。這時對於大部分求最優解的樹上問題咱們均可以用樹形動規來解。

思想

經過兒子轉移當前點,或者經過父親轉移當前點,亦或是兩者都用到。具體由遞歸實現。

核心代碼:

void dfs(int u,int pre){
    for(register int i=head[u];~i;i=e[i].next){
        int v=e[i].to;
        if(v==pre) continue;
        ......(由u轉移到v);
        dfs(v,u);
        ......(由v轉移到u);
    }
}

時間複雜度爲O(狀態數 * 轉移複雜度)


Tarjan(e-DCC)

用處

求出無向圖的邊雙連通份量,若是分析得出「一個邊雙以內信息相同」之類的結論,那麼能夠求出邊雙連通份量以後縮點,從而把無向圖上的問題轉化爲樹上的問題,達到消除後效性的目的。

思想

先任意求出圖的搜索樹,而後經過時間戳dfn和追溯值low來判斷是否構成一個邊雙。

算法流程

創建一個棧,把遍歷到的點加入棧中,並初始化low值等於dfn值。而後考慮當前節點的子節點:

若是沒去過,先往下遍歷v,那麼low[u]=min(low[u],low[v])。

不然low[u]=min(low[u],dfn[v])。

最後若是發現low[u]=dfn[u],那麼找到了一個邊雙,此時不斷地將棧頂元素出棧,直到u也出棧爲止。這一次出棧的全部點共同構成一個邊雙。

核心代碼

int low[maxn],dfn[maxn],tot;
int stack[maxn],top;
void tarjan(int u){
    dfn[u]=low[u]=++tot,stack[++top]=u;
    for(register int i=head[u];~i;i=e[i].next){
        int v=e[i].to;
        if(!dfn[v]) tarjan(v),low[u]=min(low[u],low[v]);
        else low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u]){
        int v; cnt++;
        do{ v=stack[top--],......(關於邊雙的操做); }while(v!=u);
    }
}
//若是題目不保證圖連通,那麼在main函數中寫這句話:
	for(register int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);

時間複雜度爲O(N+M)


Tarjan(LCA)

用處

若是題目數據卡log的算法,而且詢問存得下,那麼就能夠存下詢問而後用Tarjan離線處理。

優缺點

優勢:時間複雜度爲O(N+M),不須要預處理,是最快的求LCA的算法。

缺點:不靈活,處理多批LCA會致使代碼難度上升。

思想

運用並查集,經過回溯的時候更新的祖先來求LCA。

算法流程

先用鄰接表存下詢問。創建一個並查集,先dfs遍歷到底層,再回溯,並把當前點與回溯後的點合併到一個並查集中去。每到一個點u時遍歷詢問的鄰接表,看是否有與它相連的詢問而且詢問的另外一個v點已經訪問過。若是是,那麼這個詢問的LCA就是v在並查集中的祖先。

int fa[maxn];
bool vis[maxn];
int get(int x){ return x==fa[x]?x:fa[x]=get(fa[x]); }
void tarjan(int u,int pre){
    vis[u]=true;
    for(register int i=g.head[u];~i;i=g.e[i].next){
        v=g.e[i].to;
        if(v!=pre) tarjan(v,u),fa[v]=u;
    }
    for(register int i=q.head[u];~i;i=q.e[i].next){
        int v=q.e[i].to;
        if(vis[v]) q[i].lca=q[i^1].lca=get(v);
    }
}

//main函數中
	for(register int i=1;i<=n;i++) fa[i]=i;
	tarjan(1,1);

時間複雜度爲O(N+M)


Tarjan(SCC)

用處

求出有向圖的強連通份量,若是題目中說明了「一個強連通份量以內信息相同」之類的句子,那麼能夠求出強連通份量以後縮點,從而把有向圖上的問題轉化爲DAG上的問題,達到簡化問題的目的。

思想

先任意求出圖的搜索樹,而後經過時間戳dfn和追溯值low來判斷是否構成一個強連通份量。

兩者的一些歸納:

dfn:被遍歷到的順序號。

low:能夠從搜索樹的子節點追溯到的最小的時間戳。

算法流程

創建一個棧,把遍歷到的點加入棧中,並初始化low值等於dfn值。而後考慮當前節點的子節點:

若是子節點不在棧中,先往下遍歷v,那麼low[u]=min(low[u],low[v])。

不然low[u]=min(low[u],dfn[v])。

最後若是發現low[u]=dfn[u],那麼找到了一個強連通份量,此時不斷地將棧頂元素出棧,直到u也出棧爲止。這一次出棧的全部點共同構成一個強連通份量。

核心代碼

int low[maxn],dfn[maxn],tot;
void tarjan(int u){
    dfn[u]=low[u]=++tot;
    stack[++top]=u,in_stack[u]=true;
    for(register int i=head[u];~i;i=e[i].next){
        int v=e[i].to;
        if(!dfn[v]) tarjan(v),low[u]=min(low[u],low[v]);
        else if(in_stack[v]) low[u]=min(low[u],dfn[v]);
    }
    if(low[u]==dfn[u]){
        int v; cnt++;//強連通份量個數+1
        do{
            v=stack[top--],in_stack[v]=false;
            ......(關於強連通份量的操做);
        }while(v!=u);
    }
}
//若是題目不保證圖連通,那麼在main函數中寫這句話:
	for(register int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);

時間複雜度爲O(N+M)


Tarjan(v-DCC)

用處

求出無向圖的點雙連通份量,若是分析得出「一個點雙以內信息相同」之類的結論,那麼能夠求出點雙連通份量以後縮點,從而把無向圖上的問題轉化爲樹上的問題,達到簡化問題的目的。

思想

先任意求出圖的搜索樹,而後經過時間戳dfn和追溯值low來判斷是否構成一個點雙。

算法流程

創建一個棧,把遍歷到的點加入棧中,並初始化low值等於dfn值。而後考慮當前節點的子節點:

若是沒去過,先往下遍歷v,那麼low[u]=min(low[u],low[v])。

不然low[u]=min(low[u],dfn[v])。

遍歷子節點v回溯以後若是發現當前節點u爲割點,那麼找到了一個點雙,此時不斷地將棧頂元素出棧,直到v也出棧爲止。這一次出棧的全部點加上u共同構成一個點雙。

核心代碼

int low[maxn],dfn[maxn],tot;
int stack[maxn],top;
void tarjan(int u){
    dfn[u]=low[u]=++tot,stack[++top]=u;
    for(register int i=head[u];~i;i=e[i].next){
        int v=e[i].to;
        if(!dfn[v]){
            tarjan(v),low[u]=min(low[u],low[v]);
            if(dfn[u]<=low[v]){//u爲割點
                int w; cnt++;
                do{ w=stack[top--],......(關於點雙的操做); }while(w!=v);
                ......(把u也處理進點雙內);
            }
        }
        else low[u]=min(low[u],dfn[v]);
    }
}
//若是題目不保證圖連通,那麼在main函數中寫這句話:
	for(register int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);

時間複雜度爲O(N+M)

相關文章
相關標籤/搜索