RMQ、LCA

目录

RMQ

背景

RMQ VS 线段树

实现

OJ实战

POJ 3264 Balanced Lineup

LCA

背景

原理

实现

OJ实战

HDU 2586 How far away?

CSU 1079 树上的查询

力扣 面试题 04.08. 首个共同祖先

力扣 2846. 边权重均等查询

LCA相关问题

力扣 235. 二叉搜索树的最近公共祖先

力扣 236. 二叉树的最近公共祖先

力扣 1644. 二叉树的最近公共祖先 II

力扣 1650. 二叉树的最近公共祖先 III

力扣 1676. 二叉树的最近公共祖先 IV

力扣 2509. 查询树中环的长度


RMQ

背景

RMQ是区间最值查询的英文缩写。

关于RMQ和线段树的关系,有两种说法,其实不重要。

说法一:线段树算法和RMQ算法都可以求解区间最值查询。

说法二:RMQ就是区间最值查询,可以用线段树或ST(稀疏表)求解。

我个人采用说法一。

RMQ VS 线段树

线段树的功能更强大,适用场景更多,而RMQ只适用于初始化之后大量查询,不支持更新

但是RMQ的代码更简单,且单次时间复杂度为O(1),这一点优于线段树。

实现

RMQ的实现原理就是,用二维表格做预处理,v[i][j]表示以a[i]开头,长最多为2^j的这一段中的最值。最多的意思是,从a[i]开始往后取,能取到多少就取多少。

具体实现参考ACM模板​​​​​​​

OJ实战

POJ 3264 Balanced Lineup

 题目:

Description

For the daily milking, Farmer John's N cows (1 ≤ N ≤ 50,000) always line up in the same order. One day Farmer John decides to organize a game of Ultimate Frisbee with some of the cows. To keep things simple, he will take a contiguous range of cows from the milking lineup to play the game. However, for all the cows to have fun they should not differ too much in height.

Farmer John has made a list of Q (1 ≤ Q ≤ 200,000) potential groups of cows and their heights (1 ≤ height ≤ 1,000,000). For each group, he wants your help to determine the difference in height between the shortest and the tallest cow in the group.

Input

Line 1: Two space-separated integers, N and Q
Lines 2.. N+1: Line i+1 contains a single integer that is the height of cow i 
Lines N+2.. NQ+1: Two integers A and B (1 ≤ A ≤ B ≤ N), representing the range of cows from A to B inclusive.

Output

Lines 1.. Q: Each line contains a single integer that is a response to a reply and indicates the difference in height between the tallest and shortest cow in the range.

Sample Input

6 3
1
7
3
4
2
5
1 5
4 6
2 2

Sample Output

6
3
0

题意:

给出一个数列和若干查询,输出查询区间内最大值和最小值的差值。

这个题目可以用线段树来做,也可以用RMQ来做。

思路一:线段树

#include<iostream>
using namespace std;

int num[50001];
int maxx[200001];
int minn[200001];

void build(int key, int low, int high)
{
	if (low == high)
	{
		minn[key] = maxx[key] = num[low];
		return;
	}
	int mid = (low + high) / 2;
	build(key * 2, low, mid);
	build(key * 2 + 1, mid + 1, high);
	maxx[key] = (maxx[key * 2] > maxx[key * 2 + 1]) ? maxx[key * 2] : maxx[key * 2 + 1];
	minn[key] = (minn[key * 2] < minn[key * 2 + 1]) ? minn[key * 2] : minn[key * 2 + 1];
}

int querymaxx(int key, int low, int high, int x, int y)
{
	if (low == x && high == y)return maxx[key];
	int mid = (low + high) / 2;
	if (mid < x)return querymaxx(key * 2 + 1, mid + 1, high, x, y);
	if (mid >= y)return querymaxx(key * 2, low, mid, x, y);
	int a = querymaxx(key * 2, low, mid, x, mid);
	int b = querymaxx(key * 2 + 1, mid + 1, high, mid + 1, y);
	return  (a>b) ? a : b;
}

int queryminn(int key, int low, int high, int x, int y)
{
	if (low == x && high == y)return minn[key];
	int mid = (low + high) / 2;
	if (mid < x)return queryminn(key * 2 + 1, mid + 1, high, x, y);
	if (mid >= y)return queryminn(key * 2, low, mid, x, y);
	int a = queryminn(key * 2, low, mid, x, mid);
	int b = queryminn(key * 2 + 1, mid + 1, high, mid + 1, y);
	return  (a<b) ? a : b;
}

