圖的問題基本就是 BFS和DFS,還有拓撲排序、最短路、最小生成樹,有時也會用並查集進行分類,還要注意節點的入度出度等特徵。
文章目錄
- [207. 課程表](https://leetcode-cn.com/problems/course-schedule/)
- [210. 課程表 II](https://leetcode-cn.com/problems/course-schedule-ii/)
- [684. 冗餘連接](https://leetcode-cn.com/problems/redundant-connection/)
- [785. 判斷二分圖](https://leetcode-cn.com/problems/is-graph-bipartite/)
- [997. 找到小鎮的法官](https://leetcode-cn.com/problems/find-the-town-judge/)
- [841. 鑰匙和房間](https://leetcode-cn.com/problems/keys-and-rooms/)
- [面試題 04.01. 節點間通路](https://leetcode-cn.com/problems/route-between-nodes-lcci/)
- [1306. 跳躍遊戲 III](https://leetcode-cn.com/problems/jump-game-iii/)
207. 課程表
你這個學期必須選修 numCourse
門課程,記爲 0
到 numCourse-1
。
在選修某些課程之前需要一些先修課程。 例如,想要學習課程 0 ,你需要先完成課程 1 ,我們用一個匹配來表示他們:[0,1]
給定課程總量以及它們的先決條件,請你判斷是否可能完成所有課程的學習?
示例 1:
輸入: 2, [[1,0]]
輸出: true
解釋: 總共有 2 門課程。學習課程 1 之前,你需要完成課程 0。所以這是可能的。
示例 2:
輸入: 2, [[1,0],[0,1]]
輸出: false
解釋: 總共有 2 門課程。學習課程 1 之前,你需要先完成課程 0;並且學習課程 0 之前,你還應先完成課程 1。這是不可能的。
分析:(這個題讓我直接聯想到死鎖檢測 QAQ )
這個題實際上是有向圖,我們判斷這些課程是否可以完成,實際上就是要判斷該有向圖是否有環,如果有向圖有環,則說明不可以完成全部課程,反之則可以完成全部課程。
方法一:拓撲排序(拓撲排序在有向圖問題判斷環中經常使用)
利用入度表的BFS(入度:可以簡單理解爲箭頭指向自己的數量),進行寬度優先搜索,只將入讀爲0的點添加到隊列。當完成一個頂點的搜索時(將該點從隊列中pop出),將該頂點指向的所有頂點入度減1(等價於將該頂點從圖中移除),如果有新的節點入度變爲0,則將其加入隊列。以次進行,如果寬搜完成後,如果所有入度都爲0,則圖無環,否則說明有環。
如圖所示,最終全部入度爲0,故無環。如果有環,會發現部分位置的入度始終大於0。
代碼:
class Solution
{
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites)
{
vector<int> degree(numCourses); //入度表
vector<vector<int>> Graph; //臨界矩陣 用vector表示的話不用存沒有關係的位置值
vector<int> v;
for(int i=0;i<numCourses;i++)
{
degree.push_back(0); //初始化degree和Graph
Graph.push_back(v);
}
for(int i=0;i<prerequisites.size();i++) //將 prerequisites信息處理爲degree和Graph的信息
{
degree[prerequisites[i][0]]++; //[1,0]表示學1前需要學0,就是0指向1,故[a,b]中a的入度加1
Graph[prerequisites[i][1]].push_back(prerequisites[i][0]); //0指向1,故0的指向節點中加上1
}
queue<int> Q; //隊列
for(int i=0;i<numCourses;i++)
{
if(degree[i]==0) Q.push(i);//
}
int nums=0; //記錄入度爲0的點
while(!Q.empty()) //BFS的方法
{
int tmp=Q.front();
Q.pop();
nums++;
for(int i=0;i<Graph[tmp].size();i++)
{
degree[Graph[tmp][i]]--; //將指向的節點的入度減1
if(degree[Graph[tmp][i]]==0)//直接在這裏判斷是否入度變爲0了,這樣避免了每次遍歷所有degree數組並且不用標記是否遍歷過了
Q.push(Graph[tmp][i]); //如果入度爲0加入隊列
}
}
if(nums==numCourses) return true;
else return false;
}
};
210. 課程表 II
現在你總共有 n 門課需要選,記爲 0 到 n-1。
在選修某些課程之前需要一些先修課程。 例如,想要學習課程 0 ,你需要先完成課程 1 ,我們用一個匹配來表示他們: [0,1]
給定課程總量以及它們的先決條件,返回你爲了學完所有課程所安排的學習順序。
可能會有多個正確的順序,你只要返回一種就可以了。如果不可能完成所有課程,返回一個空數組。
示例 1:
輸入: 2, [[1,0]]
輸出: [0,1]
解釋: 總共有 2 門課程。要學習課程 1,你需要先完成課程 0。因此,正確的課程順序爲 [0,1] 。
分析:實際上,利用拓撲排序,採用入度爲0的點依次入隊列的方式,最終如果沒有環路,則出隊列的順序,其實就是一種可行的上課策略,因爲每次選擇的課程都是沒有先決課程的(因爲將其入隊列時入度爲0,故可以視爲沒有先修課程的限制,就算有也可以將先修課程完成),因此在上個問題的基礎上,調整輸出即可。
class Solution {
public:
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites)
{
vector<int> degree(numCourses); //入度表
vector<vector<int>> Graph; //臨界矩陣 用vector表示的話不用存沒有關係的位置值
vector<int> v;
for(int i=0;i<numCourses;i++)
{
degree.push_back(0); //初始化degree和Graph
Graph.push_back(v);
}
for(int i=0;i<prerequisites.size();i++) //將 prerequisites信息處理爲degree和Graph的信息
{
degree[prerequisites[i][0]]++; //[1,0]表示學1前需要學0,就是0指向1,故[a,b]中a的入度加1
Graph[prerequisites[i][1]].push_back(prerequisites[i][0]); //0指向1,故0的指向節點中加上1
}
queue<int> Q; //隊列
for(int i=0;i<numCourses;i++)
{
if(degree[i]==0) Q.push(i);//
}
int nums=0; //記錄入度爲0的點
vector<int> result;
while(!Q.empty()) //BFS的方法
{
int tmp=Q.front();
Q.pop();
result.push_back(tmp);
nums++;
for(int i=0;i<Graph[tmp].size();i++)
{
degree[Graph[tmp][i]]--; //將指向的節點的入度減1
if(degree[Graph[tmp][i]]==0)//直接在這裏判斷是否入度變爲0了,這樣避免了每次遍歷所有degree數組並且不用標記是否遍歷過了
Q.push(Graph[tmp][i]); //如果入度爲0加入隊列
}
}
if(nums==numCourses) return result;
else return {};
}
};
684. 冗餘連接
在本問題中, 樹指的是一個連通且無環的無向圖。輸入一個圖,該圖由一個有着N個節點 (節點值不重複1, 2, …, N) 的樹及一條附加的邊構成。附加的邊的兩個頂點包含在1到N中間,這條附加的邊不屬於樹中已存在的邊。結果圖是一個以邊組成的二維數組。每一個邊的元素是一對[u, v] ,滿足 u < v,表示連接頂點u 和v的無向圖的邊。
返回一條可以刪去的邊,使得結果圖是一個有着N個節點的樹。如果有多個答案,則返回二維數組中最後出現的邊。答案邊 [u, v] 應滿足相同的格式 u < v。
示例 1:
輸入: [[1,2], [1,3], [2,3]]
輸出: [2,3]
解釋: 給定的無向圖爲:
1
/ \
2 - 3
示例 2:
輸入: [[1,2], [2,3], [3,4], [1,4], [1,5]]
輸出: [1,4]
解釋: 給定的無向圖爲:
5 - 1 - 2
| |
4 - 3
分析:該題實際上是考察的是,如果當前給定的邊已經令A,B可以連通,則新的邊<A,B> 就是冗餘的。
因此,很顯然可以用並查集進行處理。 並查集可以參考一文搞定並查集
每次給定一個邊後,判斷該邊所指的兩個點是否已經連通(即是否已經在並查集的同一個類別中),如果是說明改邊冗餘,否則將該邊所指的兩個點連通起來。
(這裏的並查集代碼是具有優化策略的並查集代碼,因爲該題數據較小其實不需要考慮rank進行優化,但總體並查集代碼是固定的,學會該優化的即可)
class Solution {
public:
int par[1001]; //父節點, 存儲該節點對應的根節點的位置
int rank[1001]; //樹的高度,存儲該樹的位置
void init(int n) //初始化n個元素,使得該n個元素最初的根節點均爲自身
{
for(int i=0;i<n;i++)
{
par[i]=i;
rank[i]=0;
}
}
int find (int x) //查詢第x個節點所在樹的根
{
if(par[x]==x)
return x;
else
return par[x]=find(par[x]); //這一步很巧妙,既通過遞歸找到根節點,又同時完成了路徑的壓縮
}
void unite(int x,int y) //將x和y所在的集合合併
{ //合併時主要考慮兩個根即可
x=find(x);
y=find(y);
if(x==y) return ; //在同一集合,不需操作
if(rank[x]<rank[y]) //rank大的節點作爲合併後的根節點
par[x]=y;
else
{
if(rank[x]==rank[y]) rank[x]++; //如果兩個樹高度一樣,則合併後樹的高度加1
par[y]=x;
}
}
bool same(int x,int y) //判斷x和y是否是同一個集合
{
return find(x)==find(y);
}
vector<int> findRedundantConnection(vector<vector<int> >& edges)
{
// vector<int> result;
init(1001);
for(int i=0;i<edges.size();i++)
{
if(same(edges[i][0],edges[i][1])) //如果當前兩個點已經在一個集合,返回即可
return edges[i];
else
unite(edges[i][0],edges[i][1]);
}
return edges[0];
}
};
785. 判斷二分圖
給定一個無向圖graph,當這個圖爲二分圖時返回true。
如果我們能將一個圖的節點集合分割成兩個獨立的子集A和B,並使圖中的每一條邊的兩個節點一個來自A集合,一個來自B集合,我們就將這個圖稱爲二分圖。
graph將會以鄰接表方式給出,graph[i]表示圖中與節點i相連的所有節點。每個節點都是一個在0到graph.length-1之間的整數。這圖中沒有自環和平行邊: graph[i] 中不存在i,並且graph[i]中沒有重複的值。
示例 1:
輸入: [[1,3], [0,2], [1,3], [0,2]]
輸出: true
解釋:
無向圖如下:
0----1
| |
| |
3----2
我們可以將節點分成兩組: {0, 2} 和 {1, 3}。
示例 2:
輸入: [[1,2,3], [0,2], [0,1,3], [0,2]]
輸出: false
解釋:
無向圖如下:
0----1
| \ |
| \ |
3----2
我們不能將節點分割成兩個獨立的子集。
分析:判斷二分圖問題也就是着色問題,目的是希望用兩種顏色塗所有節點,使得所有相鄰節點的顏色都不一樣。因此,我們可以用三種狀態進行標註:分別表示沒有塗色,塗紅色,塗藍色。
- 通過DFS或者BFS進行遍歷圖
- 如果起始點沒有塗色,則可以任意將其塗一種顏色
- 遍歷時,將相鄰節點塗成與當前節點不同的顏色,如果相鄰節點已經塗色且與自己顏色相同,說明不可以二分
- 直至塗色結束
代碼:(用的是BFS搜索,由於圖可能是非連通圖,因此需要逐一判斷是否每個點都遍歷過了)
class Solution {
public:
int color[101];
bool isBipartite(vector<vector<int>>& graph)
{
queue<int> Q;//bfs的隊列
for(int j=0;j<graph.size();j++)
{
if(color[j]==0)
{
Q.push(j);
color[j]=1; //沒有塗色的話將其置爲1的顏色
}
else
continue;
while(!Q.empty())
{
int tmp=Q.front();
Q.pop();
int now_color=(color[tmp]==1?2:1); //表示相鄰節點應該塗的顏色
for(int i=0;i<graph[tmp].size();i++)
{
if(color[graph[tmp][i]]==0)
{
color[graph[tmp][i]]=now_color;
Q.push(graph[tmp][i]); //只在塗色時將其加入
}
else if(color[graph[tmp][i]]==color[tmp])
return false;
else
continue;
}
}
}
return true;
}
};
997. 找到小鎮的法官
在一個小鎮裏,按從 1 到 N 標記了 N 個人。傳言稱,這些人中有一個是小鎮上的祕密法官。
如果小鎮的法官真的存在,那麼:
-
小鎮的法官不相信任何人。
-
每個人(除了小鎮法官外)都信任小鎮的法官。
-
只有一個人同時滿足屬性 1 和屬性 2 。
給定數組 trust,該數組由信任對 trust[i] = [a, b] 組成,表示標記爲 a 的人信任標記爲 b 的人。
如果小鎮存在祕密法官並且可以確定他的身份,請返回該法官的標記。否則,返回 -1。
示例 1:
輸入:N = 2, trust = [[1,2]]
輸出:2
示例 2:
輸入:N = 3, trust = [[1,3],[2,3]]
輸出:3
示例 3:
輸入:N = 3, trust = [[1,3],[2,3],[3,1]]
輸出:-1
分析:將輸入可視化,如果A信任B,則從A向B畫一個箭頭,因此,法官應當是這樣一個人:所有其他人都有指向他的箭頭,他沒有指向任何人的箭頭。很顯然,**可以將其視爲有向圖中的入度和出度問題,法官的入度應當爲 ,出度應當爲 . ** 分析易得,只需要存儲總度即可,即入度-出度,如果出度則總度減1,入度則總度加1,因此如果有法官,其度應當爲N-1,其他人的度一定小於N-1
代碼如下:
class Solution {
public:
int findJudge(int N, vector<vector<int> >& trust)
{
vector<int> degree(N+1); //入度
for(int i=0;i<trust.size();i++)
{
degree[trust[i][1]]++;
degree[trust[i][0]]--;
}
for(int i=1;i<=N;i++)
{
if(indegree[i]==N-1)
return i;
}
return -1;
}
};
841. 鑰匙和房間
有 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。
分析:該題實際上是判斷從一點出發,該圖是否是連通的。考慮用BFS或者DFS遍歷,從房間0出發,將獲得的鑰匙的房間依次遍歷,最終如果所有房間都可以進入,則返回true.
代碼:BFS
class Solution {
public:
bool canVisitAllRooms(vector< vector<int> >& rooms)
{
int num=rooms.size();
vector<bool> visit(num); //標記是否訪問過
for(int i=0;i<num;i++)
{
visit[i]=false;
}
int ans=1; //記錄已經訪問過的房間數量
queue<int> Q;
Q.push(0); //進入0房間
visit[0]=true;
while(!Q.empty())
{
int tmp=Q.front();
Q.pop();
for(int i=0;i<rooms[tmp].size();i++)
{
if(visit[rooms[tmp][i]]==false) //添加新房間時直接更新visit和ans
{
Q.push(rooms[tmp][i]);
visit[rooms[tmp][i]]=true;
ans++;
}
}
if(ans==num) return true;
}
return false;
}
};
面試題 04.01. 節點間通路
節點間通路。給定有向圖,設計一個算法,找出兩個節點之間是否存在一條路徑
提示:
- 節點數量n在[0, 1e5]範圍內。
- 節點編號大於等於 0 小於 n。
- 圖中可能存在自環和平行邊
示例1:
輸入:n = 3, graph = [[0, 1], [0, 2], [1, 2], [1, 2]], start = 0, target = 2
輸出:true
示例2:
輸入:n = 5, graph = [[0, 1], [0, 2], [0, 4], [0, 4], [0, 1], [1, 3], [1, 4], [1, 3], [2, 3], [3, 4]], start = 0, target = 4
輸出 true
分析:採用鄰接矩陣+BFS的方式遍歷,需要採用一個數組標記是否已經訪問過,將一個節點加入隊列時就直接將訪問數組進行標記,避免多次將其添加。
class Solution {
public:
bool findWhetherExistsPath(int n, vector< vector<int> >& graph, int start, int target)
{
vector< vector<int> > Graph(n); //鄰接矩陣
for(int i=0;i<graph.size();i++)
Graph[graph[i][0]].push_back(graph[i][1]);
queue<int> Q; //BFS的隊列
bool visit[n]; //標記數組
for(int i=0;i<n;i++)
visit[i]=false;
Q.push(start);
visit[start]=true;
while(!Q.empty())
{
int tmp=Q.front();
Q.pop();
for(int i=0;i<Graph[tmp].size();i++)
{
if(visit[Graph[tmp][i]]==false)
{
if(Graph[tmp][i]==target) return true;
visit[Graph[tmp][i]]=true;
Q.push(Graph[tmp][i]);
}
}
}
return false;
}
};
1306. 跳躍遊戲 III
這裏有一個非負整數數組 arr,你最開始位於該數組的起始下標 start 處。當你位於下標 i 處時,你可以跳到 i + arr[i] 或者 i - arr[i]。
請你判斷自己是否能夠跳到對應元素值爲 0 的 任意 下標處。
注意,不管是什麼情況下,你都無法跳到數組之外。
示例 1:
輸入:arr = [4,2,3,0,3,1,2], start = 5
輸出:true
解釋:
到達值爲 0 的下標 3 有以下可能方案:
下標 5 -> 下標 4 -> 下標 1 -> 下標 3
下標 5 -> 下標 6 -> 下標 4 -> 下標 1 -> 下標 3
示例 2:
輸入:arr = [4,2,3,0,3,1,2], start = 0
輸出:true
解釋:
到達值爲 0 的下標 3 有以下可能方案:
下標 0 -> 下標 4 -> 下標 1 -> 下標 3
示例 3:
輸入:arr = [3,0,2,1,2], start = 2
輸出:false
解釋:無法到達值爲 0 的下標 1 處。
分析:注意該跳躍遊戲中,在某個位置,只可以跳到 i + arr[i] 或者 i - arr[i]的位置,因此,實際上可以將其視爲一個有向圖,如果一個點可以跳到一個位置,則說明可以形成一個有向邊。故該問題,轉換成了從與上一個問題類似的問題,即從起始點出發能否達到值爲0的位置。依舊採用 BFS 的方式,採用一個 visit 表示是否已經訪問過。在這裏不顯示創建鄰接矩陣了,因爲每一個點能到達的至多爲兩個位置,故就在BFS搜索時遍歷即可。(注意的是,爲0的點可能有很多,只需要到達一個即可)
class Solution {
public:
bool canReach(vector<int>& arr, int start)
{
int size=arr.size();
bool visit[size];
vector<int> target;
for(int i=0;i<size;i++)
{
visit[i]=false;
if(arr[i]==0) target.push_back(i); //找到target都在哪裏
}
if(find(target.begin(),target.end(),start)!=target.end()) return true;
queue<int> Q;
Q.push(start);
visit[start]=true;
while(!Q.empty())
{
int tmp=Q.front();
Q.pop();
if(tmp-arr[tmp]>=0&&visit[tmp-arr[tmp]]==false)
{
if(find(target.begin(),target.end(),tmp-arr[tmp])!=target.end()) return true;
Q.push(tmp-arr[tmp]);
visit[tmp-arr[tmp]]=true;
}
if(tmp+arr[tmp]<size&&visit[tmp+arr[tmp]]==false)
{
if(find(target.begin(),target.end(),tmp+arr[tmp])!=target.end()) return true;
Q.push(tmp+arr[tmp]);
visit[tmp+arr[tmp]]=true;
}
}
return false;
}
};