數據結構(三)——圖

無向圖

  • 深度優先搜索查找(DFS):

    • 添加了一個數組edgeTo[]。這個數組可以找到每個與s連通的頂點回到s的路徑。它會記住每個頂點到起點的路徑,而不是記錄當前頂點到起點的路徑。爲了做到這一點,在由邊v-w第一次訪問任意w時,將edgeTo[w]設爲v來記住這條路徑。換句話說,v-w是從s到我的路徑上的最後一條已知的邊。這樣,搜索的結果是一棵以起點爲根節點的樹,edgeTo[]是一棵父鏈接表示的樹。

    • 深度優先搜索標記與起點連通的所有頂點所需的時間和頂點的度數之和成正比

  • 廣度優先搜索查找(BFS):

    • 深度優先搜索得到的路徑不僅取決於圖的結構,還取決於圖的表示和遞歸調用的性質。廣度優先搜索可以用來解決單點最短路徑問題。深度優先搜索就好像是一個人在走迷宮,廣度優先搜索則好像是一組人在一起朝各個方向走這個迷宮,每個人都有自己的繩子。當出現新的的叉路時,可以假設一個探索者可以分裂爲更多的人來搜索它們,當兩個探索者相遇時,會合二爲一(並繼續使用先到達者的繩子)。在程序中,在搜索一幅圖時遇到有多條邊需要遍歷的情況時,我們會選擇其中一條並將其他通道留到以後再繼續搜索。在深度優先搜索中,我們我們用了一個可以下壓的棧(這是由系統管理的,以支持遞歸搜索方法)。使用LIFO(後進後出)的規則來描述壓棧和走迷宮先探索相鄰的通道相似。從有待搜索的通道中選擇最晚遇到過的那條。在廣度優先搜索中,我們希望按照與起點的距離的順序來遍歷所有頂點,看起來這種順序很容易實現:使用(FIFO,先進先出)隊列來代替棧(LIFO,後進先出)即可。我們將從有待搜索的通道中選擇最早遇到的那條。

    • 對於從s可達的任意頂點頂點v,廣度優先搜索都能找到一條從s到v的最短路徑(沒有其他從s到v的路徑所含的邊比這條路徑更少)。廣度優先搜索所需的時間在最壞情況下和V+E成正比。

    • 這兩個算法的不同之處僅在於從數據結構中獲取下一個頂點的規則(對於廣度優先搜索來說是最早加入的頂點,對於深度優先搜索來說是最晚加入的頂點)。

有向圖

  • 拓撲排序:

    • 給定一幅有向圖,將所有的頂點排序,使得所有的有向邊均從排在前面的元素指向排到後面的元素(或者說明無法做到這一點)。如果一個有優先級限制的問題中存在有向環,那麼這個問題肯定是無解的。

    • 有向圖中基於深度優先搜索的頂點排序:

      	private Queue<Integer> pre; //所有頂點的前序排列
      	private Queue<Integer> post; //所有頂點的後序排列
      	private Stack<Integer> reversePost; //所有頂點的逆後序排列,即拓撲排序
      	public DepthFirstOrder(Digraph G)
      	{
      	pre         = new Queue<Integer>();
      	post        = new Queue<Integer>();
      	reversePost = new Stack<Integer>();
      	marked = new boolean[G.V()];
      	for(int v = 0;v < G.V();v++)
      	if(!marked[v])dfs(G,v);
      	}
      	private void dfs(Digraph G,int v)
      	{
      	pre.enqueue(v);
      	marked[v] = true;
      	for(int w : G.adj(v))
      	if(!Marked[w])
      	dfs(G,w);
      	post.enqueue(v);
      	reversePost.push(v);
      	}
      
    • 一幅有向無環圖的拓撲順序即爲所有頂點的逆後序排列。

  • Kosaraju算法

    • 爲了找到所有強連通分量,它會在反向圖中進行深度優先搜索來將頂點排序(搜索順序的逆後序),在給定有向圖中用這個順序再進行一次深度優先搜索。

    • 有向圖中的連通性:
      如果兩個頂點v和w是互相可達的,則稱它們爲強連通的。兩個頂點是強連通當且僅當它們都在一個普通的有向環中。

    • 實現:

     private int[] id; //強連通分量的標識符
 			public kosarajuSCC(Digraph G)
 			{
 			marked = new boolean[G.V()];
 			id = new int[G.V()];
 			DepthFirstOrder order = new DepthFirstOrder(G.reverse);
 			for(int s : order.reversePost())
 			If(!marked[s]){dfs(G,s);count++;}
 			}
 			private void dfs(Digraph G,int v)
 			{
 			marked[v] = true;
 			id[v] = count;
 			for(int w : G.adj(v))if(!marked[w])dfs(G,w);
 			}
 			public boolean stronglyConnected(int v,int w)
 			{return id[v] == id[w];}

