算法(第四版)C# 習題題解——1.5


寫在前面

整個項目都托管在了 Github 上:https://github.com/ikesnowy/Algorithms-4th-Edition-in-Csharp

這一節內容可能會用到的庫文件有 Measurement 和 TestCase,同樣在 Github 上可以找到。

善用 Ctrl + F 查找題目。

習題&題解

1.5.1

解答

quick-find 的官方實現:QuickFindUF.java

只要實現相應並查集,然后輸入內容即可。

增加一個記錄訪問數組次數的類成員變量,在每次訪問數組的語句執行后自增即可。

樣例輸出:

0 1 2 3 4 5 6 7 8 0 數組訪問:13
0 1 2 4 4 5 6 7 8 0 數組訪問:13
0 1 2 4 4 8 6 7 8 0 數組訪問:13
0 1 2 4 4 8 6 2 8 0 數組訪問:13
0 1 1 4 4 8 6 1 8 0 數組訪問:14
0 1 1 4 4 1 6 1 1 0 數組訪問:14
4 1 1 4 4 1 6 1 1 4 數組訪問:14
1 1 1 1 1 1 6 1 1 1 數組訪問:16
代碼

QuickFindUF.cs,這個類繼承了 UF.cs,重新實現了 Union() 和 Find() 等方法。

關於 UF.cs 可以參見原書中文版 P138 或英文版 P221 的算法 1.5。

namespace UnionFind
{
    /// <summary>
    /// 用 QuickFind 算法實現的並查集。
    /// </summary>
    public class QuickFindUF : UF
    {
        public int ArrayVisitCount { get; private set; } //記錄數組訪問的次數。

        /// <summary>
        /// 新建一個使用 quick-find 實現的並查集。
        /// </summary>
        /// <param name="n">並查集的大小。</param>
        public QuickFindUF(int n) : base(n) { }

        /// <summary>
        /// 重置數組訪問計數。
        /// </summary>
        public void ResetArrayCount()
        {
            this.ArrayVisitCount = 0;
        }
        
        /// <summary>
        /// 尋找 p 所在的連通分量。
        /// </summary>
        /// <param name="p">需要尋找的結點。</param>
        /// <returns>返回 p 所在的連通分量。</returns>
        public override int Find(int p)
        {
            Validate(p);
            this.ArrayVisitCount++;
            return this.parent[p];
        }

        /// <summary>
        /// 判斷兩個結點是否屬於同一個連通分量。
        /// </summary>
        /// <param name="p">需要判斷的結點。</param>
        /// <param name="q">需要判斷的另一個結點。</param>
        /// <returns>如果屬於同一個連通分量則返回 true,否則返回 false。</returns>
        public override bool IsConnected(int p, int q)
        {
            Validate(p);
            Validate(q);
            this.ArrayVisitCount += 2;
            return this.parent[p] == this.parent[q];
        }

        /// <summary>
        /// 將兩個結點所在的連通分量合並。
        /// </summary>
        /// <param name="p">需要合並的結點。</param>
        /// <param name="q">需要合並的另一個結點。</param>
        public override void Union(int p, int q)
        {
            Validate(p);
            Validate(q);
            int pID = this.parent[p];
            int qID = this.parent[q];
            this.ArrayVisitCount += 2;

            // 如果兩個結點同屬於一個連通分量,那么什么也不做。
            if (pID == qID)
            {
                return;
            }

            for (int i = 0; i < this.parent.Length; ++i)
            {
                if (this.parent[i] == pID)
                {
                    this.parent[i] = qID;
                    this.ArrayVisitCount++;
                }
            }

            this.ArrayVisitCount += this.parent.Length;
            this.count--;
            return;
        }

        /// <summary>
        /// 獲得 parent 數組。
        /// </summary>
        /// <returns>id 數組。</returns>
        public int[] GetParent()
        {
            return this.parent;
        }
    }
}

 

Main 方法:

using System;
using UnionFind;

namespace _1._5._1
{
    /*
     * 1.5.1
     * 
     * 使用 quick-find 算法處理序列 9-0 3-4 5-8 7-2 2-1 5-7 0-3 4-2 。
     * 對於輸入的每一對整數,給出 id[] 數組的內容和訪問數組的次數。
     * 
     */
    class Program
    {
        static void Main(string[] args)
        {
            string[] input = "9-0 3-4 5-8 7-2 2-1 5-7 0-3 4-2".Split(' ');
            var quickFind = new QuickFindUF(10);

            foreach (string s in input)
            {
                quickFind.ResetArrayCount();

                string[] numbers = s.Split('-');
                int p = int.Parse(numbers[0]);
                int q = int.Parse(numbers[1]);

                int[] id = quickFind.GetParent();
                quickFind.Union(p, q);
                foreach (int root in id)
                {
                    Console.Write(root + " ");
                }
                Console.WriteLine("數組訪問:" + quickFind.ArrayVisitCount);
            }
        }
    }
}

 

1.5.2

解答

quick-union 的官方實現:QuickUnionUF.java

和上題一樣的方式,增加一個記錄訪問數組次數的類成員變量,在每次訪問數組的語句執行后自增即可。

程序輸出的森林,用縮進表示子樹:

|---- 0
    |---- 9
|---- 1
|---- 2
|---- 3
|---- 4
|---- 5
|---- 6
|---- 7
|---- 8
數組訪問:1
|---- 0
    |---- 9
|---- 1
|---- 2
|---- 4
    |---- 3
|---- 5
|---- 6
|---- 7
|---- 8
數組訪問:1
|---- 0
    |---- 9
|---- 1
|---- 2
|---- 4
    |---- 3
|---- 6
|---- 7
|---- 8
    |---- 5
數組訪問:1
|---- 0
    |---- 9
|---- 1
|---- 2
    |---- 7
|---- 4
    |---- 3
|---- 6
|---- 8
    |---- 5
數組訪問:1
|---- 0
    |---- 9
|---- 1
    |---- 2
        |---- 7
|---- 4
    |---- 3
|---- 6
|---- 8
    |---- 5
數組訪問:1
|---- 0
    |---- 9
|---- 1
    |---- 2
        |---- 7
    |---- 8
        |---- 5
|---- 4
    |---- 3
|---- 6
數組訪問:7
|---- 1
    |---- 2
        |---- 7
    |---- 8
        |---- 5
|---- 4
    |---- 0
        |---- 9
    |---- 3
|---- 6
數組訪問:3
|---- 1
    |---- 2
        |---- 7
    |---- 4
        |---- 0
            |---- 9
        |---- 3
    |---- 8
        |---- 5
|---- 6
數組訪問:3
代碼

QuickUnionUF.cs,這個類繼承了 UF.cs,重新實現了 Union() 和 Find() 等方法。

關於 UF.cs 可以參見原書中文版 P138 或英文版 P221 的算法 1.5。

namespace UnionFind
{
    /// <summary>
    /// 用 QuickUnion 算法實現的並查集。
    /// </summary>
    public class QuickUnionUF : UF
    {
        public int ArrayVisitCount { get; private set; } //記錄數組訪問的次數。

        /// <summary>
        /// 建立使用 QuickUnion 的並查集。
        /// </summary>
        /// <param name="n">並查集的大小。</param>
        public QuickUnionUF(int n) : base(n) { }

        /// <summary>
        /// 重置數組訪問計數。
        /// </summary>
        public virtual void ResetArrayCount()
        {
            this.ArrayVisitCount = 0;
        }

        /// <summary>
        /// 獲得 parent 數組。
        /// </summary>
        /// <returns>返回 parent 數組。</returns>
        public int[] GetParent()
        {
            return this.parent;
        }

        /// <summary>
        /// 尋找一個結點所在的連通分量。
        /// </summary>
        /// <param name="p">需要尋找的結點。</param>
        /// <returns>該結點所屬的連通分量。</returns>
        public override int Find(int p)
        {
            Validate(p);
            while (p != this.parent[p])
            {
                p = this.parent[p];
                this.ArrayVisitCount += 2;
            }
            return p;
        }

        /// <summary>
        /// 將兩個結點所屬的連通分量合並。
        /// </summary>
        /// <param name="p">需要合並的結點。</param>
        /// <param name="q">需要合並的另一個結點。</param>
        public override void Union(int p, int q)
        {
            int rootP = Find(p);
            int rootQ = Find(q);
            if (rootP == rootQ)
            {
                return;
            }

            this.parent[rootP] = rootQ;
            this.ArrayVisitCount++;
            this.count--;
        }
    }

}

 

Main 方法

using System;
using UnionFind;

namespace _1._5._2
{
    /*
     * 1.5.2
     * 
     * 使用 quick-union 算法(請見 1.5.2.3 節代碼框)完成練習 1.5.1。
     * 另外,在處理完輸入的每對整數之后畫出 id[] 數組表示的森林。
     * 
     */
    class Program
    {
        static void Main(string[] args)
        {
            string[] input = "9-0 3-4 5-8 7-2 2-1 5-7 0-3 4-2".Split(' ');
            var quickUnion = new QuickUnionUF(10);

            foreach (string s in input)
            {
                quickUnion.ResetArrayCount();
                string[] numbers = s.Split('-');
                int p = int.Parse(numbers[0]);
                int q = int.Parse(numbers[1]);

                quickUnion.Union(p, q);
                int[] parent = quickUnion.GetParent();
                for (int i = 0; i < parent.Length; ++i)
                {
                    if (parent[i] == i)
                    {
                        Console.WriteLine("|---- " + i);
                        DFS(parent, i, 1);
                    }
                }
                Console.WriteLine("數組訪問:" + quickUnion.ArrayVisitCount);
            }
        }

