Table of Contents

  1. Algorithm
  2. Review
    1. 为类型扮演定义一个类层级
    2. 检查类型
    3. 向下转换
    4. Any 和 AnyObject 的类型转换
  3. Tips
    1. 行为中的嵌套类型
    2. 引用嵌套类型
  4. Share
    1. 历史
    2. 应用程序
    3. Kadane 算法
      1. 不允许空数组
      2. 允许空子数组
      3. 计算最好子数组的位置
      4. 复杂度
    4. 一般化

Algorithm

Leetcode 1192: Critical Connections in a Network

https://dreamume.medium.com/leetcode-1192-cirtical-connections-in-a-network-ef9a84ed67b8

Review

Type Casting

类型扮演是检查实例类型的一种方式,或在它自己的类层次中以不同的腹类或子类来处理

Swift 的类型扮演用 is 和 as 操作符实现。这两个操作符提供一个简单可表达的方式检查值的类型或转换值到另一个类型

为类型扮演定义一个类层级

你可使用类型扮演在一个类层次和子类中来检查一个特殊类实例的类型且在相同的层次中扮演实例到另一个类。下面三个代码片段定义一个类层次和一个包含这些类实例的数组

class MediaItem {
    var name: String
    init(name: String) {
        self.name = name
    }
}

class Movie: MediaItem {
    var director: String
    init(name: String, director: String) {
        self.director = director
        super.init(name: name)
    }
}


class Song: MediaItem {
    var artist: String
    init(name: String, artist: String) {
        self.artist = artist
        super.init(name: name)
    }
}

let library = [
    Movie(name: "Casablanca", director: "Michael Curtiz"),
    Song(name: "Blue Suede Shoes", artist: "Elvis Presley"),
    Movie(name: "Citizen Kane", director: "Orson Welles"),
    Song(name: "The One And Only", artist: "Chesney Hawkes"),
    Song(name: "Never Gonna Give You Up", artist: "Rick Astley")
]
// the type of "library" is inferred to be [MediaItem]

检查类型

使用类型检查操作符(is)来检查是否一个实例是某个子类型。类型检查操作符成功则返回 true 否则返回 false

下面例子定义两个变量,对 library 数组里的两种不同实例进行统计

var movieCount = 0
var songCount = 0

for item in library {
    if item is Movie {
        movieCount += 1
    } else if item is Song {
        songCount += 1
    }
}

print("Media library contains \(movieCount) movies and \(songCount) songs")
// Prints "Media library contains 2 movies and 3 songs"

向下转换

一个某种类类型的常量或变量可能事实上引用到一个子类的实例。当你相信是这种情况时,你可尝试用类型转换操作符(as? 或 as!)向下转换到子类型

因为向下转换可能失败,类型转换操作符有两种不同的形式。条件形式,as?,返回你尝试向下转换类型的一个可选值。强制形式,as!,作为单个复杂操作尝试向下转换且强制解封结果

当你不确定向下转换是否会成功时使用类型转换操作符(as?)条件形式,如果向下转换是不可能时值为 nil。这使得你可以检查一个成功的向下转换

当你确定向下转换总是会成功时使用类型转换符(as!)的强制形式。如果你尝试向下转换到一个不正确的类类型,你将会触发一个运行时错误

for item in library {
    if let movie = item as? Movie {
        print("Movie: \(movie.name), dir. \(movie.director)")
    } else if let song = item as? Song {
        print("Song: \(song.name), by \(song.artist)")
    }
}

// Movie: Casablanca, dir. Michael Curtiz
// Song: Blue Suede Shoes, by Elvis Presley
// Movie: Citizen Kane, dir. Orson Welles
// Song: The One And Only, by Chesney Hawkes
// Song: Never Gonna Give You Up, by Rick Astley

Any 和 AnyObject 的类型转换

Swift 提供两个特别的类型来处理未指明的类型:

  • Any 可表示任意类型的一个实例,包括函数类型
  • AnyObject 可表示任意类类型的一个实例

只有当你直接需要它们提供的能力和行为时才使用 Any 和 AnyObject。在代码中指明你希望工作的类型总是更好的方式

下面是不同类型混合使用 Any 的例子,包含函数类型和非类类型

var things: [Any] = []

things.append(0)
things.append(0.0)
things.append(42)
things.append(3.14159)
things.append("hello")
things.append((3.0, 5.0))
things.append(Movie(name: "Ghostbusters", director: "Ivan Reitman"))
things.append({ (name: String) -> String in "Hello, \(name)" })

为发现只知道为 Any 或 AnyObject 的常量或变量的具体类型,你可使用 is 或 as 操作符

for thing in things {
    switch thing {
    case 0 as Int:
        print("zero as an Int")
    case 0 as Double:
        print("zero as a Double")
    case let someInt as Int:
        print("an integer value of \(someInt)")
    case let someDouble as Double where someDouble > 0:
        print("a positive double value of \(someDouble)")
    case is Double:
        print("some other double value that I don't want to print")
    case let someString as String:
        print("a string value of \"\(someString)\"")
    case let (x, y) as (Double, Double):
        print("an (x, y) point at \(x), \(y)")
    case let movie as Movie:
        print("a movie called \(movie.name), dir. \(movie.director)")
    case let stringConverter as (String) -> String:
        print(stringConverter("Michael"))
    default:
        print("something else")
    }
}