int main()
{
	int n, q, low, high;
	cin >> n >> q;
	for (int i = 1; i <= n; i++)scanf("%d", &num[i]);
	build(1, 1, n);
	while (q--)
	{
		scanf("%d%d", &low, &high);
		printf("%d\n", querymaxx(1, 1, n, low, high) - queryminn(1, 1, n, low, high));
	}
	return 0;
}

因为没有更新操作,所以省掉了update函数。

思路二:RMQ:

#include<iostream>
using namespace std;

......


int maxs[50000][16];
int mins[50000][16];

int main()
{
	int n, q, k, low, high, rmax, rmin;
	cin >> n >> q;
	for (int i = 0; i < n; i++)
	{
		scanf("%d", &maxs[i][0]);
		mins[i][0] = maxs[i][0];
	}
	for (int j = 1; (1 << j) <= n; j++)
	{
		for (int i = 0; i < n; i++)
		{
			maxs[i][j] = maxs[i][j - 1];
			mins[i][j] = mins[i][j - 1];
			k = i + (1 << (j - 1));
			if (k < n && maxs[i][j] < maxs[k][j - 1])maxs[i][j] = maxs[k][j - 1];
			if (k < n && mins[i][j] > mins[k][j - 1])mins[i][j] = mins[k][j - 1];
		}
	}
	while (q--)
	{
		scanf("%d%d", &low, &high);
		low--;
		high--;
		if (high == low)printf("0\n");
		else {
			int j = GetBitLength(high - low) - 1;
			rmax = maxs[high + 1 - (1 << j)][j];
			if (rmax < maxs[low][j])rmax = maxs[low][j];
			rmin = mins[high + 1 - (1 << j)][j];
			if (rmin > mins[low][j])rmin = mins[low][j];
			printf("%d\n", rmax - rmin);
		};
	}
	return 0;
}

用模板的话,代码会简洁一点,可惜超时了,主要是有大量函数调用,但是POJ的编译器又比较老,用static inline的方式改写又编译不过,所以只能放弃了。

用模板的代码:


int main()
{
	std::ios::sync_with_stdio(false);
	int n, q, low, high;
	cin >> n >> q;
	vector<int>v(n);
	for (int i = 0; i < n; i++)cin >> v[i];
	RMQ<1>r1(v);
	RMQ<2>r2(v);
	while (q--)
	{
		cin >> low >> high;
		low--,high--;
		cout << r2.getMinMax(low, high) - r1.getMinMax(low, high) << endl;
	}
	return 0;
}

LCA

背景

LCA就是求树上的最近公共祖先问题。

原理

深度优先搜索:

利用深度优先,把树映射到一维数组,这是哈希的一种。

得到这个表有什么用呢?

也不知道是哪个神人想出来的,仅用这个表就可以得到最近公共祖先。

比如B的遍历数为3,C的遍历数为6,从3到6,深度依次为3 2 3 4,

最小的是2,对应的是A,所以A就是B和C的最近公共祖先。

注意,遍历数不一定是唯一的,比如求A和I的最近公共祖先,A有3个不同的遍历数,

但是不管选哪个,结果都是一样的。

所以说,只需要用minId型的RMQ方法求出区间的最小值,即可求出公共祖先。

实现

(1)节点数n=9

(2)遍历数visitn分配9个空间,visitDeep分配17个空间,ids分配17个空间

(3)执行一次DFS:

接口:void dfs(TreeWithId& tree, int k, int d),其中k是节点编号,d是深度

调用点:dfs(tree, 0, 1),根节点的编号是0,深度是1,其他节点就不用管了,反正递归完成遍历。

遍历数vnum依次是0,1,2...16,同时给3个数组赋值:

visitDeep=[1,2,3,2,3,4,3,4,3,2,1,2,1,2,3,2,1],下标是所有vnum,数值是d

visitn=[16,9,2,8,5,11,15,14],下标是节点编号,数值是部分vnum(每个节点的最后一个vnum)

ids=[0,1,2,1,3,4,3,5,3,1,0,6,0,7,8,7,0],下标是所有vnum,数值是节点编号

(4)使用方式

比如B的编号是2,C的编号是4,

首先根据visitn,visitn[2]=2,visitn[4]=5

其次取出visitDeep的2..5这一段,即[3,2,3,4],最小值是visitDeep[3]=2,即最小值的下标是3

最后,根据ids,ids[3]=1,所以B和C的最近公共祖先的编号是1,即节点A

模板实现参考ACM模板

OJ实战

HDU 2586 How far away?

 题目:

