用BFS验证金蛋1025部分最小循环——算法、代码及其细节

文章最初发表于 2025 年 10 月 26 日。

这是一个关于验证互动视频《你被困在2025年10月25日》最短路径的方案。通过将视频中的人物和事件抽象为图结构,使用宽度优先搜索(BFS)算法找到了完成所有关键事件的最短路径。

读者应当充分探索了原视频。指路:【你被困在2025年10月25日(全新9个循环)】

实际上这是一个相当基础的爆搜方法,适合数据结构与算法入门者学习参考。

2025/10/28 新增了从村民到末影人的事件,发现添加后程序仍然输出 14 事件结果,搜索量从 600 万增长到到 1500 万。以下不再列出。

声明:代码的实现部分使用了GitHub Copilot辅助。源码放到文章最下方了。

演示视频

由于某些原因视频暂时隐藏【14/26循环!用BFS验证金蛋1025部分最小循环https://www.bilibili.com/video/BV165sfz5E6t/?share_source=copy_web&vd_source=69a97851fb5ec8d8597580177d37666f

形式化问题

给定若干节点(人物)和有向边(事件),其中有向边包括从某一节点指向自身的。某些边要求此前走过特定的边才能走。目标是找到从起始节点到目标节点,并且触发过所有关键事件的最短路径。

思路

建模

将视频中的部分人物和事件抽象为有向图结构。

  • 每个人物可以触发特定的事件,而每个事件又可能影响其他人物或事件。人物和事件分别代表图的节点和边。
    • 原作的某些事件,对于寻找最短路径,很明显是无意义的循环事件,这些事件不计入图中。
  • 互动中的“学习机制”通过事件的约束关系表示,即某些事件具有它的前置事件
  • 目标是找到最短路径,使所有关键事件都被触发。

构建出来的图暂时留空,需等待作者更新咕咕咕

算法

使用宽度优先搜索(BFS)。从起始人物(Steve)开始,逐步探索所有可能的事件组合。

  • BFS保证了找到的路径是最短的,因为它按层次遍历图。
  • 在每一步中,检查当前事件是否满足所有关键事件的条件,并记录路径。
  • 由于编写此算法前已经有全流程30循环、28循环、26循环等的解答,目标的路径长度不会大于14,从而认为BFS的复杂度可以接受。

DeepSeek: 可以考虑引入A*算法,在带约束的图搜索中结合启发式函数(如剩余未触发的关键事件数量)来优化路径搜索效率,同时保持最优性。

代码细节

由于代码编写较为仓促,存在一些冗余、复杂、缺少优化的地方,但在现有问题的复杂度下可以给出答案。

数据结构

使用结构体,表示事件和BFS队列中的事件列表。

  • Event结构体表示一个事件,包括起始人物、目标人物和事件ID
  • BFSEventList结构体表示BFS队列事件列表元素,包括当前事件、事件计数、前一个事件索引和关键事件标志。
  • EventList二维数组表示有向边,EventConstraint用于存储边的约束。
  • BFSQueue数组用于存储BFS队列中的事件列表。
  • finaleEventIndex用于记录找到的目标事件索引。

还包括一些宏定义来简化代码中的人物和事件标识。

  • Steve, Creeper, Diamond, Enderman, Villager, (Iron)Golem分别用0到5表示。
  • 事件用位掩码表示,例如CS1表示Creeper杀死Steve的事件。
  • ALL_EVENTS表示所有关键事件的组合。
  • MAXLEN定义了BFS队列的最大长度。

算法

  • CheckConstraintHappened()函数检查某个事件是否已经发生。
    • 通过回溯BFS队列,检查前面的事件是否满足约束条件。
  • isCriticalEvent()函数判断当前事件是否为关键事件。
  • BFS()函数实现了BFS算法,逐步探索所有可能的事件组合。

