ASCII 艺术练习,但我不明白这个解决方案

ASCII art exercise, but I don't understand this solution

我正在做编码游戏练习,我设法完成了 ASCII 艺术问题,它又长又难,但我做到了。 现在我阅读了其中一个解决方案,它是用 5 行代码完成的! 我试图理解它,但我没有这样做,有人可以帮助我一步一步地解释吗?

测试给你一个输入:

第 1 行:以 ASCII 艺术表示的字母的宽度 L。所有字母的宽度相同。

第2行:ASCII艺术表示的字母的高度H。所有字母高度相同。

第3行:文本T行,由N个ASCII字符组成。

以下几行:字符串ABCDEFGHIJKLMNOPQRSTUVWXYZ?以 ASCII 艺术表示。

这是解决方案之一:

fun main(args : Array<String>) {
    val L = readLine()!!.toInt()
    val H = readLine()!!.toInt()
    val T = readLine()!!.toUpperCase().replace("[^A-Z]".toRegex(), "[")

    val rows = (0 until H).map{readLine()!!}

    (0 until H).map{h -> T.fold(""){ a, c -> a + rows[h].substring((c-'A')*L, (c-'A')*L+L) }}.forEach{println(it)}
}

非常感谢你的帮助:)

PS。我不确定 .fold.map.

是什么

我们逐行分析。

val L = readLine()!!.toInt()

读取一个 Int 并放入变量 L

val H = readLine()!!.toInt()

读取另一个 Int 并放入变量 H

val T = readLine()!!
    .toUpperCase()
    .replace("[^A-Z]".toRegex(), "[")

读取 String,将其所有字母转换为大写字符(因此 a 变为 A),将所有非字母替换为 [ 字符([^A-Z] 语法来自 RegEx,而不是 Kotlin)并放入变量 T.

val rows = (0 until H).map{ readLine()!! }

创建一个包含从 0 到 H - 1 的数字的列表,并且对于每个数字,将其转换为用户提供的 Stringmap 函数转换类型为 T1T2 类型的列表;在这种情况下,它将 0 until H 创建的 Int 列表转换为 String 列表,这是readLine()!! 的结果)。将列表放入变量 rows.

(0 until H).map{ h ->
    T.fold(""){ a, c ->
        a + rows[h].substring((c-'A')*L, (c-'A')*L+L)
    }
}
.forEach{ println(it) }

创建一个包含从 0 到 H - 1 的数字的列表,并且对于每个数字 h,将其转换为 String

在解释这个String是如何创建的之前,我们先分析一下fold里面发生了什么。

  • fold接收者(即调用fold.之前的变量)是一个String,因为变量TString.

  • 有两个参数,ac,其中a是一个String(相同类型的接收器)和c 是一个 Char(因为 StringChar 的集合)。我们只能在集合中使用 fold,这样做时,第一个参数将与其接收方元素类型相同,第二个参数将与其接收方元素类型相同。

  • 有一个隐含的return,它是参数a与变量h的第rows个元素的子字符串连接] (因为 rows 是一个 List<String>,正如我们在上面看到的,这个列表的任何元素都将是一个 String;还要考虑 a)这个列表的第一个元素在第 0 个此列表的位置等等,以及 b) 符号 x[i] 是列表 x 的第 i 个元素;这就是 Kotlin 管理列表访问的方式。

  • 该子字符串从位置 (c - 'A') * L 开始,到位置 (c - 'A') * L + L 结束。要理解它,请将 Char 视为一个数字。所以,当你写 'A' 时,实际上你写的是 65。这是一个名为 ASCII 的约定。所以当你做 'A' - 'A' 时,你做的是 65 - 65,那是 0。同样适用于 'B' - 'A',这意味着 66 - 65(因为 'B'A 在 ASCII 中),即 1。由于 L 也是一个 Int,只需将其替换为表达式,您将得到一个 Int.

但这两个参数是什么,acfold 只不过是一个循环。在第一次迭代中,a == ""(调用fold后括号内的值)和c == T[0](即变量T的第一个Char,即接收者)。某些值被 returned(在这种情况下,a 的值与 rows[h] 的子字符串连接),并且在下一次迭代中,a == <returned value by a and T[0]>c == T[1].在下一次迭代中,a == <returned value by a and T[1]>c == T[2]。重复循环,直到处理完 T 的所有元素。

最后,在forEach中,打印了fold创建的每个String。如果还有疑问,就在屏幕上打印每个变量并尝试理解:

(0 until H).map{ h ->
    println("h=$h")
    T.fold(""){ a, c ->
        println("a=$a, c=$c")
        println("c - 'A' = ${c - 'A'}")
        a + rows[h].substring((c-'A')*L, (c-'A')*L+L)
    }
}

希望你明白了。


编辑:我试图将回复放在评论中,但我认为它太长了,所以我进行了编辑。