Description

There are n houses in the village and some bidirectional roads connecting them. Every day peole always like to ask like this "How far is it if I want to go from house A to house B"? Usually it hard to answer. But luckily int this village the answer is always unique, since the roads are built in the way that there is a unique simple path("simple" means you can't visit a place twice) between every two houses. Yout task is to answer all these curious people.

Input

First line is a single integer T(T<=10), indicating the number of test cases. 
  For each test case,in the first line there are two numbers n(2<=n<=40000) and m (1<=m<=200),the number of houses and the number of queries. The following n-1 lines each consisting three numbers i,j,k, separated bu a single space, meaning that there is a road connecting house i and house j,with length k(0<k<=40000).The houses are labeled from 1 to n. 
  Next m lines each has distinct integers i and j, you areato answer the distance between house i and house j.

Output

For each test case,output m lines. Each line represents the answer of the query. Output a bland line after each test case.

Sample Input

2
3 2
1 2 10
3 1 15
1 2
2 3

2 2
1 2 100
1 2
2 1

Sample Output

10
25
100
100

题意:
n个房子用n-1条路连接起来(也就是一棵树),对于每一个询问,

求出2个房子之间的线路距离

思路:

这个题目只要建立一个树,然后查询任意2个点之间的距离,没有更新操作,所以可以用LCA来做。

LCA就是寻找最近公共祖先,这有什么用呢?

这是因为有一个性质,假设B和C的最近公共祖先是A,那么对于整个树的根节点D,

都有:|BD|+|CD|-|AD|*2=|BC|

也就是说,只要事先求出所有点到D的距离dist(dist的大小为n),

然后对于输入的B和C,只需要求出最近公共祖先,即可利用上式得到答案。

步骤:

1,存边

每次输入1条边,把2个点都存入对方的儿子列表中

因为儿子的数量未知,但是总数是2n-2,所以用向量数组v来存比较合适。(v的大小为n

2,建树

建树的方法有很多,而且n个点都是可以用来当做根节点的。

这里我采用的是以第一个节点作为根节点,利用深度优先搜索建树

因为在计算dist之前需要找到根节点,所以需要1个数组fa记录父亲的标号(fa的大小为n

3,深度优先搜索

4,空间分析

首先,我们需要对每个点存1个遍历数,任选1个存起来即可。(visitn的大小为n

然后,我们还需要把所有的遍历数对应的是哪个点存起来。

那么,一共有多少个遍历数呢?

规律很明显,总结如下:1个叶子节点只有1个遍历数,每个节点的遍历数等于出度加1

所以遍历数一共有:节点总数+出度总数=n+n-1(visitnum的大小为n+n-1

mins第一个维度的大小为n+n-1,第二个维度约为log2(n)+1

上面的9个节点就有17个遍历数。

5,计算每个点到根节点的距离。

visit函数是一个递归调用的函数,用来实现深度优先搜索。

搜索的过程中,除了要计算visitn和visitnum,还要计算deep和dist(deep的大小为n

(至此,7个数组的用途和大小都用蓝色粗体标注了)

deep和dist都可以利用递归的参数d和dis非常方便的计算出来。

6,RMQ

7,LCA

这个和RMQ对应,求的是visitnum数组的一段区间中,拥有最小deep的那个点对应的visitnum。

只需要根据visitnum便可知道到底哪个点是最近公共祖先

8,查询

输入x,y,取出他们的遍历数visitn,由此求出他们的最近公共祖先。

需要注意的是,因为遍历数有很多个,随便取一个存到visitn数组中,

那么x和y的遍历数谁大谁小就完全不知道了,需要判断。

代码:

#include<iostream>
#include<vector>
using namespace std;

struct node
{
    int son;
    int distance;
};

int n;
vector<node>v[40001];//存儿子标号
int deep[40001];//每个点的深度
int visitnum[80001];//遍历数是2*n-1
int visitn[40001];//每个点的任意一个遍历数
int vnum;
int mins[80001][18];        //区间最小值
int dist[40001];        //每个点到祖先的距离distance
int fa[40001];

void visit(int m, int d, int dis)        //遍历重编号、计算distance
{
    vector<node>::iterator p;
    deep[m] = d;
    dist[m] = dis;
    for (p = v[m].begin(); p != v[m].end(); p++)
    {
        if (fa[(*p).son]>-1)continue;
        fa[(*p).son] = m;
        visitnum[vnum++] = m;    //存入访问的第vnum个点是哪个点
        visit((*p).son, d + 1, dis + (*p).distance);
    }
    visitn[m] = vnum;        //注意这2句的顺序
    visitnum[vnum++] = m;
}

void rmq()        //计算区间最小值
{
    for (int i = 1; i <= 2 * n - 1; i++)mins[i][0] = visitnum[i];
    for (int j = 1; (1 << j) <= 2 * n - 1; j++)
    {
        for (int i = 1; i <= 2 * n - 1; i++)
        {
            mins[i][j] = mins[i][j - 1];
            int k = i + (1 << (j - 1));
            if (k <= 2 * n - 1 && deep[mins[i][j]] > deep[mins[k][j - 1]])
                mins[i][j] = mins[k][j - 1];
        }
    }
}

int lca(int x, int y)    //求最近公共祖先
{
    x = visitn[x], y = visitn[y];
    if (x > y)x ^= y ^= x ^= y;
    int j = 0;
    while ((1 << j) <= y - x + 1)j++;
    j--;
    int min = mins[y + 1 - (1 << j)][j];
    if (deep[min] > deep[mins[x][j]])min = mins[x][j];
    return min;
}

int main()
{
    int t, m, x, y, l;
    cin >> t;
    while (t--)
    {
        cin >> n >> m;
        vnum = 1;
        for (int i = 1; i <= n; i++)
        {
            v[i].clear();    //初始化
            fa[i] = -1;
        }
        for (int i = 1; i < n; i++)
        {
            scanf("%d%d%d", &x, &y, &l);
            node nod1, nod2;
            nod1.distance = l, nod1.son = y;
            v[x].insert(v[x].end(), nod1);
            nod2.distance = l, nod2.son = x;
            v[y].insert(v[y].end(), nod2);
        }
        fa[1] = 1;
        visit(1, 1, 0);
        rmq();
        while (m--)
        {
            scanf("%d%d", &x, &y);
            printf("%d\n", dist[x] + dist[y] - dist[lca(x, y)] * 2);
        }
    }
    return 0;
}

CSU 1079 树上的查询

题目:

Description

现有一棵有N个顶点的树,顶点的标号分别为1, 2, …, N。对于每个形如a b k的询问,你需要回答在从点a到点b的路径上是否包含点k。

Input

输入包含多组测试数据。

对于每组测试数据,第一行包含两个正整数N, Q (1<=N, Q<=10^5),分别表示这棵树一共有N个顶点,接下来你需要回答Q个询问。接下来N-1行中,第i行描述了这棵树的第i条边:包含两个正整数xi、yi,表示点xi和点yi之间有一条无向边。再接下来一共Q行,每行均包含三个正整数a、b、k (1<=a, b, k<=N),表示你需要回答在从点a到点b的路径上是否包含点k。

Output

对于每组测试数据,你需要按顺序依次回答每个询问。对于任意一个询问a b k,如果点a到点b的路径上包含点k,则输出“YES”(不包括引号),否则输出“NO”(不包括引号)。每组数据结尾输出一个空行。

Sample Input

3 4
1 2
3 2
2 3 2
2 1 1
1 2 3
1 3 2

Sample Output

YES
YES
NO
YES

Hint

       由于数据量较大,推荐使用scanf/printf。

代码:

#include<iostream>
#include<stdio.h>
#include<vector>
using namespace std;

int n;
vector<int>v[100001];//存儿子标号
int deep[100001];//每个点的深度
int visitnum[200001];//遍历数是2*n-1
int visitn[100001];//每个点的任意一个遍历数
int vnum;
int mins[200001][20];		//区间最小值
int fa[100001];

void visit(int m, int d)		//遍历重编号
{
	vector<int>::iterator p;
	deep[m] = d;
	for (p = v[m].begin(); p != v[m].end(); p++)
	{
		if (fa[*p]>-1)continue;
		fa[*p] = m;
		visitnum[vnum++] = m;	//存入访问的第vnum个点是哪个点
		visit(*p, d + 1);
	}
	visitn[m] = vnum;		//注意这2句的顺序
	visitnum[vnum++] = m;
}

void rmq()		//计算区间最小值
{
	for (int i = 1; i <= 2 * n - 1; i++)mins[i][0] = visitnum[i];
	for (int j = 1; (1 << j) <= 2 * n - 1; j++)
	{
		for (int i = 1; i <= 2 * n - 1; i++)
		{
			mins[i][j] = mins[i][j - 1];
			int k = i + (1 << (j - 1));
			if (k <= 2 * n - 1 && deep[mins[i][j]] > deep[mins[k][j - 1]])
				mins[i][j] = mins[k][j - 1];
		}
	}
}

int lca(int x, int y)	//求最近公共祖先
{
	x = visitn[x], y = visitn[y];
	if (x > y)x ^= y ^= x ^= y;
	int j = 0;
	while ((1 << j) <= y - x + 1)j++;
	j--;
	int min = mins[y + 1 - (1 << j)][j];
	if (deep[min] > deep[mins[x][j]])min = mins[x][j];
	return min;
}

int main()
{
	int q, x, y, k;
	bool fl = false;
	while (scanf("%d%d", &n, &q) != EOF)
	{
		if (fl)printf("\n");
		fl = true;
		vnum = 1;
		for (int i = 1; i <= n; i++)
		{
			v[i].clear();	//初始化
			fa[i] = -1;
		}
		for (int i = 1; i < n; i++)
		{
			scanf("%d%d", &x, &y);
			v[x].insert(v[x].end(), y);//存入儿子的标号
			v[y].insert(v[y].end(), x);//存入儿子的标号
		}
		fa[1] = 1;
		visit(1, 1);		
		rmq();
		while (q--)
		{
			scanf("%d%d%d", &x, &y, &k);
			int lca1 = lca(x, y), lca2 = lca(x, k), lca3 = lca(y, k);
			bool flag = (lca1 == lca2 && lca3 == k);
			if (lca1 == lca3 && lca2 == k)flag = true;
			if (flag)printf("YES\n");
			else printf("NO\n");
		}
	}
	return 0;
}

力扣 面试题 04.08. 首个共同祖先

设计并实现一个算法,找出二叉树中某两个节点的第一个共同祖先。不得将其他的节点存储在另外的数据结构中。注意:这不一定是二叉搜索树。

例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]

    3
   / \
  5   1
 / \ / \
6  2 0  8
  / \
 7   4

示例 1:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。

示例 2:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。

说明:

所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉树中。
class Solution {
public:
	TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
		TreeWithId tree(root);
		LCA opt(tree);
		int id = opt.getLca(tree.m[p], tree.m[q]);
		for (auto mi : tree.m)if (mi.second == id)return mi.first;
		return nullptr;
	}
};

力扣 2846. 边权重均等查询

现有一棵由 n 个节点组成的无向树,节点按从 0 到 n - 1 编号。给你一个整数 n 和一个长度为 n - 1 的二维整数数组 edges ,其中 edges[i] = [ui, vi, wi] 表示树中存在一条位于节点 ui 和节点 vi 之间、权重为 wi 的边。

另给你一个长度为 m 的二维整数数组 queries ,其中 queries[i] = [ai, bi] 。对于每条查询,请你找出使从 ai 到 bi 路径上每条边的权重相等所需的 最小操作次数 。在一次操作中,你可以选择树上的任意一条边,并将其权重更改为任意值。

注意:

  • 查询之间 相互独立 的,这意味着每条新的查询时,树都会回到 初始状态 。
  • 从 ai 到 bi的路径是一个由 不同 节点组成的序列,从节点 ai 开始,到节点 bi 结束,且序列中相邻的两个节点在树中共享一条边。

返回一个长度为 m 的数组 answer ,其中 answer[i] 是第 i 条查询的答案。

示例 1:

输入:n = 7, edges = [[0,1,1],[1,2,1],[2,3,1],[3,4,2],[4,5,2],[5,6,2]], queries = [[0,3],[3,6],[2,6],[0,6]]
输出:[0,0,1,3]
解释:第 1 条查询,从节点 0 到节点 3 的路径中的所有边的权重都是 1 。因此,答案为 0 。
第 2 条查询,从节点 3 到节点 6 的路径中的所有边的权重都是 2 。因此,答案为 0 。
第 3 条查询,将边 [2,3] 的权重变更为 2 。在这次操作之后,从节点 2 到节点 6 的路径中的所有边的权重都是 2 。因此,答案为 1 。
第 4 条查询,将边 [0,1]、[1,2]、[2,3] 的权重变更为 2 。在这次操作之后,从节点 0 到节点 6 的路径中的所有边的权重都是 2 。因此,答案为 3 。
对于每条查询 queries[i] ,可以证明 answer[i] 是使从 ai 到 bi 的路径中的所有边的权重相等的最小操作次数。

示例 2:

输入:n = 8, edges = [[1,2,6],[1,3,4],[2,4,6],[2,5,3],[3,6,6],[3,0,8],[7,0,2]], queries = [[4,6],[0,4],[6,5],[7,4]]
输出:[1,2,2,3]
解释:第 1 条查询,将边 [1,3] 的权重变更为 6 。在这次操作之后,从节点 4 到节点 6 的路径中的所有边的权重都是 6 。因此,答案为 1 。
第 2 条查询,将边 [0,3]、[3,1] 的权重变更为 6 。在这次操作之后,从节点 0 到节点 4 的路径中的所有边的权重都是 6 。因此,答案为 2 。
第 3 条查询,将边 [1,3]、[5,2] 的权重变更为 6 。在这次操作之后,从节点 6 到节点 5 的路径中的所有边的权重都是 6 。因此,答案为 2 。
第 4 条查询,将边 [0,7]、[0,3]、[1,3] 的权重变更为 6 。在这次操作之后,从节点 7 到节点 4 的路径中的所有边的权重都是 6 。因此,答案为 3 。
对于每条查询 queries[i] ,可以证明 answer[i] 是使从 ai 到 bi 的路径中的所有边的权重相等的最小操作次数。 

提示:

  • 1 <= n <= 104
  • edges.length == n - 1
  • edges[i].length == 3
  • 0 <= ui, vi < n
  • 1 <= wi <= 26
  • 生成的输入满足 edges 表示一棵有效的树
  • 1 <= queries.length == m <= 2 * 104
  • queries[i].length == 2
  • 0 <= ai, bi < n

思路:

只需要化作一棵多叉树,求出任意2个节点的LCA即可。

我的实现比较繁琐,不过都是套用模板,代码量并不大。

首先,把边集转化成邻接表。

然后,再转化成多叉树。

再然后,进行一次遍历,把每个节点编号对应的指针记下来,用于在TreeWithId中索引新编号。在遍历的同时,记录每个节点到根节点的累计和。

再然后,对于每次查询,根据新编号求lca的新编号,由于我的MultiTree::edgesToMultiTree是把原编号存在了多叉树的val中,也就变成了TreeWithId 中的vals里面的值,根据lca的新编号就可以索引出lca的原编号。

最后,根据2个节点和lca节点各自到根节点的累计和,加减之后得到这2个节点之间的和。


class Solution {
public:
	vector<int> minOperationsQueries(int n, vector<vector<int>>& edges, vector<vector<int>>& queries) {
		UndirectedGraphData<int> g(edges);
		MultiTreeNode* root = MultiTree::edgesToMultiTree(g.adjaList);

		vector<vector<int>>m;
		m.resize(n);
		for (auto& mi : m)mi.resize(27);
        mt.clear();
		bfs(root, g.edgeMap, m);
		TreeWithId tree(root);
		LCA lca(tree);
		vector<int> ans;

		for (auto q : queries) {
			int snum = 0, maxnum = 0;
			auto& m1 = m[q[0]];
			auto& m2 = m[q[1]];
			auto& m3 = m[tree.vals[lca.getLca(tree.m2[mt[q[0]]], tree.m2[mt[q[1]]])]];
			for (int i = 1; i <= 26; i++) {
				int x = m1[i] + m2[i] - m3[i] * 2;
				maxnum = maxnum < x ? x : maxnum;
				snum += x;
			}
			ans.push_back(snum - maxnum);
		}
		return ans;
	}
	map<int, MultiTreeNode*>mt;
	void bfs(MultiTreeNode* root, map<pair<int, int>, int>& edgeMap, vector<vector<int>>& m)
	{
		queue<MultiTreeNode*>q;
		q.push(root);
		while (!q.empty()) {
			auto root = q.front();
			mt[root->val] = root;
			q.pop();
			for (auto p : root->son) {
				m[p->val] = m[root->val];
				m[p->val][edgeMap[make_pair(p->val, root->val)]]++;
				q.push(p);
			}
		}
	}
};

LCA相关问题

力扣 235. 二叉搜索树的最近公共祖先

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉搜索树:  root = [6,2,8,0,4,7,9,null,null,3,5]

示例 1:

输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6 
解释: 节点 2 和节点 8 的最近公共祖先是 6。
示例 2:

输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
 

说明:

所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉搜索树中。

代码:

class Solution {
public:
	TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
		if (p->val > q->val){
			TreeNode* tmp = p;
			p = q, q = tmp;
		}
		if (root->val < p->val)return lowestCommonAncestor(root->right, p, q);
		if (root->val > q->val)return lowestCommonAncestor(root->left, p, q);
		return root;
	}
};

力扣 236. 二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉树:  root = [3,5,1,6,2,0,8,null,null,7,4]

示例 1:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
 

说明:

所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉树中。

 class Solution {
  public:
      TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
          if (!root || root == p || root == q) {
              return root;
          }
          TreeNode* ret1 = lowestCommonAncestor(root->left, p, q);
          TreeNode* ret2 = lowestCommonAncestor(root->right, p, q);
          if (ret1 && ret2) {
              return root;
          }
          return ret1?ret1:ret2;
      }
 };

力扣 1644. 二叉树的最近公共祖先 II

给定一棵二叉树的根节点 root,返回给定节点 p 和 q 的最近公共祖先(LCA)节点。如果 p 或 q 之一 不存在 于该二叉树中,返回 null。树中的每个节点值都是互不相同的。

根据维基百科中对最近公共祖先节点的定义:“两个节点 p 和 q 在二叉树 T 中的最近公共祖先节点是 后代节点 中既包括 p 又包括 q 的最深节点(我们允许 一个节点为自身的一个后代节点 )”。一个节点 x 的 后代节点 是节点 x 到某一叶节点间的路径中的节点 y。

示例 1:


输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和 1 的共同祖先节点是 3。
示例 2:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和 4 的共同祖先节点是 5。根据共同祖先节点的定义,一个节点可以是自身的后代节点。
示例 3:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 10
输出: null
解释: 节点 10 不存在于树中,所以返回 null。
 

提示:

树中节点个数的范围是 [1, 104]
-109 <= Node.val <= 109
所有节点的值 Node.val 互不相同
p != q
 

进阶: 在不检查节点是否存在的情况下,你可以遍历树找出最近公共祖先节点吗?

class Solution {
public:
	TreeNode* ans;
	int dfs(TreeNode* root, TreeNode* p, TreeNode* q)
	{
		if (!root)return 0;
		int a = dfs(root->left, p, q) + dfs(root->right, p, q);
		if (root == p || root == q)a++;
		if (a == 2 && ans == NULL)ans = root;
		return a;
	}
	TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
		ans = NULL;
		dfs(root, p, q);
		return ans;
	}
};

力扣 1650. 二叉树的最近公共祖先 III

给定一棵二叉树中的两个节点 p 和 q,返回它们的最近公共祖先节点(LCA)。

每个节点都包含其父节点的引用(指针)。Node 的定义如下:

class Node {
    public int val;
    public Node left;
    public Node right;
    public Node parent;
}
根据维基百科中对最近公共祖先节点的定义:“两个节点 p 和 q 在二叉树 T 中的最近公共祖先节点是后代节点中既包括 p 又包括 q 的最深节点(我们允许一个节点为自身的一个后代节点)”。一个节点 x 的后代节点是节点 x 到某一叶节点间的路径中的节点 y。

示例 1:


输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和 1 的最近公共祖先是 3。
示例 2:


输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和 4 的最近公共祖先是 5,根据定义,一个节点可以是自身的最近公共祖先。
示例 3:

输入: root = [1,2], p = 1, q = 2
输出: 1
 

提示:

树中节点个数的范围是 [2, 105]。
-109 <= Node.val <= 109

所有的 Node.val 都是互不相同的。
p != q
p 和 q 存在于树中。

#define TreeNode Node
class Solution {
public:
	TreeNode* ans;
	int dfs(TreeNode* root, TreeNode* p, TreeNode* q)
	{
		if (!root)return 0;
		int a = dfs(root->left, p, q) + dfs(root->right, p, q);
		if (root == p || root == q)a++;
		if (a == 2 && ans == NULL)ans = root;
		return a;
	}
	TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
		ans = NULL;
		dfs(root, p, q);
		return ans;
	}
	Node* lowestCommonAncestor(Node* p, Node * q) {
		Node* h = p;
		while (h->parent)h = h->parent;
		return lowestCommonAncestor(h, p, q);
	}
};

