tarjan算法

LCA和tarjan

在这里插入图片描述

LCA 时间复杂度(mlogn):
例题链接:
Acwing:1172祖孙询问
(https://https://www.acwing.com/problem/content/1174/)

####预处理

void bfs(int root)
{
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0, depth[root] = 1;
    int hh = 0, tt = 0;
    q[tt ++ ] = root;

    while (hh < tt)
    {
        int t = q[hh ++ ];
        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (depth[j] > depth[t] + 1)
            {
                depth[j] = depth[t] + 1;
                q[tt ++ ] = j;
                fa[j][0] = t;
                for (int k = 1; k <= 15; k ++ ) fa[j][k] = fa[fa[j][k - 1]][k - 1];
            }
        }
    }
}

####查询(跳步操作)

int lca(int a, int b) //查询操作
{
    if (depth[a] < depth[b]) swap(a, b);
    for (int k = 15; k >= 0; k -- )
        if (depth[fa[a][k]] >= depth[b]) a = fa[a][k];

    if (a == b) return a;
    for (int k = 15; k >= 0; k -- )
        if (fa[a][k] != fa[b][k])
        {
            a = fa[a][k];
            b = fa[b][k];
        }
    return fa[a][0];
}

###tarjan() 时间复杂度(O(n + m))

Acwing:1173距离
(https://www.acwing.com/problem/content/1173/)


####预处理:

void dfs(int u, int fa)
{
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa) continue;
        dist[j] = dist[u] + w[i];
        dfs(j, u);
    }
}

int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

####查询与合并(也可以叫做查询和路径压缩过程)

void tarjan(int u)
{
    st[u] = 1;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            tarjan(j);
            p[j] = u;
        }
    }
    for (auto it : q[u])
    {
        int a = it.first, id = it.second;
        if (st[a] == 2)
        {
            int ans = find(a);
            res[id] = dist[u] + dist[a] - dist[ans] * 2;
        }
    }
    st[u] = 2;
}

LCA(mlogn):在线算法

void bfs(int root) // 初始化操作, root传进来的是祖先节点
{
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0, depth[root] = 1;
    int hh = 0, tt = 0;
    q[tt ++ ] = root;
    while (hh < tt)
    {
        int t = q[hh ++ ];
        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (depth[j] > depth[t] + 1)
            {
                depth[j] = depth[t] + 1;
                q[tt ++ ] = j;
                fa[j][0] = t;
                for (int k = 1; k <= 15; k ++ ) fa[j][k] = fa[fa[j][k - 1]][k - 1];
            }
        }
    }
}
int lca(int a, int b) //查询操作
{
    if (depth[a] < depth[b]) swap(a, b);
    for (int k = 15; k >= 0; k -- )
        if (depth[fa[a][k]] >= depth[b]) a = fa[a][k];

    if (a == b) return a;
    for (int k = 15; k >= 0; k -- )
        if (fa[a][k] != fa[b][k])
        {
            a = fa[a][k];
            b = fa[b][k];
        }
    return fa[a][0];
}

###tarjan(O(n + m)):离线做法

void dfs(int u, int fa)
{
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa) continue;
        dist[j] = dist[u] + w[i];
        dfs(j, u);
    }
}

int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}


void tarjan(int u)
{
    st[u] = 1;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            tarjan(j);
            p[j] = u;
        }
    }
    for (auto it : q[u])
    {
        int a = it.first, id = it.second;
        if (st[a] == 2)
        {
            int ans = find(a);
            res[id] = dist[u] + dist[a] - dist[ans] * 2;
        }
    }
    st[u] = 2;
}

##补充:


###树剖解法:

树链剖分,正如其名,这个算法的主要思想就是 把“树”“剖分”成“链”

那怎么实现以及它的作用:

已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:

操作1: 格式: 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z

操作2: 格式: 2 x y 表示求树从x到y结点最短路径上所有节点的值之和

操作3: 格式: 3 x z 表示将以x为根节点的子树内所有节点值都加上z

操作4: 格式: 4 x 表示求以x为根节点的子树内所有节点值之和

####树剖的话很明显——一跳就是一条链,对于n极大的情况就相当于是倍增的再一优化