为了更好地理解,让我们定义一个函数 printAscii,这是您的原始函数,但参数是输入。如果您不知道函数是什么,请不要担心。

fun printAscii(L: Int, H: Int, T: String, rows: List<String>) {
    (0 until H).map{ h ->
        T.fold(""){ a, c ->
            a + rows[h].substring((c-'A')*L, (c-'A')*L+L)
        }
    }.forEach{ println(it) }
}

你可以这样调用一个函数:

printAscii(L = 1, H = 2, T = "some string", rows = listOf("string1", "string2"))

在这种情况下,L 将等于 1H 将等于 2T 将等于 "some string"rows 将等于 listOf("string1", "string2")。现在,让我们来玩一下:

printAscii(L = 1, H = 1, T = "ABCDE", rows = listOf("12345")) // the output is 12345
printAscii(L = 1, H = 1, T = "AACDE", rows = listOf("12345")) // the output is 11345
printAscii(L = 1, H = 1, T = "ABCBA", rows = listOf("12345")) // the output is 12321
printAscii(L = 1, H = 1, T = "EDCBA", rows = listOf("12345")) // the output is 54321
printAscii(L = 1, H = 1, T = "AAAAA", rows = listOf("12345")) // the output is 11111

您可以看到 T 变量定义了 rows 的打印方式。

现在,让我们将 L 变量更改为 2

printAscii(L = 2, H = 1, T = "ABCDE", rows = listOf("1234567890")) // the output is 1234567890
printAscii(L = 2, H = 1, T = "AACDE", rows = listOf("1234567890")) // the output is 1212567890
printAscii(L = 2, H = 1, T = "ABCBA", rows = listOf("1234567890")) // the output is 1234563412
printAscii(L = 2, H = 1, T = "EDCBA", rows = listOf("1234567890")) // the output is 9078563412
printAscii(L = 2, H = 1, T = "AAAAA", rows = listOf("1234567890")) // the output is 1212121212

现在可以看到字符串T中的字符A与[=38=中字符串中的子串12相关,字符B in Trows 中的子字符串 34 相关,依此类推。当我们将 L 更改为 2 时,T 中的每个字符都与 rows 中大小为 2 的子字符串相关(如果你愿意,你可以尝试 L 等于 3,但一定要在 rows 的第一个元素中添加 5 个字符,这是字符串 T).

的大小

一般来说,当H等于1时,T中的每个字符都与[=第一个元素的大小为L的子串相关38=](记住这一点!)。但是如何计算哪个子串属于某个字符呢?

请注意,

  • L == 1时,A0开始的子串相关,B与[=97开始的子串相关=], C 与从 2 开始的子串相关,所以上;

  • L == 2A与[=129=开始的子串相关时]、B与[=99开始的子串相关=], C 与从 4 开始的子串相关,依此类推。

你能猜出 L == 3 的模式吗?模式很简单:计算某个字母到A的距离(例如,AB之间的距离为1,A和[=146之间的距离=] 是 4)。将这个距离乘以L,得到子串的起始位置。

因此,例如,当 L == 2 时,C 与从 4 开始的子字符串相关。应用我们的方法,CA 之间的距离为 2。乘以 L,即 2,我们有 2 * 2 = 4 , 这是子串的起始位置。

由于子串的大小为L,正如我们上面发现的,为了得到子串的最终位置,我们可以在开始位置加上L

根据以上信息,我们可以编写Kotlin代码了。

  • 如何计算某个字符c'A'之间的距离?答案:c - 'A'

  • 如何获取子串的起始位置?答案:(c - 'A') * L

  • 如何获取子串的结束位置?答案:(c - 'A') * L + L

  • 如何从rows的第一个元素中得到一个子串?答案:rows[0].substring((c - 'A') * L, (c - 'A') * L + L)

这是给H == 1的,但你可以增加它的值,得出同样的结论。

关于你的第二个问题,你可以看一下this site,特别是否定字符类部分。 Regex 与 Kotlin 无关,但您可以在 Kotlin 代码中使用 Regex。

简单来说,Regex中的[^A-Z]表示“匹配一个不在A和Z之间的字符”,[^0-9]表示“匹配一个不在0和9之间的字符” ”。如果要匹配 A 和 Z 之间的单个字符,只需删除插入符号,即 [A-Z].

话虽这么说,.replace("[^A-Z]".toRegex(), "[") 的意思是“匹配一个不在 A 和 Z 之间的字符,并用 [ 替换这个字符”。为什么 [?还记得 'B' - 'A' == 1'E' - 'A' == 4 吗?如果你跟着火车,你会看到 'Z' - 'A' == 25。 ASCII table 中紧接着 'Z' 的字符是 [,因此 '[' - 'A' == 26。因此,任何不在 A 和 Z 之间的字符 c 都会将表达式 c - 'A' 计算为 26.