力扣 1676. 二叉树的最近公共祖先 IV

给定一棵二叉树的根节点 root 和 TreeNode 类对象的数组(列表) nodes,返回 nodes 中所有节点的最近公共祖先(LCA)。数组(列表)中所有节点都存在于该二叉树中,且二叉树中所有节点的值都是互不相同的。

我们扩展二叉树的最近公共祖先节点在维基百科上的定义:“对于任意合理的 i 值, n 个节点 p1 、 p2、...、 pn 在二叉树 T 中的最近公共祖先节点是后代中包含所有节点 pi 的最深节点(我们允许一个节点是其自身的后代)”。一个节点 x 的后代节点是节点 x 到某一叶节点间的路径中的节点 y。

示例 1:


输入: root = [3,5,1,6,2,0,8,null,null,7,4], nodes = [4,7]
输出: 2
解释: 节点 4 和 7 的最近公共祖先是 2。
示例 2:


输入: root = [3,5,1,6,2,0,8,null,null,7,4], nodes = [1]
输出: 1
解释: 单个节点的最近公共祖先是该节点本身。

示例 3:


输入: root = [3,5,1,6,2,0,8,null,null,7,4], nodes = [7,6,2,4]
输出: 5
解释: 节点 7、6、2 和 4 的最近公共祖先节点是 5。
示例 4:


