(1)倍增法(在线处理,读入一个询问即处理一个答案)
预处理o(nlogn) 询问o(logn)
①fa[i][j]表示节点向上跳步的节点是谁,depth[i]表示节点的深度,通过bfs初始化(具体方法看下面代码逻辑)
注意:
②先将较低的点向上跳,直到两点在同一层,此时如果两点重合,则其最近公共祖先为该点,否则让两点再同时向上跳,直到跳到其最近公共祖先的下一层(判断条件为)
③此时两者的最近公共祖先即为向上再跳一步的点,即
//N为节点个数,深度为logN
int depth[N]; //logN = 16
void bfs(int root)
{
memset(depth,0x3f,sizeof depth);
// 哨兵depth[0] = 0: 如果从i开始跳2^j步会跳过根节点
// fa[fa[j][k-1]][k-1] = 0
// 那么fa[i][j] = 0 depth[fa[i][j]] = depth[0] = 0
depth[0] = 0,depth[root] = 1;
queue<int> q;
q.push(root);
while(q.size())
{
int t = q.front();
q.pop();
for(int i=h[t];i!=-1;i=ne[i])
{
int j = e[i];
if(depth[j]>depth[t]+1)//说明j还没被搜索过
{
depth[j] = depth[t]+1;
q.push(j);//把第depth[j]层的j加进队列
fa[j][0] = t;//j往上跳2^0步后就是t
for(int k=1;k<=15;k++)
fa[j][k] = fa[fa[j][k-1]][k-1];
}
}
}
}
//询问a b 的最近公共祖先
int lca(int a, int b)
{
//让a在下面
if (depth[a] < depth[b]) swap(a, b);
//让a往上跳
//当a跳完2^k依然在b下面 我们就一直跳
for (int k = 15; k >= 0; k -- )
if (depth[fa[a][k]] >= depth[b])
a = fa[a][k];
//如果跳到了b
if (a == b) return a;
//a,b同层但不同节点
//a,b同时往上跳
//如果a,b都跳出根节点,fa[a][k]==fa[b][k]==0 不符合更新条件
for (int k = 15; k >= 0; k -- )
if (fa[a][k] != fa[b][k])
{
a = fa[a][k];
b = fa[b][k];
}
//循环结束,到达最近公共祖先的子节点
//lca(a,b) = 再往上跳1步即可
return fa[a][0];
}
bfs(root);//建fa[i][j]
二叉树
题意:二叉树中两个结点之间的最短路径长度
倍增寻找最近公共祖先的过程中,记录跳的距离,即为两点间路径长度
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N=1e3+5,M=N<<1;
int n,m;
int h[N],e[M],ne[M],idx;
void add(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
int fa[N][12];
int depth[N];
void bfs(int root){
//注意初始化 depth[0]和 depth[root]
memset(depth,0x3f,sizeof depth);
depth[0]=0;
depth[root]=1;
queue<int> q;
q.push(root);
while(!q.empty()){
int t=q.front();q.pop();
for(int i=h[t];~i;i=ne[i]){
int j=e[i];
if(depth[j]>depth[t]+1){//如果j没被搜索到才更新
depth[j]=depth[t]+1;
fa[j][0]=t;
for(int k=1;k<=10;k++){
fa[j][k]=fa[fa[j][k-1]][k-1];
}
q.push(j);
}
}
}
}
int query(int a,int b){
int res=0;
if(depth[a]<depth[b]) swap(a,b);
for(int k=10;k>=0;k--){
if(depth[fa[a][k]]>=depth[b]){
a=fa[a][k];
res+=pow(2,k);//往上跳2^k步
}
}
if(a==b) return res;
for(int k=10;k>=0;k--){
if(fa[a][k]!=fa[b][k]){
a=fa[a][k];
b=fa[b][k];
res+=2*pow(2,k);
}
}
//树里的两个点一定有公共祖先(最远为根节点)
//但在其他图里最后需要判断a是否等于b
a=fa[a][0];
b=fa[b][0];
return res+2;
}
int main(){
int t;cin>>t;
while(t--){
memset(h,-1,sizeof h);
idx=0;
cin>>n>>m;
for(int i=1;i<=n;i++){
int x,y;cin>>x>>y;
if(x!=-1) add(i,x);
if(y!=-1) add(i,y);
}
bfs(1);
while(m--){
int x,y;cin>>x>>y;
cout<<query(x,y)<<endl;
}
}
}
(2)tarjan算法(离线处理,需要先读入所有询问)
整体复杂度o(n+m) n为节点数量,m为询问次数
① dfs所有的点,过程中将所有的点分成三类
第一类为已经访问且回溯过的点(即访问过且不在搜索栈中),标记为2
第二类为正在搜索的分支,标记为1
第三类为还未搜索到的点,标记为0
②可以看出,第一类点(绿色),即被访问且回溯过的点,第二类节点x(红色)与第一类点中的一整个分支的最近公共祖先都是相同的,即X与Y所在分支的所有点的最近公共祖先均为Z
③所以我们可以使用并查集,将第一类的点都合并到其根节点上,即将Y所在分支的所有点都合并到Z上,当询问X与Y的最近公共祖先时,即查询Y被合并到了哪个点上
④将所有的询问存储到vector<PII> query[u]中,下标u表示关于点u的询问,存储的PII first表示除了u的另外一点,second表示询问的编号
⑤对于某一点u,对于u的所有子节点j进行dfs,然后将子节点合并到u上
⑥当处理完某一点u后,处理u的询问,然后更新u的状态
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int,int> PII;
const int N=1e4+5,M=N<<1;
int n,m;
int h[N],e[M],ne[M],w[M],idx;
vector<PII> query[N];//query[i]存i的相关询问 {另一节点,问题编号}
int res[M];//问题答案
int p[N];
int dis[N];//点i到根节点的距离
int st[N];//点i在tarjan搜索的状态
void add(int a,int b,int c){
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
}
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
//dfs搜索每个点到根节点的距离
void dfs(int u,int fa){
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==fa) continue;
dis[j]=dis[u]+w[i];
dfs(j,u);
}
}
void tarjan(int u){
st[u]=1;//u正在搜
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(!st[j]){//如果子节点未搜,对子节点tarjan,并将子节点并到u
tarjan(j);
p[j]=u;
}
}
//处理u相关询问
for(int i=0;i<query[u].size();i++){
PII t=query[u][i];
int y=t.first,id=t.second;
if(st[y]==2){//如果另一节点已搜完,find(y)即为最近公共祖先
int lca=find(y);
res[id]=dis[u]+dis[y]-dis[lca]*2;//两点最短距离=两点各自到根节点距离-最近公共祖先到根节点距离*2
}
}
st[u]=2;//u搜完了
}
int main(){
cin>>n>>m;
memset(h,-1,sizeof h);
for(int i=1;i<=n;i++) p[i]=i;
for(int i=0;i<n-1;i++){
int a,b,c;cin>>a>>b>>c;
add(a,b,c),add(b,a,c);
}
//存询问
for(int i=0;i<m;i++){
int a,b;cin>>a>>b;
if(a!=b){
query[a].push_back({b,i});
query[b].push_back({a,i});
}
}
dfs(1,-1);//选择任意一个点为根节点,此处选1
tarjan(1);//从根节点开始tarjan
for(int i=0;i<m;i++){
cout<<res[i]<<endl;
}
return 0;
}
机房
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
typedef long long ll;
typedef pair<int,int> PII;
int h[N],e[N<<1],ne[N<<1],idx;
void add(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
int n,m;
vector<PII> query[N];
int res[N];
int dis[N];
int w[N];
void dfs(int u,int fa){
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==fa) continue;
dis[j]=dis[u]+w[j];
dfs(j,u);
}
}
int p[N];
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int st[N];
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(int i=0;i<query[u].size();i++){
int y=query[u][i].first,id=query[u][i].second;
if(st[y]==2){
int lca=find(y);
res[id]=dis[u]+dis[y]-2*dis[lca]+w[lca];
}
}
st[u]=2;
}
int main(){
cin>>n>>m;
memset(h,-1,sizeof h);
for(int i=1;i<=n;i++) p[i]=i;
for(int i=1;i<n;i++){
int x,y;cin>>x>>y;
add(x,y);add(y,x);
w[x]++;w[y]++;
}
for(int i=1;i<=m;i++){
int u,v;cin>>u>>v;
if(u==v){
res[i]=w[u];
continue;
}
query[u].push_back({v,i});
query[v].push_back({u,i});
}
dis[1]=w[1];
dfs(1,-1);
tarjan(1);
for(int i=1;i<=m;i++){
cout<<res[i]<<endl;
}
}
次小生成树
思路:① 首先Kruskal求出最小生成树,并建图
② 然后倍增法BFS预处理出depth,以及所有路径离根节点的最大边和次大边
③ 对于每个非树边,利用lca,求出这两点与公共祖先的路径上的最大边和次打边,然后替换
// 建树,建造出最小生成树
void build() {
memset(head, -1, sizeof head);
for (int i = 0; i < m; i ++)
if (edges[i].used) {
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
add(a, b, w);
add(b, a, w);
}
}
// 用最近公共祖先来求所有路径离根节点的最大的一条边,和次大的一条边
void bfs() {
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[n] = 1;
q[0] = n;
int hh = 0, tt = 0;
while (hh <= tt) {
int t = q[hh ++];
for (int i = head[t]; i != -1; i = ne[i]) {
int j = e[i];
if (depth[j] > depth[t] + 1) {
depth[j] = depth[t] + 1; // bfs更新层次
q[++ tt] = j; // 宽度优先
fa[j][0] = t; // 更新一下 fa(i, 0);
d1[j][0] = w[i], d2[j][0] = -INF; // 更新一下d1, d2
// 只有一条边不存在次长路
// 都已经更新到这个点了, 之前路径上的距离也都算出来了
// 所以都可以更新出d1, d2 fa,
for (int k = 1; k <= 16; k ++) { // 开始跳
int anc = fa[j][k - 1]; // 这anc是跳一半的位置
fa[j][k] = fa[anc][k - 1]; // 求得跳2^k之后的位置
// distance存储的是j开始跳一半的数据
// d1[j][k - 1] 跳一般的最大边
// d2[j][k - 1] 跳一半的次大边
// d1[anc][k - 1] 跳另一半的最大边
// d2[anc][k - 1] 跳另一半的次大边
int distance[4] = {d1[j][k - 1], d2[j][k - 1],
d1[anc][k - 1], d2[anc][k - 1]};
d1[j][k] = d2[j][k] = -INF; // 整条路径的最大值和次大值先初始化为-INF
for (int u = 0; u < 4; u ++) {
// 更新一下最大值和次大值
int d = distance[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;
}
}
}
}
}
}
// a -> b w是一条非树边
int lca(int a, int b, int w) {
static int distance[N * 2];
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]) {
distance[cnt ++] = d1[a][k]; // 每跳一次,记录一下这路径上的最大值
distance[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]) { // 把这所有东西都存下来
distance[cnt ++] = d1[a][k];
distance[cnt ++] = d2[a][k];
distance[cnt ++] = d1[b][k];
distance[cnt ++] = d2[b][k];
a = fa[a][k], b = fa[b][k];
}
// 已经找到公共祖先了,公共祖先就是 fa[a][0] = fa[b][0] = anc
// 把这边也存下来
distance[cnt ++] = d1[a][0];
distance[cnt ++] = d1[b][0];
}
// 找到公共祖先的
int dist1 = -INF, dist2 = -INF;
for (int i = 0; i < cnt; i ++) {
int d = distance[i];
// 这里规定dist1 一定严格大于 dist2 (dist2是严格次小值)
if (d > dist1) dist2 = dist1, dist1 = d;
// 如果这里 d = dist 而且 d = dist2, 令dist2 = d的话,就会有 dist1 = dist2
else if (d != dist1 && d > dist2) dist2 = d;
}
if (w > dist1) return w - dist1; // 看一下这个非树边是否大于dist1, 是的话就替换
// 返回变大了多少
// w 一定是 >= dist1 > dist2
// 如果第一个if不满足, 那一定有 w = dist1
if (w > dist2) return w - dist2; // 看一下这个非树边是否大于dist2, 是的话就替换
// 返回变大了多少
}
LL kruskal() {
for (int i = 1; i <= n; i ++ ) p[i] = i;
sort(edges, edges + m);
LL res = 0;
for (int i = 0; i < m; i ++) {
int a = find(edges[i].a), b = find(edges[i].b), w = edges[i].w;
if (a != b) {
res += w;
p[a] = b;
edges[i].used = true;
}
}
return res;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i ++) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edges[i] = {a, b, c};
}
LL sum = kruskal();
build();
bfs();
LL res = 1e18;
for (int i = 0; i < m; i ++) {
if (!edges[i].used) { // 枚举一下所有非树边
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
res = min(res, sum + lca(a, b, w)); // 看看替换后的结果, 取最小值
}
}
printf("%lld", res);
return 0;
}