1. 算法竞赛中的数组与字符串基础
作为一名参加过多次ACM/ICPC竞赛的老选手,我深知数组和字符串在算法竞赛中的基础地位。在《算法竞赛入门经典(第2版)》第3章中,刘汝佳老师系统地介绍了这两大基础数据结构。让我从实战角度分享一些关键代码笔记和心得。
数组是算法竞赛中最常用的数据结构之一,它的随机访问特性(O(1)时间复杂度)使其成为高效算法的基石。在C/C++中,数组的声明和使用有几个关键点需要注意:
cpp复制// 全局数组会自动初始化为0
const int MAXN = 1000000;
int global_array[MAXN];
int main() {
// 局部数组不会自动初始化,内容随机
int local_array[100];
// 更安全的初始化方式
int safe_array[100] = {0}; // 全部初始化为0
int partial_init[10] = {1, 2}; // 前两个为1,2,其余为0
// 动态大小数组(C99/VLA特性,但竞赛中慎用)
int n = 100;
int vla_array[n]; // 可能引发栈溢出
}
重要提示:在算法竞赛中,建议总是使用全局数组而非局部数组,因为全局数组会自动初始化为0,且栈空间通常比堆空间大得多,可以避免栈溢出问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 字符串处理的特殊技巧
字符串处理是算法竞赛中的另一大核心内容。C风格的字符串以'\0'结尾,这带来了一些独特的处理方式:
cpp复制#include <cstring>
char str[100] = "algorithm";
// 常用字符串操作
int len = strlen(str); // 长度计算
strcpy(str, "competition"); // 字符串复制
strcat(str, " is fun"); // 字符串连接
int cmp = strcmp(str1, str2); // 字符串比较
// 安全的字符串输入方式
fgets(str, sizeof(str), stdin); // 包括换行符
scanf("%99s", str); // 限制最大长度
在实际竞赛中,我总结了几条字符串处理的黄金法则:
- 总是为字符串数组预留额外1字节的空间给'\0'
- 避免使用gets(),它已被弃用且不安全
- 对于大规模字符串处理,考虑预先计算长度而非反复调用strlen()
- 使用const char*指针可以避免不必要的字符串拷贝
3. 多维数组与内存布局
理解多维数组的内存布局对编写高效算法至关重要。C/C++中多维数组是按行优先存储的:
cpp复制int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 内存中实际布局:1,2,3,4,5,6,7,8,9,10,11,12
// 动态分配二维数组的推荐方式
int** create_matrix(int rows, int cols) {
int** mat = new int*[rows];
mat[0] = new int[rows * cols];
for (int i = 1; i < rows; ++i)
mat[i] = mat[i-1] + cols;
return mat;
}
这种连续内存分配方式有几个优势:
- 内存局部性好,缓存命中率高
- 单次分配/释放,减少内存碎片
- 可以像一维数组一样遍历所有元素
4. 字符串匹配算法实战
KMP算法是字符串匹配的经典算法,也是竞赛常考点。以下是经过优化的KMP实现:
cpp复制void computeLPS(const char* pat, int* lps) {
int len = 0; // 当前最长前缀后缀长度
lps[0] = 0;
int i = 1;
while (pat[i]) {
if (pat[i] == pat[len]) {
lps[i++] = ++len;
} else {
if (len != 0) {
len = lps[len-1];
} else {
lps[i++] = 0;
}
}
}
}
int KMP(const char* txt, const char* pat) {
int M = strlen(pat);
int N = strlen(txt);
int lps[M];
computeLPS(pat, lps);
int i = 0, j = 0;
while (i < N) {
if (pat[j] == txt[i]) {
i++; j++;
}
if (j == M) {
return i - j; // 匹配成功
j = lps[j-1];
} else if (i < N && pat[j] != txt[i]) {
if (j != 0) j = lps[j-1];
else i++;
}
}
return -1; // 未找到
}
在实际竞赛中,KMP算法有几个常见变种:
- 统计模式串出现次数而非首次位置
- 多模式串匹配(结合AC自动机)
- 允许一定容错率的模糊匹配
5. 数组与字符串的竞赛技巧
5.1 滑动窗口技术
滑动窗口是处理数组/字符串子区间问题的利器,如最大子数组和、最小覆盖子串等:
cpp复制// 寻找和为target的最短子数组长度
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size();
int ans = INT_MAX;
int left = 0, sum = 0;
for (int right = 0; right < n; right++) {
sum += nums[right];
while (sum >= target) {
ans = min(ans, right - left + 1);
sum -= nums[left++];
}
}
return ans == INT_MAX ? 0 : ans;
}
5.2 双指针技巧
双指针技术可以高效解决许多数组/字符串问题:
cpp复制// 反转字符串
void reverseString(char* s, int sSize){
int left = 0, right = sSize - 1;
while (left < right) {
char tmp = s[left];
s[left++] = s[right];
s[right--] = tmp;
}
}
// 移除重复元素
int removeDuplicates(vector<int>& nums) {
if (nums.empty()) return 0;
int slow = 0;
for (int fast = 1; fast < nums.size(); fast++) {
if (nums[fast] != nums[slow]) {
nums[++slow] = nums[fast];
}
}
return slow + 1;
}
5.3 位运算优化
对于某些特殊问题,位运算可以极大提升性能:
cpp复制// 判断字符串是否有重复字符
bool isUnique(string s) {
int checker = 0;
for (char c : s) {
int val = c - 'a';
if (checker & (1 << val)) return false;
checker |= (1 << val);
}
return true;
}
6. 常见错误与调试技巧
在算法竞赛中,数组和字符串相关的错误往往难以调试。以下是我总结的常见陷阱:
-
数组越界访问:这是最常见的运行时错误
- 总是检查循环边界条件
- 使用assert或防御性编程验证索引有效性
-
字符串终止符问题:
cpp复制char buf[10]; strcpy(buf, "hello world"); // 缓冲区溢出! -
多维数组初始化问题:
cpp复制int arr[10][10] = {0}; // 正确,全部初始化为0 int arr[10][10] = {1}; // 只有arr[0][0]为1,其余为0! -
输入缓冲区问题:
cpp复制int n; char str[100]; scanf("%d", &n); fgets(str, 100, stdin); // 会读取前一个scanf留下的换行符!
对于调试,我推荐以下策略:
- 对于数组问题,打印整个数组内容
- 对于字符串问题,确保打印出可见的终止符:
cpp复制for (int i = 0; i <= strlen(s); i++) { printf("%d: %c (%d)\n", i, s[i], s[i]); } - 使用边界值测试,特别是空数组/字符串、单元素等情况
7. 性能优化实战
在大型竞赛中,即使是基础操作也需要优化。以下是一些实测有效的技巧:
-
用指针替代数组索引:
cpp复制// 传统方式 for (int i = 0; i < n; i++) arr[i] = 0; // 指针优化 int* p = arr; int* end = arr + n; while (p != end) *p++ = 0; -
避免重复计算字符串长度:
cpp复制// 低效 for (int i = 0; i < strlen(s); i++) {...} // 高效 int len = strlen(s); for (int i = 0; i < len; i++) {...} -
使用更快的输入输出方法:
cpp复制// 快速读取整数 int read() { int x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); } return x * f; } -
内存池技术:
cpp复制// 预先分配大块内存 const int POOL_SIZE = 1000000; char memory_pool[POOL_SIZE]; char* pool_ptr = memory_pool; char* alloc(int size) { char* ret = pool_ptr; pool_ptr += size; return ret; }
8. STL中的数组与字符串工具
虽然算法竞赛中常使用C风格数组和字符串,但C++ STL提供的vector和string也非常实用:
cpp复制#include <vector>
#include <string>
#include <algorithm>
// vector使用技巧
vector<int> vec(100, 0); // 100个0
vec.reserve(1000); // 预分配空间
sort(vec.begin(), vec.end()); // 排序
// string高级操作
string s = "Hello";
s += " World"; // 字符串连接
s.substr(6, 5); // 子串"World"
reverse(s.begin(), s.end()); // 反转字符串
// 使用移动语义避免拷贝
string processString(string&& str) {
// 处理str
return move(str); // 移动而非拷贝
}
STL容器的优势在于:
- 自动管理内存
- 提供丰富的内置方法
- 与算法库完美配合
但需要注意:
- 某些操作可能比C风格慢
- 在极端性能要求的场景慎用
- 了解其底层实现以避免意外开销
9. 实战案例分析
让我们看几个典型竞赛题目及其解法:
9.1 最大连续子数组和
cpp复制int maxSubArray(vector<int>& nums) {
int max_sum = INT_MIN;
int current_sum = 0;
for (int num : nums) {
current_sum = max(num, current_sum + num);
max_sum = max(max_sum, current_sum);
}
return max_sum;
}
9.2 字符串的全排列
cpp复制void permute(string& s, int l, int r, vector<string>& result) {
if (l == r) {
result.push_back(s);
} else {
for (int i = l; i <= r; i++) {
swap(s[l], s[i]);
permute(s, l+1, r, result);
swap(s[l], s[i]); // 回溯
}
}
}
9.3 旋转数组搜索
cpp复制int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) return mid;
if (nums[left] <= nums[mid]) { // 左半部分有序
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else { // 右半部分有序
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
10. 进阶话题与扩展学习
掌握了基础之后,可以进一步研究以下高级主题:
-
后缀数组与后缀自动机
- 用于解决复杂字符串匹配问题
- 可以高效处理子串搜索、最长公共子串等问题
-
滚动数组技术
- 动态规划中的空间优化技巧
- 将二维DP降为一维,减少内存使用
-
位图法处理大规模数据
- 用bit位表示数据存在性
- 适用于海量数据去重、统计等场景
-
字符串哈希
- 将字符串映射为唯一哈希值
- 支持O(1)时间的子串比较
-
分块处理技术
- 将大数组分为小块处理
- 平衡预处理成本和查询效率
对于想深入算法竞赛的选手,我建议:
- 每天至少解决2-3道数组/字符串相关题目
- 参加在线编程比赛积累实战经验
- 研究优秀选手的代码风格和解题思路
- 建立自己的代码模板库,但不要死记硬背
