C# 廣度優先搜索

 

 

廣度優先搜索是一種用於圖的查找算法,它主要解決兩個問題:

1.從節點S到節點E有路徑嗎?

2.從節點S到節點E的所有路線中,哪條最短?

廣度優先搜索的執行過程中,搜索範圍從起點開始逐漸向外延伸,即先檢查一度關係,再檢查二度關係.

所謂一度關係:我的朋友和我就是一度關係.

所謂二度關係:我的朋友的朋友和我就是二度關係.

以此類推.

曾經不知道在哪裡看到過一句話:

解決問題,先確定數據結構,數據結構確定好了,算法自然而然就出來了.不過我覺得我離這個境界還有點遠….

那麼圖應該用哪種數據結構表示呢?

因為一個節點可以指向多個節點,當然,也可以不指向任何節點.所以我們可以用散列表: Dictionary<T,List<T>> (也許還有其他方式,但是小弟確實不懂,只懂這個)

由於多個節點可以指向同一個節點,自己也可以指向自己,所以在搜索的過程中,對於檢查過的節點,我們不能再去檢查,否則可能會導致無限循環,因此我們需要一個數據結構來保存搜索過的節點,這個用 List<T> 或者 HashSet<T> 都可以.

前面說到了,該算法搜索的時候是從內到外,一層一層搜索,最裏面一層搜索完了,遞增一層繼續搜索,逐漸向外延伸.

那麼這裡肯定需要一種數據結構來存儲需要檢查的節點.

同時,由於是從內到外,層層遞增搜索,所有這裡就有個先後順序的問題了.那就是必須要把第一層的節點搜索完,才能搜索第2層,第2層的節點搜索完,才能搜索第3層.像這種有先後順序要求的數據結構,必須隊列:Queue<T>,我們將需要搜索的節點放到隊列中,比如:

起點S的第1層關係中有2個節點:A和B,我們先把這兩個節點插入隊列.那麼這個插入有順序要求嗎?沒有!同一層的節點插入到隊列的順序不重要,因為該算法計算的是最短距離,不是最快距離.

假設順序是A,B,我們先檢查A,如果A不是我們要找的終點(假設是E),那麼我們就把A指向的所有節點插入到隊列,插入到隊列後,它們是排在B的後面,所以只有等第1層的B檢查完了,才會檢查它們.當B檢查完了,也不是我們要找的終點,我們再把B指向的所有節點插入到隊列,這也就實現了1層1層的遞增檢查.

 

通過上面的方法,我們可以解決開篇提到的第1個問題:從S到E是否有路徑.但是無法解決第2個問題:最短路徑是哪條.

要解決這個問題,在搜索的過程中就必須保存當前搜索的節點是從哪個節點過來的.因為前面也提到了,在圖這種數據結構中,多個節點是可以指向同一個節點的.並且一個節點也是可以指向多個節點的,所以我們必須清楚的知道,當前搜索的節點是從哪個節點(哪條線路)過來的.

打個不太恰當的比方:

5+5=10

但是10!=5+5 ,10還可以==1+9,==2+8,==3+7 ……

所以,前面提到的,創建一個隊列來保存需要檢查的節點是不行的,我們還需要保存節點的父節點,因為我們需要知道我們是怎麼來的!

因此,我封裝了一下當前節點類型,提供了一個屬性來保存它的父親.

(這個設計可能不太好,額外空間可能耗得比較多,小弟暫時沒想到什麼好的辦法)

 

代碼如下:

 

    public class Route<T>
    {
        /// <summary>
        /// 終點相對於起點的維度
        /// </summary>
        public int Dimension { get; }

        /// <summary>
        /// 完整路線
        /// </summary>
        public string FullRoute { get; }

        public Route(Stack<T> stack)
        {
            FullRoute = string.Join(",", stack);
            Dimension = stack.Count - 1;
        }
    }

 

    public class RouteNode<T>
    {
        public T Value { get; }
        public RouteNode<T> Parent { get; set; }

        public RouteNode(T value)
        {
            Value = value;
        }

        public Route<T> Route
        {
            get
            {
                var stack = new Stack<T>();
                stack.Push(this.Value);
                var parent = Parent;
                while (parent != null)
                {
                    stack.Push(parent.Value);
                    parent = parent.Parent;
                }
                Route<T> route = new Route<T>(stack);
                return route;
            }
        }
    }

 

    public class MyGraph<T> where T : IComparable<T>
    {
        //節點集合
        private readonly Dictionary<T, IList<T>> _nodes;

        public MyGraph() : this(new Dictionary<T, IList<T>>())
        {

        }

        public MyGraph(Dictionary<T, IList<T>> nodes)
        {
            _nodes = nodes;
        }

        public void Add(T key, IList<T> value)
        {
            _nodes.Add(key, value);
        }

        /// <summary>
        /// 判斷是否有從 start 到 end 的路徑
        /// </summary>
        /// <param name="start">開始點</param>
        /// <param name="end">結束點</param>
        /// <param name="route">路線</param>
        /// <returns></returns>
        public bool TryFindMinRoute(T start, T end, out Route<T> route)
        {
            route = null;
            if (_nodes.TryGetValue(start, out var nodes) == false)
            {
                throw new Exception("not find the element:" + start);
            }
            if (nodes == null || nodes.Count == 0)
            {
                return false;
            }
            //已搜索元素的集合
            var searched = new HashSet<T>();
            //將要搜索的節點隊列
            var searching = new Queue<RouteNode<T>>();
            foreach (var item in nodes)
            {
                searching.Enqueue(new RouteNode<T>(item)
                {
                    Parent = new RouteNode<T>(start),
                });
            }

            while (searching.Count > 0)
            {
                RouteNode<T> node = searching.Dequeue();

                //判斷當前元素是否搜索過,避免無限循環.
                if (searched.Contains(node.Value))
                {
                    continue;
                }

                if (node.Value.CompareTo(end) == 0)
                {
                    route = node.Route;
                    return true;
                }

                searched.Add(node.Value);

                if (_nodes.TryGetValue(node.Value, out var forwardNodes) == false || forwardNodes == null || forwardNodes.Count == 0)
                {
                    //說明 當前元素 沒有指向任何元素
                    continue;
                }

                foreach (var item in forwardNodes.Where(item => searched.Contains(item) == false))
                {
                    searching.Enqueue(new RouteNode<T>(item)
                    {
                        Parent = node
                    });
                }
            }
            return false;
        }
    }

 

Test:

            MyGraph<string> dic = new MyGraph<string>();
            dic.Add("start", new List<string> { "bob", "alice", "claire" });
            dic.Add("bob", new List<string> { "anuj", "dandan1" });
            dic.Add("alice", new List<string> { "peggy" });
            dic.Add("claire", new List<string> { "thom", "jonny" });
            dic.Add("anuj", new List<string>() { "wahaha", "dandan2" });
            dic.Add("dandan2", new List<string>() { "wahaha", "claire" });
            dic.Add("peggy", new List<string>() { "gongwei" });
            dic.Add("gongwei", new List<string>() { "jonny" });
            dic.Add("thom", new List<string>());
            dic.Add("jonny", new List<string> { "refuge" });
            dic.Add("wjire", new List<string>());
            dic.Add("refuge", new List<string>() { "dandan", "bob" });
            var r = dic.TryFindMinRoute("alice", "claire", out var route);
            if (r)
            {
                Console.WriteLine($"在第{route.Dimension}層找到:" + route.FullRoute);
            }
            else
            {
                Console.WriteLine(r);
            }