        static void DFS(int[] parent, int root, int level)
        {
            for (int i = 0; i < parent.Length; ++i)
            {
                if (parent[i] == root && i != root)
                {
                    for (int j = 0; j < level; ++j)
                    {
                        Console.Write("    ");
                    }
                    Console.WriteLine("|---- " + i);
                    DFS(parent, i, level + 1);
                }
            }
        }
    }
}

 

1.5.3

解答

加權 quick-union 的官方實現:WeightedQuickUnionUF.java

樣例輸出:

9 1 2 3 4 5 6 7 8 9 數組訪問:3
9 1 2 3 3 5 6 7 8 9 數組訪問:3
9 1 2 3 3 5 6 7 5 9 數組訪問:3
9 1 7 3 3 5 6 7 5 9 數組訪問:3
9 7 7 3 3 5 6 7 5 9 數組訪問:5
9 7 7 3 3 7 6 7 5 9 數組訪問:3
9 7 7 9 3 7 6 7 5 9 數組訪問:5
9 7 7 9 3 7 6 7 5 7 數組訪問:9
代碼

WeightedQuickUnionUF.cs,這個類繼承了 QuickUnion.cs,重新實現了 Union() 和 Find() 等方法。

關於 QuickUnion.cs 可以參見 1.5.2 的代碼部分。

namespace UnionFind
{
    /// <summary>
    /// 使用加權 quick-union 算法的並查集。
    /// </summary>
    public class WeightedQuickUnionUF : QuickUnionUF
    {
        protected int[] size; // 記錄各個樹的大小。

        public int ArrayParentVisitCount { get; private set; } // 記錄 parent 數組的訪問次數。
        public int ArraySizeVisitCount { get; private set; } //記錄 size 數組的訪問次數。

        /// <summary>
        /// 建立使用加權 quick-union 的並查集。
        /// </summary>
        /// <param name="n">並查集的大小。</param>
        public WeightedQuickUnionUF(int n) : base(n)
        {
            this.size = new int[n];
            for (int i = 0; i < n; ++i)
            {
                this.size[i] = 1;
            }
            this.ArrayParentVisitCount = 0;
            this.ArraySizeVisitCount = 0;
        }

        /// <summary>
        /// 清零數組訪問計數。
        /// </summary>
        public override void ResetArrayCount()
        {
            this.ArrayParentVisitCount = 0;
            this.ArraySizeVisitCount = 0;
        }

        /// <summary>
        /// 獲取 size 數組。
        /// </summary>
        /// <returns>返回 size 數組。</returns>
        public int[] GetSize()
        {
            return this.size;
        }

        /// <summary>
        /// 尋找一個結點所在的連通分量。
        /// </summary>
        /// <param name="p">需要尋找的結點。</param>
        /// <returns>該結點所屬的連通分量。</returns>
        public override int Find(int p)
        {
            Validate(p);
            while (p != this.parent[p])
            {
                p = this.parent[p];
                this.ArrayParentVisitCount += 2;
            }
            this.ArrayParentVisitCount++;
            return p;
        }

        /// <summary>
        /// 將兩個結點所屬的連通分量合並。
        /// </summary>
        /// <param name="p">需要合並的結點。</param>
        /// <param name="q">需要合並的另一個結點。</param>
        public override void Union(int p, int q)
        {
            int rootP = Find(p);
            int rootQ = Find(q);
            if (rootP == rootQ)
            {
                return;
            }

            if (this.size[rootP] < this.size[rootQ])
            {
                this.parent[rootP] = rootQ;
                this.size[rootQ] += this.size[rootP];
            }
            else
            {
                this.parent[rootQ] = rootP;
                this.size[rootP] += this.size[rootQ];
            }
            this.ArrayParentVisitCount++;
            this.ArraySizeVisitCount += 4;
            this.count--;
        }
    }
}

 

Main 方法

using System;
using UnionFind;

namespace _1._5._3
{
    /*
     * 1.5.3
     * 
     * 使用加權 quick-union 算法(請見算法 1.5)完成練習 1.5.1 。
     * 
     */
    class Program
    {
        static void Main(string[] args)
        {
            string[] input = "9-0 3-4 5-8 7-2 2-1 5-7 0-3 4-2".Split(' ');
            var weightedQuickUnion = new WeightedQuickUnionUF(10);

            foreach (string s in input)
            {
                weightedQuickUnion.ResetArrayCount();
                string[] numbers = s.Split('-');
                int p = int.Parse(numbers[0]);
                int q = int.Parse(numbers[1]);

                weightedQuickUnion.Union(p, q);
                int[] parent = weightedQuickUnion.GetParent();
                for (int i = 0; i < parent.Length; ++i)
                {
                    Console.Write(parent[i] + " ");
                }
                Console.WriteLine("數組訪問:" + weightedQuickUnion.ArrayParentVisitCount);
            }
        }
    }
}

 

1.5.4

解答

對照輸入和最壞輸入均在書中出現,中文版見:P146,英文版見:P229。

樣例輸出:

4 3
index:  0 1 2 3 4 5 6 7 8 9
parent: 0 1 2 4 4 5 6 7 8 9
size:   1 1 1 1 2 1 1 1 1 1
parent visit count:3
size visit count:4

3 8
index:  0 1 2 3 4 5 6 7 8 9
parent: 0 1 2 4 4 5 6 7 4 9
size:   1 1 1 1 3 1 1 1 1 1
parent visit count:5
size visit count:4

6 5
index:  0 1 2 3 4 5 6 7 8 9
parent: 0 1 2 4 4 6 6 7 4 9
size:   1 1 1 1 3 1 2 1 1 1
parent visit count:3
size visit count:4

9 4
index:  0 1 2 3 4 5 6 7 8 9
parent: 0 1 2 4 4 6 6 7 4 4
size:   1 1 1 1 4 1 2 1 1 1
parent visit count:3
size visit count:4

2 1
index:  0 1 2 3 4 5 6 7 8 9
parent: 0 2 2 4 4 6 6 7 4 4
size:   1 1 2 1 4 1 2 1 1 1
parent visit count:3
size visit count:4

8 9
index:  0 1 2 3 4 5 6 7 8 9
parent: 0 2 2 4 4 6 6 7 4 4
size:   1 1 2 1 4 1 2 1 1 1
parent visit count:6
size visit count:0

5 0
index:  0 1 2 3 4 5 6 7 8 9
parent: 6 2 2 4 4 6 6 7 4 4
size:   1 1 2 1 4 1 3 1 1 1
parent visit count:5
size visit count:4

7 2
index:  0 1 2 3 4 5 6 7 8 9
parent: 6 2 2 4 4 6 6 2 4 4
size:   1 1 3 1 4 1 3 1 1 1
parent visit count:3
size visit count:4

6 1
index:  0 1 2 3 4 5 6 7 8 9
parent: 6 2 6 4 4 6 6 2 4 4
size:   1 1 3 1 4 1 6 1 1 1
parent visit count:5
size visit count:4

1 0
index:  0 1 2 3 4 5 6 7 8 9
parent: 6 2 6 4 4 6 6 2 4 4
size:   1 1 3 1 4 1 6 1 1 1
parent visit count:8
size visit count:0

6 7
index:  0 1 2 3 4 5 6 7 8 9
parent: 6 2 6 4 4 6 6 2 4 4
size:   1 1 3 1 4 1 6 1 1 1
parent visit count:6
size visit count:0

-------------------------------------
0 1
index:  0 1 2 3 4 5 6 7 8 9
parent: 0 0 2 3 4 5 6 7 8 9
size:   2 1 1 1 1 1 1 1 1 1
parent visit count:3
size visit count:4

0 2
index:  0 1 2 3 4 5 6 7 8 9
parent: 0 0 0 3 4 5 6 7 8 9
size:   3 1 1 1 1 1 1 1 1 1
parent visit count:3
size visit count:4

0 3
index:  0 1 2 3 4 5 6 7 8 9
parent: 0 0 0 0 4 5 6 7 8 9
size:   4 1 1 1 1 1 1 1 1 1
parent visit count:3
size visit count:4

0 4
index:  0 1 2 3 4 5 6 7 8 9
parent: 0 0 0 0 0 5 6 7 8 9
size:   5 1 1 1 1 1 1 1 1 1
parent visit count:3
size visit count:4

0 5
index:  0 1 2 3 4 5 6 7 8 9
parent: 0 0 0 0 0 0 6 7 8 9
size:   6 1 1 1 1 1 1 1 1 1
parent visit count:3
size visit count:4

0 6
index:  0 1 2 3 4 5 6 7 8 9
parent: 0 0 0 0 0 0 0 7 8 9
size:   7 1 1 1 1 1 1 1 1 1
parent visit count:3
size visit count:4