最小生成樹

  • Prim(普里姆)算法
    • 它的每一步都會爲一棵生長中的樹添加一條邊。一開始這棵樹只有一個頂點,然後會向它添加V-1條邊,每次總是將下一條連接樹中的頂點與不在樹中的頂點且權重最小的邊加入樹中(即由樹中的頂點所定義的切分中的一條橫切邊)

    • 實現:

       private boolean[] marked; //最小生成樹的頂點
   			private Queue<Edge> mst; //最小生成樹的邊
   			private MinPQ<Edge> pq; //橫切邊(包括失效的邊)
   			public LazyPrimMST(EdgeWeightedGraph G)
   			{
   			pq = new MinPQ<Edge>();
   			marked = new boolean[G.V()];
   			mst = new Queue<Edge>();
   			visit(G,0); //假設G是連通的
   			while(!Pq.isEmpty())
   			{
   			Edge e = pq.delMin(); //從pq中得到權重最小的邊
   			int v = e.either(),w = e.other(v);
   			if(marked[v]&&marked[w])continue; //跳過失效的邊
   			mst.enqueue(e); //將邊添加到樹中
   			If(!marked[v])visit(G,v); //將頂點(v或w)添加到樹中
   			if(!marked[w])visit(G,v); 
   			}
   			}
   			private void visit(EdgeWeightedGraph G,int v)
   			{ //標記頂點v並將所有連接v和未被標記頂點的邊加入pq
   			marked[v] = true;
   			for(Edge e : G.adj(v))
   			If(!marked[e.other(v)])pq.insert(e);
   			}
  • Kruskal(克魯斯卡爾)算法
    • 該算法的主要思想是按照邊的權重順序(從小到大)處理它們,將邊加入最小生成樹,加入的邊不會與已經加入的邊構成成環,直到樹中含有V-1條邊爲止。
      這些邊逐漸由一片森林合併爲一棵樹,也就是圖的最小生成樹。

    • Kruskal算法一般還是要比Prim算法要慢,因爲在處理每條邊時除了兩種算法都要完成的優先隊列操作之外,它還需要進行一次connect操作。

    • 實現:

        private Queue<Edge> mst;
				public KruskalMST(EdgeWeightedGraph G)
				{
				mst = new Queue<Edge>();
				minPQ<Edge> pq = new MinPQ<Edge>();
				for(Edge e : G.edges())pq.insert(e);
				UF uf = new UF(G.V());
				while(!pq.isEmpty() && mst.size() < G.V()-1)
				{
				Edge e = pq.delMin(); //
				int v = e.either(),w = e.other(v);
				if(uf.connected(v,w))continue; //
				uf.union(v,w); //
				mst.enqueue(e); //
				}
				}
  • Prim和Kruskal算法不能處理有向圖問題,有向圖問題是更加困難的最小樹形問題

最短路徑

  • 邊的鬆弛:
    放鬆邊v->w意味着檢查從s到w的最短路徑是否先從s到v,然後再由v到w。如果是,則根據這個情況更新數據結構的內容。其中由v到達w的最短路徑是distTo[w]與e.weight()之和——如果這個值不小於distTo[w],稱這條邊失效了並將它忽略;如果這個值更小,就更新數據。點的鬆弛是類似的(對該點所連接的邊進行操作)。

  • Dijkstra(迪克斯特拉)算法

    • Dijkstra算法能夠解決邊權重非負的加權有向圖的單起點最短路徑

    • 這個算法主要分爲兩種情況:要麼邊的to()得到的頂點還不在優先隊列中,此時需要使用insert()方法將它加入到優先隊列中;要麼它已經在優先隊列中且優先級(即到該點的總路徑長度)需要降低,此時可以用change()方法實現

    • Prim算法每次添加的都是離樹最近的非樹頂點,Dijkstra算法每次添加的都是離起點最近的非樹頂點。

  • 無環加權有向圖中的最短路徑算法

    • 許多應用中的加權有向圖都是不含有有向環的。而該算法比Dijkstra算法更快、更簡單。它能在線性時間(E+V)內解決單點最短路徑問題; 能夠解決負權重的邊;能夠解決相關的問題,例如找出最長的路徑。

    • 首先,將distTo[s]初始化爲0,其他distTo[]元素初始化爲無窮大,然後一個一個地按照拓撲順序放鬆所有頂點。