BFS详解

  • 初始化BFS队列,起始事件为Steve,没有前置事件,事件计数为1,关键事件标志为0b000000。
  • 使用两个指针frontrear来管理BFS队列。
  • 在每次循环中,取出当前事件(不清除元素),遍历所有可能的下一个事件。
  • 对于每个可能的下一个事件,通过回溯前面的事件,检查是否满足约束条件。
  • 如果满足条件,创建新的BFSEventList元素,更新事件计数和关键事件标志,并将其加入BFS队列。
  • 检查是否达到了目标状态(Steve且所有关键事件都发生),如果是则记录索引并返回。
  • 如果队列长度超过MAXLEN,则停止搜索。
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
void BFS()
{
int front = 0, rear = 0;
BFSQueue[rear++] = {{-1, Steve, 0}, 1, -1, 0};

while (front < rear)
{
BFSEventList current = BFSQueue[front++];
for (char i = 0; i < 6; i++)
{
for (char j = 0; j < 3; j++)
{
if (EventList[current.currentEvent.to][i][j] == true)
{
Event constraint = EventConstraint[current.currentEvent.to][i][j];
if (constraint.from != -1)
{
if (!CheckConstraintHappened(constraint, front - 1))
continue;
}

BFSEventList next;
next.currentEvent = {current.currentEvent.to, i, j};
next.eventCount = current.eventCount + 1;
next.previousEventIndex = front - 1;

// check critical event and character
next.CriticalEvent = current.CriticalEvent | isCriticalEvent(next.currentEvent);
BFSQueue[rear++] = next;
if (rear >= MAXLEN)
break;

// check target achieved
if (i == Steve && next.CriticalEvent == ALL_EVENTS)
{
finaleEventIndex = rear - 1;
return;
}
}
}
}
if (rear > MAXLEN)
break;
}
cout << rear << endl;
cout << BFSQueue[MAXLEN].eventCount << endl;
}

主函数逻辑

  • InitEventList()函数初始化事件列表,定义了每个人物可以触发的事件。
  • InitEventConstraint()函数初始化事件约束,定义了每个事件的前置条件。
  • BFS()函数执行BFS搜索。
  • CheckResult()函数检查BFS结果,回溯路径并输出事件序列。

输出结果

经过两轮调试,最终输出了最短路径长度和事件序列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
14
(SS)
(SC)
(CS)
(SD)
(DS)
(SE)
(ES)
(SV)
(VV)
(VD)
(DS)
(SE)
(EG)
(GS)
// 事件序列的含义见文末“形象化问题描述”

BFS过程中,实际使用的队列长度超过了 10^6,幸运的是在 10^7 范围内程序求解出了其中一个解。

你能想象吗?在原视频中,最短路径长度为14,BFS搜索过程模拟了百万级别的世界线。

后记

算法实际上有多种优化空间,例如:

  • 进一步压缩事件的状态表示
  • 特判某些原地循环事件并跳过(史蒂夫每天被苦力怕炸,村民每天给铁傀儡涨工资,……)
  • ……

时隔多年后再次编写BFS相关代码,感触颇深。BFS作为一种基础的图搜索算法,解决这个现实问题依然有效。


附:完整代码与形象的问题描述

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
/*
1025 BFS Solution of 5 Events & Enderman's Slack-off & Returning to Cycle 1(Steve)
Author: Lylighte
Assisted by GitHub Copilot
*/
#include <bits\stdc++.h>
using namespace std;

#define Steve 0
#define Creeper 1
#define Diamond 2
#define Enderman 3
#define Villager 4
#define Golem 5

#define CS1 1<<0
#define DS2 1<<1
#define ES1 1<<2
#define EG 1<<3
#define VD1 1<<4
#define GS1 1<<5
#define ALL_EVENTS (CS1|DS2|ES1|EG|VD1|GS1)

#define MAXLEN 10000000

struct Event
{
char from;
char to;
char eventID;
};

struct BFSEventList
{
Event currentEvent;
int eventCount;
int previousEventIndex;
char CriticalEvent;
};

bool EventList[6][6][3];

Event EventConstraint[6][6][3];

BFSEventList BFSQueue[MAXLEN+10];

int finaleEventIndex = -1;

