#1067 : 最近公共祖先·二

题目链接   及  思路参考

输入

每个测试点(输入文件)有且仅有一组测试数据。

每组测试数据的第1行为一个整数N,意义如前文所述。

每组测试数据的第2~N+1行,每行分别描述一对父子关系,其中第i+1行为两个由大小写字母组成的字符串Father_i, Son_i,分别表示父亲的名字和儿子的名字。

每组测试数据的第N+2行为一个整数M,表示小Hi总共询问的次数。

每组测试数据的第N+3~N+M+2行,每行分别描述一个询问,其中第N+i+2行为两个由大小写字母组成的字符串Name1_i, Name2_i,分别表示小Hi询问中的两个名字。

对于100%的数据,满足N<=10^5,M<=10^5, 且数据中所有涉及的人物中不存在两个名字相同的人(即姓名唯一的确定了一个人),所有询问中出现过的名字均在之前所描述的N对父子关系中出现过,第一个出现的名字所确定的人是其他所有人的公共祖先

输出

对于每组测试数据,对于每个小Hi的询问,按照在输入中出现的顺序,各输出一行,表示查询的结果:他们的所有共同祖先中辈分最低的一个人的名字。


思路:

在最近公共祖先一中,每一次询问都查找一次。
询问量大时:求解最近公共祖先的离线算法。一次性收集若干询问之后才能通过这个算法一同计算出答案
深度优先搜索的过程中对结点染色,如果发现当前访问的结点是涉及到某个询问,那么就看这个询问中另一个结点的颜色,如果是白色,则留待之后处理,如果是灰色,那么最近公共祖先必然就是这个灰色结点,如果是黑色,那么最近公共祖先就是这个黑色结点向上的第一个灰色结点
通过graph进行dfs,edge存查询的所有连接(双向),Edge的id表示查询顺序(1,2...)v表示查询的另一点

结合并查集:每个结点最开始都是一个独立的集合,每当一个结点由灰转黑的时候,就将它所在的集合合并到其父亲结点所在的集合中去。这样无论什么时候,任意一个黑色结点所在集合的代表元素就是这个结点向上的第一个灰色结点

AC代码:

class Edge {
	int v,id;
	Edge(int v,int id){  
        this.v = v;  
        this.id = id;  
    }
}
public class Main {
	static int n;
	static int m;
	static int[] ans;
	static int[] father; //存father的id
	static boolean[] hasfa;
	static int[] color; // 表颜色,0白色,1灰色,2黑色
	static Map<Integer,List<Integer>> graph; // 存孩子
	static Map<Integer, List<Edge>> edge;
	
	static Map<String, Integer> idx; // string与id对应
	static int id = 0;
	static String[] str; // 输出时返回name
	
	public static void main(String[] args) {
		input();
		solve();
	}
	/*
	 * 输入
	 */
	public static void input() {
		Scanner in = new Scanner(System.in);
		n = Integer.parseInt(in.nextLine());
		idx = new HashMap<String, Integer>();
		hasfa = new boolean[n * 2]; // 注意hasfa的size有可能超过n + 1
		str = new String[n * 2];
		color = new int[n * 2];
		father = new int[n * 2];
		
		graph = new HashMap<Integer,List<Integer>>();
		edge = new HashMap<Integer, List<Edge>>();
		
		ans = new int[n];
		for (int i = 0; i < n; i++) {
			String[] s = in.nextLine().split(" ");
			int id1 = get_idx(s[0]);
			int id2 = get_idx(s[1]);
			hasfa[id2] = true;
			father[i] = i;
			if (!graph.containsKey(id1)) {
				List<Integer> l = new ArrayList<Integer>();
				graph.put(id1,l);
			}
			graph.get(id1).add(id2);
		}
		m = Integer.parseInt(in.nextLine());
		for (int i = 1; i <= m; i++) {
			String[] s = in.nextLine().split(" ");
			int idx1 = get_idx(s[0]);  
	        int idx2 = get_idx(s[1]);
	        if (!edge.containsKey(idx1)) {
	        	List<Edge> l = new ArrayList<Edge>();
	        	edge.put(idx1, l);
			}
	        edge.get(idx1).add(new Edge(idx2, i));
	        if (!edge.containsKey(idx2)) {
	        	List<Edge> l = new ArrayList<Edge>();
	        	edge.put(idx2, l);
			}
			edge.get(idx2).add(new Edge(idx1, i));
		}
		in.close();
	}
	public static void solve() {
		int root = -1;
		for (int i = 1; i <= n; i++) {
			if (!hasfa[i]) {
				root = i;
			}
		}
		tarjan(root); // 从根节点开始dfs
		// 输出结果
		for (int i = 1; i <= m; i++) {
			System.out.println(str[ans[i]]);
		}
	}
	public static void tarjan (int u) {
		color[u] = 1;
		if (edge.containsKey(u)) {
			for(int i = 0; i < edge.get(u).size(); i++) { // 遍历所有查询,是否有当前id
				int ID = edge.get(u).get(i).id; // 指查询顺序,1开始
				if (ans[ID] != 0) { 
					continue;
				}
				int v = edge.get(u).get(i).v;
				// 0代表未访问过
				if (color[v] == 0) {
					continue;
				} else if (color[v] == 1) { // 1代表正在访问,所以u和v的公共祖先就是v
					ans[ID] = v;
				} else if (color[v] == 2) { // 2表示访问完毕的节点,所以u和v的公共祖先就是v当前的祖先
					ans[ID] = Find(v);
				}
			}
		}
		if (!graph.containsKey(u)) {
			return;
		}
		for (int i = 0; i < graph.get(u).size(); i++) {
			int vv = graph.get(u).get(i);
			tarjan(vv);
			// 一个节点dfs完毕,标记vv为访问完毕的点,并更新父节点
			color[vv] = 2;
			father[vv] = u; // 注意这里更新father
		}
	}
	/*
	 * 寻找x的祖先
	 */
	public static int Find(int x) {
		return x != father[x] ? father[x] = Find(father[x]) : father[x]; 
	}
	// 获得name对应的id
	public static int get_idx(String name){  
	    if(idx.containsKey(name))  
	        return idx.get(name);  
	    idx.put(name, ++id);
	    str[id] = name;
	    return id;
	}
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值