一、求最近公共祖先结点(LCA)

1. 定义

LCA(Lowest Common Ancestors),即最近公共祖先,是指在有根树中找出某两个结点u和v最近的公共祖先结点。

2. 解析

这里只说一种我个人比较喜欢的在线算法:欧拉序 + RMQ

  • 时间复杂度:预处理 O(n+nlgn),查询只需要O(1)

  • 原理:
    欧拉序 就是在dfs遍历一棵树时经过的结点序列(包括回溯时经过的结点),显然两个结点u、v的最近公共祖先就是 欧拉序中 区间[u, v] 中深度最小的结点。(其中u、v指的是u、v在欧拉序中的第一次出现,即递归过程中的记录)
    因此我们只需要通过一遍dfs得到欧拉序euler[2 * maxn]、对应的结点深度dep[2 * maxn],以及每个结点在欧拉序中的位置 pos[maxn],就可以通过RMQ在O(1)实现内找到欧拉序中 区间[pos[u], pos[v]] 中深度最小的结点位置。

  • 需要维护的结构:

    • euler[2 * maxn]:欧拉序
    • dep[2 * maxn]:欧拉序中结点对应的深度
    • pos[maxn]:每个结点在欧拉序中第一次出现的位置(对应于进入递归的时候)
    • cnt:欧拉序长度
    • dp[2 * maxn][log_2maxn = 25]:对欧拉序根据dep进行RMQ预处理,dp[i][j]表示区间[i,i+2^j)中深度最小的 结点下标
  • 为了防止混乱,规定RMQ存储、询问的是区间中深度最浅的 结点下标值,并且也是通过 结点下标区间 进行询问。之后只需要利用euler数组即可映射到对应的点。另外,区间表示为 左闭右开

3. 模板

int n, m, s;
vector<int> G[maxn];
int cnt, euler[2 * maxn], dep[2 * maxn], pos[maxn];
int dp[2 * maxn][log_2maxn];

void dfs(int u, int fa, int d) {
    pos[u] = cnt, euler[cnt] = u, dep[cnt] = d, cnt ++;
    for(int v : G[u]) if(v != fa) {
        dfs(v, u, d + 1);
        euler[cnt] = u, dep[cnt] = d, cnt ++;
    }
}

void init_RMQ() {
    for(int i = 0; i < cnt; i ++) dp[i][0] = i;  // 最短的区间
    for(int j = 1; (1 << j) <= cnt; j ++) {  // 注意先计算小区间,所以先从小到大遍历j
        for(int i = 0; i + (1 << j) - 1 < cnt; i ++) {
            int idx1 = dp[i][j - 1], idx2 = dp[i + (1 << (j-1))][j - 1];
            dp[i][j] = (dep[idx1] < dep[idx2] ? idx1 : idx2);  // RMQ中存放的是深度最小结点的下标值
        }
    }
}

int min_RMQ(int l, int r) {
    int k = log2(r - l);
    int idx1 = dp[l][k], idx2 = dp[r - (1 << k)][k];
    return dep[idx1] < dep[idx2] ? idx1 : idx2;
}

int main() {
    ......
    dfs(s, -1, 0);  // 求欧拉序euler、dep、pos
    init_RMQ();  // RMQ预处理
    while(m --) {
        ...... 
        if(pos[u] > pos[v]) swap(u, v);  // 询问u、v两个点的LCA,注意要保持欧拉序中u在v前面
        cout << euler[min_RMQ(pos[u], pos[v] + 1)] << endl;  // min_RMQ的输入、输出都是下标值,区间表示为左闭右开
    }
}

4. 例题

Luogu P3379:(题目链接)https://www.luogu.com.cn/problem/P3379


二、最大流问题(Dinic算法)

1. 定义

设源点为1, 终点为N。用 G[u][v] 表示点u、v之间的容量大小,求这样一个网络图 G 中的最大流量大小ans。

2. 解析

  • Dinic算法 求最大流 用邻接矩阵G存图更方便
  • 先进行bfs, 判断有无增广路的同时,标记每个点的层号
  • 然后根据层号进行dfs, 找增广路, 更新流值, 可能有多条不相交的增广路
  • 循环往复

3. 模板

bool bfs(){
    memset(dist, -1, sizeof(dist)); // 每次重新标记dist, 因为增加了反向边, 图已经不同
    dist[1] = 0;
    que.push(1);
    while(!que.empty()){
        int u = que.front();
        que.pop();
        for(int v = 1; v <= N; i ++){
            if(G[u][v] && dist[v] == -1){ 
            //    连接  && 未被标记 的点 
            dist[v] = dist[u] + 1;
            que.push(v); 
        }
    }
    if(dist[N] == -1) return 0;  // 若无法增广到终点,则表示没有增广路了
    return 1; 
} 

int find(int u, int low){ // low是源点到现在最窄的(剩余流量最小)的边的剩余流量 
    if(u == N) return low; // dfs出口
    for(int v = 1, a; v <= N; i ++){
        if(G[u][v] && dist[v] == dist[u] + 1 && (a = find(v, min(low, G[u][v])))){
        //   连接的    &&  是上下层关系的      && 能连通到汇点  min维护low
            G[u][v] -= a;
            G[v][u] += a; // 增加反向边 起容错作用
            return a; //只要发现一条增广路, 就返回增广的流量 
        }
    } 
    return 0; 
}

int main(){
    //....
    int ans = 0;
    while(bfs()){ // 利用bfs为每个点标记dis, 若标记不到汇点, 说明已经没有增广路 
        int temp;  // 每次增广的流量
        while(temp = find(1, INF)) ans += temp; // 根据dist进行dfs找增广路, 直到找不到为止 
    }
    cout << ans << endl;
} 

全原创