6. LintCode 宽度优先搜索(二)
[5. LintCode 宽度优先搜索(一)]·(https://blog.csdn.net/weixin_42183401/article/details/111332644)
LintCode 615:课程表
https://www.lintcode.com/problem/course-schedule
描述
现在你总共有 n 门课需要选,记为 0
到 n - 1
.
一些课程在修之前需要先修另外的一些课程,比如要学习课程 0 你需要先学习课程 1 ,表示为[0,1]
给定n门课以及他们的先决条件,判断是否可能完成所有课程?
样例
输入: n = 2, prerequisites = [[1,0]]
输出: true
输入: n = 2, prerequisites = [[1,0],[0,1]]
输出: false
解题思路
拓扑排序,不能有环,和https://www.lintcode.com/problem/topological-sorting 题一样的解法,最后如果有课程入度不为0则表示不能完成所有课程。
AC代码
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 存储每个课程的入度(直接用数组更好)
Map<Integer, Integer> map = new HashMap<>();
// 存储每个课程的后置课程
Map<Integer, Set<Integer>> dict = new HashMap<>();
// 获取每个课程的入度和其后置课程
for (int[] arr : prerequisites) {
int x = arr[0];
int y = arr[1];
if (!dict.containsKey(y)) {
Set<Integer> set = new HashSet<>();
dict.put(y, set);
}
// 数据中有重复的边。。需要去重
if (dict.get(y).contains(x)) {
continue;
}
if (!map.containsKey(x)) {
map.put(x, 0);
}
map.put(x, map.get(x) + 1);
if (!map.containsKey(y)) { map.put(y, 0); }
dict.get(y).add(x);
}
// 宽搜队列,入度为0的课程首先加入队列
Deque<Integer> deque = new ArrayDeque<>();
for (Integer course : map.keySet()) {
if (map.get(course) == 0) {
deque.add(course);
}
}
// 简单宽搜过程,当一个课程入度为0了加入队列
while (!deque.isEmpty()) {
int course = deque.removeFirst();
// 取出的课程从map中删除,当map中没有课程了时表明所有课程都可以被选上
map.remove(course);
if (!dict.containsKey(course)) {
continue;
}
for (Integer x : dict.get(course)) {
map.put(x, map.get(x) - 1);
if (map.get(x) == 0) {
deque.add(x);
}
}
}
// 返回答案
return map.size() == 0;
}
LintCode 618:搜索图中节点
https://www.lintcode.com/problem/search-graph-nodes
描述
给定一张 无向图
,一个 节点
以及一个 目标值
,返回距离这个节点最近 且 值为目标值的节点,如果不能找到则返回 NULL
。
在给出的参数中, 我们用 map
来存节点的值
保证答案唯一
样例
输入:
{1,2,3,4#2,1,3#3,1,2#4,1,5#5,4}
[3,4,5,50,50]
1
50
输出:
4
解释:
2------3 5
\ | |
\ | |
\ | |
\ | |
1 --4
Give a node 1, target is 50
there a hash named values which is [3,4,10,50,50], represent:
Value of node 1 is 3
Value of node 2 is 4
Value of node 3 is 10
Value of node 4 is 50
Value of node 5 is 50
Return node 4
解题思路
标准的宽搜题,每次从节点扩展它能到达的节点,为target直接返回节点,不是加入宽搜队列。
AC代码
public UndirectedGraphNode searchNode(ArrayList<UndirectedGraphNode> graph,
Map<UndirectedGraphNode, Integer> values,
UndirectedGraphNode node,
int target) {
// 出发点的value便是target直接返回
if (values.get(node) == target) {
return node;
}
// 宽搜队列
Deque<UndirectedGraphNode> deque = new ArrayDeque<>();
deque.add(node);
// 记录已经走过的节点
Set<Integer> set = new HashSet<>();
while (!deque.isEmpty()) {
// 取出节点
UndirectedGraphNode unode = deque.removeFirst();
set.add(unode.label);
// 拓展
for (UndirectedGraphNode node1 : unode.neighbors) {
if (values.get(node1) == target) {
return node1;
}
if (!set.contains(node1.label)) {
deque.add(node1);
}
}
}
return null;
}
LintCode 605:序列重构
https://www.lintcode.com/problem/sequence-reconstruction
描述
判断是否序列 org
能唯一地由 seqs
重构得出. org
是一个由从1到n的正整数排列而成的序列,1 \leq n \leq 10^41≤_n_≤104。 重构表示组合成seqs
的一个最短的父序列 (意思是,一个最短的序列使得所有 seqs
里的序列都是它的子序列).
判断是否有且仅有一个能从 seqs
重构出来的序列,并且这个序列是org
。
样例
输入:org = [1,2,3], seqs = [[1,2],[1,3]]
输出: false
解释:
[1,2,3] 并不是唯一可以被重构出的序列,还可以重构出 [1,3,2]
输入: org = [1,2,3], seqs = [[1,2],[1,3],[2,3]]
输出: true
解释:
序列 [1,2], [1,3], 和 [2,3] 可以唯一重构出 [1,2,3].
输入:org = [4,1,5,2,6,3], seqs = [[5,2,6,3],[4,1,5,2]]
输出:true
解题思路
拓扑排序
观看题目可以想象 seqs
的一个序列可以构成一个顺序,所有序列加起来就能构成一个拓扑排序。
而 org
要是唯一的序列,那么seqs
只能 构成一个拓扑排序,如果有多的拓扑排序那便不能唯一。
AC代码
写的比较垃圾。。。。
public boolean sequenceReconstruction(int[] org, int[][] seqs) {
// 边界条件
if (org.length < 1) { return true;}
// 存储拓扑顺序
Map<Integer, List<Integer>> map = new HashMap<>();
// 存储每个点的入度
int[] in = new int[org.length + 1];
// 预处理,有不在org序列中的数或者seqs长度不够org可以直接返回false。
int length = 0;
for (int[] arr : seqs) {
for (int i = 1; i < arr.length; i++) {
int x = arr[i - 1], y = arr[i];
if (y > org.length) {
return false;
}
if (!map.containsKey(x)) {
map.put(x, new ArrayList<>());
}
// 存储顺序和入度
map.get(x).add(y);
in[y]++;
}
// 记录seqs长度
length += arr.length;
if (arr.length > 0 && arr[0] > org.length) {
return false;
}
}
//seqs长度不够org可以直接返回false
if (length < org.length) {
return false;
}
// 宽搜队列,按找org序列顺序进出队列。
Deque<Integer> deque = new ArrayDeque<>();
int index = 0;
deque.add(org[index]);
while (!deque.isEmpty()) {
int x = deque.removeFirst();
index++;
// index 等于 org.length表明org序列已经走完且唯一返回true
if (index == org.length) {
return true;
}
// x 没有扩展了,而org还没结束,返回false
if (!map.containsKey(x)) {
return false;
}
int next = org[index];
for (Integer y : map.get(x)) {
in[y]--;
// 不是 org序列的下一个元素入度为 0 代表拓扑顺序不唯一,返回false
if (in[y] == 0 && y != next) {
return false;
}
}
// 序列的下一个不为0代表序列不成功,返回false
if (in[next] != 0) {
return false;
}
deque.add(next);
}
return false;
}
LintCode 598:僵尸矩阵
https://www.lintcode.com/problem/zombie-in-matrix
描述
给一个二维网格,每一个格子都有一个值,2
代表墙,1
代表僵尸,0
代表人类(数字 0, 1, 2)。僵尸每天可以将上下左右最接近的人类感染成僵尸,但不能穿墙。将所有人类感染为僵尸需要多久,如果不能感染所有人则返回 -1
。
样例
输入:
[[0,1,2,0,0],
[1,0,0,2,1],
[0,1,0,0,0]]
输出:
2
输入:
[[0,0,0],
[0,0,0],
[0,0,1]]
输出:
4
解题思路
简单宽搜题,首先将僵尸加入宽搜队列,然后每步将队列的僵尸都去感染附件的人类,已经传播过的僵尸设为2。避免重复感染。
最后还有0表示不能感染所有人,否则返回宽搜了多少次。
AC代码
public class Solution {
public int zombie(int[][] grid) {
// 宽搜队列
Deque<Point> deque = new ArrayDeque<>();
// 预处理,将初始僵尸加入宽搜队列
int n = grid.length, m = grid[0].length;
int count = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 0) {
count++;
} else if (grid[i][j] == 1) {
Point point = new Point(i, j);
deque.add(point);
// 已经传播过的僵尸设为2
grid[i][j] = 2;
}
}
}
// 方便计算上下左右
int[] a = {0, 0, 1, -1};
int[] b = {1, -1, 0, 0};
int day = 0;
while (!deque.isEmpty()) {
day++;
int size = deque.size();
for (int i = 0; i < size; i++) {
Point point = deque.removeFirst();
for (int j = 0; j < 4; j++) {
int x = point.x + a[j];
int y = point.y + b[j];
if (x >= n || x < 0 || y >= m || y < 0 || grid[x][y] == 2) {
continue;
}
count--;
grid[x][y] = 2;
deque.add(new Point(x, y));
}
}
if (count == 0) {
return day;
}
}
return count > 0 ? -1 : day;
}
}
class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
LintCode 178:图是否是树
https://www.lintcode.com/problem/graph-valid-tree
描述
给出 n
个节点,标号分别从 0
到 n - 1
并且给出一个 无向
边的列表 (给出每条边的两个顶点), 写一个函数去判断这张`无向`图是否是一棵树
你可以假设我们不会给出重复的边在边的列表当中. 无向
边 [0, 1]
和 [1, 0]
是同一条边, 因此他们不会同时出现在我们给你的边的列表当中。
样例
输入: n = 5 edges = [[0, 1], [0, 2], [0, 3], [1, 4]]
输出: true.
输入: n = 5 edges = [[0, 1], [1, 2], [2, 3], [1, 3], [1, 4]]
输出: false.
解题思路
BFS
一棵n个节点的树只有n-1条边,当一个图有n-1条边且没有环时便是一棵树。
对输入预处理存储图的关系后,随便从一个节点宽搜,最后队列能走过n个点便是没有环的图,也就是树。
并查集
用并查集合并相连的点,遍历每条边,合并前判断两个节点是否属于同一个集合,属于同一个集合表明有环,不是树;不是同一个集合则合并。
最后如果所有点都在一个集合中,则说明这个图联通且无环,是一棵树。
AC代码
public boolean validTree(int n, int[][] edges) {
// 不是n-1条边便不是一棵树。
if (n == 0 || edges.length != n - 1) {
return false;
}
Map<Integer, Set<Integer>> map = new HashMap<>();
Set<Integer> set = new HashSet<>();
Deque<Integer> deque = new ArrayDeque<>();
for (int[] arr : edges) {
int x = arr[0], y = arr[1];
if (!map.containsKey(x)) {map.put(x, new HashSet<>());}
if (!map.containsKey(y)) {map.put(y, new HashSet<>());}
map.get(x).add(y);
map.get(y).add(x);
}
// 标准宽搜过程,走过的节点都加入set中
deque.add(0);
while (!deque.isEmpty()) {
int x = deque.removeFirst();
set.add(x);
if (!map.containsKey(x)) {
continue;
}
for (Integer next : map.get(x)) {
if (!set.contains(next)) {
set.add(next);
deque.add(next);
}
}
}
// 宽搜没有走完n个节点表明有环,不是树
return set.size() == n;
}
LintCode 892:外星人字典
https://www.lintcode.com/problem/alien-dictionary
描述
有一种新的使用拉丁字母的外来语言。但是,你不知道字母之间的顺序。你会从词典中收到一个非空的单词列表,其中的单词在这种新语言的规则下按字典顺序排序。请推导出这种语言的字母顺序。
- 你可以假设所有的字母都是小写。
- 如果a是b的前缀且b出现在a之前,那么这个顺序是无效的。
- 如果顺序是无效的,则返回空字符串。
- 这里可能有多个有效的字母顺序,返回以正常字典顺序看来最小的。
样例
输入:["wrt","wrf","er","ett","rftt"]
输出:"wertf"
解释:
从 "wrt"和"wrf" ,我们可以得到 't'<'f'
从 "wrt"和"er" ,我们可以得到'w'<'e'
从 "er"和"ett" ,我们可以得到 get 'r'<'t'
从 "ett"和"rftt" ,我们可以得到 'e'<'r'
所以返回 "wertf"
输入:["z","x"]
输出:"zx"
解释:
从 "z" 和 "x",我们可以得到 'z' < 'x'
所以返回"zx"
解题思路
拓扑排序。每两个单词之间可以确定一个顺序,这样就可以得到拓扑排序的一个指向。例如 “wrt"和"wrf”,就可以得到 t -> f。
两个单词中的第一个相同位置不同字母可以确定出一个指向。
得到所有指向后便可整理出一个拓扑顺序,判断是否可以排出一个字典顺序(有环便没有)。
AC代码
// 九章
public String alienOrder(String[] words) {
// 先构造拓扑排序的每一条边
Map<Character, Set<Character>> graph = constructGraph(words);
if (graph == null) {
return "";
}
// 宽搜找寻拓扑顺序
return topologicalSorting(graph);
}
// 建图,找出拓扑排序中的每一条边,也就是字母的顺序
private Map<Character, Set<Character>> constructGraph(String[] words) {
Map<Character, Set<Character>> graph = new HashMap<>();
// 先给每个字母初始化
for (int i = 0; i < words.length; i++) {
for (int j = 0; j < words[i].length(); j++) {
char c = words[i].charAt(j);
if (!graph.containsKey(c)) {
graph.put(c, new HashSet<>());
}
}
}
for (int i = 0; i < words.length - 1; i++) {
int index = 0;
while (index < words[i].length() && index < words[i + 1].length()) {
// 找到第一个字母不相同的位置,建立边
if (words[i].charAt(index) != words[i + 1].charAt(index)) {
graph.get(words[i].charAt(index)).add(words[i + 1].charAt(index));
break;
}
index++;
}
// 如果长单词前缀是短单词,并且短单词还排在长单词后面,表明无解。
if (index == Math.min(words[i].length(), words[i + 1].length())) {
if (words[i].length() > words[i + 1].length()) {
return null;
}
}
}
return graph;
}
// 获取每个字母的入度,同之前的拓扑排序一样
private Map<Character, Integer> getIndegree(Map<Character, Set<Character>> graph) {
Map<Character, Integer> indegree = new HashMap<>();
for (Character u : graph.keySet()) {
indegree.put(u, 0);
}
for (Character u : graph.keySet()) {
for (Character v : graph.get(u)) {
indegree.put(v, indegree.get(v) + 1);
}
}
return indegree;
}
private String topologicalSorting(Map<Character, Set<Character>> graph) {
Map<Character, Integer> indgree = getIndegree(graph);
// 优先队列,因为要返回正常字典顺序看起来最小的
Queue<Character> queue = new PriorityQueue<>();
// 将入度为0的单词都加入队列中
for (Character u : indgree.keySet()) {
if (indgree.get(u) == 0) {
queue.offer(u);
}
}
// 标准宽搜过程
StringBuilder sb = new StringBuilder();
while (!queue.isEmpty()) {
Character head = queue.poll();
sb.append(head);
for (Character neighbor : graph.get(head)) {
indgree.put(neighbor, indgree.get(neighbor) - 1);
if (indgree.get(neighbor) == 0) {
queue.offer(neighbor);
}
}
}
// 表明有环
if (sb.length() != indgree.size()) {
return "";
}
return sb.toString();
}