0 7
index:  0 1 2 3 4 5 6 7 8 9
parent: 0 0 0 0 0 0 0 0 8 9
size:   8 1 1 1 1 1 1 1 1 1
parent visit count:3
size visit count:4

0 8
index:  0 1 2 3 4 5 6 7 8 9
parent: 0 0 0 0 0 0 0 0 0 9
size:   9 1 1 1 1 1 1 1 1 1
parent visit count:3
size visit count:4

0 9
index:  0 1 2 3 4 5 6 7 8 9
parent: 0 0 0 0 0 0 0 0 0 0
size:   10 1 1 1 1 1 1 1 1 1
parent visit count:3
size visit count:4
代碼

Main 方法:

using System;
using UnionFind;

namespace _1._5._4
{
    /*
     * 1.5.4
     * 
     * 在正文的加權 quick-union 算法示例中,
     * 對於輸入的每一對整數(包括對照輸入和最壞情況下的輸入),
     * 給出 id[] 和 sz[] 數組的內容以及訪問數組的次數。
     * 
     */
    class Program
    {
        static void Main(string[] args)
        {
            char[] split = { '\n', '\r' };
            string[] inputReference = TestCase.Properties.Resources.tinyUF.Split(split, StringSplitOptions.RemoveEmptyEntries);
            string[] inputWorst = TestCase.Properties.Resources.worstUF.Split(split, StringSplitOptions.RemoveEmptyEntries);
            
            RunTest(inputReference);
            Console.WriteLine("-------------------------------------");
            RunTest(inputWorst);
        }

        static void RunTest(string[] input)
        {
            var weightedQuickUnion = new WeightedQuickUnionUF(10);
            int n = int.Parse(input[0]);
            int[] parent = weightedQuickUnion.GetParent();
            int[] size = weightedQuickUnion.GetSize();

            for (int i = 1; i < input.Length; ++i)
            {
                string[] unit = input[i].Split(' ');
                int p = int.Parse(unit[0]);
                int q = int.Parse(unit[1]);

                Console.WriteLine($"{p} {q}");
                weightedQuickUnion.Union(p, q);

                Console.Write("index:\t");
                for (int j = 0; j < 10; ++j)
                {
                    Console.Write(j + " ");
                }
                Console.WriteLine();

                Console.Write("parent:\t");
                foreach (int m in parent)
                {
                    Console.Write(m + " ");
                }
                Console.WriteLine();
                Console.Write("size:\t");
                foreach (int m in size)
                {
                    Console.Write(m + " ");
                }
                Console.WriteLine();
                Console.WriteLine("parent visit count:" + weightedQuickUnion.ArrayParentVisitCount);
                Console.WriteLine("size visit count:" + weightedQuickUnion.ArraySizeVisitCount);
                Console.WriteLine();
                weightedQuickUnion.ResetArrayCount();
            }
        }
    }
}

 

1.5.5

解答

106 條連接 = 106 組輸入。
對於 quick-find 算法,每次 union() 都要遍歷整個數組。
因此總共進行了 109 * 106 = 1015 次 for 循環迭代。
每次 for 循環迭代都需要 10 條機器指令,
因此總共執行了 10 * 1015 = 1016 條機器指令。
已知計算機每秒能夠執行 109 條機器指令,
因此執行完所有指令需要 1016 / 109 = 107 秒 = 115.74 天

 

1.5.6

解答

加權 quick-union 算法最多只需要 lgN 次迭代就可以完成一次 union()。
因此按照上題思路,總共需要 (lg(109) * 106 * 10) / 109 = 0.299 秒。

 

1.5.7

解答

見 1.5.1 和 1.5.2 的解答。

 

1.5.8

解答

當有多個元素需要修改的時候,這個直觀算法可能會出現錯誤。
例如如下情況:
index 0 1 2 3 4 5 6 7 8 9
id    0 0 0 0 0 5 5 5 5 5
輸入 0, 5
i = 0 時,id[i] == id[p],此時 id[i] = id[q]。
數組變為 5 0 0 0 0 5 5 5 5 5
i = 1 時,id[i] != id[p],算法出現錯誤。

如果在 id[p] 之后還有需要修改的元素,那么這個算法就會出現錯誤。

 

1.5.9

解答

不可能。
樹如下所示。

image

由於加權 quick-union 算法任意節點的最大深度為 lgN (節點總數為 N)。
(這個結論可以在中文版 P146,或者英文版 P228 找到)
上面這個樹的最大深度為 4 > lg10
因此這棵樹不可能是通過加權 quick-union 算法得到的。

 

1.5.10

解答

本題答案已經給出,也很好理解。

如果合並時只是把子樹掛到結點 q 上而非其根節點,樹的高度會明顯增加,進而增加每次 Find() 操作的開銷。

 

1.5.11

解答

類似於加權 quick-union 的做法,新增一個 size[] 數組以記錄各個根節點的大小。

每次合並時先比較一下兩棵樹的大小,再進行合並。

這樣會略微減少賦值語句的執行次數,提升性能。

代碼

WeightedQuickFindUF.cs

using System;

namespace _1._5._11
{
    /// <summary>
    /// 用加權 QuickFind 算法實現的並查集。
    /// </summary>
    public class WeightedQuickFindUF
    {
        private int[] size; // 記錄每個連通分量的大小。
        private int[] id; // 記錄每個結點的連通分量。
        private int count;// 連通分量總數。

        public int ArrayVisitCount { get; private set; } //記錄數組訪問的次數。

        /// <summary>
        /// 新建一個使用加權 quick-find 實現的並查集。
        /// </summary>
        /// <param name="n">並查集的大小。</param>
        public WeightedQuickFindUF(int n)
        {
            this.count = n;
            this.id = new int[n];
            this.size = new int[n];
            for (int i = 0; i < n; ++i)
            {
                this.id[i] = i;
                this.size[i] = 1;
            }
        }

        /// <summary>
        /// 重置數組訪問計數。
        /// </summary>
        public void ResetArrayCount()
        {
            this.ArrayVisitCount = 0;
        }

        /// <summary>
        /// 表示並查集中連通分量的數量。
        /// </summary>
        /// <returns>返回並查集中連通分量的數量。</returns>
        public int Count()
        {
            return this.count;
        }
        
        /// <summary>
        /// 尋找 p 所在的連通分量。
        /// </summary>
        /// <param name="p">需要尋找的結點。</param>
        /// <returns>返回 p 所在的連通分量。</returns>
        public int Find(int p)
        {
            Validate(p);
            this.ArrayVisitCount++;
            return this.id[p];
        }

        /// <summary>
        /// 判斷兩個結點是否屬於同一個連通分量。
        /// </summary>
        /// <param name="p">需要判斷的結點。</param>
        /// <param name="q">需要判斷的另一個結點。</param>
        /// <returns>如果屬於同一個連通分量則返回 true,否則返回 false。</returns>
        public bool IsConnected(int p, int q)
        {
            Validate(p);
            Validate(q);
            this.ArrayVisitCount += 2;
            return this.id[p] == this.id[q];
        }

        /// <summary>
        /// 將兩個結點所在的連通分量合並。
        /// </summary>
        /// <param name="p">需要合並的結點。</param>
        /// <param name="q">需要合並的另一個結點。</param>
        public void Union(int p, int q)
        {
            Validate(p);
            Validate(q);
            int pID = this.id[p];
            int qID = this.id[q];
            this.ArrayVisitCount += 2;

            // 如果兩個結點同屬於一個連通分量,那么什么也不做。
            if (pID == qID)
            {
                return;
            }

            // 判斷較大的連通分量和較小的連通分量。
            int larger = 0;
            int smaller = 0;
            if (this.size[pID] > this.size[qID])
            {
                larger = pID;
                smaller = qID;
                this.size[pID] += this.size[qID];
            }
            else
            {
                larger = qID;
                smaller = pID;
                this.size[qID] += this.size[pID];
            }

            // 將較小的連通分量連接到較大的連通分量上,
            // 這會減少賦值語句的執行次數,略微減少數組訪問。
            for (int i = 0; i < this.id.Length; ++i)
            {
                if (this.id[i] == smaller)
                {
                    this.id[i] = larger;
                    this.ArrayVisitCount++;
                }
            }

            this.ArrayVisitCount += this.id.Length;
            this.count--;
            return;
        }

        /// <summary>
        /// 獲得 id 數組。
        /// </summary>
        /// <returns>id 數組。</returns>
        public int[] GetID()
        {
            return this.id;
        }

        /// <summary>
        /// 驗證輸入的結點是否有效。
        /// </summary>
        /// <param name="p">需要驗證的結點。</param>
        /// <exception cref="ArgumentException">輸入的 p 值無效。</exception>
        private void Validate(int p)
        {
            int n = this.id.Length;
            if (p < 0 || p > n)
            {
                throw new ArgumentException("index " + p + " is not between 0 and " + (n - 1));
            }
        }
    }
}

 

Main 方法

using System;
using UnionFind;

