1.模板题:AcWing 1172. 祖孙询问 - AcWing
传统做法:倍增+lca
思路: 1)先预处理出每个点的祖先
2)再求LCA:先拿到层数最深的一个点a,然后吧他的层数增加到和另一个b层数一样,
特判: if a==b,b就是a,b的最近公共祖先
else 就两个点一起向上,继续找公共祖先,--一定 会存在,因为最后都会回到根节点
初始化代码:
怎么理解fa[j][k] = fa[fa[j][k - 1]][k - 1] ?
fa[i][k] --表示从i开始,向上走2^k步所能走到的节点
我们从 j 跳到他前面的的2 ^k个位置,需要跳2^k步,我们可以先跳2^(k-1)步跳到到他的2^(k-1),即fa[j][k-1]这个节点,再从这个点跳2^(k-1)步就到达目的地
(2^(k-1)+2^(k-1) =2*2^(k-1)=2^k)
//使用倍增初始化来提速
int depth[N], fa[N][16];//2^16 >=40000
//depth,fa同时初始化
//注意fa[i][k]含义: i的第2^k位祖先
void bfs(int root) {
memset(depth,0x3f,sizeof depth);
depth[0] = 0, depth[root] = 1;
int hh = 0, tt = 0;
q[0] = 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];
}
}
}
}
//求lca代码:
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];
}
//然后特判一下,两个点是否是相同的,若是,则b是a的祖先
if (a == b)return a;
//然后两个点现在处于同一层,两个点一起往上跳,直到找到公共祖先
for(int k=15;k>=0;--k)
//因为我们要跳到它们LCA的下面一层,所以它们肯定不相等,
if (fa[a][k] != fa[b][k]) {//判断二者祖先 是否相同
a = fa[a][k];
b = fa[b][k];
}
return fa[a][0];
}
解法二: Tarjan:是强制离线算法
思路:
先很自然地深优遍历下去,
如果当前节点x涉及到某一个询问且询问的另一个点已经访问过了,那么就可以得出答案了;反之标记点x已经访问过,直到访问到另一个节点。
,在访问到一个点时,我们可以一并解决所有与这个点有关的询问,所以我们的询问也要用邻接表来存贮
Tarjan--离线求LCA O(n+m)
在dfs时将所有点分为三类:
1)已经遍历过且回溯过的点--2
2)正在搜索的分支上的点--1
3)还未搜索到的点--0
我们发现绿圈中的点与红线上的点满足一定规律,即一颗子树上的根节点为二者lca
例子:
我们从上到下 分出3颗子树: a.:绿2 b: 绿4 c:绿1
我们发现绿2 上的所有点与红线上点的lca为这棵子树的根节点,即与绿2连接的红线上的点
同样下面的绿4,绿1也是,so这启发我们求lca的时候可以使用集合合并的思路--并查集'
那么聪明的 小朋友已经发现我们遗漏了一种情况:
我们只是把左边的绿色点加入到集合中了,那红线上的怎么处理,
比如我现在就想求2,3的lca怎么求呢,其实我们遍历顺序是从1到5:
观察发现: 线上2与绿4的lca为1,3与绿1的lca为2,5与三绿的lca依次为3,2,1(从下到上)
因为最后会从5回溯回去,我们只需要回溯的时候更新5的lca的同时顺便求出当前lca所能求的点即可.
因为并查集每个查询和插入都是O(1),且我们需要插入最多n个点,查询m个点,so总时间复杂度为
O(n+m)
Tarjan代码如下:
附赠练习题目:
2.AcWing 1171. 距离(算法提高课) - AcWing
//思路: 求一颗树上两个点的最短距离那么就是求这两个点到他们lca的距离之和
怎么求,我们可以先初始化d[x]表示x到根节点的距离
我们所求可以变为 d[x]+d[y]-2*d[lca(x,y)]
如图:p为lca(x,y)
注意: 本题没有指出root,是因为求两点最短距离,是相对距离的意思,选谁做根节点均可,我们默认1位根节点咯
ok,热身结束,让我们来接触一下比较复杂的一题吧:
求次小生成树: 这题当然可以用dfs去求我们的d1,d2--AcWing 1148. 秘密的牛奶运输 - AcWing
这头的dist求的是任意两点之间的边权最大值
但本题正解是用最近公共祖先去维护的d1,d2
理论铺垫:
最小生成树中加入一条非数边,一定会形成一个环,因为所有点都已经连通,再加入一条任意两点之间的边,那么这两个点就会配合其他点形成一个环,so我们只需要去掉环上 的最大边即可求出次小生成树了
思路:
1)先求出一颗最小生成树--Kruskal
2)再枚举每条非数边,加入判断
我们此时的 d1,d2--这样定义:
d1[i][k],从i开始,向上条2^k步得到的 最大边权
d2[i][k],从i开始,向上条2^k步得到的 次大边权
那么次数的d1,d2是怎么更新的呢?
来看一张图片:
我们发现d1要在4者中取最大,d2要在4者中取次大,而这步又是在lca中体现,那么我们只需要在bfs插入初始化fa,d1,d1的操作即可
代码如下:
void bfs() {
memset(depth,0x3f,sizeof depth);
depth[0] = 0,depth[1]=1;
int hh = 0, tt = 0;
q[0] = 1;
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;
d1[j][0] = w[i], d2[j][0] = -INF;
for (int k = 1; k <= 16; ++k) {
int anc = fa[j][k - 1];
fa[j][k] = fa[anc][k - 1];
int dis[4] = {d1[j][k-1],d2[j][k-1],d1[anc][k-1],d2[anc][k-1]};
for (int u = 0; u < 4; ++u) {
int d = dis[u];
if (d > d1[j][k])d2[j][k] = d1[j][k], d1[j][k] = d;
else if (d != d1[j][k] && d > d2[j][k])d2[j][k] = d;
}
}
}
}
}
}
但是,这样的d1d2还不是我们要求的a,b到lca的最大距离和次大距离:
我们还需要把a到lca,b到lca的没个点的d1,d2加入,最后取最大,次大即可:
代码如下:
int lca(int a, int b, int w) {
static int dis[2 * N];
int cnt = 0;
if (depth[a] < depth[b])swap(a,b);
for (int k = 16; k >= 0; --k)
if(depth[fa[a][k]]>=depth[b])
{
dis[cnt++] = d1[a][k];
dis[cnt++] = d2[a][k];
a = fa[a][k];
}
if (a != b) {
for (int k = 16; k >= 0; --k) {
if (fa[a][k] != fa[b][k])
{
dis[cnt++] = d1[a][k];
dis[cnt++] = d2[a][k];
dis[cnt++] = d1[b][k];
dis[cnt++] = d2[b][k];
a = fa[a][k], b = fa[b][k];
}
}
dis[cnt++] = d1[a][0];
dis[cnt++] = d1[b][0];
}
int dist1 = -INF, dist2 = -INF;
for (int i = 0; i < cnt; ++i) {
int d = dis[i];
if (d > dist1)dist2 = dist1, dist1 = d;
else if (d != dist1 && d > dist2)dist2 = d;
}
if (w > dist1)return w - dist1;
if (w > dist2)return w - dist2;
return INF; //取min不会用到
}
哎呀,我们的code农场遭到wa大魔王的进攻啦?
AC勇士快去打败他吧:
题意:求砍断一颗生成树的方法数,我们每次先可以砍一条树边,才可以砍一条非树边
如图:红色边为非树边
我们发现:左红和与他连接的数边构成的环上的边,我们砍完之后还需要再砍一条非数边(就是左红了)才能切断这个树,我们再遍历右边的环,一次 累积 +1 到每条环中边
思路:
我们遍历所有边和他们经过的环上的数边(遍历一次+1)最后得到环上的数字 c,表示,要砍了这条边使得数不连通的话还需要我们砍掉c条非树边
so我们的方法数== 遍历整颗树上的数字c:
分类:
c==0: ans+=m(都没有非树边的环经过他,把他砍了+砍1条非树边(共m条)--一共m中方案)
c==1:ans+=1(砍了还需要再砍一条非树边,只有一种方案)
c>1: ans+=0(因为我们只能砍一刀非树边,so这个边不能砍)
难点: 如何快速的给每一条边 + 一个数--差分:快速的给某一个区间+一个数-O(1)
使用 树上差分:
p =cla(x,y)
d[x]+=c, d[y]+=c,d[p]-=2*c
我们发现这样操作只会给x,y在cla(x,y)下的祖先们的边权+c
so我们求ans的时候只需要dfs一下,从下往上遍历即可
代码如下:
int dfs(int u,int fa) {//返回每颗子树的和是多少
int res = d[u]; //继承父亲的差分的求前缀和操作
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (j != fa) {
int s = dfs(j,u);
//s==0 || 1 只有 遍历到单边的时候会出现
if (s==0)ans += m;
else if (s == 1)ans++;
res += s; //体现了差分求前缀和
}
}
return res;
}