编年史:OI算法总结

目录(按字典序)

A

——A*

D

——DFS找环

J

——基环树

S

——数位动规
——树形动规

T

——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)


转载于:https://www.cnblogs.com/akura/p/10908522.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值