十、基本算法
Covers?基本算法
额……这一节不知道要写点什么好,算法这个主题太大了,游戏中要使用的算法也太多了,这部笔记不是算法讲座,姑且写两个比较经典的。
并查集
并查集就是“并集”和“查集”两个任务,现在假设有一堆集合,我们关注两件事:
- 任取两个元素,如何判断它们是否在同一个集合中
- 如果不处于同一个集合,如何用最优的方式将两个集合合并
完成这个算法的关键是“如何表示多个不相交且随时可能合并的集”,这要是直接做一个集合容器,然后不断摧毁、挪动元素还了得?这里要用数组来将各个元素连接成一棵树。
我写了一个例子,你最好写自己的,然后对照看看我写得哪里好、哪里不好,欢迎留言讨论:
并查集算法还有一个重要的功能:检查图中是否有环路,这个功能在例子中也完成了。
using static System.Console;
namespace Algorithms
{
class DisjointSets
{
int[] _parent;
public DisjointSets(int size)
{
// Initialize the tree, each node is a tree root:
_parent = new int[size];
for (int i = 0; i < size; ++i)
{
_parent[i] = -1;
}
}
int FindParentNode(int x, out int depth)
{
// Assume x is tree root:
int p = x;
depth = 0;
// Find parent:
while (_parent[p] != -1)
{
p = _parent[p];
++depth;
}
// Optimize the tree:
if (p != x)
{
_parent[x] = p;
}
return p;
}
public int GetTotalSets()
{
// We just need to count all -1 values in the array:
int ans = 0;
for (int i = 0; i < _parent.Length; ++i)
{
if (_parent[i] == -1)
{
++ans;
}
}
return ans;
}
public bool IsSameSet(int x, int y)
{
// Find tree roots of x and y, search depths are unimportant:
int px = FindParentNode(x, out int _);
int py = FindParentNode(y, out int _);
// We can make sure of the answer:
return px == py;
}
public bool JoinSet(int x, int y)
{
// Find tree roots of x and y, saving search depths:
int px = FindParentNode(x, out int dpx);
int py = FindParentNode(y, out int dpy);
// If x and y are in the same set, return false:
if (px == py)
{
return false;
}
// Join tree roots, avoiding enlarging search depth:
if (dpx > dpy)
{
_parent[py] = px;
}
else
{
_parent[px] = py;
}
return true;
}
public bool JoinSet(int[,] pairs)
{
// Assume no two elements are already in the same set before being associated:
bool ans = true;
// Make all associations according to the given matrix:
for (int i = 0; i < pairs.GetLength(0); ++i)
{
ans = JoinSet(pairs[i, 0], pairs[i, 1]) && ans;
}
// If this is false, there MUST be a circuit within the edges:
return ans;
}
}
class Program
{
static void Main()
{
// All edges in a graph that contains 3 disjoint sets:
// (Visualized graph here: https://s3.ax1x.com/2021/02/20/yIWjS0.png)
int[,] edges =
{
{1, 3}, {2, 3}, {0, 2}, {4, 3},
{5, 6}, {7, 6}
};
// Initialize the DisjointSets object:
var sets = new DisjointSets(9);
// Tell the object all the edges:
// (We can also make sure if there is any circuit in the graph)
if (!sets.JoinSet(edges))
{
WriteLine("Circuit found!\n");
}
// Check every two elements, see if they are in the same set:
for (int a = 0; a < 8; ++a)
{
for (int b = a + 1; b <= 8; ++b)
{
WriteLine(
sets.IsSameSet(a, b)
? "{0} and {1} are in the same set!"
: "{0} and {1} are NOT in the same set!", a, b);
}
}
WriteLine("\nThere are {0} disjoint sets!", sets.GetTotalSets());
}
}
}
这里用到的测试集合在这里可以看到:
二叉堆与堆排序
二叉堆是用线性数组实现的、每个节点带有可比较数值(如整型)的完全二叉树,用它可以实现很多有用的数据结构。完全二叉树是一种非常稳定的二叉树,它的特点是 除了叶子层,每层都是满二叉树(完全填满的二叉树),且 叶子层的所有元素都尽可能处于这一层的左侧。
完全二叉树很容易用线性数组实现:
- 如果按层从上到下、每层内从左到右给每个节点顺次编号 0(根)、1(第 1 层左侧节点)、2(第 1 层右侧节点)、3(第 2 层左起第一个节点)……则每个父节点(编号 n)的左孩子序号一定是 2 n + 1 2n + 1 2n+1,右孩子序号一定是 2 n + 2 2n + 2 2n+2;
- 反推上面的公式,任何一个节点,如果是其父节点的左孩子,则编号必定为奇数(这里假设根节点为 0 号),如果是其父节点的右孩子,则编号必定为偶数。由此我们可由任一节点,推出其父节点和左右孩子;
- 具有 k 层( k ⩾ 1 k \geqslant 1 k⩾1)的满二叉树节点数为 2 k − 1 2^{k} - 1 2k−1,具有 n 个节点的完全二叉树的层数 k 满足关系 2 k − 1 ⩽ n ⩽ 2 k − 1 {2^{k - 1}} \leqslant n \leqslant {2^{k} - 1} 2k−1⩽n⩽2k−1,从而可以由数组下标推知某节点所在的层。
最大二叉堆是满足最大堆特性的二叉堆,也叫 大根堆 或 最大堆,即 所有节点的值都大于其左右孩子(如果有),亦即:除根节点外,每个节点的父节点都比当前节点大。
我们关心的是如何用 C# 构建一个最大堆,并提供插入节点、提出(删除)最大值两种功能。
我们不关心删除最大堆中任意元素的算法,因为这并不是最大堆的设计意图,二叉排序树更适合进行该功能。
最大堆的建立就是一系列的插入操作,插入算法如下:
- 如果堆中没有任何节点,则新元素就是根节点;否则,转步骤 2;
- 直接将新元素插入堆的末尾(数组末端);
- 访问新元素的父节点,若自身大于父节点的值,将自身与父节点对调,转步骤 4;否则,插入完毕;
- 转步骤 3。
从最大堆中提取最大值(并删除)的算法:
- 直接取得最大堆根节点的值,这就是堆中最大值;
- 将根节点的值替换为堆末尾(数组末端)的值,堆的元素数(占用的数组长度)减 1,现在称根节点为“当前节点”;
- 访问当前节点的左右孩子(如果有),如果两个孩子中数值较大的一个大于当前节点,则将当前节点与该孩子对调,现在称该孩子原本所在的位置为“当前节点”,转步骤 4;否则,操作完毕;
- 转步骤 3。
根据上面的算法和分析,我写了一个例子,你最好写自己的,然后对照看看我写得哪里好、哪里不好,欢迎留言讨论:
using static System.Console;
namespace Algorithms
{
class Heap
{
int[] _array;
int _rear;
public Heap(int capacity = 0xffff)
{
// Pre-allocate memory for the heap:
_array = new int[capacity];
// Now it's an empty heap:
_rear = -1;
}
public bool IsFull => _rear + 1 == _array.Length;
public bool IsEmpty => _rear < 0;
public int Length => _rear + 1;
public int Capacity => _array.Length;
static int GetParentOf(int id)
{
// A little bit of technique:
return id == 0 ? -1 : id % 2 == 0 ? (id - 2) / 2 : (id - 1) / 2;
}
int GetLeftChildOf(int id)
{
// Formula:
int ans = 2 * id + 1;
// Make sure that child exists:
return ans <= _rear ? ans : -1;
}
int GetRightChildOf(int id)
{
// Formula:
int ans = 2 * id + 2;
// Make sure that child exists:
return ans <= _rear ? ans : -1;
}
int GetGreaterChildOf(int id)
{
// If it has no child:
int l = GetLeftChildOf(id);
if (l == -1)
{
return -1;
}
// If it has only left child:
int r = GetRightChildOf(id);
if (r == -1)
{
return l;
}
// Otherwise, compare, left child is preferred if equal:
return _array[r] > _array[l] ? r : l;
}
bool Swap(int a, int b)
{
// If a or b exceeds the heap:
if (a > _rear || b > _rear)
{
return false;
}
// Swap the value of two nodes:
int tmp = _array[a];
_array[a] = _array[b];
_array[b] = tmp;
return true;
}
public bool Push(int data)
{
// Refuse to insert if heap is full:
if (IsFull)
{
return false;
}
// Just make the root if heap is empty:
if (IsEmpty)
{
_rear = 0;
_array[0] = data;
return true;
}
// Algorithm:
int c = ++_rear, p = GetParentOf(c);
_array[c] = data;
while (p != -1 && _array[p] < data)
{
Swap(p, c);
c = p;
p = GetParentOf(c);
}
return true;
}
public bool Push(int[] data)
{
// Refuse to push anything if remaining capacity is not enough:
if (Capacity - Length < data.Length)
{
return false;
}
// Push each element in data:
foreach (int i in data)
{
if (!Push(i))
{
return false;
}
}
return true;
}
public bool Pop(out int data)
{
// If heap is empty, return false to notify, data is meaningless:
if (IsEmpty)
{
data = 0;
return false;
}
// Save root value as output:
data = _array[0];
// Just pop the root if heap is empty:
if (_rear == 0)
{
_rear = -1;
return true;
}
// Algorithm:
_array[0] = _array[_rear--];
int p = 0, c = GetGreaterChildOf(p);
while (c != -1 && _array[p] < _array[c])
{
Swap(p, c);
p = c;
c = GetGreaterChildOf(p);
}
return true;
}
}
class Program
{
static void Main()
{
int[] arr = {4, 14, 9, 13, -12, -21, 0, 0, 0, 127};
var heap = new Heap();
heap.Push(arr);
while (!heap.IsEmpty)
{
heap.Pop(out int ans);
Write(ans + " ");
}
}
}
}
在测试代码中,将一个数组插入到最大堆中,再从堆中取出所有元素(每次都是取最大元素),此时输出结果被自然地从大到小排列,这就是 堆排序 算法的原理。
T.B.C.