// zero as an Int
// zero as a Double
// an integer value of 42
// a positive double value of 3.14159
// a string value of "hello"
// an (x, y) point at 3.0, 5.0
// a movie called Ghostbusters, dir. Ivan Reitman
// Hello, Michael

注意 Any 类型可代表任意类型值,包括可选类型。当期望一个 Any 类型时你使用一个可选值 Swift 会给你一个警告。如果你确实需要用 Any 表达一个可选值,你可使用 as 操作符直接转换可选类型为 Any

let optionalNumber: Int? = 3
things.append(optionalNumber)        // Warning
things.append(optionalNumber as Any) // No warning

Tips

Nested Types

枚举经常创建用来支持一种特别的类或结构。相似的,它可方便地用一个更复杂的类型的上下文定义纯工具结构,和一种特别的类型正常用于连接的协议。为实现这个,Swift 可以定义嵌套类型,你可嵌套支持类型比如枚举、结构和定义类型支持的协议

为在另一个类型中嵌套一个类型,在它支持的类型括号里写它的定义。类型可根据需要嵌套很多层

行为中的嵌套类型

下面例子定义一个称为 BlackjackCard 的结构,其为 Blackjack 游戏中卡片的模型。BlackjackCard 结构包含两个嵌套枚举类型称为 Suit 和 Rank

在 Blackjack 中,Ace 卡有 1 或 11 的值。这个特性在 Values 结构中表现,其嵌套在 Rank 枚举中:

struct BlackjackCard {


    // nested Suit enumeration
    enum Suit: Character {
        case spades = "♠", hearts = "♡", diamonds = "♢", clubs = "♣"
    }


    // nested Rank enumeration
    enum Rank: Int {
        case two = 2, three, four, five, six, seven, eight, nine, ten
        case jack, queen, king, ace
        struct Values {
            let first: Int, second: Int?
        }
        var values: Values {
            switch self {
            case .ace:
                return Values(first: 1, second: 11)
            case .jack, .queen, .king:
                return Values(first: 10, second: nil)
            default:
                return Values(first: self.rawValue, second: nil)
            }
        }
    }


    // BlackjackCard properties and methods
    let rank: Rank, suit: Suit
    var description: String {
        var output = "suit is \(suit.rawValue),"
        output += " value is \(rank.values.first)"
        if let second = rank.values.second {
            output += " or \(second)"
        }
        return output
    }
}

Suit 枚举描述四种常见的卡片集,用一个原始的 Character 值代表它们的符号

Rank 枚举描述十三种可能的卡片,用一个原始的 Int 值代表它的面值

如上述提及的,Rank 枚举定义一个它自己的嵌套结构,称为 Values。这个结构封装大多数卡有一个值的事实,但 Ace 卡有两个值。Values 结构定义两个属性来表示这个:

  • fist, Int 类型
  • second,Int? 类型

Rank 也定义一个计算属性,values,返回 Values 结构的一个实例。这个计算属性考虑卡片的范围并基于 rank 初始化一个适合值的新的 Values 实例。它对 jack、queen、king 和 ace 使用特殊值。对数字卡片,它使用 rank 的原始 Int 值

BlackjackCard 结构本身有两个属性 - rank 和 suit。它也定义一个计算属性称为 description,其用 rank 和 suit 的值来构建卡片的描述

因为 BlackjackCard 是没有自定义初始化函数的结构,它有一个间接的成员初始化函数。你可使用这个初始化函数初始化一个新的常量称为 theAceOfSpades

let theAceOfSpades = BlackjackCard(rank: .ace, suit: .spades)
print("theAceOfSpades: \(theAceOfSpades.description)")
// Prints "theAceOfSpades: suit is ♠, value is 1 or 11"

引用嵌套类型

在其上下文之外使用一个嵌套类型,在它的名字前加上嵌套类型名:

let heartsSymbol = BlackjackCard.Suit.hearts.rawValue
// heartsSymbol is "♡"

Share

Maximum subarray problem

在计算机科学中,最大子数组和问题,也被称为最大段和问题,是对一维数组中找到连续子数组中最大和。它可在 O(n) 时间复杂度和 O(1) 空间复杂度下完成

形式上,该任务是找到下标 i 和 j,1 <= i < j <= n,使得和

$ \sum^{j}_ {x=i} A[x] $

最大(一些问题公式也允许考虑空子数组;为方便起见,空子数组的和为 0)。输入数组的每个数可为正、负或零

例如,对数组 [-2, 1, -3, 4, -1, 2, 1, -5, 4],最大连续子数组 [4, -1, 2, 1] 和为 6