输入: root = [3,5,1,6,2,0,8,null,null,7,4], nodes = [0,1,2,3,4,5,6,7,8]
输出: 3
解释: 树中所有节点的最近公共祖先是根节点。

提示:

树中节点个数的范围是 [1, 104] 。
-109 <= Node.val <= 109
所有的 Node.val 都是互不相同的。
所有的 nodes[i] 都存在于该树中。
所有的 nodes[i] 都是互不相同的。

class Solution {
public:
	TreeNode* ans;
	int dfs(TreeNode* root, vector<TreeNode*> &nodes)
	{
		if (!root)return 0;
		int a = dfs(root->left, nodes) + dfs(root->right, nodes);
		if (find(nodes.begin(), nodes.end(),root)!= nodes.end())a++;
		if (a == nodes.size() && ans == NULL)ans = root;
		return a;
	}
	TreeNode* lowestCommonAncestor(TreeNode* root, vector<TreeNode*> &nodes) {
		ans = NULL;
		dfs(root, nodes);
		return ans;
	}
};

力扣 2509. 查询树中环的长度

给你一个整数 n ,表示你有一棵含有 2n - 1 个节点的 完全二叉树 。根节点的编号是 1 ,树中编号在[1, 2n - 1 - 1] 之间,编号为 val 的节点都有两个子节点,满足:

  • 左子节点的编号为 2 * val
  • 右子节点的编号为 2 * val + 1

