1. 题目背景与需求分析
P7133 "小P的星空"是信息学奥赛(NOI)系列题目中的一道经典动态规划问题。题目描述了一个浪漫的场景:小P在夜晚仰望星空,将星星连成不同的星座图案。我们需要用C++编写程序,计算在给定的星空图中能够组成多少个特定的星座形状。
这道题的核心考察点在于:
- 二维矩阵中的模式匹配
- 动态规划在图形识别中的应用
- 空间复杂度的优化处理
实际解题时,我们需要处理以下几个关键问题:
- 如何高效表示星空图和星座模板
- 如何处理不同方向的星座变体(旋转、镜像等)
- 如何优化匹配算法的时间复杂度
2. 数据结构设计与输入处理
2.1 星空图的存储结构
对于N×M的星空图,最直接的存储方式是二维字符数组:
const int MAX_N = 1005; char starMap[MAX_N][MAX_N];但在实际比赛中,考虑到内存限制和访问效率,更推荐使用vector容器:
vector<string> starMap; int n, m; // 星空图的行列数 void input() { cin >> n >> m; starMap.resize(n); for(int i=0; i<n; i++) { cin >> starMap[i]; } }2.2 星座模板的表示
星座模板通常较小(比如3×3或5×5),可以用相同方式存储。但需要考虑其所有可能的变体:
struct Constellation { vector<string> pattern; int size; void rotate90() { // 顺时针旋转90度 vector<string> newPattern(size, string(size, ' ')); for(int i=0; i<size; i++) { for(int j=0; j<size; j++) { newPattern[j][size-1-i] = pattern[i][j]; } } pattern = newPattern; } void mirror() { // 水平镜像 for(auto &row : pattern) { reverse(row.begin(), row.end()); } } };3. 核心算法实现
3.1 暴力匹配算法
最直接的思路是枚举星空图中的每个位置,检查是否匹配星座模板:
bool matchAt(const vector<string>& map, const vector<string>& pattern, int x, int y) { int psize = pattern.size(); for(int i=0; i<psize; i++) { for(int j=0; j<psize; j++) { if(x+i >= map.size() || y+j >= map[0].size()) return false; if(pattern[i][j] != '.' && map[x+i][y+j] != pattern[i][j]) return false; } } return true; } int countMatches(const vector<string>& map, const Constellation& c) { int count = 0; for(int i=0; i<=map.size()-c.size; i++) { for(int j=0; j<=map[0].size()-c.size; j++) { if(matchAt(map, c.pattern, i, j)) { count++; } } } return count; }3.2 使用动态规划优化
当星座模板较大时,暴力匹配效率低下。可以采用动态规划预处理星空图:
vector<vector<int>> preprocess(const vector<string>& map, char target) { int n = map.size(), m = map[0].size(); vector<vector<int>> dp(n+1, vector<int>(m+1, 0)); for(int i=1; i<=n; i++) { for(int j=1; j<=m; j++) { dp[i][j] = dp[i-1][j] + dp[i][j-1] - dp[i-1][j-1]; if(map[i-1][j-1] == target) { dp[i][j]++; } } } return dp; } bool fastMatch(const vector<vector<int>>& dp, const Constellation& c, int x, int y, char target) { int psize = c.size; int expected = 0; for(int i=0; i<psize; i++) { for(int j=0; j<psize; j++) { if(c.pattern[i][j] == target) expected++; } } int actual = dp[x+psize][y+psize] - dp[x][y+psize] - dp[x+psize][y] + dp[x][y]; return actual == expected; }4. 完整解题代码实现
结合上述思路,完整的解题代码如下:
#include <iostream> #include <vector> #include <algorithm> using namespace std; struct Constellation { vector<string> pattern; int size; void rotate90() { vector<string> newPattern(size, string(size, ' ')); for(int i=0; i<size; i++) { for(int j=0; j<size; j++) { newPattern[j][size-1-i] = pattern[i][j]; } } pattern = newPattern; } void mirror() { for(auto &row : pattern) { reverse(row.begin(), row.end()); } } void generateAllVariants(vector<Constellation>& variants) { variants.push_back(*this); // 生成3个旋转版本 for(int i=0; i<3; i++) { rotate90(); variants.push_back(*this); } // 生成镜像版本 mirror(); variants.push_back(*this); // 生成镜像后的旋转版本 for(int i=0; i<3; i++) { rotate90(); variants.push_back(*this); } } }; bool matchAt(const vector<string>& map, const vector<string>& pattern, int x, int y) { int psize = pattern.size(); for(int i=0; i<psize; i++) { for(int j=0; j<psize; j++) { if(x+i >= map.size() || y+j >= map[0].size()) return false; if(pattern[i][j] != '.' && map[x+i][y+j] != pattern[i][j]) return false; } } return true; } int countMatches(const vector<string>& map, const Constellation& c) { int count = 0; for(int i=0; i<=map.size()-c.size; i++) { for(int j=0; j<=map[0].size()-c.size; j++) { if(matchAt(map, c.pattern, i, j)) { count++; } } } return count; } int main() { // 输入处理 int n, m, k; cin >> n >> m; vector<string> starMap(n); for(int i=0; i<n; i++) { cin >> starMap[i]; } cin >> k; Constellation c; cin >> c.size; c.pattern.resize(c.size); for(int i=0; i<c.size; i++) { cin >> c.pattern[i]; } // 生成所有可能的星座变体 vector<Constellation> variants; c.generateAllVariants(variants); // 去重 sort(variants.begin(), variants.end(), [](const Constellation& a, const Constellation& b) { return a.pattern < b.pattern; }); variants.erase(unique(variants.begin(), variants.end(), [](const Constellation& a, const Constellation& b) { return a.pattern == b.pattern; }), variants.end()); // 计算匹配总数 int total = 0; for(const auto& variant : variants) { total += countMatches(starMap, variant); } cout << total << endl; return 0; }5. 算法优化与性能分析
5.1 时间复杂度分析
原始暴力算法的时间复杂度为: O(N×M×K×P²) 其中:
- N,M是星空图尺寸
- K是星座变体数量(最多8个)
- P是星座模板尺寸
使用动态规划预处理后,可以优化到: O(N×M + K×N×M)
5.2 空间优化技巧
在实际比赛中,内存限制严格,可以采用以下优化:
- 使用bitset压缩存储星座模板
- 按行处理星空图,减少内存占用
- 复用预处理数组
// 使用bitset表示星座模板 bitset<32> patternBits[5]; // 假设最大星座尺寸为5×5 void convertToBitset(const vector<string>& pattern) { for(int i=0; i<pattern.size(); i++) { for(int j=0; j<pattern[i].size(); j++) { if(pattern[i][j] != '.') { patternBits[i].set(j); } } } }6. 常见错误与调试技巧
6.1 边界条件处理
常见错误包括:
- 数组越界访问
- 未考虑星座模板超出星空图边界的情况
- 旋转和镜像操作实现错误
调试建议:
- 先测试小规模数据(如3×3星空图和2×2星座)
- 打印中间结果验证旋转/镜像是否正确
- 使用assert检查数组访问
6.2 输入输出优化
对于大规模数据:
- 使用快速IO方法
- 避免不必要的endl
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);7. 测试用例设计
7.1 基础测试用例
3 3 *.* .*. *.* 1 3 *.* .*. *.*预期输出:1(完全匹配)
7.2 旋转测试用例
4 4 ..*. .*.. *... ...* 1 3 *.. .*. ..*预期输出:2(匹配原始和旋转版本)
7.3 边界测试用例
2 2 ** ** 1 2 *. .*预期输出:0(虽然元素相同但模式不匹配)
8. 竞赛实战建议
- 模板准备:提前准备好常用数据结构(如二维数组处理、旋转操作等)的代码模板
- 测试驱动:先写简单的测试用例验证基本功能,再逐步增加复杂度
- 性能预估:根据题目约束条件估算算法复杂度,确保在时间限制内
- 调试输出:在关键步骤添加有条件调试输出,便于快速定位问题
#define DEBUG 1 #if DEBUG #define debug(x) cout << #x << " = " << x << endl #else #define debug(x) #endif在解决这类图形模式匹配问题时,最重要的是建立清晰的思维模型。先考虑最简单的情况,再逐步增加复杂度。实际比赛中,通常不需要实现最优算法,而是在有限时间内写出正确且足够高效的解法。