LCA问题的小思考

123 阅读5分钟

leetcode 1257

  1. java的ACM模式读取数据
  2. 遍历 + hashmap辅助建立多叉树(一边遍历一边更新上下文)
  3. 多叉树的dfs
  4. 多叉树的LCA问题

换汤不换药 T235 二叉树的LCA

T236 二叉搜索树的LCA

本题: 多叉树的LCA

思路

  1. 多叉树的递归模版(dfs配合for循环)
  2. 管多个children要结果,如果只有一个非空的child返回值,返回该值,如果有两个,返回自己。证明: 不可能出现 > 2个非空子过程返回结果。

case

  1. 如果见到root为p或q之一,为什么不需要继续递归,也能保证结果正确

证明: 如果见到p或者q之一为root本身,那么另一个node要么位于递归的另一半(root的兄弟,或者root的非直系长辈),要么另一个node就在当前root的子过程中。对于后者明显成立,因为这种情况下LCA(p, q)就是p或者q。对于前者,则是我们递归的通用流程

  1. 返回root的另一个条件是,p和q 都出现在当前root的children里。这个很好理解。我们来看看"如果子过程仅有一个返回结果,则返回该结果"的含义。

我们知道,如果只有一个子过程有返回值,其他都为null,此时意味着只在这一个子过程上找到了结果。我们要理解这个结果意味着 要么是p在这个子过程上,要么是q在这个子过程上,要么这个结果就是p和q的LCA,所以这个结果要么是未来有用,要么是已经计算完的结果了,无论如何传上去就行。

实际上,递归就是一个"面向结果"然后凑过程的东西,我们假设递归就能返回这样的结果,也就是递归的返回值就意味着要么是p在这个子过程上,要么是q在这个子过程上,要么这个结果就是p和q的LCA(没错,就是先假设他会这么返回,然后再去凑他为什么会这么返回,其实就是递推式)。

import java.io.*;
import java.util.*;

public class leetcode1257 {

    // 多叉树
    public static class TreeNode {
        String val; // region name

        public TreeNode[] children; // java不存在所谓 "incomplete type"

        public TreeNode(String val) {
            this.val = val;
            this.children = new TreeNode[0];
        }


    }

    public static void main(String[] args) {
        // 最小公共区域
        FastReader in = new FastReader();
        PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));

        int lines = in.nextInt();
        String[][] ss = new String[lines][];
        for (int i = 0; i < lines; i++) {
            String[] cities = in.nextLine().split(",");
            ss[i] = cities;
        }

        for (String[] s : ss) {
            System.out.println(Arrays.toString(s));
        }

        String r1 = in.nextLine();
        String r2 = in.nextLine();

        System.out.println(r1);
        System.out.println(r2);

        // 1. 把String[][]组织为树形结构
        // 这明显是一个多叉树
        // 定义多叉树结构,遍历 + hashmap辅助建树

        TreeNode root = null;
        // hashmap 用于跟踪上下文的node
        Map<String, TreeNode> tm = new HashMap<>();

        for (int i = 0; i < ss.length; i++) {
            String[] s = ss[i];

            // 如果已经在map里存在,则不新建

            TreeNode rt = tm.getOrDefault(s[0], null) == null ? new TreeNode(s[0]) : tm.get(s[0]);
            tm.put(s[0], rt);
            if (i == 0) {
                root = rt;
            }
            TreeNode[] children = new TreeNode[s.length - 1];
            for (int j = 1; j < s.length; j++) {
                TreeNode nd = new TreeNode(s[j]);
                tm.put(s[j], nd);
                children[j-1] = nd;
            }
            rt.children = children;
        }

//        dfs(root);

        System.out.println(LCA(root, tm.get(r1), tm.get(r2)).val);
    }

    static List<TreeNode> path = new ArrayList<>();

    public static TreeNode LCA(TreeNode rt, TreeNode r1, TreeNode r2) {
        // 多叉树公共祖先

        // 1. 如果rt就是r1或r2,直接返回
        // 此时不需要继续向下递归,因为此时要么另一个node在当前node 的子过程里,此时结果就是当前node
        // 要么另一个node位于子树的另一部分,将由另一半递归返回结果

        if (rt == r1 || rt == r2) {
            return rt;
        }

        // 否则 遍历所有孩子
        int retNum = 0;
        TreeNode rootRet = null;

        /*
            如果有两个child返回非空,则表示root为 r1和r2的祖先
                           root
                      /  ... | ... \
                   child1(include q) ....... childn(include p)

                   如果child_i存在q,child_j存在p,那p和q的最近祖先肯定是root。
                   那么对于root的父过程,肯定只有root这一个孩子给他返回结果,因为p和q都在 root里,就不可能在他的兄弟上。
                   因此,如果只有一个child_i返回非空,那么就直接返回这个非空的结果

                   如果所有孩子都不是非空,则说明整个树都没有合法的结点
         */
        for (TreeNode child : rt.children) {
            // 我们知道,**最多只有两个** child会返回非空的结果
            // 因为我们只寻找两个node,也就是r1和r2,两个node不可能出现在3棵树里。
            // 相反,可能当前root的所有子树里都没有r1和r2,此时就返回null,若只有一个子树有,就返回有结果的那个结果
            // 如果两个子树存在结果,则返回自己。
            TreeNode ret = LCA(child, r1, r2);

            if (ret != null) {
                if (retNum == 0) {
                    // 如果此时自己是第一个
                    rootRet = ret;
                } else {
                    // 此时p和q分别位于root的两个子孩子上, 返回root为LCA
                    rootRet = rt;
                }
                retNum++;
            }

        }

        // 遍历完所有的结点后,将结果返回父过程
        return rootRet;
    }

    public static void dfs(TreeNode rt) {
        if (rt == null) {
            return;
        }
        // 多叉树的递归,for循环退出 = 对root为null判断

        // for循环这个代码块代表以rt为结点的所有孩子,for循环的child之间彼此为兄弟关系
        // 应该避免兄弟之间互相影响,所以选择回溯法
        path.add(rt);
        for (TreeNode child : rt.children) {
            dfs(child);
        }
        System.out.println("此时的dfs路径:");
        for (TreeNode nd : path) {
            System.out.printf(nd.val + " ");
        }
        System.out.println("");
        path.remove(rt);
    }

}

class FastReader {
    // 简单理解就是一个词法分析器
    StringTokenizer st;
    // 简单理解就是一个带缓冲的文件句柄
    BufferedReader br;

    public FastReader() {
        br = new BufferedReader(new InputStreamReader(System.in));
    }

    String next() {
        while (st == null || !st.hasMoreElements()) {
            try {
                st = new StringTokenizer(br.readLine());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return st.nextToken();
    }

    int nextInt() {
        return Integer.parseInt(next());
    }

    long nextLong() {
        return Long.parseLong(next());
    }

    double nextDouble() {
        return Double.parseDouble(next());
    }

    String nextLine() {
        String str = "";
        try {
            str = br.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return str;
    }
}