肇鑫的技术博客

业精于勤,荒于嬉

"916. 单词子集"的解题思路

916. 单词子集

原题

我们给出两个单词数组 A 和 B。每个单词都是一串小写字母。

现在,如果 b 中的每个字母都出现在 a 中,包括重复出现的字母,那么称单词 b 是单词 a 的子集。 例如,“wrr” 是 “warrior” 的子集,但不是 “world” 的子集。

如果对 B 中的每一个单词 bb 都是 a 的子集,那么我们称 A 中的单词 a通用的

你可以按任意顺序以列表形式返回 A 中所有的通用单词。

示例 1:

**输入:**A = ["amazon","apple","facebook","google","leetcode"], B = ["e","o"]
输出:["facebook","google","leetcode"]

示例 2:

**输入:**A = ["amazon","apple","facebook","google","leetcode"], B = ["l","e"]
输出:["apple","google","leetcode"]

示例 3:

**输入:**A = ["amazon","apple","facebook","google","leetcode"], B = ["e","oo"]
输出:["facebook","google"]

示例 4:

**输入:**A = ["amazon","apple","facebook","google","leetcode"], B = ["lo","eo"]
输出:["google","leetcode"]

示例 5:

**输入:**A = ["amazon","apple","facebook","google","leetcode"], B = ["ec","oc","ceo"]
输出:["facebook","leetcode"]

提示:

  1. 1 <= A.length, B.length <= 10000
  2. 1 <= A[i].length, B[i].length <= 10
  3. A[i] 和 B[i] 只由小写字母组成。
  4. A[i] 中所有的单词都是独一无二的,也就是说不存在 i != j 使得 A[i] == A[j]

解题思路1

这道题的一般思路很简单。循环A,然后循环B,B中的字符串中的字符如果在A的字符串中存在,则移除掉该字符,然后循环下一个字符。代码如下:

class Solution {
    func wordSubsets(_ A: [String], _ B: [String]) -> [String] {
        let array = A.filter { (s) -> Bool in
            for b in B {
                var s = s
                
                for bCharacter in b {
                    if let index = s.firstIndex(of: bCharacter) {
                        s.remove(at: index)
                    } else {
                        return false
                    }
                }
            }
            
            return true
        }
        
        return array
    }
}

这个思路验证时,提示会超时。也就是性能不达标。

解题思路2

我们注意到示例3中,存在两个相同的字母”oo“。

示例 3:

**输入:**A = ["amazon","apple","facebook","google","leetcode"], B = ["e","oo"]
输出:["facebook","google"]

我的优化思路是,把不重复的字符单独优化,重复的仍然按照“思路1”的方式进行处理。代码如下:

class Solution {
    func wordSubsets(_ A: [String], _ B: [String]) -> [String] {
        var bSet = Set<Character>()
        var bArray = [String]()
        
        B.forEach {
            let set:Set<Character> = Set($0)
            if set.count == $0.count {  // no duplicated
                bSet = bSet.union(set)
            } else {
                bArray.append($0)
            }
        }
        
        let array = A.filter { (s) -> Bool in
            // bSet filter
            let aSet:Set<Character> = Set(s)
            guard aSet.isSuperset(of: bSet) else {
                return false
            }
            
            
            for b in bArray {
                var s = s
                
                for bCharacter in b {
                    if let index = s.firstIndex(of: bCharacter) {
                        s.remove(at: index)
                    } else {
                        return false
                    }
                }
            }
            
            return true
        }
        
        return array
    }
}

进行验证。还是性能不达标。

最终的解题思路

没办法,只好去翻评论。结果有人说,必须全部优化重复的字母,不然就一定会超时。所谓全部优化,就是把B中所有重复的字母计算出来。

原题的B等价于所有的字母都存在,且出现的次数大于等于B中的字符串中出现的那个字符的最多次数。

单独计算B的字母频率。

func characterFrequencies(in array:[String]) -> Dictionary<Character,Int> {
    var dictionary = Dictionary<Character,Int>()
    
    for s in array {
        var sDic = [Character:Int]()
        
        for c in s {
            if let f = sDic[c] {
                sDic[c] = f + 1
            } else {
                sDic[c] = 1
            }
        }
        
        sDic.forEach { (c, f) in
            if let frequency = dictionary[c] {
                dictionary[c] = max(frequency, f)
            } else {
                dictionary[c] = f
            }
        }
    }
    
    return dictionary
}

