思路:

题目的主要信息:

  • 有两个结点数都为n,根都是1的树,设根的深度为0
  • 定义点对(x,y)的价值为树1中x和y的最近公共祖先的深度+树2中a[x]和a[y]的最近公共祖先的深度
  • 输出所有点对的最大值价值数组b与数组c分别记录树1与树2的各个节点的父节点

方法一:暴力解法(超时)
具体做法:
我们可以遍历树1的每一个点对,求得其在树1中得最近公共祖先深度,然后再用数组a转换后去求这两个点在树2中的公共祖先深度,二者相加,维护最大值。求LCA的时候,我们也是从这个节点依次往上遍历直到根,我们记录这其中的路径,然后从上往下最后一个相同的节点就是最近公共节点,记录其深度即可。

class Solution {
public:
    int LCA(int x, int y, vector<int>& z){ //暴力LCA,记录祖先路径再从上往下找最近的公共祖先
        if(x == 0)
            return 0;
        vector<int> path1; //记录节点x的路径
        vector<int> path2; //记录节点y的路径
        int temp = x;
        while(x != 0){ //x遍历祖先直到0
            if(z[x] != 0 && z[x] != y){
                x = z[x];
                path1.push_back(x);  //记录往上的路径
            }else if(z[x] == y)
                return 0;
            else if(z[x] == 0)
                break;
        }
        path1.push_back(0);
        while(y != 0){ //y遍历祖先直到0
            if(z[y] != 0 && z[y] != temp){
                y = z[y];
                path2.push_back(y); //记录往上的路径
            }else if(z[y] == temp)
                return 0;
            else if(z[y] == 0)
                break;
        }
        path2.push_back(0);
        int res = -1;
        int n = path1.size() - 1;
        int m = path2.size() - 1;
        while(n >= 0 && m >= 0 && path1[n] == path2[m]){ //从上往下找到第一个不同的祖先
            n--;
            m--;
            res++;
        }
        return res;
    }
    int wwork(int n, vector<int>& a, vector<int>& b, vector<int>& c) {
        int res = 0;
        for(int i = 1; i < n; i++){
            for(int j = i + 1; j <= n; j++){ //遍历每一个点对
                int temp = (LCA(i, j, b) + LCA(a[i], a[j], c)); //计算价值
                res = temp > res ? temp : res; //维护最大值
            }
        }
        return res;
    }
};

复杂度分析:

  • 时间复杂度:,遍历所有点对,每个点对暴力求解LCA平均为,但是最坏情况可以到
  • 空间复杂度:,记录路径的辅助数组长度最长为(退化为链表)

方法二:DFS + LCA
具体做法:
我们首先利用dfs遍历树2,找到节点的遍历次序,及从次序到到节点的关系,再记录树2每个节点的深度,然后记录树2祖先的祖先,为LCA倍增算法做预处理。用set维护树2的dfs次序建立虚树,利用dfs遍历树1枚举其每一个LCA,不断合并树2虚树的集合,其中set的lower_bound对应LCA最大的点,既可以找到我们所求的最大价值。
对于上述方法一求LCA的暴力算法,我们每次条一步,直到到根节点,但是对于倍增算法,我们对应的则是走1步、2步、4步……以此来倍增,通过dfs遍历,记录每个节点的次序及深度,并初始化求出树上每个节点u的祖先father[u][i]。求最近公共祖先时,根据两个节点的的深度,如不同,向上调整深度大的节点,使得两个节点在同一层上,如果正好是祖先结束,否则,将两个节点同时上移,查询最近公共祖先。(具体可以查看注释,注释很详细)
图片说明

class Solution {
public:
    vector<vector<int> > edge; //保存边
    vector<set<int>> Q;
    vector<vector<int>> father;
    vector<int> dfn, redfn, depth;
    int res = 0;
    int count = 0;
    void dfs1(int v, int deep){
        count++;
        dfn[v] = count; //dfs遍历次序
        redfn[count] = v; //由次序到节点
        depth[v] = deep; //记录树2某个节点的深度
        for(int i = 0; i < edge[v].size(); i++){
            dfs1(edge[v][i], deep + 1);
            father[edge[v][i]][0] = v; //记录父节点
        }
    }
    int LCA(int a, int b){ //倍增寻找最近公共祖先
        if(depth[a] < depth[b]) //a一定在下面
            swap(a, b);
        for(int i = 17; i >= 0; i--)
            if(depth[a] - depth[b] >= 1 << i) //找父亲节点
                a = father[a][i];
        if(a == b)
            return a;
        for(int i = 17; i >= 0; i--){
            if(father[a][i] != father[b][i]){   //从最大祖先开始,判断a,b祖先,是否相同
                a = father[a][i];  //如不相同,a b同时向上移动2^i
                b = father[b][i];
            }
        }
        return father[a][0];
    }
    int merge(set<int>& A, set<int>& B){ //合并AB集合
        if(A.size() < B.size())
            A.swap(B); //A为大集合,B为小集合
        int best = 0;
        for(int i: B){
            auto iter = A.lower_bound(i); //二分查找,找到A中第一个大于i的
            if(iter != A.end()) //找到
                best = max(best, depth[LCA(redfn[i], redfn[*iter])]);
            if(iter != A.begin()){ //不是第一个,向前缩
                iter--;
                best = max(best, depth[LCA(redfn[i], redfn[*iter])]);
            }
            A.insert(i);
        }
        B.clear();
        return best;
    }
    void dfs2(int v, int deep){ //遍历树1的节点
        int best = 0;
        for(int i = 0; i < edge[v].size(); i++){ 
            dfs2(edge[v][i], deep + 1);
            best = max(best, merge(Q[v], Q[edge[v][i]]));
        }
        res = max(res, best + deep); //最近的公共祖先加上本来的层
    }
    int wwork(int n, vector<int>& a, vector<int>& b, vector<int>& c) {
        edge.resize(n + 1);
        dfn.resize(n + 1, 0);
        redfn.resize(n + 1, 0);
        depth.resize(n + 1, 0);
        Q.resize(n + 1);
        father.resize(n + 1, vector<int>(18, 0)); //测试数据树最大深度不超过18
        for(int i = 2; i <= n; i++)
            edge[c[i]].push_back(i); //树2的父节点连接子节点的边
        dfs1(1, 0); // 对树2深度优先搜索记录其父节点
        for(int i = 1; i <= 17; i++) //初始化LCA
            for(int j = 1; j <= n; j++)
                father[j][i] = father[father[j][i - 1]][i - 1]; //往后依次是往上的祖先节点
        for(int i = 0; i <= n; i++)  //清空树2的边
            edge[i].clear();
        for(int i = 2; i <= n; i++)
            edge[b[i]].push_back(i); //树1的父节点连接子节点的边
        for(int i = 1; i <= n; i++)
            Q[i].insert(dfn[a[i]]); //树2对应节点进入集合
        dfs2(1, 0);
        return res;
    }
};

复杂度分析:

  • 时间复杂度:,主体函数循环都是,两次dfs也都是,合并函数为,LCA函数为
  • 空间复杂度:,各个一维辅助数组的长度都为,二维的辅助数组第二维都是常数,故看成一维