魔术截图
魔术截图

去年大年初二,我写了一篇文章「用 Go 语言实现刘谦 2024 春晚魔术,还原尼格买提汗流浃背的尴尬瞬间!」,里面揭秘了小尼魔术失败的原因,这也是我公众号的第一篇文章。

今天刚好也是大年初二,我再带大家用 Go 语言还原一下刘谦在蛇年春晚上的魔术。

先吐个槽,相比去年的魔术,今年的魔术是不是有点「降本增效」了 :)。我看有人提到今年的魔术类似冒泡排序…这个属实有亿点🤏夸张了 😅

没什么数学原理,也什么算法公式,咱们就最简单直接的使用暴力求解法,来穷举一下所有可能情况,这也正是程序代码的强项所在。

排列组合

只有筷子🥢、杯子🍺、勺子🥄三样东西,所有排列组合也仅仅只有 6 种情况。

给定这三种物品,使用 Go 代码求出所有排列组合如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

// 生成所有排列组合
func permute(items []string, start int) {
if start == len(items) {
// 当排列完成时,输出当前的排列
fmt.Println(items)
return
}

for i := start; i < len(items); i++ {
// 交换位置
items[start], items[i] = items[i], items[start]
// 递归调用
permute(items, start+1)
// 交换回原来的位置,回溯
items[start], items[i] = items[i], items[start]
}
}

func main() {
items := []string{"筷子🥢", "杯子🍺", "勺子🥄"}
permute(items, 0)
}

执行示例代码,得到输出如下:

1
2
3
4
5
6
7
$ go run main.go 
[筷子🥢 杯子🍺 勺子🥄]
[筷子🥢 勺子🥄 杯子🍺]
[杯子🍺 筷子🥢 勺子🥄]
[杯子🍺 勺子🥄 筷子🥢]
[勺子🥄 杯子🍺 筷子🥢]
[勺子🥄 筷子🥢 杯子🍺]

这几种组合其实心算也能很快求出来。

魔术实现

接着,咱们捋一下刘谦这个魔术的三个步骤:

  1. 筷子跟它左边的物品互换,如果筷子已经在最左边,则无需移动。

  2. 杯子跟它右边的物品互换,如果杯子已经在最右边,则无需移动。

  3. 勺子跟它左边的物品互换,如果勺子已经在最左边,则无需移动。

那么,我们要做的,就是把这三个步骤封装成一个小函数,然后让每一种排列组合都交给这个魔术执行一遍,最终看看得到的结果即可。

魔术步骤实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 魔术
func magic(items []string) {
// 记录当前排列顺序
old := make([]string, len(items))
copy(old, items)

// 1. 筷子跟它左边的物品互换,如果筷子已经在最左边,则无需移动
for i := 1; i < len(items); i++ {
if items[i] == "筷子🥢" {
// 筷子如果不在最左边,交换到最左边
items[i], items[0] = items[0], items[i]
break
}
}

// 2. 杯子跟它右边的物品互换,如果杯子已经在最右边,则无需移动
for i := len(items) - 2; i >= 0; i-- {
if items[i] == "杯子🍺" {
// 杯子如果不在最右边,交换到最右边
items[i], items[len(items)-1] = items[len(items)-1], items[i]
break
}
}

// 3. 勺子跟它左边的物品互换,如果勺子已经在最左边,则无需移动
for i := 1; i < len(items); i++ {
if items[i] == "勺子🥄" {
// 勺子如果不在最左边,交换到最左边
items[i], items[0] = items[0], items[i]
break
}
}

// 打印当前和经过魔术操作后的排列
fmt.Println("当前排列:", old, " => ", "魔术操作后:", items)
}

这里逻辑非常简单,就是按照魔术步骤交换物品。

现在我们再修改下 permute 函数打印排列顺序的代码 fmt.Println(items),改为直接调用魔术函数 magic(items)

最终实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package main

import "fmt"

// 生成所有排列组合
func permute(items []string, start int) {
if start == len(items) {
// 当排列完成时,输出当前的排列
// fmt.Println(items)
magic(items)
return
}

for i := start; i < len(items); i++ {
// 交换位置
items[start], items[i] = items[i], items[start]
// 递归调用
permute(items, start+1)
// 交换回原来的位置,回溯
items[start], items[i] = items[i], items[start]
}
}

// 魔术
func magic(items []string) {
old := make([]string, len(items))
copy(old, items)

// 1. 筷子跟它左边的物品互换,如果筷子已经在最左边,则无需移动
for i := 1; i < len(items); i++ {
if items[i] == "筷子🥢" {
// 筷子如果不在最左边,交换到最左边
items[i], items[0] = items[0], items[i]
break
}
}

// 2. 杯子跟它右边的物品互换,如果杯子已经在最右边,则无需移动
for i := len(items) - 2; i >= 0; i-- {
if items[i] == "杯子🍺" {
// 杯子如果不在最右边,交换到最右边
items[i], items[len(items)-1] = items[len(items)-1], items[i]
break
}
}

// 3. 勺子跟它左边的物品互换,如果勺子已经在最左边,则无需移动
for i := 1; i < len(items); i++ {
if items[i] == "勺子🥄" {
// 勺子如果不在最左边,交换到最左边
items[i], items[0] = items[0], items[i]
break
}
}

// 打印当前和经过魔术操作后的排列
fmt.Println("当前排列:", old, " => ", "魔术操作后:", items)
}

func main() {
items := []string{"筷子🥢", "杯子🍺", "勺子🥄"}
permute(items, 0)
}

执行示例代码,得到输出如下:

1
2
3
4
5
6
7
$ go run main.go
当前排列: [筷子🥢 杯子🍺 勺子🥄] => 魔术操作后: [勺子🥄 筷子🥢 杯子🍺]
当前排列: [勺子🥄 杯子🍺 筷子🥢] => 魔术操作后: [勺子🥄 筷子🥢 杯子🍺]
当前排列: [杯子🍺 勺子🥄 筷子🥢] => 魔术操作后: [勺子🥄 筷子🥢 杯子🍺]
当前排列: [勺子🥄 杯子🍺 筷子🥢] => 魔术操作后: [勺子🥄 筷子🥢 杯子🍺]
当前排列: [筷子🥢 勺子🥄 杯子🍺] => 魔术操作后: [勺子🥄 筷子🥢 杯子🍺]
当前排列: [勺子🥄 杯子🍺 筷子🥢] => 魔术操作后: [勺子🥄 筷子🥢 杯子🍺]

可以发现,无论哪一种排列顺序,经过魔术的三个步骤以后,最终结果都是一致的 [勺子🥄 筷子🥢 杯子🍺]。所以这个魔术一定会成功,小尼笑而不语😄。

魔术截图
魔术截图

总结

没啥好总结的,看个乐子,今年的魔术属实有点简陋。

嗯,又水了一篇文章 :)

延伸阅读

联系我