namespace _1._5._11
{
    /*
     * 1.5.11
     * 
     * 實現加權 quick-find 算法,其中我們總是將較小的分量重命名為較大分量的標識符。
     * 這種改變會對性能產生怎樣的影響?
     * 
     */
    class Program
    {
        static void Main(string[] args)
        {
            char[] split = { '\n', '\r' };
            string[] input = TestCase.Properties.Resources.mediumUF.Split(split, StringSplitOptions.RemoveEmptyEntries);
            int size = int.Parse(input[0]);

            QuickFindUF quickFind = new QuickFindUF(size);
            WeightedQuickFindUF weightedQuickFind = new WeightedQuickFindUF(size);

            int p, q;
            string[] pair;
            for (int i = 1; i < size; ++i)
            {
                pair = input[i].Split(' ');
                p = int.Parse(pair[0]);
                q = int.Parse(pair[1]);
                quickFind.Union(p, q);
                weightedQuickFind.Union(p, q);
            }

            Console.WriteLine("quick-find: " + quickFind.ArrayVisitCount);
            Console.WriteLine("weighted quick-find: " + weightedQuickFind.ArrayVisitCount);
        }
    }
}

 

 

1.5.12

解答

QuickUnionPathCompression 的官方實現:QuickUnionPathCompressionUF.java

在找到根節點之后,再訪問一遍 p 到根節點這條路徑上的所有結點,將它們直接和根節點相連。

重寫過后的 Find() 方法:

/// <summary>
/// 尋找結點所屬的連通分量。
/// </summary>
/// <param name="p">需要尋找的結點。</param>
/// <returns>結點所屬的連通分量。</returns>
public override int Find(int p)
{
    int root = p;
    while (root != this.parent[root])
    {
        root = this.parent[root];
    }
    while (p != root)
    {
        int newp = this.parent[p];
        this.parent[p] = root;
        p = newp;
    }
    return p;
}

由於路徑壓縮是在 Find() 方法中實現的,只要輸入保證是根節點兩兩相連即可構造較長的路徑。

代碼

QuickUnionPathCompressionUF.cs 直接從 QuickUnionUF.cs 繼承而來。
關於 QuickUnionUF.cs,參見 1.5.2 的解答。

namespace UnionFind
{
    /// <summary>
    /// 使用路徑壓縮的 quick-union 並查集。
    /// </summary>
    public class QuickUnionPathCompressionUF : QuickFindUF
    {
        /// <summary>
        /// 新建一個大小為 n 的並查集。
        /// </summary>
        /// <param name="n">新建並查集的大小。</param>
        public QuickUnionPathCompressionUF(int n) : base(n) { }

        /// <summary>
        /// 尋找結點所屬的連通分量。
        /// </summary>
        /// <param name="p">需要尋找的結點。</param>
        /// <returns>結點所屬的連通分量。</returns>
        public override int Find(int p)
        {
            int root = p;
            while (root != this.parent[root])
            {
                root = this.parent[root];
            }
            while (p != root)
            {
                int newp = this.parent[p];
                this.parent[p] = root;
                p = newp;
            }
            return p;
        }
    }
}

 

Main 方法

using System;
using UnionFind;

namespace _1._5._12
{
    /*
     * 1.5.12
     * 
     * 使用路徑壓縮的 quick-union 算法。
     * 根據路徑壓縮修改 quick-union 算法(請見 1.5.2.3 節),
     * 在 find() 方法中添加一個循環來將從 p 到根節點的路徑上的每個觸點都連接到根節點。
     * 給出一列輸入,使該方法能夠產生一條長度為 4 的路徑。
     * 注意:該算法的所有操作的均攤成本已知為對數級別。
     * 
     */
    class Program
    {
        static void Main(string[] args)
        {
            var UF = new QuickUnionPathCompressionUF(10);

            // 使用書中提到的最壞情況,0 連 1,1 連 2,2 連 3……
            for (int i = 0; i < 4; ++i)
            {
                UF.Union(i, i + 1);
            }

            int[] id = UF.GetParent();
            for (int i = 0; i < id.Length; ++i)
            {
                Console.Write(id[i]);
            }
            Console.WriteLine();
        }
    }
}

 

1.5.13

解答

官方實現:WeightedQuickUnionPathCompressionUF

加權 quick-union 中,兩個大小相等的樹合並可以有效增加高度,同時輸入必須保證是根節點以規避路徑壓縮。

代碼

WeightedQuickUnionPathCompressionUF.cs 從 WeightedQuickUnionUF.cs 繼承,詳情參見 1.5.3 的解答。

namespace UnionFind
{
    /// <summary>
    /// 使用路徑壓縮的加權 quick-union 並查集。
    /// </summary>
    public class WeightedQuickUnionPathCompressionUF : WeightedQuickUnionUF
    {
        /// <summary>
        /// 新建一個大小為 n 的並查集。
        /// </summary>
        /// <param name="n">新建並查集的大小。</param>
        public WeightedQuickUnionPathCompressionUF(int n) : base(n)
        {
            this.size = new int[n];

            for (int i = 0; i < n; ++i)
            {
                this.size[i] = 1;
            }
        }

        /// <summary>
        /// 尋找一個結點所在的連通分量。
        /// </summary>
        /// <param name="p">需要尋找的結點。</param>
        /// <returns>該結點所屬的連通分量。</returns>
        public override int Find(int p)
        {
            Validate(p);
            int root = p;
            while (root != this.parent[p])
            {
                root = this.parent[p];
            }
            while (p != root)
            {
                int newP = this.parent[p];
                this.parent[p] = root;
                p = newP;
            }
            return root;
        }
    }
}

 

Main 方法

using System;
using UnionFind;

namespace _1._5._13
{
    /*
     * 1.5.13
     * 
     * 使用路徑壓縮的加權 quick-union 算法。
     * 修改加權 quick-union 算法(算法 1.5),
     * 實現如練習 1.5.12 所述的路徑壓縮。給出一列輸入,
     * 使該方法能產生一棵高度為 4 的樹。
     * 注意:該算法的所有操作的均攤成本已知被限制在反 Ackermann 函數的范圍之內,
     * 且對於實際應用中可能出現的所有 N 值均小於 5。
     * 
     */
    class Program
    {
        static void Main(string[] args)
        {
            var UF = new WeightedQuickUnionPathCompressionUF(10);

            // 見中文版 P146 或英文版 P229 中加權 quick-union 的最壞輸入。
            UF.Union(0, 1);
            UF.Union(2, 3);
            UF.Union(4, 5);
            UF.Union(6, 7);
            UF.Union(0, 2);
            UF.Union(4, 6);
            UF.Union(0, 4);

            int[] id = UF.GetParent();
            for (int i = 0; i < id.Length; ++i)
            {
                Console.Write(id[i]);
            }
            Console.WriteLine();
        }
    }
}

 

1.5.14

解答

WeightedQuickUnionByHeight 的官方實現:WeightedQuickUnionByHeightUF.java

證明:
一次 Union 操作只可能發生如下兩種情況。
1.兩棵樹的高度相同,這樣合並后的新樹的高度等於較大那棵樹的高度 + 1。
2.兩棵樹的高度不同,這樣合並后的新樹高度等於較大那棵樹的高度。

現在證明通過加權 quick-union 算法構造的高度為 h 的樹至少包含 2h 個結點。
基礎情況,高度 h = 0, 結點數 k = 1。
為了使高度增加,必須用一棵高度相同的樹合並,而 h = 0 時結點數一定是 1,則:
h = 1, k = 2
由於兩棵大小不同的樹合並,最大高度不會增加,只會增加結點數。
因此,每次都使用相同高度的最小樹進行合並,有:
h = 2, k = 4
h = 3, k = 8
h = 4, k = 16
......
遞推即可得到結論,k ≥ 2h
因此 h <= lgk

代碼
namespace UnionFind
{
    public class WeightedQuickUnionByHeightUF : QuickUnionUF
    {
        private int[] height;

        /// <summary>
        /// 新建一個以高度作為判斷依據的加權 quick-union 並查集。
        /// </summary>
        /// <param name="n">新建並查集的大小。</param>
        public WeightedQuickUnionByHeightUF(int n) : base(n)
        {
            this.height = new int[n];
            for (int i = 0; i < n; ++i)
            {
                this.height[i] = 0;
            }
        }

        /// <summary>
        /// 將兩個結點所屬的連通分量合並。
        /// </summary>
        /// <param name="p">需要合並的結點。</param>
        /// <param name="q">需要合並的另一個結點。</param>
        public override void Union(int p, int q)
        {
            int rootP = Find(p);
            int rootQ = Find(q);

            if (rootP == rootQ)
            {
                return;
            }

            if (this.height[rootP] < this.height[rootQ])
            {
                this.parent[rootP] = rootQ;
            }
            else if (this.height[rootP] > this.height[rootQ])
            {
                this.parent[rootQ] = rootP;
            }
            else
            {
                this.parent[rootQ] = rootP;
                this.height[rootP]++;
            }
            this.count--;
        }
    }
}

 

1.5.15

解答

首先證明在最壞情況下加權 quick-union 算法生成的樹中的每一層結點數均為二項式系數。

最壞情況下,每次 union 操作都是合並相同大小的樹,如下圖所示:

image

