图包括顶点和边,图分为有向图和无向图。边还可以根据顶点之间的关系设置不同的权重,默认权重相同皆为1。对有向图而言,图中有效信息除了从顶点“指出去”的信息,还包括从别的顶点“指进来”的信息。这里的“指出去”和“指进来”的数量可以用出度和入度来表示。关于图的解释可参考业余码农
图的存储方式:
1.邻接矩阵,通过顶点的二维矩阵来存储两个顶点之间是否连接。
2.邻接表可以只存储相连顶点关系。
1.克隆图(medium)
题目:给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。
图中的每个节点都包含它的值 val(int) 和其邻居的列表(list[Node])。
class Node {
public int val;
public List neighbors;
}
思路:采用深度优先搜索的方法,为了避免多次遍历同一个节点,采用哈希表来存储已经访问过的节点。哈希表中的key 是原始图中的节点,value 是克隆图中的对应节点。
从给定的节点开始遍历图,如果该节点已经遍历过则直接返回克隆图的对应的节点。如果没有,则创建克隆节点,并将其存在哈希表中。然后递归调用每个节点的邻接点,最后返回这些克隆邻接点的列表。
class Solution:
def cloneGraph(self, node: 'Node') -> 'Node':
tmp = {} #key 是原始图中的节点,value 是克隆图中的对应节点。
if not node:
return
def dfs(node):
if node in tmp:
return tmp[node] #返回其克隆图中的对应节点。
clone = Node(node.val, []) #创建克隆节点
tmp[node] = clone #存储在哈希表中
for i in node.neighbors: #递归调用每个节点的邻接点
clone.neighbors.append(dfs(i))
return clone
return dfs(node)
2.课程表(medium)
题目:你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]
给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?
示例 1:
输入: 2, [[1,0]]
输出: true
解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。
思路:这是一道典型的拓扑排序的问题。
拓扑排序定义:给定一个有向无环图 G,将G中所有顶点排成一个线性序列,使得图G 中任意一对顶点u和v,u 在排列中都出现在 v的前面。
拓扑排序的解题思路:先统计所有节点的入度,对入度为0的先取出来,再把这个节点指向的其他节点的入度减一,不断重复,直到所有的节点都被分离出来。如果最后不存在入度为0的节点,说明不是有向无环图,则不存在拓扑排序。
本题采用广度优先搜索和拓扑排序相结合。
1)用一个字典保存节点与其邻节点的关系,字典的键为当前节点,值是一个数组存放其相邻的节点。并采用一个数组来存储对应节点的入度。
2)先考虑拓扑排序中最前面的点,其入度为0,并将其加入队列中,即他没有任何先修课程; 在广度优先搜索的每一步中,我们取出队首的节点 u:当把u加入到答案中,则其相邻的节点v就少了一门先修课程的要求【即入度-1】,直到相邻节点的入度为0,则可以开始学习这门课程v。以此类推,将入度为0 的节点加入答案,知道答案中包含了所有的节点,或者不存在入度为0的节点(图中包含环)
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
edges = collections.defaultdict(list)
indeg = [0] * numCourses #记录要学习课程,需要先学习的课程数
for cur,pre in prerequisites: #统计各节点的入度
edges[cur].append(pre)
indeg[cur] += 1
q = collections.deque()
for u in range(numCourses): #将入度为0的节点入队列
if indeg[u] == 0:
q.append(u)
while q:
numCourses-=1
u = q.popleft() #从队列中取出节点
for v in edges[u]: #将该节点的邻接节点入度-1
indeg[v] -= 1
if indeg[v] == 0: #当其邻接节点入度为0时,将该节点入队
q.append(v)
return not numCourses #当所有的节点都已经加入答案这说明所有课程都可以学
3.课程表 || (medium)
题目:现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。
可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
示例 1:
输入: 2, [[1,0]]
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
思路:就是在上题的基础上将每次出队的节点保存下来
import collections
class Solution:
def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
edge = collections.defaultdict(list)
indeg = [0] * numCourses
for cur,pre in prerequisites:
edge[pre].append(cur)
indeg[cur]+=1
q = collections.deque()
for u in range(numCourses):
if indeg[u] == 0:
q.append(u)
res = []
while q:
numCourses -=1
u = q.popleft()
res.append(u)
for v in edge[u]:
indeg[v]-=1
if indeg[v] == 0:
q.append(v)
if numCourses:
return []
else:
return res
4.钥匙和房间(medium)
题目:有 N 个房间,开始时你位于 0 号房间。每个房间有不同的号码:0,1,2,…,N-1,并且房间里可能有一些钥匙能使你进入下一个房间。在形式上,对于每个房间 i 都有一个钥匙列表 rooms[i],每个钥匙 rooms[i][j] 由 [0,1,…,N-1] 中的一个整数表示,其中 N = rooms.length。 钥匙 rooms[i][j] = v 可以打开编号为 v 的房间。最初,除 0 号房间外的其余所有房间都被锁住。你可以自由地在房间之间来回走动。如果能进入每个房间返回 true,否则返回 false。
示例 1:
输入: [[1],[2],[3],[]]
输出: true
解释:
我们从 0 号房间开始,拿到钥匙 1。
之后我们去 1 号房间,拿到钥匙 2。
然后我们去 2 号房间,拿到钥匙 3。
最后我们去了 3 号房间。
由于我们能够进入每个房间,我们返回 true。
思路:第n个房间有第m个房间的钥匙时,就有从n–>m房间,这总个M间房间可看成是有向图中的M个节点,n–>m可看成是一条有向边。问题就转化为求从第一个节点开始是否能遍历完所有节点。采用深度优先搜索的方法遍历整张图,统计下到达的节点数,判断其是否等于节点总数。考虑到节点是否已经被访问过(重复),可以用一个集合记录访问过的节点
class Solution:
def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
def dfs(n):
nonlocal num
num+=1
vis.add(n)
for i in rooms[n]:
if i not in vis:
dfs(i)
vis = set()
num = 0
length = len(rooms)
dfs(0)
return num==length