Add files via upload
This commit is contained in:
172
IOS/Task02:算法实现/1.三种递归问题求解.md
Normal file
172
IOS/Task02:算法实现/1.三种递归问题求解.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# 三种递归问题求解
|
||||
|
||||
## 斐波那契数列
|
||||
|
||||
斐波那契数列是一系列数字,除了第一个和第二个数字外,任何数字都是前两个数字之和。
|
||||
$$
|
||||
0、1、2、3、5、8、13、21...
|
||||
$$
|
||||
于是我们可以写出斐波那契数列的伪代码:
|
||||
|
||||
```swift
|
||||
fib(n) = fib(n - 1) + fib(n - 2)
|
||||
```
|
||||
|
||||
这种方式十分适合递归求解,对于斐波那契数列而言,我们可以指定数列最前面两个元素为:0与1。于是我们可以将伪代码翻译成Swift中源码:
|
||||
|
||||
```swift
|
||||
func fib2(n: UInt) -> UInt {
|
||||
if (n < 2) {
|
||||
// base cases
|
||||
return n
|
||||
}
|
||||
return fib2(n: n - 2) + fib2(n: n - 1)
|
||||
// recursive cases
|
||||
}
|
||||
```
|
||||
|
||||
这种方式虽然可以运行,但是时间复杂度过大,随着调用数值的不断增加,这种算法的时间复杂度达到了指数级别!每次的计算都会存在大量的重复计算:
|
||||
|
||||
- 当我们计算fib(4)时,算法会计算fib(3)与fib(2)
|
||||
- 在计算fib(3)时,算法会计算fib(2)与fib(1),这样fib(2)被计算了两次,当计算fib(100)时,算法会存在大量计算重复的内容
|
||||
|
||||
如何修复这种问题呢?既然fib(2)被计算了两次,那么我们可以开启一份`备忘录`,每当算法计算到fib(2)时,如果这个值已经存在,那么直接从备忘录中调用即可!
|
||||
|
||||
```swift
|
||||
var fibMemo: [UInt: UInt] = [0: 0, 1: 1] // our old base cases
|
||||
func fib3(n: UInt) -> UInt {
|
||||
if let result = fibMemo[n] { // our new base case
|
||||
return result
|
||||
} else {
|
||||
fibMemo[n] = fib3(n: n - 1) + fib3(n: n - 2) // memoization
|
||||
}
|
||||
return fibMemo[n]!
|
||||
}
|
||||
```
|
||||
|
||||
保持斐波那契数列还有一种性能更高的办法:迭代
|
||||
|
||||
```swift
|
||||
func fib4(n: UInt) -> UInt {
|
||||
if (n == 0) { // special case
|
||||
return n
|
||||
}
|
||||
var last: UInt = 0, next: UInt = 1 // initially set to fib(0) & fib(1)
|
||||
for _ in 1..<n {
|
||||
(last, next) = (next, last + next)
|
||||
}
|
||||
return next
|
||||
}
|
||||
```
|
||||
|
||||
这种方法使用了for循环中的元组(tuple),last被设置为next的前一个值,而next被设置为last的前一个值加上next的前一个值,这样可以避免创建一个临时变量来保存last更新后next的旧值。这种方法是目前计算斐波那契数列最高效的方法。
|
||||
|
||||
|
||||
|
||||
## $\pi$的计算
|
||||
|
||||
数学上计算$\pi$的公式纷繁多样,其中最简单的公式即为莱布尼兹公式,莱布尼兹公式假设下列无穷级数的收敛等于$\pi$
|
||||
$$
|
||||
\pi = \frac{4}{1}-\frac{4}{3}+\frac{4}{5}-\frac{4}{7}+\frac{4}{9}-\cdots
|
||||
$$
|
||||
计算该部分我们可以直接将公式中的片段转换为函数中的变量。莱布尼兹公式的规律为分子都为4,而分母每次递增2,最后用一个变量收集公式中的累加和。
|
||||
|
||||
```swift
|
||||
func calculatePi(nTerms: UInt) -> Double {
|
||||
let numerator: Double = 4
|
||||
var denominator: Double = 1
|
||||
var operation: Double = -1
|
||||
var pi: Double = 0
|
||||
for _ in 0..<nTerms {
|
||||
pi += operation * (numerator / denominator)
|
||||
denominator += 2
|
||||
operation *= -1
|
||||
}
|
||||
return abs(pi)
|
||||
}
|
||||
```
|
||||
|
||||
## 汉诺塔
|
||||
|
||||
**汉诺塔**(Tower of Hanoi),又称**河内塔**,是一个源于印度古老传说的益智玩具。假设存在三根柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。$^{[1]}$
|
||||
|
||||
栈是汉诺塔问题中塔的完美替身。栈的两个最基本操作为`压入(push)`与`弹出(pop)` ,对应着添加元素入栈与删除栈顶元素。Swift代码实现如下:
|
||||
|
||||
```swift
|
||||
public class Stack<T>: CustomStringConvertible {
|
||||
private var container: [T] = [T]()
|
||||
public func push(_ thing: T) { container.append(thing) }
|
||||
public func pop() -> T { return container.removeLast() }
|
||||
public var description: String { return container.description }
|
||||
}
|
||||
```
|
||||
|
||||
下面我们将塔定义为Stack,并初始化汉诺塔
|
||||
|
||||
```swift
|
||||
var numDiscs = 3
|
||||
var towerA = Stack<Int>()
|
||||
var towerB = Stack<Int>()
|
||||
var towerC = Stack<Int>()
|
||||
for i in 1...numDiscs { // initialize the first tower
|
||||
towerA.push(i)
|
||||
}
|
||||
```
|
||||
|
||||
如何求解汉诺塔问题?我们只需要处理两种情况:
|
||||
|
||||
- 移动一个盘子(base case)
|
||||
- 移动多个盘子(递归处理)
|
||||
|
||||
我们可以将汉诺塔问题的递归解决方案分为3个步骤:
|
||||
|
||||
1. 以塔C为中介,蒋上面的n-1个盘子从塔A移动到塔B(临时塔)
|
||||
2. 将底部的盘子从塔A移动到塔C
|
||||
3. 将n-1个盘子从塔B移动到塔C
|
||||
|
||||
这种算法不仅用于3个盘子的情况,甚至可以递归解决任意数量的盘子。我们只需要完成base case的搭建,剩下的任务交给递归处理即可:
|
||||
|
||||
```swift
|
||||
func hanoi(from: Stack<Int>, to: Stack<Int>, temp: Stack<Int>, n: Int) {
|
||||
if n == 1 { // base case
|
||||
to.push(from.pop()) // move 1 disk
|
||||
} else { // recursive case
|
||||
hanoi(from: from, to: temp, temp: to, n: n-1)
|
||||
hanoi(from: from, to: to, temp: temp, n: 1)
|
||||
hanoi(from: temp, to: to, temp: from, n: n-1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我们不必理解将多个盘子从塔A移动到塔C所需的每一步。这就是采用递归方法的魅力所在——只需要考虑抽象的解决方案,而不是罗列出每一种单独的动作。不过值得注意的是,上述算法的时间复杂度处在指数级别。
|
||||
|
||||
## 练习作业
|
||||
|
||||
1. 自己设计一种斐波那契数列的计算算法,编写测试单元并对比上述算法的性能差异
|
||||
2. 寻找相应数学公式,计算无理数e
|
||||
3. 编写汉诺塔问题的解决程序,以解决任意数量的汉诺塔问题
|
||||
|
||||
|
||||
|
||||
[1] 百度百科-汉诺塔
|
||||
|
||||
*参考代码开源版权说明:
|
||||
|
||||
```swift
|
||||
// Copyright 2017 David Kopec
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
```
|
||||
|
||||
|
||||
|
||||
214
IOS/Task02:算法实现/2.搜索问题.md
Normal file
214
IOS/Task02:算法实现/2.搜索问题.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# 搜索问题
|
||||
|
||||
## 二分搜索
|
||||
|
||||
搜索问题通常有较多的解决方法。一般而言,**线性搜索**按照原始的数据结构顺序,遍历搜索空间中的每一个元素。线性搜索是最简单、最自然、最直观的搜索方法。该算法的时间复杂度最坏情况下为 $O(n)$ ,其中 $n$ 为待搜索结构中的元素总个数。
|
||||
|
||||
线性搜索的代码也十分简单:
|
||||
|
||||
```swift
|
||||
func linearContains(_ array: Array, Target: ourTarget ) -> Bool {
|
||||
for element in Array where Target == ourTarget {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
然而,如果我们已经知道了数据结构的顺序,即每一种元素都是排列好的,并且可以通过索引立即访问数据结构内部的任何项,那么我们就可以执行**二分搜索**(Binary Search)。
|
||||
|
||||
假设存在一个按照字母序排列的数组Array,我们的目标是搜索f:
|
||||
|
||||
```swift
|
||||
Array = [a, b, c, d, e, f, g]
|
||||
```
|
||||
|
||||
这7个字母的中间值为 $d$,我们可以确定要搜索的目标位于 $d$ 之后,因此我们再于 $e, f, g$ 中搜索中间元素 $f$,即可完成目标。
|
||||
|
||||
二分搜索与线性搜索不同,它不需要遍历结构中的每一个元素。二分搜索能不断将搜索空间减半,因此最坏情况下的时间复杂度为 $O(\text{lg } n)$ 。二分搜索的缺点也显而易见:我们需要对数组进行排序,排序的最佳时间复杂度也需要 $O(n \text{lg }n)$ ,实际上,如果只运行一次,且原始数据为未排序数组,线性搜索的效果要好于二分搜索。
|
||||
|
||||
以基因和密码子的二分搜索为例,其中Gene类型为Array,而Codon可以与其他Codon进行比较,代码如下
|
||||
|
||||
```swift
|
||||
func binaryContains(_ array: Gene, item: Codon) -> Bool {
|
||||
var low = 0
|
||||
var high = array.count - 1
|
||||
while low <= high {
|
||||
let mid = (low + high) / 2
|
||||
if array[mid] < item {
|
||||
low = mid + 1
|
||||
} else if array[mid] > item {
|
||||
high = mid - 1
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
## 深度优先搜索(DFS)
|
||||
|
||||
**深度优先搜索**(Depth-First Search, DFS)为首先尽可能深入地搜索,如果到达终点,则回溯到最后一个决策点。此处我们以一款小游戏为例,假设右下角的绿色顶点为起始点,橙色方块为障碍物,最左上角的白色方块为终点,DFS的算法过程如下:
|
||||
|
||||
<img src="/Users/mac/Desktop/iOSdev/Task03:算法实现/img/1.PNG" alt="1" style="zoom: 20%;" />
|
||||
|
||||
深度优先搜索给出的答案:
|
||||
|
||||
<img src="/Users/mac/Desktop/iOSdev/Task03:算法实现/img/2.PNG" alt="2" style="zoom:20%;" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
深度优搜索依赖于**栈**(Stack)这种数据结构,前文的递归中我们已经提到过:栈有两种最基本的操作
|
||||
|
||||
1. push() 压栈,将元素送入栈顶
|
||||
2. pop() 移除栈顶元素并返回该元素
|
||||
|
||||
在Swift中栈的实现也十分简单:
|
||||
|
||||
```swift
|
||||
public class Stack<T> {
|
||||
private var container: [T] = [T]()
|
||||
public var isEmpty: Bool { return container.isEmpty }
|
||||
public func push(_ thing: T) { container.append(thing) }
|
||||
public func pop() -> T { return container.removeLast() }
|
||||
}
|
||||
```
|
||||
|
||||
DFS的精髓在于可以回溯,要实现这个功能,我们需要添加一个类Node,用于记录当前搜索的状态(或从一个状态到另一个状态的方式)。Node可以看成包围状态的包装器。我们将状态来源的Node称为parent。此外,我们还需定义Node的类拥有cost和heuristic属性,并且能够比较Comparable和散列化Hashable
|
||||
|
||||
```swift
|
||||
class Node<T>: Comparable, Hashable {
|
||||
let state: T
|
||||
let parent: Node?
|
||||
let cost: Float
|
||||
let heuristic: Float
|
||||
init(state: T, parent: Node?, cost: Float = 0.0, heuristic: Float = 0.0) {
|
||||
self.state = state
|
||||
self.parent = parent
|
||||
self.cost = cost
|
||||
self.heuristic = heuristic
|
||||
}
|
||||
|
||||
var hashValue: Int { return Int(cost + heuristic) }
|
||||
}
|
||||
|
||||
func < <T>(lhs: Node<T>, rhs: Node<T>) -> Bool {
|
||||
return (lhs.cost + lhs.heuristic) < (rhs.cost + rhs.heuristic)
|
||||
}
|
||||
|
||||
func == <T>(lhs: Node<T>, rhs: Node<T>) -> Bool {
|
||||
return lhs === rhs
|
||||
}
|
||||
```
|
||||
|
||||
深度优先搜索中需要记录两种结构:正准备搜索的状态和已经搜索的状态,分别用frontier和explored表示。只要frontier中还有更多的状态要访问,DFS就会继续检查他们是否为目标值,并且将这些状态的后者加入frontier中。DFS还会把搜索过的点标记为explored,从而避免DFS陷入死循环,到达那些作为后继者先前已经访问过的状态。如果frontier是空的,则意味着没有地方可以继续搜索。
|
||||
|
||||
```swift
|
||||
func dfs<StateType: Hashable>(initialState: StateType, goalTestFn: (StateType) -> Bool, successorFn: (StateType) -> [StateType]) -> Node<StateType>? {
|
||||
// frontier is where we've yet to go
|
||||
let frontier: Stack<Node<StateType>> = Stack<Node<StateType>>()
|
||||
frontier.push(Node(state: initialState, parent: nil))
|
||||
// explored is where we've been
|
||||
var explored: Set<StateType> = Set<StateType>()
|
||||
explored.insert(initialState)
|
||||
|
||||
// keep going while there is more to explore
|
||||
while !frontier.isEmpty {
|
||||
let currentNode = frontier.pop()
|
||||
let currentState = currentNode.state
|
||||
// if we found the goal, we're done
|
||||
if goalTestFn(currentState) { return currentNode }
|
||||
// check where we can go next and haven't explored
|
||||
for child in successorFn(currentState) where !explored.contains(child) {
|
||||
explored.insert(child)
|
||||
frontier.push(Node(state: child, parent: currentNode))
|
||||
}
|
||||
}
|
||||
return nil // never found the goal
|
||||
}
|
||||
```
|
||||
|
||||
## 广度优先搜索(BFS)
|
||||
|
||||
深度优先搜索尽管提出了一种较为可靠的搜索方式,但是时间复杂度较大。通过深度优先遍历的路径通常不是最短路径。与之对应的**广度优先算法**(Breadth-First Search,BFS)总是查找到最短的路径,因为广度优先搜索的策略为优先遍历距离当前节点较近的点,找到的路径一定是最短的。但是,广度优先搜索并不总是比深度优先搜索性能优异,有些情况下深度优先搜索可能在广度优先搜索之前找到解决办法。运用DFS还是BFS,取决于我们在快速找到解决方法的可能性与查找到目标的最短路径的确定性之间的权衡。
|
||||
|
||||
<img src="/Users/mac/Desktop/iOSdev/Task03:算法实现/img/3.png" alt="3" style="zoom: 50%;" />
|
||||
|
||||
值得注意的是,BFS给出的路径是最短的,但并不一定唯一。
|
||||
|
||||
与深度优先搜索不同,广度优先搜索依赖于**队列**(queue)结构,栈与队列的区别在于:
|
||||
|
||||
- 栈为后进先出(LIFO)
|
||||
- 队列为先进先出(FIFO,First-In-First-Out)
|
||||
|
||||
队列也对应着至少两种操作:
|
||||
|
||||
1. push() 将元素添加到队列中
|
||||
2. pop() 将先添加的元素从队列中删除
|
||||
|
||||
实际上,在Swift中Array的队列实现与栈的实现几乎完全相同,区别就是从Array的左侧而不是右侧移除元素。Array最左侧元素是Array中存在最久的一个元素。队列的参考实现如下:
|
||||
|
||||
```swift
|
||||
public class Queue<T> {
|
||||
private var container: [T] = [T]()
|
||||
public var isEmpty: Bool { return container.isEmpty }
|
||||
public func push(_ thing: T) { container.append(thing) }
|
||||
public func pop() -> T { return container.removeFirst() }
|
||||
}
|
||||
```
|
||||
|
||||
BFS的实现很大程度上与DFS类似甚至相同,只是frontier从栈变成了队列。将frontier从栈更改为队列就会更改状态被搜索的顺序,确保首先搜索最长接近初始状态的状态。
|
||||
|
||||
```swift
|
||||
func bfs<StateType: Hashable>(initialState: StateType, goalTestFn: (StateType) -> Bool, successorFn: (StateType) -> [StateType]) -> Node<StateType>? {
|
||||
// frontier is where we've yet to go
|
||||
let frontier: Queue<Node<StateType>> = Queue<Node<StateType>>()
|
||||
frontier.push(Node(state: initialState, parent: nil))
|
||||
// explored is where we've been
|
||||
var explored: Set<StateType> = Set<StateType>()
|
||||
explored.insert(initialState)
|
||||
// keep going while there is more to explore
|
||||
while !frontier.isEmpty {
|
||||
let currentNode = frontier.pop()
|
||||
let currentState = currentNode.state
|
||||
// if we found the goal, we're done
|
||||
if goalTestFn(currentState) { return currentNode }
|
||||
// check where we can go next and haven't explored
|
||||
for child in successorFn(currentState) where !explored.contains(child) {
|
||||
explored.insert(child)
|
||||
frontier.push(Node(state: child, parent: currentNode))
|
||||
}
|
||||
}
|
||||
return nil // never found the goal
|
||||
}
|
||||
```
|
||||
|
||||
## 练习
|
||||
|
||||
1. 如果二分搜索过程中数组长度为偶数该怎么办?代码如何实现?
|
||||
2. 为dfs()、bfs()函数各添加一个计数器,以查看每个函数在搜索相同迷宫时所遍历的状态数量。并在上图中画出dfs和bfs的实现过程。
|
||||
3. 运用dfs与bfs实现“八皇后”问题。
|
||||
|
||||
*参考代码开源版权说明:
|
||||
|
||||
```swift
|
||||
// Copyright 2017 David Kopec
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
```
|
||||
|
||||
372
IOS/Task02:算法实现/3.图问题.md
Normal file
372
IOS/Task02:算法实现/3.图问题.md
Normal file
@@ -0,0 +1,372 @@
|
||||
# 图问题
|
||||
|
||||
## 构建图框架
|
||||
|
||||
Swift语言的定义为面向协议的编程范式(区别于传统的面相对象或函数范式)。尽管这种新范式的正统性仍然在充实阶段,但可以明确的是:这种语义将接口和复合放在继承之前。相对于类是面向对象语言的基本构建块,函数是函数式编程语言的基本构建块,所以在面向协议的编程语言中,协议是基本的构建块。下面,我们将以协议的方式构建块。
|
||||
|
||||
```swift
|
||||
public protocol Edge: CustomStringConvertible {
|
||||
var u: Int { get set } // index of the "from" vertex
|
||||
var v: Int { get set } // index of the "to" vertex
|
||||
var reversed: Edge { get }
|
||||
}
|
||||
```
|
||||
|
||||
我们对Edge定义为两个顶点之间的连接。用一个整数索引表示。按照习惯,我们通常用 $u$ 表示第一个顶点,用 $v$ 表示第二个顶点。本章中,我们只处理双向边,而在图论中,有向边也常常是讨论的对象。
|
||||
|
||||
我们使用Swfit语言关键字associatedtype来定义Graph采用者(adopters)可以配置的类型。
|
||||
|
||||
```swift
|
||||
protocol Graph: class, CustomStringConvertible {
|
||||
associatedtype VertexType: Equatable
|
||||
associatedtype EdgeType: Edge
|
||||
var vertices: [VertexType] { get set }
|
||||
var edges: [[EdgeType]] { get set }
|
||||
}
|
||||
```
|
||||
|
||||
实现图数据结构的方法有很多种,最常用的两种就是顶点矩阵和邻接矩阵。在顶点矩阵方法中,每个矩阵单元表示两个个顶点的交接,下面是对Graph的一种完整协议拓展,为协议拓展添加了基本函数:
|
||||
|
||||
```swift
|
||||
extension Graph {
|
||||
/// How many vertices are in the graph?
|
||||
public var vertexCount: Int { return vertices.count }
|
||||
|
||||
/// How many edges are in the graph?
|
||||
public var edgeCount: Int { return edges.joined().count }
|
||||
|
||||
/// Get a vertex by its index.
|
||||
///
|
||||
/// - parameter index: The index of the vertex.
|
||||
/// - returns: The vertex at i.
|
||||
public func vertexAtIndex(_ index: Int) -> VertexType {
|
||||
return vertices[index]
|
||||
}
|
||||
|
||||
/// Find the first occurence of a vertex if it exists.
|
||||
///
|
||||
/// - parameter vertex: The vertex you are looking for.
|
||||
/// - returns: The index of the vertex. Return nil if it can't find it.
|
||||
public func indexOfVertex(_ vertex: VertexType) -> Int? {
|
||||
if let i = vertices.index(of: vertex) {
|
||||
return i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Find all of the neighbors of a vertex at a given index.
|
||||
///
|
||||
/// - parameter index: The index for the vertex to find the neighbors of.
|
||||
/// - returns: An array of the neighbor vertices.
|
||||
public func neighborsForIndex(_ index: Int) -> [VertexType] {
|
||||
return edges[index].map({self.vertices[$0.v]})
|
||||
}
|
||||
|
||||
/// Find all of the neighbors of a given Vertex.
|
||||
///
|
||||
/// - parameter vertex: The vertex to find the neighbors of.
|
||||
/// - returns: An optional array of the neighbor vertices.
|
||||
public func neighborsForVertex(_ vertex: VertexType) -> [VertexType]? {
|
||||
if let i = indexOfVertex(vertex) {
|
||||
return neighborsForIndex(i)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Find all of the edges of a vertex at a given index.
|
||||
///
|
||||
/// - parameter index: The index for the vertex to find the children of.
|
||||
public func edgesForIndex(_ index: Int) -> [EdgeType] {
|
||||
return edges[index]
|
||||
}
|
||||
|
||||
/// Find all of the edges of a given vertex.
|
||||
///
|
||||
/// - parameter vertex: The vertex to find the edges of.
|
||||
public func edgesForVertex(_ vertex: VertexType) -> [EdgeType]? {
|
||||
if let i = indexOfVertex(vertex) {
|
||||
return edgesForIndex(i)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Add a vertex to the graph.
|
||||
///
|
||||
/// - parameter v: The vertex to be added.
|
||||
/// - returns: The index where the vertex was added.
|
||||
public func addVertex(_ v: VertexType) -> Int {
|
||||
vertices.append(v)
|
||||
edges.append([EdgeType]())
|
||||
return vertices.count - 1
|
||||
}
|
||||
|
||||
/// Add an edge to the graph.
|
||||
///
|
||||
/// - parameter e: The edge to add.
|
||||
public func addEdge(_ e: EdgeType) {
|
||||
edges[e.u].append(e)
|
||||
edges[e.v].append(e.reversed as! EdgeType)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
正如前面所述,本章我们只讨论双向边。除了分为双向边和无向边外,边还可以赋予权重。下面我们实现一种不带权重的边UnweightedEdge,当然也会实现Edge协议。Edge协议必须定义“from”顶点u “to” 顶点v,以及一种反转Edge的方式。Edge协议还要按照Edge的要求必须实现CustomStringConvertible,这意味着定义一个description属性。
|
||||
|
||||
```swift
|
||||
open class UnweightedEdge: Edge {
|
||||
public var u: Int // "from" vertex
|
||||
public var v: Int // "to" vertex
|
||||
public var reversed: Edge {
|
||||
return UnweightedEdge(u: v, v: u)
|
||||
}
|
||||
|
||||
public init(u: Int, v: Int) {
|
||||
self.u = u
|
||||
self.v = v
|
||||
}
|
||||
|
||||
//MARK: CustomStringConvertable
|
||||
public var description: String {
|
||||
return "\(u) <-> \(v)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Graph的具体实现十分简单,UnweightedGraph就是一个顶点可以是任意Equatable类型、边是UnweightedEdge类型的Graph。通过定义vertices和edges数组的类型,我们在Graph协议中隐式地填充关联类型VertexType和EdgeType
|
||||
|
||||
```swift
|
||||
open class UnweightedGraph<V: Equatable>: Graph {
|
||||
var vertices: [V] = [V]()
|
||||
var edges: [[UnweightedEdge]] = [[UnweightedEdge]]() //adjacency lists
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public init(vertices: [V]) {
|
||||
for vertex in vertices {
|
||||
_ = self.addVertex(vertex)
|
||||
}
|
||||
}
|
||||
|
||||
/// This is a convenience method that adds an unweighted edge.
|
||||
///
|
||||
/// - parameter from: The starting vertex's index.
|
||||
/// - parameter to: The ending vertex's index.
|
||||
public func addEdge(from: Int, to: Int) {
|
||||
addEdge(UnweightedEdge(u: from, v: to))
|
||||
}
|
||||
|
||||
/// This is a convenience method that adds an unweighted, undirected edge between the first occurence of two vertices.
|
||||
///
|
||||
/// - parameter from: The starting vertex.
|
||||
/// - parameter to: The ending vertex.
|
||||
public func addEdge(from: V, to: V) {
|
||||
if let u = indexOfVertex(from) {
|
||||
if let v = indexOfVertex(to) {
|
||||
addEdge(UnweightedEdge(u: u, v: v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// MARK: Implement CustomStringConvertible
|
||||
public var description: String {
|
||||
var d: String = ""
|
||||
for i in 0..<vertices.count {
|
||||
d += "\(vertices[i]) -> \(neighborsForIndex(i))\n"
|
||||
}
|
||||
return d
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 寻找最短路径
|
||||
|
||||
在上面定义的图中,可以简单地认为路径就是边的数组。
|
||||
|
||||
```swift
|
||||
public typealias Path = [Edge]
|
||||
```
|
||||
|
||||
为了美化打印结果,我们可以在协议中进行拓展:
|
||||
|
||||
```swift
|
||||
extension Graph {
|
||||
/// Prints a path in a readable format
|
||||
public func printPath(_ path: Path) {
|
||||
for edge in path {
|
||||
print("\(vertexAtIndex(edge.u)) > \(vertexAtIndex(edge.v))")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
根据上面的原理,我们可以定带权重的图结构:
|
||||
|
||||
```swift
|
||||
/// A subclass of Graph that has convenience methods for adding and removing WeightedEdges. All added Edges should have the same generic Comparable type W as the WeightedGraph itself.
|
||||
open class WeightedGraph<V: Equatable & Hashable, W: Comparable & Summable>: Graph {
|
||||
var vertices: [V] = [V]()
|
||||
var edges: [[WeightedEdge<W>]] = [[WeightedEdge<W>]]() //adjacency lists
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public init(vertices: [V]) {
|
||||
for vertex in vertices {
|
||||
_ = self.addVertex(vertex)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find all of the neighbors of a vertex at a given index.
|
||||
///
|
||||
/// - parameter index: The index for the vertex to find the neighbors of.
|
||||
/// - returns: An array of tuples including the vertices as the first element and the weights as the second element.
|
||||
public func neighborsForIndexWithWeights(_ index: Int) -> [(V, W)] {
|
||||
var distanceTuples: [(V, W)] = [(V, W)]()
|
||||
for edge in edges[index] {
|
||||
distanceTuples += [(vertices[edge.v], edge.weight)]
|
||||
}
|
||||
return distanceTuples
|
||||
}
|
||||
|
||||
/// This is a convenience method that adds a weighted edge.
|
||||
///
|
||||
/// - parameter from: The starting vertex's index.
|
||||
/// - parameter to: The ending vertex's index.
|
||||
/// - parameter weight: the Weight of the edge to add.
|
||||
public func addEdge(from: Int, to: Int, weight:W) {
|
||||
addEdge(WeightedEdge<W>(u: from, v: to, weight: weight))
|
||||
}
|
||||
|
||||
/// This is a convenience method that adds a weighted edge between the first occurence of two vertices. It takes O(n) time.
|
||||
///
|
||||
/// - parameter from: The starting vertex.
|
||||
/// - parameter to: The ending vertex.
|
||||
/// - parameter weight: the Weight of the edge to add.
|
||||
public func addEdge(from: V, to: V, weight: W) {
|
||||
if let u = indexOfVertex(from) {
|
||||
if let v = indexOfVertex(to) {
|
||||
addEdge(WeightedEdge<W>(u: u, v: v, weight:weight))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Implement Printable protocol
|
||||
public var description: String {
|
||||
var d: String = ""
|
||||
for i in 0..<vertices.count {
|
||||
d += "\(vertices[i]) -> \(neighborsForIndexWithWeights(i))\n"
|
||||
}
|
||||
return d
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
从某个节点寻找任何节点的cost是“单源最短路径”问题的一个版本。该问题是:在带权图中,从某个顶点到其他所有顶点的最短路径是什么?
|
||||
|
||||
Dijkstra算法解决了单源最短路径问题。只要为了Dijkstra算法提供一个起始顶点,Dijkstra算法就会返回带权图中从这个顶点到其他任意顶点的最小权重路径。Dijkstra算法还会返回从起点到其他点最近的顶点,Dijkstra的本质就是不断探索距离起始顶点最近的顶点,这是一种贪婪算法。
|
||||
|
||||
下面是Dijkstra算法的步骤:
|
||||
|
||||
1. 将起始顶点添加到优先队列中
|
||||
2. 从优先级队列中弹出最接近的顶点,我们称之为当前顶点。
|
||||
3. 查看当前节点到所有节点的邻居,如果这些邻居之前没有被记录过,或者连接到这些邻居的边拥有新的最短路径,则可以为每个邻居记录他们与起始顶点之间的距离,记录产生这个距离的边,并将新顶点添加到优先队列中
|
||||
4. 重复步骤2和步骤3,知道优先级队列为空
|
||||
5. 返回起始顶点到每个顶点的最短距离,以及达到每个顶点的路径
|
||||
|
||||
下面是对权重图的扩展代码:
|
||||
|
||||
```swift
|
||||
public extension WeightedGraph {
|
||||
|
||||
//MARK: Dijkstra Utilites
|
||||
|
||||
/// Represents a node in the priority queue used
|
||||
/// for selecting the next
|
||||
struct DijkstraNode: Comparable, Equatable {
|
||||
let vertex: Int
|
||||
let distance: W
|
||||
|
||||
public static func < (lhs: DijkstraNode, rhs: DijkstraNode) -> Bool {
|
||||
return lhs.distance < rhs.distance
|
||||
}
|
||||
|
||||
public static func == (lhs: DijkstraNode, rhs: DijkstraNode) -> Bool {
|
||||
return lhs.distance == rhs.distance
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the shortest paths from some route vertex to every other vertex in the graph.
|
||||
///
|
||||
/// - parameter graph: The WeightedGraph to look within.
|
||||
/// - parameter root: The index of the root node to build the shortest paths from.
|
||||
/// - parameter startDistance: The distance to get to the root node (typically 0).
|
||||
/// - returns: Returns a tuple of two things: the first, an array containing the distances, the second, a dictionary containing the edge to reach each vertex. Use the function pathDictToPath() to convert the dictionary into something useful for a specific point.
|
||||
public func dijkstra(root: Int, startDistance: W) -> ([W?], [Int: WeightedEdge<W>]) {
|
||||
var distances: [W?] = [W?](repeating: nil, count: vertexCount) // how far each vertex is from start
|
||||
distances[root] = startDistance // the start vertex is startDistance away
|
||||
var pq: PriorityQueue<DijkstraNode> = PriorityQueue<DijkstraNode>(ascending: true)
|
||||
var pathDict: [Int: WeightedEdge<W>] = [Int: WeightedEdge<W>]() // how we got to each vertex
|
||||
pq.push(DijkstraNode(vertex: root, distance: startDistance))
|
||||
|
||||
while let u = pq.pop()?.vertex { // explore the next closest vertex
|
||||
guard let distU = distances[u] else { continue } // should already have seen it
|
||||
for we in edgesForIndex(u) { // look at every edge/vertex from the vertex in question
|
||||
let distV = distances[we.v] // the old distance to this vertex
|
||||
if distV == nil || distV! > we.weight + distU { // if we have no old distance or we found a shorter path
|
||||
distances[we.v] = we.weight + distU // update the distance to this vertex
|
||||
pathDict[we.v] = we // update the edge on the shortest path to this vertex
|
||||
pq.push(DijkstraNode(vertex: we.v, distance: we.weight + distU)) // explore it soon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (distances, pathDict)
|
||||
}
|
||||
|
||||
|
||||
/// A convenience version of dijkstra() that allows the supply of the root
|
||||
/// vertex instead of the index of the root vertex.
|
||||
public func dijkstra(root: V, startDistance: W) -> ([W?], [Int: WeightedEdge<W>]) {
|
||||
if let u = indexOfVertex(root) {
|
||||
return dijkstra(root: u, startDistance: startDistance)
|
||||
}
|
||||
return ([], [:])
|
||||
}
|
||||
|
||||
/// Helper function to get easier access to Dijkstra results.
|
||||
public func distanceArrayToVertexDict(distances: [W?]) -> [V : W?] {
|
||||
var distanceDict: [V: W?] = [V: W?]()
|
||||
for i in 0..<distances.count {
|
||||
distanceDict[vertexAtIndex(i)] = distances[i]
|
||||
}
|
||||
return distanceDict
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 练习
|
||||
|
||||
1. 拓展图的协议,定义可以添加或者删除边的带权图
|
||||
2. 用本章内容否定柯尼斯堡七桥问题
|
||||
|
||||
*参考代码开源版权说明:
|
||||
|
||||
```swift
|
||||
// Copyright 2017 David Kopec
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
```
|
||||
|
||||
199
IOS/Task02:算法实现/4.动态规划与旅行商问题.md
Normal file
199
IOS/Task02:算法实现/4.动态规划与旅行商问题.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# 动态规划与旅行商问题
|
||||
|
||||
|
||||
|
||||
除了前面比较著名的问题和算法外,我们还将介绍一些比较小巧的技巧。
|
||||
|
||||
## 背包问题
|
||||
|
||||
背包问题是一个优化问题,它代表一种常见的计算需求——给定一组有限的使用选项,要找到有限资源的最佳利用方案——并演化成为一个趣味故事。一个小偷潜入某户人家实行盗窃,他有一个背包,所能偷窃的物品数量受到背包的容量限制,他该如何决定将哪些物品放入背包?各种物品的重量如下:
|
||||
|
||||
| 物品 | 重量 |
|
||||
| ------ | ----- |
|
||||
| 电视机 | 50磅 |
|
||||
| 首饰 | 1磅 |
|
||||
| 灯烛 | 2磅 |
|
||||
| 冰箱 | 200磅 |
|
||||
| 书籍 | 100磅 |
|
||||
| 电脑 | 3磅 |
|
||||
|
||||
如果盗贼可以不限量地拿走任意物品,那么只需要将每件物品的价值初一重量,从而找出在可用容量哪些物品最有价值。为了更贴近真实情况,我们规定小偷不能拿走一部分物品:例如一部分电视机。下面我们将逐步解决这些问题。
|
||||
|
||||
首先定义一个结构体struct来存放物品
|
||||
|
||||
```swift
|
||||
struct Item {
|
||||
let name: String
|
||||
let weight: Int
|
||||
let value: Float
|
||||
}
|
||||
```
|
||||
|
||||
如果尝试用暴力的办法来解决这个问题,我们需要遍历所有放入背包的可能组合。然而从数学上来说,这一种算法显然是一种幂集形式,时间复杂度为 $O(2^n)$ 。我们并不建议采用这种方式进行求解,而是考虑一种动态规划的方法,这有点类似于我们在递归问题中的`备忘录`功能。
|
||||
|
||||
动态规划的精髓在于将庞大的问题分解成若干最优子结构进行求解。例如,对于3磅容量且为3件物品的背包问题,首先可以解决1磅容量和1件物品的背包问题、2磅容量和1件物品的背包问题,以及3磅容量和1件物品的背包问题。然后可以使用该解决方案的结果解决1磅容量和2件物品的背包问题、2磅容量和2件物品的背包问题,以及3磅容量和2件物品的背包问题。最后可以解出全部3件物品的背包问题。
|
||||
|
||||
由于本部分的重点为Swift的算法实现,因此我们不会投入太多精力在动态规划的过程上,你可以参考本章附带的算法延拓,或是参考一些权威的教材例如《算法导论》等内容进行理解。
|
||||
|
||||
在整个算法实现的过程中,需要填写一张所有物品和容量的表单,为每一种物品和容量找到最佳解决方案。
|
||||
|
||||
```swift
|
||||
func knapsack(items: [Item], maxCapacity: Int) -> [Item] {
|
||||
//build up dynamic programming table
|
||||
var table: [[Float]] = [[Float]](repeating: [Float](repeating: 0.0, count: maxCapacity + 1), count: items.count + 1) //initialize table - overshooting in size
|
||||
for (i, item) in items.enumerated() {
|
||||
for capacity in 1...maxCapacity {
|
||||
let previousItemsValue = table[i][capacity]
|
||||
if capacity >= item.weight { // item fits in knapsack
|
||||
let valueFreeingWeightForItem = table[i][capacity - item.weight]
|
||||
table[i + 1][capacity] = max(valueFreeingWeightForItem + item.value, previousItemsValue) // only take if more valuable than previous combo
|
||||
} else { // no room for this item
|
||||
table[i + 1][capacity] = previousItemsValue //use prior combo
|
||||
}
|
||||
}
|
||||
}
|
||||
// figure out solution from table
|
||||
var solution: [Item] = [Item]()
|
||||
var capacity = maxCapacity
|
||||
for i in stride(from: items.count, to: 0, by: -1) { // work backwards
|
||||
if table[i - 1][capacity] != table[i][capacity] { // did we use this item?
|
||||
solution.append(items[i - 1])
|
||||
capacity -= items[i - 1].weight // if we used an item, remove its weight
|
||||
}
|
||||
}
|
||||
return solution
|
||||
}
|
||||
```
|
||||
|
||||
我们将各种物品填入表单中,进行最优求解:
|
||||
|
||||
```swift
|
||||
let items = [Item(name: "television", weight: 50, value: 500),
|
||||
Item(name: "candlesticks", weight: 2, value: 300),
|
||||
Item(name: "stereo", weight: 35, value: 400),
|
||||
Item(name: "laptop", weight: 3, value: 1000),
|
||||
Item(name: "food", weight: 15, value: 50),
|
||||
Item(name: "clothing", weight: 20, value: 800),
|
||||
Item(name: "jewelry", weight: 1, value: 4000),
|
||||
Item(name: "books", weight: 100, value: 300),
|
||||
Item(name: "printer", weight: 18, value: 30),
|
||||
Item(name: "refrigerator", weight: 200, value: 700),
|
||||
Item(name: "painting", weight: 10, value: 1000)]
|
||||
knapsack(items: items, maxCapacity: 75)
|
||||
```
|
||||
|
||||
即可解决背包问题。
|
||||
|
||||
## 旅行商问题
|
||||
|
||||
旅行商问题是最经典、最常被讨论的内容之一。一名推销员必须对地图上的所有城市访问一次,最终回到起点城市。每座城市之间都有一条路直连路径,推销员可以按任何顺序访问这些城市。那么对于推销员而言的最短路径是什么?
|
||||
|
||||
这个问题看起来简单,但是还没有能够针对任意数量的城市快速解决该问题的算法。“快速”的含义在此处指的是:这个问题是一个NP难题。NP难题是不存在多项式时间算法的。随着推销员需要访问的城市数量的增加,解决这个问题的难度急剧提高。20个城市的旅行推销员问题相比10个城市的旅行推销员问题要难得多。当城市数量达到上百万个,在合理的时间内要完美解决这个问题是不可能的。
|
||||
|
||||
```swift
|
||||
let vtCities = ["Rutland", "Burlington", "White River Junction", "Bennington", "Brattleboro"]
|
||||
|
||||
let vtDistances = [
|
||||
"Rutland":
|
||||
["Burlington": 67, "White River Junction": 46, "Bennington": 55, "Brattleboro": 75],
|
||||
"Burlington":
|
||||
["Rutland": 67, "White River Junction": 91, "Bennington": 122, "Brattleboro": 153],
|
||||
"White River Junction":
|
||||
["Rutland": 46, "Burlington": 91, "Bennington": 98, "Brattleboro": 65],
|
||||
"Bennington":
|
||||
["Rutland": 55, "Burlington": 122, "White River Junction": 98, "Brattleboro": 40],
|
||||
"Brattleboro":
|
||||
["Rutland": 75, "Burlington": 153, "White River Junction": 65, "Bennington": 40]
|
||||
]
|
||||
```
|
||||
|
||||
上面的表格输入了旅行商问题需要到达的城市与对应距离,对应的解决办法完全代码如下:
|
||||
|
||||
```swift
|
||||
// backtracking permutations algorithm
|
||||
func allPermutationsHelper<T>(contents: [T], permutations: inout [[T]], n: Int) {
|
||||
guard n > 0 else { permutations.append(contents); return }
|
||||
var tempContents = contents
|
||||
for i in 0..<n {
|
||||
tempContents.swapAt(i, n - 1) // move the element at i to the end
|
||||
// move everything else around, holding the end constant
|
||||
allPermutationsHelper(contents: tempContents, permutations: &permutations, n: n - 1)
|
||||
tempContents.swapAt(i, n - 1) // backtrack
|
||||
}
|
||||
}
|
||||
|
||||
// find all of the permutations of a given array
|
||||
func allPermutations<T>(_ original: [T]) -> [[T]] {
|
||||
var permutations = [[T]]()
|
||||
allPermutationsHelper(contents: original, permutations: &permutations, n: original.count)
|
||||
return permutations
|
||||
}
|
||||
|
||||
// test allPermutations
|
||||
let abc = ["a","b","c"]
|
||||
let testPerms = allPermutations(abc)
|
||||
print(testPerms)
|
||||
print(testPerms.count)
|
||||
|
||||
// make complete paths for tsp
|
||||
func tspPaths<T>(_ permutations: [[T]]) -> [[T]] {
|
||||
return permutations.map {
|
||||
if let first = $0.first {
|
||||
return ($0 + [first]) // append first to end
|
||||
} else {
|
||||
return [] // empty is just itself
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print(tspPaths(testPerms))
|
||||
|
||||
func solveTSP<T>(cities: [T], distances: [T: [T: Int]]) -> (solution: [T], distance: Int) {
|
||||
let possiblePaths = tspPaths(allPermutations(cities)) // all potential paths
|
||||
var bestPath: [T] = [] // shortest path by distance
|
||||
var minDistance: Int = Int.max // distance of the shortest path
|
||||
for path in possiblePaths {
|
||||
if path.count < 2 { continue } // must be at least one city pair to calculate
|
||||
var distance = 0
|
||||
var last = path.first! // we know there is one becuase of above line
|
||||
for next in path[1..<path.count] { // add up all pair distances
|
||||
distance += distances[last]![next]!
|
||||
last = next
|
||||
}
|
||||
if distance < minDistance { // found a new best path
|
||||
minDistance = distance
|
||||
bestPath = path
|
||||
}
|
||||
}
|
||||
return (solution: bestPath, distance: minDistance)
|
||||
}
|
||||
|
||||
let vtTSP = solveTSP(cities: vtCities, distances: vtDistances)
|
||||
print("The shortest path is \(vtTSP.solution) in \(vtTSP.distance) miles.")
|
||||
```
|
||||
|
||||
现实世界中,很少用简单的方法来解决大规模的旅行商问题。当城市数量巨大事,解决这个问题的大多数算法都是近似方法。动态规划与遗传算法是解决这类问题的一种窍门。已有许多期刊论文发表,他们将遗传算法归于针对大量城市条件下旅行商问题的近似最优解决方案。
|
||||
|
||||
## 练习
|
||||
|
||||
1. 尝试用动态规划解决钢管切割问题
|
||||
2. 请用图论内容重新规划旅行商问题
|
||||
|
||||
*参考代码开源版权说明:
|
||||
|
||||
```swift
|
||||
// Copyright 2017 David Kopec
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
```
|
||||
|
||||
BIN
IOS/Task02:算法实现/img/1.PNG
Normal file
BIN
IOS/Task02:算法实现/img/1.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 500 KiB |
BIN
IOS/Task02:算法实现/img/2.PNG
Normal file
BIN
IOS/Task02:算法实现/img/2.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 469 KiB |
BIN
IOS/Task02:算法实现/img/3.png
Normal file
BIN
IOS/Task02:算法实现/img/3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Reference in New Issue
Block a user