設第 i 層的結點數為 ki,那么最壞情況下每次合並后的 ki’ = ki + ki-1 。
這符合二項式系數的構造特點(詳情可以搜索楊輝三角),第一個結論證明完畢。

 

接下來求平均深度,首先根據二項式的求和公式,一棵深度為 n 的樹(根結點的深度為零)結點總數為:

image

每層結點數 × 該層深度后的和為:

image

這里用到了這個公式化簡:

image

相除可以求得平均深度:

image

 

1.5.16

解答

給出繪圖結果樣例:

imageimage

代碼

僅給出繪圖相關的代碼,窗體部分見 github 上的代碼:

using System;
using System.Linq;
using System.Windows.Forms;
using System.Drawing;
using UnionFind;

namespace _1._5._16
{
    /*
    * 1.5.16
    * 
    * 均攤成本的圖像。
    * 修改你為練習 1.5.7 給出的實現,
    * 繪出如正文所示的均攤成本的圖像。
    * 
    */
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Compute();
            Application.Run(new Form1());
        }

        static void Compute()
        {
            char[] split = { '\n', '\r' };
            string[] input = TestCase.Properties.Resources.mediumUF.Split(split, StringSplitOptions.RemoveEmptyEntries);
            int size = int.Parse(input[0]);
            QuickFindUF quickFind = new QuickFindUF(size);
            QuickUnionUF quickUnion = new QuickUnionUF(size);

            string[] pair;
            int p, q;
            int[] quickFindResult = new int[size];
            int[] quickUnionResult = new int[size];
            for (int i = 1; i < size; ++i)
            {
                pair = input[i].Split(' ');
                p = int.Parse(pair[0]);
                q = int.Parse(pair[1]);

                quickFind.Union(p, q);
                quickUnion.Union(p, q);
                quickFindResult[i - 1] = quickFind.ArrayVisitCount;
                quickUnionResult[i - 1] = quickUnion.ArrayVisitCount;

                quickFind.ResetArrayCount();
                quickUnion.ResetArrayCount();
            }

            Draw(quickFindResult);
            Draw(quickUnionResult);
        }

        static void Draw(int[] cost)
        {
            // 構建 total 數組。
            int[] total = new int[cost.Length];
            total[0] = cost[0];
            for (int i = 1; i < cost.Length; ++i)
            {
                total[i] = total[i - 1] + cost[i];
            }

            // 獲得最大值。
            int costMax = cost.Max();

            // 新建繪圖窗口。
            Form2 plot = new Form2();
            plot.Show();
            Graphics graphics = plot.CreateGraphics();

            // 獲得繪圖區矩形。
            RectangleF rect = plot.ClientRectangle;
            float unitX = rect.Width / 10;
            float unitY = rect.Width / 10;

            // 添加 10% 邊距作為文字區域。
            RectangleF center = new RectangleF
                (rect.X + unitX, rect.Y + unitY, 
                rect.Width - 2 * unitX, rect.Height - 2 * unitY);

            // 繪制坐標系。
            graphics.DrawLine(Pens.Black, center.Left, center.Top, center.Left, center.Bottom);
            graphics.DrawLine(Pens.Black, center.Left, center.Bottom, center.Right, center.Bottom);
            graphics.DrawString(costMax.ToString(), plot.Font, Brushes.Black, rect.Location);
            graphics.DrawString(cost.Length.ToString(), plot.Font, Brushes.Black, center.Right, center.Bottom);
            graphics.DrawString("0", plot.Font, Brushes.Black, rect.Left, center.Bottom);

            // 初始化點。
            PointF[] grayPoints = new PointF[cost.Length];
            PointF[] redPoints = new PointF[cost.Length];
            unitX = center.Width / cost.Length;
            unitY = center.Width / costMax;

            for (int i = 0; i < cost.Length; ++i)
            {
                grayPoints[i] = new PointF(center.Left + unitX * (i + 1), center.Bottom - (cost[i] * unitY));
                redPoints[i] = new PointF(center.Left + unitX * (i + 1), center.Bottom - ((total[i] / (i + 1)) * unitY));
            }

            // 繪制點。
            for (int i = 0; i < cost.Length; ++i)
            {
                graphics.DrawEllipse(Pens.Gray, new RectangleF(grayPoints[i], new SizeF(2, 2)));
                graphics.DrawEllipse(Pens.Red, new RectangleF(redPoints[i], new SizeF(2, 2)));
            }

            graphics.Dispose();
        }
    }
}

 

1.5.17

解答

官方給出的 ErdosRenyi:ErdosRenyi.java

為了方便之后做題,除了 Count() 之外,這個類還包含其他方法,具體可以查看注釋。

代碼

ErdosRenyi.cs

using System;
using System.Collections.Generic;

namespace UnionFind
{
    /// <summary>
    /// 提供一系列對並查集進行隨機測試的靜態方法。
    /// </summary>
    public class ErdosRenyi
    {
        /// <summary>
        /// 隨機生成一組能讓並查集只剩一個連通分量的連接。
        /// </summary>
        /// <param name="n">並查集大小。</param>
        /// <returns>一組能讓並查集只剩一個連通分量的連接。</returns>
        public static Connection[] Generate(int n)
        {
            Random random = new Random();
            List<Connection> connections = new List<Connection>();
            WeightedQuickUnionPathCompressionUF uf = new WeightedQuickUnionPathCompressionUF(n);

            while (uf.Count() > 1)
            {
                int p = random.Next(n);
                int q = random.Next(n);
                uf.Union(p, q);
                connections.Add(new Connection(p, q));
            }

            return connections.ToArray();
        }

        /// <summary>
        /// 隨機生成連接,返回令並查集中只剩一個連通分量所需的連接總數。
        /// </summary>
        /// <param name="uf">用於測試的並查集。</param>
        /// <returns>需要的連接總數。</returns>
        public static int Count(UF uf)
        {
            Random random = new Random();
            int size = uf.Count();
            int edges = 0;
            while (uf.Count() > 1)
            {
                int p = random.Next(size);
                int q = random.Next(size);
                uf.Union(p, q);
                edges++;
            }

            return edges;
        }

        /// <summary>
        /// 使用指定的連接按順序合並。
        /// </summary>
        /// <param name="uf">需要測試的並查集。</param>
        /// <param name="connections">用於輸入的連接集合。</param>
        public static void Count(UF uf, Connection[] connections)
        {
            foreach (Connection c in connections)
            {
                uf.Union(c.P, c.Q);
            }
        }
    }
}

 

Main 方法:

using System;
using UnionFind;

namespace _1._5._17
{
    /*
     * 1.5.17
     * 
     * 隨機鏈接。
     * 設計 UF 的一個用例 ErdosRenyi,
     * 從命令行接受一個整數 N,在 0 到 N-1 之間產生隨機整數對,
     * 調用 connected() 判斷它們是否相連,
     * 如果不是則調用 union() 方法(和我們的開發用例一樣)。
     * 不斷循環直到所有觸點均相互連通並打印出生成的連接總數。
     * 將你的程序打包成一個接受參數 N 並返回連接總數的靜態方法 count(),
     * 添加一個 main() 方法從命令行接受 N,調用 count() 並打印它的返回值。
     * 
     */
    class Program
    {
        static void Main(string[] args)
        {
            int N = 10;
            int[] edges = new int[5];
            for (int i = 0; i < 5; ++i)
            {
                var uf = new UF(N);
                Console.WriteLine(N + "\t" + ErdosRenyi.Count(uf));
                N *= 10;
            }
        }
    }
}

 

1.5.18

解答

具體生成的連接樣式見下題,這里給出 RandomGrid 的實現,需要使用 1.3 節中的隨機背包輔助。

代碼

RandomGrid.cs

using System;
using System.Collections.Generic;