给你一个长度为 m 的查询数组 queries ,它是一个二维整数数组,其中 queries[i] = [ai, bi] 。对于每个查询,求出以下问题的解:

  1. 在节点编号为 ai 和 bi 之间添加一条边。
  2. 求出图中环的长度。
  3. 删除节点编号为 ai 和 bi 之间新添加的边。

注意:

  •  是开始和结束于同一节点的一条路径,路径中每条边都只会被访问一次。
  • 环的长度是环中边的数目。
  • 在树中添加额外的边后,两个点之间可能会有多条边。

请你返回一个长度为 m 的数组 answer ,其中 answer[i] 是第 i 个查询的结果

示例 1:

输入:n = 3, queries = [[5,3],[4,7],[2,3]]
输出:[4,5,3]
解释:上图是一棵有 23 - 1 个节点的树。红色节点表示添加额外边后形成环的节点。
- 在节点 3 和节点 5 之间添加边后,环为 [5,2,1,3] ,所以第一个查询的结果是 4 。删掉添加的边后处理下一个查询。
- 在节点 4 和节点 7 之间添加边后,环为 [4,2,1,3,7] ,所以第二个查询的结果是 5 。删掉添加的边后处理下一个查询。
- 在节点 2 和节点 3 之间添加边后,环为 [2,1,3] ,所以第三个查询的结果是 3 。删掉添加的边。

示例 2:

输入:n = 2, queries = [[1,2]]
输出:[2]
解释:上图是一棵有 22 - 1 个节点的树。红色节点表示添加额外边后形成环的节点。
- 在节点 1 和节点 2 之间添加边后,环为 [2,1] ,所以第一个查询的结果是 2 。删掉添加的边。

提示:

  • 2 <= n <= 30
  • m == queries.length
  • 1 <= m <= 105
  • queries[i].length == 2
  • 1 <= ai, bi <= 2n - 1
  • ai != bi
class Solution {
public:
	vector<int> cycleLengthQueries(int n, vector<vector<int>>& queries) {
		vector<int>ans;
		for (auto v : queries)ans.push_back(f(v[0], v[1]));
		return ans;
	}
	int f(int x, int y) {
		if (x < y)x ^= y ^= x ^= y;
		int lenx = GetBitLength(x);
		int leny = GetBitLength(y);
		int dif = lenx - leny;
		int ans = dif + 1;
		while (dif--)x /= 2;
		x ^= y;
		ans += GetBitLength(x) * 2;
		return ans;
	}
};

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值