完整代码:

class Solution {
    func wordSubsets(_ A: [String], _ B: [String]) -> [String] {
        let dictionary = characterFrequencies(in: B)
        
        let array = A.filter { (s) -> Bool in
            for (c,f) in dictionary {
                var index = s.startIndex
                
                for _ in 1...f {
                    if index < s.endIndex, let firstIndex = s[index...].firstIndex(of: c) {
                        let nextIndex = s.index(after: firstIndex)
                        index = nextIndex
                    } else {
                        return false
                    }
                }
            }
            
            return true
        }
        
        
        return array
    }
    
    func characterFrequencies(in array:[String]) -> Dictionary<Character,Int> {
        var dictionary = Dictionary<Character,Int>()
        
        for s in array {
            var sDic = [Character:Int]()
            
            for c in s {
                if let f = sDic[c] {
                    sDic[c] = f + 1
                } else {
                    sDic[c] = 1
                }
            }
            
            sDic.forEach { (c, f) in
                if let frequency = dictionary[c] {
                    dictionary[c] = max(frequency, f)
                } else {
                    dictionary[c] = f
                }
            }
        }
        
        return dictionary
    }
}

小结

我们经常需要用到字典结构来挺高应用的性能。

"329. 矩阵中的最长递增路径"的解题思路

329. 矩阵中的最长递增路径。这道题没有官方的题解,写下我的思路。

原题

给定一个整数矩阵,找出最长递增路径的长度。

对于每个单元格,你可以往上,下,左,右四个方向移动。 你不能在对角线方向上移动或移动到边界外(即不允许环绕)。

示例 1:

输入: nums =
[
[9,9,4],
[6,6,8],
[2,1,1]
]
输出: 4
解释: 最长递增路径为 [1, 2, 6, 9]

示例 2:

输入: nums =
[
[3,4,5],
[3,2,6],
[2,2,1]
]
输出: 4
解释: 最长递增路径是 [3, 4, 5, 6]。注意不允许在对角线方向上移动。

思路

我的思路是先将所有元素按照从小到大排序。然后开始计算,规则如下:

  1. 周边的数字如果都大于当前数字,那个当前数字只能走一步,记角标1
  2. 如果周边的数字存在小于当前数字的,因为是从小到大开始计算的,那个较小的数字之前已经计算过了。因此该方向的步数为1+角标数字。当前数字的最终角标为这四个方向中的最大的。

其实也可以按照从大到小排序。

思路图

my_solution

最后,还要避免空集。

最终代码

class Solution {
    func longestIncreasingPath(_ matrix: [[Int]]) -> Int {
        guard !matrix.flatMap({$0}).isEmpty else{
            return 0
        }
        
        var longest = 1
        var allElements = Array<Item>()
        var allElementsDictionary = Dictionary<IndexPath, Int>()
        var allSteps = Dictionary<IndexPath,Int>()
        
        for (row, rowElements) in matrix.enumerated() {
            for (column, element) in rowElements.enumerated() {
                allElements.append(Solution.Item(value: element, indexPath: Solution.IndexPath(row: row, column: column)))
                allElementsDictionary[IndexPath(row: row, column: column)] = element
            }
        }
        
        allElements.sort { (currentItem, nextItem) -> Bool in
            return currentItem.value < nextItem.value
        }
        
        allElements.forEach {
            let row = $0.indexPath.row
            let column = $0.indexPath.column
            let value = $0.value
            
            var step = 1
            
            let upIndexPath = IndexPath(row: row - 1, column: column)
            if let currentStep = getStep(currentValue: allElementsDictionary[upIndexPath], currentStep: allSteps[upIndexPath], comparingWith: value) {
                
                step = max(currentStep + 1, step)
            }
            
            let leftIndexPath = IndexPath(row: row, column: column - 1)
            if let currentStep = getStep(currentValue: allElementsDictionary[leftIndexPath], currentStep: allSteps[leftIndexPath], comparingWith: value) {
                
                step = max(currentStep + 1, step)
            }
            
            let downIndexPath = IndexPath(row: row + 1, column: column)
            if let currentStep = getStep(currentValue: allElementsDictionary[downIndexPath], currentStep: allSteps[downIndexPath], comparingWith: value) {
                
                step = max(currentStep + 1, step)
            }
            
            let rightIndexPath = IndexPath(row: row, column: column + 1)
            if let currentStep = getStep(currentValue: allElementsDictionary[rightIndexPath], currentStep: allSteps[rightIndexPath], comparingWith: value) {
                
                step = max(currentStep + 1, step)
            }
            
            allSteps[IndexPath(row: row, column: column)] = step
            longest = max(step, longest)
        }
        
        return longest
    }
    