other

概念性的東西

  • 關於線性與順序

    • 線性是線性,順序是順序,線性是邏輯結構,順序是存儲結構,兩者不是一個概念,線性是指一個元素後繼只有唯一的一個元素或節點,非線性是一個元素後面可以有多個後繼或前繼節點,順序是指存儲結構連續,例如數組是順序的,鏈表不是順序的,但他們都是線性的。當然順序也可以是非線性的,例如順序結構存儲非線性結構的二叉樹!!!
  • 二叉鏈表存儲結構

    • 以二叉鏈表作爲樹的存儲結構。鏈表中結點的兩個鏈域分別指向該結點的第一個孩子結點和下一個兄弟結點。所以根節點的右指針指向根節點的右兄弟,根節點沒有兄弟節點,因此爲空
  • 樹是圖的特例,不能爲空;空樹是二叉樹的特例,即二叉樹可以爲空。

鍵索引計數法

		int N = a.length;
		String[] aux = new String[N];
		int[] count = new int[R+1];
		//計算出現頻率
		for(int i = 0;i < N;i++)
		count(a[i].key()+1]++;
		//將頻率轉換爲索引
		for(int r = 0;r < R;r++)
		count[r+1] += count[r];
		//將元素分類
		for(int i = 0;i < N;i++)
		aux[count[a[i].key()]++] = a[i];
		//回寫
		for(int i = 0;i < N;i++)
		a[i] = aux[i];
  • 鍵索引計數法是一種對於小整數鍵排序非常有效卻常常被忽略的排序算法

  • 低、高位優先的字符串排序便是基於鍵索引計數法的

KMP算法

		void createNext(const char* str, int* next, int length)
		{
		    int i = 1, k = 0;
		    next[0] = 0;//爲0則表示不存在相同的最長前綴和最長後綴

		    while(i < length)
		    {
		        while(k>0 && str[i] != str[k])
		            k = next[k-1];//如果前面k>0,而當前不匹配,則回溯
		        if(str[i] == str[k])
		            k++;//匹配到相同的,k+1
		        next[i] = k;//真正給next賦值的地方
		        i++;
		    }
		}
		int Kmp(const char *src, int slen, const char *str, int len)
		{
		    int *next = new int[len];
		    createNext(str, next, len);

		    int i = 0;
		    int k = 0;
		    while(i < slen && k < len)
		    {
		        if(src[i] == str[k])
		            i++,k++;//匹配則兩個同時向後移動
		        else if(k>0)
		            k = next[k-1];//匹配失敗且k>0,則回溯
		        else
		            i++;//若匹配失敗且k=0,則往下尋找匹配字符
		    }

		    delete[] next;
		    if(k == len)//尋找到需要的子串
		        return i - len;
		    else
		        return -1;
		}

動態規劃和貪心算法的聯繫與區別

動態規劃和貪心算法都是一種遞推算法 ,均由局部最優解來推導全局最優解

不同點:
貪心算法:
1.貪心算法中,作出的每步貪心決策都無法改變,因爲貪心策略是由上一步的最優解推導下一步的最優解,而上一步之前的最優解則不作保留。
2.由(1)中的介紹,可以知道貪心法正確的條件是:每一步的最優解一定包含上一步的最優解。

	該算法存在問題:   
			1.   不能保證求得的最後解是最佳的;   
			2.   不能用來求最大或最小解問題;   
			3.   只能求滿足某些約束條件的可行解的範圍。實現該算法的過程:   

動態規劃算法:
1.全局最優解中一定包含某個局部最優解,但不一定包含前一個局部最優解,因此需要記錄之前的所有最優解
2.動態規劃的關鍵是狀態轉移方程,即如何由以求出的局部最優解來推導全局最優解
3.邊界條件:即最簡單的,可以直接得出的局部最優解

	通過動態規劃算法解決多段圖最短路徑規劃問題的思想是,我們要計算每一個子段圖到下一個子段圖的所有可能的連接情況,並保留每種可能選擇中最短的那個連接。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章