遍历x到y的路径,我们亦容易想到LCA——两个点同时往上跳,直到某个值相同,可以一起操作。所以我们的思路就是:两个点不在同一条链就往链头的父亲节点跳,在同一条链上就直接处理。而处理方法也很简单——因为全程都在链上以连续的新节点编号来操作,所以线段树维护区间距离就很方便了,完全不受树剖影响地敲一个基本的建树、查询、区间修改+延迟标记的代码就可以了

#include <cstdio>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 40010, M = N << 1;

int n, m;
int h[N], e[M], w[M], ne[M], idx;

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
int fa[M],top[M],dep[M],sz[M],son[M];
long long dis[M];

void dfs1(int x, int f)
{
	fa[x]=f;
	dep[x]=dep[f]+1;
	sz[x]=1;
	son[x]=0;
	for(int i = h[x]; ~i ; i = ne[i])
	{
		int y = e[i];
		if(y == f) continue;
		dis[y] = dis[x] + w[i];
		dfs1(y, x);
		sz[x] += sz[y];
		if(sz[y] > sz[son[x]]) son[x] = y;
	}
}
void dfs2(int x, int tp)
{
	top[x] = tp;
	if(son[x]) dfs2(son[x], tp);
	for(int i = h[x]; ~i ; i = ne[i])
	{
		int y = e[i];
		if(y == fa[x] || y == son[x]) continue;
		dfs2(y, y);
	}
}

int lca(int x,int y)
{
	while(top[x] != top[y])
	{
		if(dep[top[x]] < dep[top[y]]) swap(x, y);
		x = fa[top[x]];
	}
	if(dep[x] > dep[y]) return y;
	return x;
}
int main()
{
	scanf("%d %d",&n,&m);
	memset(h, -1, sizeof h);
	
	for(int i = 1; i < n; i ++)
	{
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c), add(b, a, c);	
	}
	
	dfs1(1, 0);
	dfs2(1, 1);
	
	while(m --)
	{
		int a, b;
		scanf("%d%d", &a, &b);
		int p = lca(a, b);
		printf("%lld\n", dis[a] + dis[b] - dis[p] * 2); 
	} 
	return 0;
}

