leetcode 經典 圖相關題目(思路、方法、code)

圖的問題基本就是 BFS和DFS,還有拓撲排序、最短路、最小生成樹,有時也會用並查集進行分類,還要注意節點的入度出度等特徵。

207. 課程表

你這個學期必須選修 numCourse 門課程,記爲 0numCourse-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. 每個人(除了小鎮法官外)都信任小鎮的法官。

  3. 只有一個人同時滿足屬性 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畫一個箭頭,因此,法官應當是這樣一個人:所有其他人都有指向他的箭頭,他沒有指向任何人的箭頭。很顯然,**可以將其視爲有向圖中的入度和出度問題,法官的入度應當爲 n1n-1 ,出度應當爲 00 . ** 分析易得,只需要存儲總度即可,即入度-出度,如果出度則總度減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. 節點間通路

節點間通路。給定有向圖,設計一個算法,找出兩個節點之間是否存在一條路徑

提示:

  1. 節點數量n在[0, 1e5]範圍內。
  2. 節點編號大於等於 0 小於 n。
  3. 圖中可能存在自環和平行邊
示例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;
    }
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章