void InitEventList()
{
// Steve Events
EventList[Steve][Steve][0] = '1';
EventList[Steve][Creeper][0] = '1';
EventList[Steve][Enderman][0] = '1';
EventList[Steve][Diamond][0] = '1';
EventList[Steve][Golem][1] = '1';
EventList[Steve][Villager][0] = '1';

// Creeper Events
EventList[Creeper][Steve][1] = '1';
EventList[Creeper][Villager][0] = '1';

// Diamond Events
EventList[Diamond][Villager][0] = '1';
EventList[Diamond][Steve][1] = '1';
EventList[Diamond][Steve][2] = '1';

// Enderman Events
EventList[Enderman][Diamond][0] = '1';
EventList[Enderman][Steve][0] = '1';
EventList[Enderman][Steve][1] = '1';
EventList[Enderman][Golem][0] = '1';

// Villager Events
EventList[Villager][Villager][0] = '1';
EventList[Villager][Golem][1] = '1';
EventList[Villager][Diamond][1] = '1';

// Golem Events
EventList[Golem][Villager][1] = '1';
EventList[Golem][Steve][0] = '1';
EventList[Golem][Steve][1] = '1';
}

void InitEventConstraint()
{
memset(EventConstraint, 0xff, sizeof(EventConstraint));
// Steve Events
EventConstraint[Steve][Golem][1] = {Villager, Villager, 0};

// Creeper Events
EventConstraint[Creeper][Steve][1] = {Steve, Steve, 0};

// Diamond Events
EventConstraint[Diamond][Steve][1] = {Steve, Steve, 0};
EventConstraint[Diamond][Steve][2] = {Steve, Diamond, 0};

// Enderman Events
EventConstraint[Enderman][Steve][1] = {Steve, Diamond, 0};

// Villager Events
EventConstraint[Villager][Golem][1] = {Villager, Villager, 0};
EventConstraint[Villager][Diamond][1] = {Steve, Enderman, 0};

// Golem Events
EventConstraint[Golem][Villager][1] = {Villager, Villager, 0};
EventConstraint[Golem][Steve][1] = {Villager, Villager, 0};
}

bool CheckConstraintHappened(const Event &constraint, const int position)
{
int idx = position;
while (idx != -1)
{
BFSEventList event = BFSQueue[idx];
if (event.currentEvent.from == constraint.from &&
event.currentEvent.to == constraint.to &&
event.currentEvent.eventID == constraint.eventID)
{
return true;
}
idx = event.previousEventIndex;
}
return false;
}

char isCriticalEvent(const Event &event)
{
if (event.from == Creeper && event.to == Steve && event.eventID == 1)
return CS1;
if (event.from == Diamond && event.to == Steve && event.eventID == 2)
return DS2;
if (event.from == Enderman && event.to == Steve && event.eventID == 1)
return ES1;
if (event.from == Enderman && event.to == Golem && event.eventID == 0)
return EG;
if (event.from == Villager && event.to == Diamond && event.eventID == 1)
return VD1;
if (event.from == Golem && event.to == Steve && event.eventID == 1)
return GS1;
return 0;
}

void BFS()
{
int front = 0, rear = 0;
BFSQueue[rear++] = {{-1, Steve, 0}, 1, -1, 0};

while (front < rear)
{
BFSEventList current = BFSQueue[front++];
for (char i = 0; i < 6; i++)
{
for (char j = 0; j < 3; j++)
{
if (EventList[current.currentEvent.to][i][j] == true)
{
Event constraint = EventConstraint[current.currentEvent.to][i][j];
if (constraint.from != -1)
{
if (!CheckConstraintHappened(constraint, front - 1))
continue;
}

BFSEventList next;
next.currentEvent = {current.currentEvent.to, i, j};
next.eventCount = current.eventCount + 1;
next.previousEventIndex = front - 1;

// check critical event and character
next.CriticalEvent = current.CriticalEvent | isCriticalEvent(next.currentEvent);
BFSQueue[rear++] = next;
if (rear >= MAXLEN)
break;

// check target achieved
if (i == Steve && next.CriticalEvent == ALL_EVENTS)
{
finaleEventIndex = rear - 1;
return;
}
}
}
}
if (rear > MAXLEN)
break;
}
cout << rear << endl;
cout << BFSQueue[MAXLEN].eventCount << endl;
}