namespace UnionFind
{
    public class RandomGrid
    {
        /// <summary>
        /// 隨機生成 n × n 網格中的所有連接。
        /// </summary>
        /// <param name="n">網格邊長。</param>
        /// <returns>隨機排序的連接。</returns>
        public static RandomBag<Connection> Generate(int n)
        {
            var result = new RandomBag<Connection>();
            var random = new Random();

            // 建立橫向連接
            for (int i = 0; i < n; ++i)
            {
                for (int j = 0; j < n - 1; ++j)
                {
                    if (random.Next(10) > 4)
                    {
                        result.Add(new Connection(i * n + j, (i * n) + j + 1));
                    }
                    else
                    {
                        result.Add(new Connection((i * n) + j + 1, i * n + j));
                    }
                }
            }

            // 建立縱向連接
            for (int j = 0; j < n; ++j)
            {
                for (int i = 0; i < n - 1; ++i)
                {
                    if (random.Next(10) > 4)
                    {
                        result.Add(new Connection(i * n + j, ((i + 1) * n) + j));
                    }
                    else
                    {
                        result.Add(new Connection(((i + 1) * n) + j, i * n + j));
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// 隨機生成 n × n 網格中的所有連接,返回一個連接數組。
        /// </summary>
        /// <param name="n">網格邊長。</param>
        /// <returns>連接數組。</returns>
        public static Connection[] GetConnections(int n)
        {
            RandomBag<Connection> bag = Generate(n);
            List<Connection> connections = new List<Connection>();

            foreach (Connection c in bag)
            {
                connections.Add(c);
            }

            return connections.ToArray();
        }
    }
}

 

1.5.19

解答

最后繪出的圖像:

image

代碼

給出繪圖部分的代碼,窗體部分見 GitHub。

using System;
using System.Drawing;
using System.Collections.Generic;
using System.Windows.Forms;
using UnionFind;

namespace _1._5._19
{
    /*
     * 1.5.19
     * 
     * 動畫。
     * 編寫一個 RandomGrid(請見練習 1.5.18)的用例,
     * 和我們開發用例一樣使用 UnionFind 來檢查觸點的連通性並在處理時用 StdDraw 將它們繪出。
     * 
     */
    static class Program
    {
        static RandomBag<Connection> bag;
        static Graphics graphics;
        static TextBox logBox;
        static PointF[] points;
        static Timer timer;
        static List<Connection> connections;
        static int count = 0;

        /// <summary>
        /// 應用程序的主入口點。
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }

        /// <summary>
        /// 繪制連接圖像。
        /// </summary>
        /// <param name="n">矩陣邊長。</param>
        public static void Draw(int n, TextBox log, Log WinBox)
        {
            logBox = log;

            // 生成路徑。
            log.AppendText("\r\n開始生成連接……");
            bag = RandomGrid.Generate(n);
            log.AppendText("\r\n生成連接完成");

            // 新建畫布窗口。
            log.AppendText("\r\n啟動畫布……");
            Form2 matrix = new Form2();
            matrix.StartPosition = FormStartPosition.Manual;
            matrix.Location = new Point(WinBox.Left - matrix.ClientRectangle.Width, WinBox.Top);
            matrix.Show();
            log.AppendText("\r\n畫布已啟動,開始繪圖……");
            graphics = matrix.CreateGraphics();

            // 獲取繪圖區域。
            RectangleF rect = matrix.ClientRectangle;
            float unitX = rect.Width / (n + 1);
            float unitY = rect.Height / (n + 1);

            // 繪制點。
            log.AppendText("\r\n繪制點……");
            points = new PointF[n * n];
            for (int row = 0; row < n; ++row)
            {
                for (int col = 0; col < n; ++col)
                {
                    points[row * n + col] = new PointF(unitX * (col + 1), unitY * (row + 1));
                    graphics.FillEllipse(Brushes.Black, unitX * (col + 1), unitY * (row + 1), 5, 5);
                }
            }
            log.AppendText("\r\n繪制點完成");

            // 繪制連接。
            log.AppendText("\r\n開始繪制連接……");
            connections = new List<Connection>();
            foreach (Connection c in bag)
            {
                connections.Add(c);
            }
            timer = new Timer
            {
                Interval = 500
            };
            timer.Tick += DrawOneLine;
            timer.Start();              
        }

        private static void DrawOneLine(object sender, EventArgs e)
        {
            Connection c = connections[count];
            count++;
            graphics.DrawLine(Pens.Black, points[c.P], points[c.Q]);
            logBox.AppendText("\r\n繪制" + "(" + c.P + ", " + c.Q + ")");
            if (count == bag.Size())
            {
                timer.Stop();
                logBox.AppendText("\r\n繪制結束");
                count = 0;
            }
        }
    }
}

 

1.5.20

解答

將 parent 數組和 size 數組用鏈表代替即可,很容易實現。

代碼

修改后的 WeightedQuickUnionUF.cs

using System;

namespace _1._5._20
{
    /// <summary>
    /// 使用加權 quick-union 算法的並查集。
    /// </summary>
    public class WeightedQuickUnionUF
    {
        protected LinkedList<int> parent;       // 記錄各個結點的父級。
        protected LinkedList<int> size;         // 記錄各個樹的大小。
        protected int count;                    // 分量數目。

        /// <summary>
        /// 建立使用加權 quick-union 的並查集。
        /// </summary>
        /// <param name="n">並查集的大小。</param>
        public WeightedQuickUnionUF()
        {
            this.parent = new LinkedList<int>();
            this.size = new LinkedList<int>();
        }

        /// <summary>
        /// 獲取 parent 數組。
        /// </summary>
        /// <returns>parent 數組。</returns>
        public LinkedList<int> GetParent()
        {
            return this.parent;
        }

        /// <summary>
        /// 獲取 size 數組。
        /// </summary>
        /// <returns>返回 size 數組。</returns>
        public LinkedList<int> GetSize()
        {
            return this.size;
        }

        /// <summary>
        /// 在並查集中增加一個新的結點。
        /// </summary>
        /// <returns>新結點的下標。</returns>
        public int NewSite()
        {
            this.parent.Insert(this.parent.Size(), this.parent.Size());
            this.size.Insert(1, this.size.Size());
            this.count++;
            return this.parent.Size() - 1;
        }

        /// <summary>
        /// 尋找一個結點所在的連通分量。
        /// </summary>
        /// <param name="p">需要尋找的結點。</param>
        /// <returns>該結點所屬的連通分量。</returns>
        public int Find(int p)
        {
            Validate(p);
            while (p != this.parent.Find(p))
            {
                p = this.parent.Find(p);
            }
            return p;
        }

        /// <summary>
        /// 將兩個結點所屬的連通分量合並。
        /// </summary>
        /// <param name="p">需要合並的結點。</param>
        /// <param name="q">需要合並的另一個結點。</param>
        public void Union(int p, int q)
        {
            int rootP = Find(p);
            int rootQ = Find(q);
            if (rootP == rootQ)
            {
                return;
            }

            if (this.size.Find(rootP) < this.size.Find(rootQ))
            {
                this.parent.Motify(rootP, rootQ);
                this.size.Motify(rootQ, this.size.Find(rootQ) + this.size.Find(rootP));
            }
            else
            {
                this.parent.Motify(rootQ, rootP);
                this.size.Motify(rootP, this.size.Find(rootQ) + this.size.Find(rootP));
            }
            this.count--;
        }

        /// <summary>
        /// 檢查輸入的 p 是否符合條件。
        /// </summary>
        /// <param name="p">輸入的 p 值。</param>
        protected void Validate(int p)
        {
            int n = this.parent.Size();
            if (p < 0 || p >= n)
            {
                throw new ArgumentException("index" + p + " is not between 0 and " + (n - 1));
            }
        }
    }
}

 

1.5.21

解答

給出我電腦上的結果:

實驗結果:10
1/2NlnN:11.5129254649702

實驗結果:29
1/2NlnN:29.9573227355399

實驗結果:132
1/2NlnN:73.7775890822787

實驗結果:164
1/2NlnN:175.281065386955

實驗結果:418
1/2NlnN:406.013905218706

實驗結果:1143
1/2NlnN:922.931359327004

實驗結果:2004
1/2NlnN:2067.66981643319

實驗結果:4769
1/2NlnN:4578.95382842474

實驗結果:10422
1/2NlnN:10045.1360479662

實驗結果:21980
1/2NlnN:21864.7288781659
代碼
using System;
using UnionFind;

namespace _1._5._21
{
    /*
     * 1.5.21
     * 
     * Erdös-Renyi 模型。
     * 使用練習 1.5.17 的用例驗證這個猜想:
     * 得到單個連通分量所需生成的整數對數量為 ~1/2NlnN。
     * 
     */
    class Program
    {
        static void Main(string[] args)
        {
            for (int n = 10; n < 10000; n *= 2)
            {
                int total = 0;
                for (int i = 0; i < 100; ++i)
                {
                    UF uf = new UF(n);
                    total += ErdosRenyi.Count(uf);
                }

                Console.WriteLine("實驗結果:" + total / 100);
                Console.WriteLine("1/2NlnN:" + Math.Log(n) * n * 0.5);
                Console.WriteLine();
            }
        }
    }
}

 

1.5.22

解答

平方級別算法在輸入加倍后耗時應該增加四倍,線性則是兩倍。

下面給出我電腦上的結果,數據量較大時比較明顯:

N:16000
quick-find
平均次數:8452
用時:143 比值:4.46875

quick-union
平均次數:7325
用時:202 比值:3.25806451612903

weighted-quick-union
平均次數:6889
用時:1


N:32000
quick-find
平均次數:15747
用時:510 比值:3.56643356643357

quick-union
平均次數:15108
用時:801 比值:3.96534653465347

weighted-quick-union
平均次數:17575
用時:3 比值:3


N:64000
quick-find
平均次數:33116
用時:2069 比值:4.05686274509804

quick-union
平均次數:38608
用時:4635 比值:5.78651685393258

weighted-quick-union
平均次數:34850
用時:6 比值:2
代碼
using System;
using System.Diagnostics;
using UnionFind;

namespace _1._5._22
{
    /*
     * 1.5.22
     * 
     * Erdös-Renyi 的倍率實驗。
     * 開發一個性能測試用例,
     * 從命令行接受一個 int 值 T 並進行 T 次以下實驗:
     * 使用練習 1.5.17 的用例生成隨機連接,
     * 和我們的開發用例一樣使用 UnionFind 來檢查觸點的連通性,
     * 不斷循環知道所有觸點都相互連通。
     * 對於每個 N,打印出 N 值和平均所需的連接數以及前后兩次運行時間的比值。
     * 使用你的程序驗證正文中的猜想:
     * quick-find 算法和 quick-union 算法的運行時間是平方級別的,
     * 加權 quick-union 算法則接近線性級別。
     * 
     */
    class Program
    {
        static void Main(string[] args)
        {
            long lastTimeQuickFind = 0;
            long lastTimeQuickUnion = 0;
            long lastTimeWeightedQuickUnion = 0;

            long nowTime = 0;
            for (int n = 2000; n < 100000; n *= 2)
            {
                Console.WriteLine("N:" + n);
                QuickFindUF quickFindUF = new QuickFindUF(n);
                QuickUnionUF quickUnionUF = new QuickUnionUF(n);
                WeightedQuickUnionUF weightedQuickUnionUF = new WeightedQuickUnionUF(n);

                // quick-find
                Console.WriteLine("quick-find");
                nowTime = RunTest(quickFindUF);
                if (lastTimeQuickFind == 0)
                {
                    Console.WriteLine("用時:" + nowTime);
                    lastTimeQuickFind = nowTime;
                }
                else
                {
                    Console.WriteLine("用時:" + nowTime + 
                        " 比值:" + (double)nowTime / lastTimeQuickFind);
                    lastTimeQuickFind = nowTime;
                }
                Console.WriteLine();

                // quick-union
                Console.WriteLine("quick-union");
                nowTime = RunTest(quickUnionUF);
                if (lastTimeQuickUnion == 0)
                {
                    Console.WriteLine("用時:" + nowTime);
                    lastTimeQuickUnion = nowTime;
                }
                else
                {
                    Console.WriteLine("用時:" + nowTime + 
                        " 比值:" + (double)nowTime / lastTimeQuickUnion);
                    lastTimeQuickUnion = nowTime;
                }
                Console.WriteLine();

                // weighted-quick-union
                Console.WriteLine("weighted-quick-union");
                nowTime = RunTest(weightedQuickUnionUF);
                if (lastTimeWeightedQuickUnion == 0)
                {
                    Console.WriteLine("用時:" + nowTime);
                    lastTimeWeightedQuickUnion = nowTime;
                }
                else
                {
                    Console.WriteLine("用時:" + nowTime + 
                        " 比值:" + (double)nowTime / lastTimeWeightedQuickUnion);
                    lastTimeWeightedQuickUnion = nowTime;
                }
                Console.WriteLine();

                Console.WriteLine();
            }
            
        }

        /// <summary>
        /// 進行若干次隨機試驗,輸出平均 union 次數,返回平均耗時。
        /// </summary>
        /// <param name="uf">用於測試的並查集。</param>
        /// <returns>平均耗時。</returns>
        static long RunTest(UF uf)
        {
            Stopwatch timer = new Stopwatch();
            int total = 0;
            int repeatTime = 10;
            timer.Start();
            for (int i = 0; i < repeatTime; ++i)
            {
                total += ErdosRenyi.Count(uf);
            }
            timer.Stop();
            Console.WriteLine("平均次數:" + total / repeatTime);

            return timer.ElapsedMilliseconds / repeatTime;
        }
    }
}

 

1.5.23

解答

先用速度最快的 WeightedQuickUnionUF 生成一系列連接,保存后用這些連接進行測試,生成連接的方法見 1.5.17 的解答。

下面給出我電腦上的結果:

N:2000
quick-find 耗時(毫秒):4
quick-union 耗時(毫秒):5
比值:0.8

N:4000
quick-find 耗時(毫秒):19
quick-union 耗時(毫秒):24
比值:0.791666666666667

N:8000
quick-find 耗時(毫秒):57
quick-union 耗時(毫秒):74
比值:0.77027027027027

N:16000
quick-find 耗時(毫秒):204
quick-union 耗時(毫秒):307
比值:0.664495114006515

N:32000
quick-find 耗時(毫秒):1127
quick-union 耗時(毫秒):1609
比值:0.700435052827843
代碼
using System;
using System.Diagnostics;
using UnionFind;

namespace _1._5._23
{
    /*
     * 1.5.23
     * 
     * 在 Erdös-Renyi 模型下比較 quick-find 算法和 quick-union 算法。
     * 開發一個性能測試用例,從命令行接受一個 int 值 T 並進行 T 次以下實驗:
     * 使用練習 1.5.17 的用例生成隨機連接。
     * 保存這些連接並和我們的開發用例一樣分別用 quick-find 和 quick-union 算法檢查觸點的連通性,
     * 不斷循環直到所有觸點均相互連通。
     * 對於每個 N,打印出 N 值和兩種算法的運行時間比值。
     * 
     */
    class Program
    {
        static void Main(string[] args)
        {
            int n = 1000;
            for (int t = 0; t < 5; ++t)
            {
                Connection[] input = ErdosRenyi.Generate(n);
                QuickFindUF quickFind = new QuickFindUF(n);
                QuickUnionUF quickUnion = new QuickUnionUF(n);

                Console.WriteLine("N:" + n);

                long quickFindTime = RunTest(quickFind, input);
                long quickUnionTime = RunTest(quickUnion, input);

                Console.WriteLine("quick-find 耗時(毫秒):" + quickFindTime);
                Console.WriteLine("quick-union 耗時(毫秒):" + quickUnionTime);
                Console.WriteLine("比值:" + (double)quickFindTime / quickUnionTime);
                Console.WriteLine();

                n *= 2;
            }
        }

        /// <summary>
        /// 進行若干次隨機試驗,輸出平均 union 次數,返回平均耗時。
        /// </summary>
        /// <param name="uf">用於測試的並查集。</param>
        /// <param name="connections">用於測試的輸入。</param>
        /// <returns>平均耗時。</returns>
        static long RunTest(UF uf, Connection[] connections)
        {
            Stopwatch timer = new Stopwatch();
            int repeatTime = 5;
            timer.Start();
            for (int i = 0; i < repeatTime; ++i)
            {
                ErdosRenyi.Count(uf, connections);
            }
            timer.Stop();

            return timer.ElapsedMilliseconds / repeatTime;
        }
    }
}

 

1.5.24

解答

根據上題的代碼略作修改即可,路徑壓縮大概可以快 1/3。

N:10000
加權 quick-find 耗時(毫秒):9
帶路徑壓縮的加權 quick-union 耗時(毫秒):6
比值:1.5

N:20000
加權 quick-find 耗時(毫秒):12
帶路徑壓縮的加權 quick-union 耗時(毫秒):8
比值:1.5

N:40000
加權 quick-find 耗時(毫秒):18
帶路徑壓縮的加權 quick-union 耗時(毫秒):12
比值:1.5

N:80000
加權 quick-find 耗時(毫秒):36
帶路徑壓縮的加權 quick-union 耗時(毫秒):30
比值:1.2

N:160000
加權 quick-find 耗時(毫秒):67
帶路徑壓縮的加權 quick-union 耗時(毫秒):41
比值:1.63414634146341
代碼
using System;
using UnionFind;
using System.Diagnostics;

namespace _1._5._24
{
    /*
     * 1.5.24
     * 
     * 適用於 Erdös-Renyi 模型的快速算法。
     * 在練習1.5.23 的測試中增加加權 quick-union 算法和使用路徑壓縮的加權 quick-union 算法。
     * 你能分辨出這兩種算法的區別嗎?
     * 
     */
    class Program
    {
        static void Main(string[] args)
        {
            int n = 5000;
            for (int t = 0; t < 5; ++t)
            {
                var input = ErdosRenyi.Generate(n);
                var weightedQuickUnionUF = new WeightedQuickUnionUF(n);
                var weightedQuickUnionPathCompressionUF = new WeightedQuickUnionPathCompressionUF(n);

                Console.WriteLine("N:" + n);

                long weightedQuickUnionTime = RunTest(weightedQuickUnionUF, input);
                long weightedQuickUnionPathCompressionTime = RunTest(weightedQuickUnionPathCompressionUF, input);

                Console.WriteLine("加權 quick-find 耗時(毫秒):" + weightedQuickUnionTime);
                Console.WriteLine("帶路徑壓縮的加權 quick-union 耗時(毫秒):" + weightedQuickUnionPathCompressionTime);
                Console.WriteLine("比值:" + (double)weightedQuickUnionTime / weightedQuickUnionPathCompressionTime);
                Console.WriteLine();

                n *= 2;
            }
        }

        /// <summary>
        /// 進行若干次隨機試驗,輸出平均 union 次數,返回平均耗時。
        /// </summary>
        /// <param name="uf">用於測試的並查集。</param>
        /// <param name="connections">用於測試的輸入。</param>
        /// <returns>平均耗時。</returns>
        static long RunTest(UF uf, Connection[] connections)
        {
            Stopwatch timer = new Stopwatch();
            int repeatTime = 5;
            timer.Start();
            for (int i = 0; i < repeatTime; ++i)
            {
                ErdosRenyi.Count(uf, connections);
            }
            timer.Stop();

            return timer.ElapsedMilliseconds / repeatTime;
        }
    }
}

 

1.5.25

解答

略微修改 1.5.22 的代碼即可。

我電腦上的結果:

Quick-Find
N:1600
平均用時(毫秒):4
N:6400
平均用時(毫秒):67    比值:16.75
N:25600
平均用時(毫秒):1268  比值:18.9253731343284
N:102400
平均用時(毫秒):20554 比值:16.2097791798107
Quick-Union
N:1600
平均用時(毫秒):5     比值:0.000243261652233142
N:6400
平均用時(毫秒):66    比值:13.2
N:25600
平均用時(毫秒):1067  比值:16.1666666666667
N:102400
平均用時(毫秒):18637 比值:17.4667291471415
Weighted Quick-Union
N:1600
平均用時(毫秒):0     比值:0
N:6400
平均用時(毫秒):2
N:25600
平均用時(毫秒):12    比值:6
N:102400
平均用時(毫秒):64    比值:5.33333333333333
代碼
using System;
using System.Diagnostics;
using UnionFind;

namespace _1._5._25
{
    /*
     * 1.5.25
     * 
     * 隨機網格的倍率測試。
     * 開發一個性能測試用例,
     * 從命令行接受一個 int 值 T 並進行 T 次以下實驗:
     * 使用練習 1.5.18 的用例生成一個 N×N 的隨機網格,
     * 所有連接的方向隨機且排列隨機。
     * 和我們的開發用例一樣使用 UnionFind 來檢查觸點的連通性,
     * 不斷循環直到所有觸點均相互連通。
     * 對於每個 N,打印出 N 值和平均所需的連接數以及前后兩次運行時間的比值。
     * 使用你的程序驗證正文中的猜想:
     * quick-find 和 quick-union 算法的運行時間是平方級別的,
     * 加權 quick-union 算法則接近線性級別。
     * 
     * 注意:隨着 N 值加倍,網格中觸點的數量會乘以 4,
     * 因此平方級別的算法運行時間會變為原來的 16 倍,
     * 線性級別的算法的運行時間則變為原來的 4 倍
     * 
     */
    class Program
    {
        static void Main(string[] args)
        {
            int n = 40;
            int t = 4;

            // quick-find
            Console.WriteLine("Quick-Find");
            long last = 0;
            long now = 0;
            for (int i = 0; i < t; ++i, n *= 2)
            {
                Console.WriteLine("N:" + n * n);
                var connections = RandomGrid.GetConnections(n);

                QuickFindUF quickFind = new QuickFindUF(n * n);
                now = RunTest(quickFind, connections);
                if (last == 0)
                {
                    Console.WriteLine("平均用時(毫秒):" + now);
                    last = now;
                }
                else
                {
                    Console.WriteLine("平均用時(毫秒):" + now + "\t比值:" + (double)now / last);
                    last = now;
                }
            }

            // quick-union
            Console.WriteLine("Quick-Union");
            n = 40;
            for (int i = 0; i < t; ++i, n *= 2)
            {
                Console.WriteLine("N:" + n * n);
                var connections = RandomGrid.GetConnections(n);

                QuickUnionUF quickFind = new QuickUnionUF(n * n);
                now = RunTest(quickFind, connections);
                if (last == 0)
                {
                    Console.WriteLine("平均用時(毫秒):" + now);
                    last = now;
                }
                else
                {
                    Console.WriteLine("平均用時(毫秒):" + now + "\t比值:" + (double)now / last);
                    last = now;
                }
            }

            // 加權 quick-union
            Console.WriteLine("Weighted Quick-Union");
            n = 40;
            for (int i = 0; i < t; ++i, n *= 2)
            {
                Console.WriteLine("N:" + n * n);
                var connections = RandomGrid.GetConnections(n);

                WeightedQuickUnionUF quickFind = new WeightedQuickUnionUF(n * n);
                now = RunTest(quickFind, connections);
                if (last == 0)
                {
                    Console.WriteLine("平均用時(毫秒):" + now);
                    last = now;
                }
                else
                {
                    Console.WriteLine("平均用時(毫秒):" + now + "\t比值:" + (double)now / last);
                    last = now;
                }
            }
        }

        /// <summary>
        /// 進行若干次隨機試驗,輸出平均 union 次數,返回平均耗時。
        /// </summary>
        /// <param name="uf">用於測試的並查集。</param>
        /// <param name="connections">用於測試的輸入。</param>
        /// <returns>平均耗時。</returns>
        static long RunTest(UF uf, Connection[] connections)
        {
            Stopwatch timer = new Stopwatch();
            long repeatTime = 3;
            timer.Start();
            for (int i = 0; i < repeatTime; ++i)
            {
                ErdosRenyi.Count(uf, connections);
            }
            timer.Stop();

            return timer.ElapsedMilliseconds / repeatTime;
        }
    }
}

 

1.5.26

解答

和 1.5.16 的程序類似,將測試的內容改為 Erdos-Renyi 即可。

樣例輸出:

imageimageimage

代碼
using System;
using System.Linq;
using System.Windows.Forms;
using System.Drawing;
using UnionFind;

namespace _1._5._26
{
    /*
     * 1.5.26
     * 
     * Erdös-Renyi 模型的均攤成本圖像。
     * 開發一個用例,
     * 從命令行接受一個 int 值 N,在 0 到 N-1 之間產生隨機整數對,
     * 調用 connected() 判斷它們是否相連,
     * 如果不是則用 union() 方法(和我們的開發用例一樣)。
     * 不斷循環直到所有觸點互通。
     * 按照正文的樣式將所有操作的均攤成本繪制成圖像。
     * 
     */
    static class Program
    {
        /// <summary>
        /// 應用程序的主入口點。
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Compute();
            Application.Run(new Form1());
        }

        static void Compute()
        {
            int size = 200;
            QuickFindUF quickFind = new QuickFindUF(size);
            QuickUnionUF quickUnion = new QuickUnionUF(size);
            WeightedQuickUnionUF weightedQuickUnion = new WeightedQuickUnionUF(size);
            Connection[] connections = ErdosRenyi.Generate(size);

            int[] quickFindResult = new int[size];
            int[] quickUnionResult = new int[size];
            int[] weightedQuickUnionResult = new int[size];
            int p, q;
            for (int i = 0; i < size; ++i)
            {
                p = connections[i].P;
                q = connections[i].Q;

                quickFind.Union(p, q);
                quickUnion.Union(p, q);
                weightedQuickUnion.Union(p, q);
                quickFindResult[i] = quickFind.ArrayVisitCount;
                quickUnionResult[i] = quickUnion.ArrayVisitCount;
                weightedQuickUnionResult[i] = weightedQuickUnion.ArrayParentVisitCount + weightedQuickUnion.ArraySizeVisitCount;

                quickFind.ResetArrayCount();
                quickUnion.ResetArrayCount();
                weightedQuickUnion.ResetArrayCount();
            }

            Draw(quickFindResult, "Quick-Find");
            Draw(quickUnionResult, "Quick-Union");
            Draw(weightedQuickUnionResult, "Weighted Quick-Union");
        }

        static void Draw(int[] cost, string title)
        {
            // 構建 total 數組。
            int[] total = new int[cost.Length];
            total[0] = cost[0];
            for (int i = 1; i < cost.Length; ++i)
            {
                total[i] = total[i - 1] + cost[i];
            }

            // 獲得最大值。
            int costMax = cost.Max();

            // 新建繪圖窗口。
            Form2 plot = new Form2();
            plot.Text = title;
            plot.Show();
            Graphics graphics = plot.CreateGraphics();

            // 獲得繪圖區矩形。
            RectangleF rect = plot.ClientRectangle;
            float unitX = rect.Width / 10;
            float unitY = rect.Width / 10;

            // 添加 10% 邊距作為文字區域。
            RectangleF center = new RectangleF
                (rect.X + unitX, rect.Y + unitY,
                rect.Width - 2 * unitX, rect.Height - 2 * unitY);

            // 繪制坐標系。
            graphics.DrawLine(Pens.Black, center.Left, center.Top, center.Left, center.Bottom);
            graphics.DrawLine(Pens.Black, center.Left, center.Bottom, center.Right, center.Bottom);
            graphics.DrawString(costMax.ToString(), plot.Font, Brushes.Black, rect.Location);
            graphics.DrawString(cost.Length.ToString(), plot.Font, Brushes.Black, center.Right, center.Bottom);
            graphics.DrawString("0", plot.Font, Brushes.Black, rect.Left, center.Bottom);

            // 初始化點。
            PointF[] grayPoints = new PointF[cost.Length];
            PointF[] redPoints = new PointF[cost.Length];
            unitX = center.Width / cost.Length;
            unitY = center.Width / costMax;

            for (int i = 0; i < cost.Length; ++i)
            {
                grayPoints[i] = new PointF(center.Left + unitX * (i + 1), center.Bottom - (cost[i] * unitY));
                redPoints[i] = new PointF(center.Left + unitX * (i + 1), center.Bottom - ((total[i] / (i + 1)) * unitY));
            }

            // 繪制點。
            for (int i = 0; i < cost.Length; ++i)
            {
                graphics.FillEllipse(Brushes.Gray, new RectangleF(grayPoints[i], new SizeF(5, 5)));
                graphics.FillEllipse(Brushes.Red, new RectangleF(redPoints[i], new SizeF(5, 5)));
            }

            graphics.Dispose();
        }
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM