作为一名算法竞赛选手,赛后补题是提升实力的重要环节。今天我想分享最近在补NOIP2002和CERC1995两道经典搜索题时的解题思路和心得。这两道题虽然年代久远,但考察的搜索技巧至今仍具价值。
先简单介绍下背景:这两道题都来自知名编程竞赛,一道是中国的NOIP(全国青少年信息学奥林匹克联赛),另一道是CERC(中欧地区程序设计竞赛)。它们共同的特点是都需要运用搜索算法,但单纯的暴力搜索无法通过,必须配合巧妙的剪枝策略。
在正式解题前,我想强调几个重要习惯:
下面我们就深入分析这两道题目。
题目给出两个字符串A和B(长度≤20),以及最多6个变换规则。要求通过不超过10步的变换,将A变成B。每次变换可以选择一个规则,将当前字符串中的某个子串替换成另一个子串。
这本质上是一个状态空间搜索问题:
由于步数限制较小(≤10),广度优先搜索(BFS)是更合适的选择,因为:
基础BFS的实现框架如下:
但这样效率可能不够高,我们可以采用双向BFS优化:
双向BFS的理论优势在于将时间复杂度从O(b^d)降到O(b^(d/2)),其中b是分支因子,d是深度。对于本题d=10的情况,这能显著减少搜索空间。
以下是双向BFS的核心代码实现(C++):
cpp复制struct Node { string s; int step; };
queue<Node> q[2]; // 两个方向的队列
unordered_map<string, int> vis[2]; // 记录访问状态和步数
int extend(int dir, vector<pair<string,string>>& rules) {
Node cur = q[dir].front(); q[dir].pop();
for (auto& r : rules) {
string from = dir ? r.second : r.first;
string to = dir ? r.first : r.second;
size_t pos = 0;
while ((pos = cur.s.find(from, pos)) != string::npos) {
string nxt = cur.s.substr(0, pos) + to +
cur.s.substr(pos + from.length());
if (nxt.length() > 20) { ++pos; continue; }
if (vis[dir].count(nxt)) { ++pos; continue; }
if (vis[1-dir].count(nxt)) { // 双向搜索相遇
return cur.step + 1 + vis[1-dir][nxt];
}
vis[dir][nxt] = cur.step + 1;
q[dir].push({nxt, cur.step + 1});
++pos;
}
}
return -1;
}
几个值得注意的实现细节:
最坏情况下,每个字符串可能有LM个可能的变换(L是字符串长度,M是规则数),深度为10,所以朴素BFS复杂度是O((LM)^10)。但实际中:
在实际测试中,双向BFS能在毫秒级解决问题,而朴素BFS可能需要几秒甚至更久。
题目给出若干长度不超过50的小木棍,它们是由若干根等长的原始木棍剪断得到的。要求找出原始木棍的最小可能长度。
关键点:
这显然是一个组合优化问题,可能的解法包括:
采用深度优先搜索(DFS)框架:
为了提高效率,必须加入强力剪枝:
剪枝1:可行性剪枝
原始长度L必须满足sum%L==0,否则直接跳过。这减少了不必要的尝试。
剪枝2:优化搜索顺序
将木棍按长度降序排列。长木棍选择少,更容易快速发现矛盾。
剪枝3:排除等效冗余
如果一根木棍无法在当前位置使用,那么所有相同长度的木棍也跳过。因为它们在当前位置的效果相同。
剪枝4:及时回溯
如果某次拼接失败时当前棒还是空的,说明这个L无解,直接回溯。
剪枝5:记录失败长度
如果某长度木棍无法完成当前拼接,记录下来避免后续重复尝试。
cpp复制void dfs(int completed, int lastIdx, int targetLen, int currentLen) {
if (completed == numTargetSticks - 1) {
bestAnswer = targetLen;
foundSolution = true;
return;
}
if (currentLen == targetLen) {
dfs(completed + 1, 0, targetLen, 0);
return;
}
for (int i = lastIdx; i < totalSticks && !foundSolution; ) {
if (!used[i] && currentLen + stickLengths[i] <= targetLen) {
used[i] = true;
dfs(completed, i + 1, targetLen, currentLen + stickLengths[i]);
used[i] = false;
if (foundSolution) return;
if (currentLen == 0 || currentLen + stickLengths[i] == targetLen) {
return;
}
i = nextIdx[i]; // 跳过相同长度
} else {
i++;
}
}
}
实现细节:
不加剪枝的DFS在n=64时完全无法在合理时间内解决。加入全部剪枝后:
通过这两道题,我们可以总结出解决搜索类问题的通用方法:
最后分享一个个人心得:当遇到难题时,试着从以下几个角度思考:
算法竞赛之路漫长而有趣,希望这些经验对你有帮助。记住,每个高手都是从"蒟蒻"开始的,持续学习和实践才是进步的关键。