    func getStep(currentValue:Int?, currentStep:Int?, comparingWith value:Int) -> Int? {
        if let currentValue = currentValue, currentValue < value, let currentStep = currentStep {
            return currentStep
        }
        
        return nil
    }

    struct Item {
        let value:Int
        let indexPath:IndexPath
    }
    
    struct IndexPath:Hashable {
        let row:Int
        let column:Int
    }
}

制作上架应用的视频预览

为了展示产品功能,我们可以制作并上传视频。

视频规范

我们可以通过真机录制视频,也可以通过模拟器录制。真机录制视频很简单,只需要在设备的控制中心选择录制屏幕就可以了。录完之后,经过剪辑就可以直接上传。

如果你没有特定型号的真机,也可以通过模拟器来录制视频。模拟器录制视频与真机相比,存在一些限制,本文后面会讲到如何解决这些问题。

推荐使用H.264格式。虽然它会比ProRes 422格式的文件大一些。但是这个格式现有的设备都支持硬件加速。因此保存(转码)速度最快,工作效率最高。

模拟器录制视频

录制视频

先打开Xcode,加载项目,然后选择你需要录制视频的模拟器,运行项目。当项目成功在模拟器中运行后,在终端中输入

xcrun simctl io booted recordVideo record.mov

最后的record.mov是录制的视频的文件名。它会自动保存在终端当前的文件夹。录制完成后按ctrl+c结束录制。

  • 如果你开了多个模拟器,那么可能不会录制到你当前使用的模拟器。关闭掉其余的模拟器,仅保留你需要录制的模拟器。
  • 模拟器录制的视频不完全与你的操作同步。如果你操作完成就立即结束录制,可能结尾部分会没有路上。因此,建议结尾之后进行额外的操作,比如点击按钮之类的。然后在后期制作中,将多余的部分删除,这样可以保证视频录制的完整性。
  • 虽然Xcode可以在Scheme中修改语言,来改变运行时应用的语言,但是这个对于语言的修改,是应用级别的。在录制演示文件时,应该在模拟器/设备中修改系统的语言偏好。这是因为,一些提示,比如通知权限/地理位置权限的提示窗口,是基于系统语言的。如果你只在Xcode中设置,那么就会遇到系统权限提示窗口的语言与应用语言不一致的情况。

转码

根据苹果的规范(见下图),存在两个不同的分辨率,前一个叫原生分辨率(Native resolutions),后一个叫接受的分辨率(Accepted resolutions)。前者是模拟器录制视频时得到视频的分辨率,后一个是App Store Connect上传视频时,所接受的视频分辨率。如果二者分辨率不同,就需要转码。

App Preview Resolutions

当真机录制视频时,得到的视频直接就是接受的分辨率。通过模拟器录制得到的视频,是原生分辨率,需要转码。
转码可以通过你喜欢的任意软件进行。我用的是HandBrake

添加空音轨

转码完成后,如果你直接上传模拟器录制的视频,App Store Connect会在上传后错误提示:音频不符合规范。原因是模拟器录制的视频,只有视频的部分,没有音频。因此,你需要添加额外的音轨。可以使用FFmpeg添加空音轨。

安装FFmpeg

brew install ffmpeg
brew link ffmpeg

添加空音轨

ffmpeg -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 -i video.mov \
  -shortest -c:v copy -c:a aac output.mov

video.mov是源文件,output.mov是目标文件。

剪辑

剪辑我推荐使用Quicktime Player。它的保存速度是最快的。

当剪辑完成之后,有时保存按钮是灰色的。这时可以直接点击关闭,就会弹出保存对话框。