这个问题的某些属性:

  1. 如果数组包含所有非负数,则最大子数组为整个数组
  2. 如果数组包含所有非正数,则结果是包含最大值的长度为 1 的子数组(或空数组,如果允许的话)
  3. 一些不同的子数组可能有相同的最大和

虽然这个问题可使用不同的算法技巧解决,包括暴力求解,分治,动态规划和缩变为最短路径,一个简单的被称为 Kadane 算法的一次处理算法解决这个问题非常高效

历史

最大子数组问题于 1977 年由 Ulf Grenander 在数字化图形中作为最大可能估计范型的简化模型提出

Grenander 在寻找一个矩形子数组最大和,在两维实数数组中。一个暴力求解算法运行要 $ O(n^{6}) $ 时间复杂度;因为这明显地慢,Grenander 提出一维问题来获得它的结构的一些洞察力。Grenander 分隔一个算法在 $ O(n^{2}) $ 时间复杂度下解决一维问题,改进暴力求解的 $ O(n^{3}) $。当 Michael Shamos 听说这个问题后,他花了整晚发明了一个 $ O(n \log{n}) $ 分治算法。不久之后,Shamos 在卡梅隆大学的一个研讨会上描述了一维问题和他的历史,Jay Kadane 出席了该研讨会,其用一分钟时间设计了一个 $ O(n) $ 时间复杂度算法。在 1982 年,David Gries 通过应用 Dijkstra 的标准策略获得了相同的 $ O(n) $ 时间复杂度算法;其起源于 1989 年 Richard Bird 使用 Bird-Meertens 形式通过纯代数乘法的暴力求解算法

Grenander 的两维一般化形式作为子程序通过使用 Kadane 的算法可在 $ O(n^{3}) $ 时间复杂度下解决,或通过分治处理。更快的算法基于 Tamaki & Tokuyama(1998) 和 Takaoka(2002) 提出的距离矩阵乘法。有一些证据显示没有明显更快的算法存在;一个在 $ O(n^{3 - \varepsilon}), \forall \varepsilon > 0 $ 时间复杂度下解决两维最大子数组问题算法,可应用于所有配对最短路径问题的相似算法

应用程序

最大子数组问题在许多领域出现过,比如基因序列分析和计算机视觉

基因序列分析应用最大子数组算法确定不寻常的重要的蛋白质序列的生物段,通过对序列中对点打分,当一个范型被识别存在则为正,否则为负,且然后在这些分数中寻找最大子数组。这些问题包括节省段、GC-rich 范围、协同重复、低复杂度过滤、DNA 绑定域和高收费区域

在计算机视觉中,位图图像一般只包含正值,则最大子数组问题是显然的:结果总是整个数组。然而,在对每个像素减去一个阙值(比如像素平均值),这样在平均值之上的像素为正,之下则为负,最大子数组问题可被应用于修改图像来检测明亮区域

Kadane 算法

不允许空数组

Kadane 算法从左到右扫描给定数组 A[1 … n]。在第 j 步,它计算以 j 结尾的最大子数组和;这个和维护在变量 current_sum 中。且它计算任意在 A[1 … j] 中的子数组和,维护在变量 best_sum 中

作为一个循环不变量,在第 j 步中,current_sum 的旧值持有和 $ A[i] + \cdots + A[j - 1], \forall i \in \{1, \ldots, j-1\} $。因此,current_sum + A[j] 是最大和 $ A[i] + \cdots + A[j], i \in \{1, \ldots, j-1\} $。为扩展后续的最大覆盖 i = j 的情况,可考虑单子数组 $ A[j \ldots j] $。在第 6 行通过赋值 max(A[j], current_sum + A[j]) 作为 current_sum 的新值,其之后持有对 $ i \in \{1, \ldots, j \}, A[i] + \cdots + A[j] $ 和的最大

这样,问题可被如下 Python 代码解决

def max_subarray(numbers):
    """Find the largest sum of any contiguous subarray."""
    best_sum = - infinity
    current_sum = 0
    for x in numbers:
        current_sum = max(x, current_sum + x)
        best_sum = max(best_sum, current_sum)
    return best_sum

如果输入不包含正元素,返回值为最大的元素,或负无穷如果输入为空。为正确性,当输入数组为空时一个异常应该抛出,因为一个空数组没有最大非空子数组。如果数组非空,它的第一个元素可被用来替换负无穷,如果需要避免混淆数和非数值

允许空子数组

Kadane 原始的算法解决空子数组为允许的情况。如果输入不包含正元素这个变种会返回 0。它对代码包含两个改变:在第 3 行,best_sum 应该初始化为 0 和第 6 行 current_sum 应该更新为 max(0, current_sum + x)

计算最好子数组的位置

算法可被修改跟踪最大子数组的开始和结束索引

因为这个算法使用最优子结构,这个算法可视为一个简单的动态规划问题

复杂度

Kadane 算法的时间复杂度为 O(n),空间复杂度为 O(1)

一般化

相似的问题可被用于高维数组,但它们的解决方案会更复杂

k 个不相交子数组最大和也可在时间复杂度 O(n+k) 内计算