void CheckResult()
{
if (finaleEventIndex == -1)
{
cout << "Impossible" << endl;
return;
}
vector<Event> resultEvents;
int idx = finaleEventIndex;
while (idx != -1)
{
BFSEventList event = BFSQueue[idx];
resultEvents.push_back(event.currentEvent);
idx = event.previousEventIndex;
}
reverse(resultEvents.begin(), resultEvents.end());
cout << resultEvents.size() - 1 << endl;
for (size_t i = 1; i < resultEvents.size(); i++)
{
Event event = resultEvents[i];
char fromChar, toChar;
switch (event.from)
{
case Steve:
fromChar = 'S';
break;
case Creeper:
fromChar = 'C';
break;
case Diamond:
fromChar = 'D';
break;
case Enderman:
fromChar = 'E';
break;
case Villager:
fromChar = 'V';
break;
case Golem:
fromChar = 'G';
break;
}
switch (event.to)
{
case Steve:
toChar = 'S';
break;
case Creeper:
toChar = 'C';
break;
case Diamond:
toChar = 'D';
break;
case Enderman:
toChar = 'E';
break;
case Villager:
toChar = 'V';
break;
case Golem:
toChar = 'G';
break;
}
cout << "(" << fromChar << toChar;
if (event.eventID > 0)
cout << event.eventID;
cout << ")" << endl;
}
}

int main()
{
InitEventList();
InitEventConstraint();
BFS();
CheckResult();
return 0;
}

Result:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
14
(SS)
(SC)
(CS)
(SD)
(DS)
(SE)
(ES)
(SV)
(VV)
(VD)
(DS)
(SE)
(EG)
(GS)

Figurative problem description

Characters:

Steve, Creeper, Diamond, Enderman, Villager, Golem

Events:

Events are coded like (ABx), where A is the initiator character, B is the target character, and x is the event ID (if multiple events exist between the same characters). Omit x if x=0. Event whose x is 1 or 2 has constraint.

Useful Relationships:

Steve

  • (SS)Killed by Creeper -> Steve
  • (SC)Fight using wood sword -> Creeper
  • (SE)Successful Trade -> Enderman
  • (SD)Make a diamond sword -> Diamond
  • (SG1)Send Flowers -> Golem
    • (VV) needed
  • (SV)Kill the villager -> Villager

Creeper

  • (CS1)Kill Steve in the cave -> Steve
    • (SS) needed
  • (CV)Kill Villager -> Villager

Diamond

  • (DV)Witness Villager’s Death -> Villager
  • (DS1)Witness Steve’s Death -> Steve
    • (SS) needed
  • (DS2)Turn into a sword -> Steve
    • (SD) needed

Enderman

  • (ED)Steal the Diamond -> Diamond
  • (ES0)Kill Steve outside -> Steve
  • (ES1)Kill Steve in stronghold -> Steve
    • (SD) needed
  • (EG)Slack off in village -> Golem

Villager

  • (VV)Pay Golem -> Villager
  • (VG1)Protected by Golem -> Golem
    • (VV)needed
  • (VD1)Make a deal with Steve -> Diamond
    • (SE) needed

Golem

  • (GV1)Paid by Villager -> Villager
    • (VV) needed
  • (GS0)Killed Steve -> Steve
  • (GS1)Protect Villager -> Steve
    • (VV) needed

Target Status:

Charater - Steve
Triggered Events - (CS1), (DS2), (ES1), (EG), (VD1), (GS1)

Start At:

Steve, No Events


用BFS验证金蛋1025部分最小循环——算法、代码及其细节
https://lyernest.240725.xyz/2026/01/20251025-solution/
发布于
2026年1月20日
许可协议