神秘国度的爱情故事
[问题描述]
某个太空神秘国度中有很多美丽的小村,从太空中可以想见,小村间有路相连,更精确
一点说,任意两村之间有且仅有一条路径。小村 A 中有位年轻人爱上了自己村里的美
丽姑娘。每天早晨,姑娘都会去小村 B 里的面包房工作,傍晚 6 点回到家。年轻人终
于决定要向姑娘表白,他打算在小村 C 等着姑娘路过的时候把爱慕说出来。问题是,
他不能确定小村 C 是否在小村 B 到小村 A 之间的路径上。你可以帮他解决这个问题
吗?
[基本要求]
(1)输入由若干组测试数据组成。每组数据的第 1 行包含一正整数 N ( l 《 N
《 50000 ) , 代表神秘国度中小村的个数,每个小村即从0到 N - l 编号。接下来有 N -1 行输入,每行包含一条双向道路的两个端点小村的编号,中间用空格分开。之后一行包含一正整数 M ( l 《 M 《 500000
) ,代表着该组测试问题的个数。接下来 M 行,每行给出 A 、 B 、 C 三个小村的编号,中间用空格分开。当 N 为 O 时,表示全部测试结束,不要对该数据做任何处理。
(2)对每一组测试给定的 A 、 B 、C,在一行里输出答案,即:如果 C 在 A 和 B 之间的路径上,输出 Yes ,否则输出 No.
解题思路
问题用到最近公共祖先算法,求C是否在A和B的路径上
村落分布可抽象化为图,用邻接表保存,定义两个结构Edge和Node,Edge用于保存节点的邻接节点,里面还有指向下一个链头节点邻接节点的指针,Node里面定义了节点的编号,节点的深度(遍历时初始化,用于寻找最近公共祖先时先使指向深层的节点指针往上跳到与浅层节点指针同一层次),节点的父亲数组fa[],保存该节点路径上的父亲节点。找最近公共祖先的时候,如果两节点在同一层次时,两级点相等,那么该节点就是最近公共节点,否则,设村庄A与村庄B的最近公共祖先为D,A和C的最近公共祖先为AC,B和C的最近公共祖先为BC,如果ACC或者BCC,这里需分情况讨论,如果C==D,那么C就是最近公共祖先,C在AB的路径上,否则说明C不是A和B的最近公共祖先,C是C和A的最近公共祖先或者是B和C的最近公共祖先,C在AD路径上或在BD路径上,如果C并不是A的最近公共祖先并且不是B的最近公共祖先,那么B不是A和B路径上的点
LCA中倍增法
将2的次幂排成一个序列,1 、2 、4 、8~,任何数都可以用其中的几个数相加得到,比如20=16+4,也就是每个正整数都可以拆分成多个2的幂次数。
这样就可以使两个结点不再一层一层地往上跳,而是每次都跳2的幂次方层。例如a点要向上跳10层,就可以跳两次:先跳8层,再跳2层。
有数组tv[u].fa[i],表示下标为u的节点向上跳2^i 个节点,等于跳了2^(i-1) 个节点再加2^(i-1) 个节点,所以tv[u].fa[i]=tv[tv[u].fa[i-1]].fa[i-1],有了这个结论就可以得到每个节点向上跳2^i 格,这个初始化在BFS中完成,在LCA中,先把两个不同层次的节点跳到同一节点,就运用了倍增思想,先求他两节点的层次差,用2的次幂表示(本题中用到的处理方法是转换成二进制,变量i表示二进制第几位,第i位为1则跳到他的第2^ i父亲节点那里,为0则不跳同时i自增,fa[i]是第2^ i个父亲),再结合BFS中初始化的各节点保存的父亲数组,就可以跳到任意下标的节点了
代码实现
//LCA.h
lude<iostream>
#include<queue>
#define Height 20//因为最多有50000个村庄,所以log2(50000)=20
extern int num, line;
using namespace std;
typedef struct Edge {//边节点
int adjnode;//邻接点的下标
Edge* next;//指向下一个节点
}Edge;
typedef struct Node {
int ID;
int fa[Height];//用于保存节点的父亲节点,方便找共同祖先
int depth;//保存节点的深度,用于判断两个节点的层次关系,深层的要先往上跳至浅层的,两层再一起网上跳找共同祖先
Edge* firstNode;//邻接表中表头指向第一个节点用
}Node;
//创建邻接表
void CreateGraph(Node*& node, int n);
//广度优先遍历,初始化该图,找到每个节点的父亲和所在的深度
void BFS(Node*& node, int root);
//寻找最近公共祖先
int LCA(Node* node, int u, int v);
//判断节点是否在两节点的路径上
void Search(Node* node, int a, int b, int c);
//LAC.cpp
#include "LCA.h"
int num = 0, line = 0;
void CreateGraph(Node*& node, int n){
int u, v;
Edge* p;
num = n;//村庄数
line = n - 1;//行数
for (int i = 0; i < num;
i++) {
node[i].ID = i;//初始化头节点数组,每个小村从0~n-1编号
node[i].firstNode = NULL;//每个头节点的第一个邻接点为空
}
cout << "请输入村子的" << line << "条路" << endl;
for (int i = 0; i < line;
i++) {
cin >> u >> v;//输入两个村庄
p = new Edge;//构建边结点
p->adjnode
= v;
p->next =
node[u].firstNode;
node[u].firstNode = p;//头插进去
p = new Edge;
p->adjnode
= u;//节点间是双向的
p->next =
node[v].firstNode;
node[v].firstNode = p;
}
}
void BFS(Node*& node, int root){
int u;//暂存队首节点的下标
int v;//暂存下一邻接节点的下标
node[root].fa[0] = root;//根节点的父亲是他自己
node[root].depth = 0;//根节点的深度为0
queue<int>que;//队列,每访问一个节点,该节点进队列
que.push(root);
while (!que.empty()) {
u =
que.front();
que.pop();//将队首节点出队列
for (int i = 1; i < Height; i++)//寻找节点的所以父亲节点,据u的第2^i个父亲结点等于u的第2^(i-1)个父亲结点的第2^(i-1)个父亲结点
node[u].fa[i] = node[node[u].fa[i - 1]].fa[i
- 1];
Edge* p;
p = node[u].firstNode;
while (p != NULL) {
v =
p->adjnode;
if (v == node[u].fa[0]) {
p =
p->next;//指针指向下一个同一层的节点
}
else {//因为存储的是双向边,所以防止再访问到已经访问过的父亲结点
node[v].depth = node[u].depth + 1;//节点深度为父亲节点深度加1
node[v].fa[0] = u;//记录v的父亲节点下标
que.push(v);//将v入队列
}
}
}
}
int LCA(Node* node, int u, int v){
if (node[u].depth > node[v].depth)
swap(u, v);//使u存放较浅层次的节点,v存放较深层次的节点
int hu = node[u].depth, hv = node[v].depth;
int tu = u, tv = v;//游走找最近公共祖先
for (int dec = hv - hu, i =
0; dec; dec >>= 1, i++)//使两个节点在同一层次
if (dec & 1)//层次差相与为1的时候代表该为对应十进制中的i,这里的i是循环增大的,每一次循环对应于差的二进制后几位的和
tv = node[tv].fa[i];//通过数制之间的关系减少移动次数
if (tu == tv)//如果两节点在同一层次,且他们相等,他们tu即为他们的公共祖先
return tu;
for (int i = Height - 1; i
>= 0; i--) {
if (node[tv].fa[i] == node[tu].fa[i])//如果一不小心跳过头了,则跳过此步,虽然层次可能没有9层,但超出的部分fa[i]都是等于0的,
continue;
tv = node[tv].fa[i];
tu = node[tu].fa[i];//两节点下标同时往上跳,找最近公共祖先
}
return node[tu].fa[0];//返回最近公共祖先
}
void Search(Node* node, int a, int b, int c){
int d = LCA(node, a, b);//寻找a,b的最近公共祖先
int ac = LCA(node, a, c);
int bc = LCA(node, b, c);
bool flag;
if (ac == c && bc == c) {//如果c是a,b的公共祖先
if (c == d)//如果c是a,d的最近公共祖先
flag = true;
else//如果c不是a,b的最近公共祖先
flag = false;
}
else if (ac == c || bc == c) {//c是a的祖先或者是b的祖先,说明c在a到d的路径上或者在b到d的路径上
flag = true;
}
else//如果c不是a的祖先,也不是b的祖先,则a和b的路径上不会经过c点
flag = false;
if (flag)
cout << "YES..." << endl;
else
cout << "NO..." << endl;
}
// Curriculum_design.cpp
#include <iostream>
#include"LCA.h"
int main()
{
int a, b, c;
int N, M;
cout << "请输入村子数:";
cin >> N;
Node* node = new Node[N +1];//动态创建邻接表的头结点数组
CreateGraph(node,
N);//创建邻接表
BFS(node, 0);//初始化邻接表,使每个节点都保存有其路径上的所以父亲节点的下标和他的访问深度(也就是在二叉树中的层数)
cin >> M;//输入测试用例的个数
for (int i = 0; i < M;
i++) {
cin >> a >> b >> c;
Search(node,
a, b, c);//检查c是否在a,b的路径上
}
}
测试数据
13
1 2
1 3
1 4
2 5
3 6
6 10
10 13
6 11
4 7
4 8
8 12
4 9
6
13 11 6
7 12 8
12 13 3
10 11 3
5 6 3
2 3 7