非树边w的值域是一定≥dist1 否则在当w<dist1,则之前kruskal求最小生成树的时候把w替换dist1连接a和b
就得到一个更小的生成树(且依然能是一个生成树–原因如下图)了 与kruskal得到的是最小生成树矛盾
dist1
-c - d- -c d-
a b → a b
- -
w w
/*
解法a
AcWing 1148. 秘密的牛奶运输 暴力枚举
解法b
lca倍增O(logn)求新加入非树边边的两点a,b间的最大边和最小边
定理:对于一张无向图,如果存在最小生成树和次小生成树,那么对于任何一颗最小生成树都存在一颗次小生成树
使得这两棵树只有一条边不同
o-o
/
o o
\ /
o-o
那么我们就是要在现有的最小生成树中找一条非树边w去替换w[i]
则对于每一条非树边w,变换后的边权和=sum+w-w[i] 则我们想要边权和越小,则对应替换的w[i]越大

即我们要做的就是找这条路径上的最大边权替换
为了防止w == max(w[i] for i in path) 替换后w[i]依然==w 导致不是严格最小生成树
在这种情况下 我们用w替换这条路上的次大边
所以用d1 d2存最大边和次大边

2 预处理每个点i跳2^j后的父节点f[i][j]

      lca
       o 
      / \
     o   o
    /     \
   o       o
  /        y
 o
 x

在x和y向 lca[x][y]跳的过程中维护各自路径中的最大值d1[x → lca] d1[y → lca] 和次大值d2[x → lca] d2[y → lca] 路径
最大值d1 o o o
| | |
次大值d2 o o o
| | |
假设最大值为. 则次大值从ci中找
ci . ci
| | |
o ci o
| | |

[x → y]的最大值 = max(d1[i] for i in path)
次大值 = 次大(max(d1[i],d2[i]) for i in path)

3 预处理:
每个点i跳2^j路径上的最大边权d1[i][j]
每个点i跳2^j路径上的次大边权d2[i][j]

d1[i][j]跳2^j次
→ →
o—o—o
i anc j
d1[i,j-1],d2[i,j-1] d1[anc,j-1],d2[anc,j-1]


分析条件
题目中给出的关键点,就是严格和次小.

什么是严格
就是题目强制要求严格单调性,不可以有==号的出现.

什么是次小
我们应该都知道,最小生成树,它要求边集合的边总和最小,那么次小生成树,要求边集合的边总和只比最小生成树边集合权值大.

总结性质
有至少一个(严格)次小生成树,和最小生成树之间只有一条边的差异。和真理只有一点差异,那就是出题人毒瘤

我们来粗略证明一下.(强行伪证)

我们知道最小生成树,是由n−1n−1条构成的.

那么其他的M−N+1M−N+1就是多余边.

假如说我们把一条多余边(x,y,z)(x,y,z),加入到了最小生成树中,那么一定会在(x,y)(x,y)之间的路径上形成一个环.

那么这个环上面,最大的边称之为
Val1
Val1

次大的边,称之为
Val2
Val2

而且为了保证严格这个单调性质,我们必须设
Val1>Val2最大的边一定大于次大的边
Val1>Val2最大的边一定大于次大的边

接下来,我们就需要好好分析一下这条多余边了.

我们知道多余边,替换任何一条树上的一条边,都会使得最小生成树,不再最小

为什么?

因为最小生成树上的每一条边,一定是满足贪心性质下的最小的边.为什么啊?相信你的直觉啊

这个证明,我们使用的克鲁斯卡尔算法,已经告诉我们为什么.真相只有一个,我懒了

总而言之,言而总之,我们现在知道了这条多余边的加入.,一定会产生非最小生成树.

我们不妨令
ans=最小生成树边权之和
ans=最小生成树边权之和

假如说我们将多余边,替换掉最大权值边.
Val1==>z此时我们发现当前生成树W=ans+z−Val1W=最小生成边权之和+加上多余边−最大权值边
Val1==>z此时我们发现当前生成树W=ans+z−Val1W=最小生成边权之和+加上多余边−最大权值边

这一轮替换,我们可以认为这棵生成树有潜力成为次小生成树.

然后,我们发现,换一换次大边,也是可以的.

我们将多余边,强行替换掉次大权值边.
Val2==>z此时当前生成树W=ans+z−Val2W=最小生成树之和+加入多余边−次大权值边
Val2==>z此时当前生成树W=ans+z−Val2W=最小生成树之和+加入多余边−次大权值边

#####题目链接:356. 次小生成树
(https://www.acwing.com/problem/content/description/358/)

#####题目链接:352. 闇の連鎖
(https://www.acwing.com/problem/content/description/354/)

两个例题的代码这里就不摆出来了, 感觉太长了 💢 💢 💢
另外不管是LCA算法还是tarjan算法都是书上算法, 如果想要快速了解算法 原理的话最好是参照算法书上的描述或者是提高课里面的讲解, 学算法嘛,很多时候都是要花很多精力去证明算法的正确性,


,也是可以的.

我们将多余边,强行替换掉次大权值边.
Val2==>z此时当前生成树W=ans+z−Val2W=最小生成树之和+加入多余边−次大权值边
Val2==>z此时当前生成树W=ans+z−Val2W=最小生成树之和+加入多余边−次大权值边

#####题目链接:356. 次小生成树
(https://www.acwing.com/problem/content/description/358/)

#####题目链接:352. 闇の連鎖
(https://www.acwing.com/problem/content/description/354/)

两个例题的代码这里就不摆出来了, 感觉太长了 💢 💢 💢
另外不管是LCA算法还是tarjan算法都是书上算法, 如果想要快速了解算法 原理的话最好是参照算法书上的描述或者是提高课里面的讲解, 学算法嘛,很多时候都是要花很多精力去证明算法的正确性,


虽然有句话说:不经过证明的算法都是在耍流氓, 不过我们在考试或者比赛的过程中能直接套模板就直接套模板,方便快捷,简单暴力

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星辰予曦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值