1. 离散化:大数据范围处理的利器
离散化是算法竞赛中处理大范围数据的常用技巧。当题目给出的数据范围很大(比如1e9),但实际数据量相对较小(比如1e5)时,我们就可以用离散化将原始数据映射到一个紧凑的连续区间。
1.1 离散化的核心思想
离散化的本质是将稀疏的原始数据重新编号,使其变得密集。举个例子,原始数据[9, 99, 999, 9999]经过离散化后可以变成[1, 2, 3, 4]。这种映射保持了数据间的相对大小关系,但大大缩小了数值范围。
离散化的典型应用场景包括:
- 需要以数据值为数组下标但数值范围过大时
- 需要统计区间信息但区间端点分布稀疏时
- 需要去重并排序大量数据时
1.2 离散化的两种实现方式
1.2.1 排序+去重+二分查找
这是最经典的离散化实现方式,分为三个步骤:
- 将所有数据存入数组并排序
- 使用unique函数去重
- 通过二分查找确定每个值的离散化结果
cpp复制// 离散化方式一:排序+去重+二分查找
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e5+10;
int n;
int a[N]; // 原始数据
int disc[N]; // 离散化数组
int pos; // 记录离散化后元素个数
int find(int x) {
int l=1, r=pos;
while(l<r) {
int mid=(l+r)>>1;
if(disc[mid]>=x) r=mid;
else l=mid+1;
}
return l;
}
int main() {
cin>>n;
for(int i=1;i<=n;i++) {
cin>>a[i];
disc[i]=a[i];
}
sort(disc+1,disc+n+1);
pos=unique(disc+1,disc+n+1)-disc-1;
for(int i=1;i<=n;i++) {
cout<<"离散化结果:"<<find(a[i])<<endl;
}
return 0;
}
1.2.2 使用STL容器简化实现
借助unordered_map可以简化离散化的实现过程:
cpp复制// 离散化方式二:使用STL容器
#include<iostream>
#include<unordered_map>
#include<algorithm>
using namespace std;
const int N=1e5+10;
int n;
int a[N], tmp[N];
int cnt;
unordered_map<int,int> id; // 离散化映射表
int main() {
cin>>n;
for(int i=1;i<=n;i++) {
cin>>a[i];
tmp[i]=a[i];
}
sort(tmp+1,tmp+1+n);
for(int i=1;i<=n;i++) {
if(id.count(tmp[i])) continue;
id[tmp[i]]=++cnt;
}
for(int i=1;i<=n;i++) {
cout<<a[i]<<"离散化为:"<<id[a[i]]<<endl;
}
return 0;
}
1.3 离散化实战:火烧赤壁问题
火烧赤壁问题要求计算所有起火区间的总长度,考虑区间可能重叠的情况。直接模拟会因数据范围过大而无法实现,这时就需要离散化+差分的组合技巧。
解题思路:
- 收集所有区间端点进行离散化
- 在离散化后的坐标上进行差分标记
- 还原差分数组计算实际覆盖长度
cpp复制#include <iostream>
#include <unordered_map>
#include <algorithm>
using namespace std;
const int N = 2e4 + 10;
int n;
int a[N], b[N]; // 存储区间端点
int disc[N*2], pos;
unordered_map<int,int> id;
int f[N*2]; // 差分数组
int main() {
cin>>n;
for(int i=1;i<=n;i++) {
cin>>a[i]>>b[i];
disc[++pos]=a[i], disc[++pos]=b[i];
}
sort(disc+1,disc+1+pos);
pos=unique(disc+1,disc+1+pos)-(disc+1);
for(int i=1;i<=pos;i++) id[disc[i]]=i;
// 差分标记
for(int i=1;i<=n;i++) {
int l=id[a[i]], r=id[b[i]];
f[l]++, f[r]--;
}
// 还原差分数组
for(int i=1;i<=pos;i++) f[i]+=f[i-1];
int res=0;
for(int i=1;i<=pos;i++) {
if(f[i]>0) res+=disc[i+1]-disc[i];
}
cout<<res<<endl;
return 0;
}
1.4 离散化注意事项
-
边界处理:离散化可能改变原始数据的间距,在处理区间问题时需要特别注意。例如在贴海报问题中,简单的离散化可能导致相邻区间合并,这时需要在离散化时额外插入中间点。
-
去重必要性:离散化前必须先排序去重,否则相同的原始值会被映射到不同位置,破坏数据关系。
-
映射一致性:确保在整个程序中使用的都是同一套离散化结果,避免混淆原始值和离散化值。
-
空间估算:根据问题规模预先估算离散化数组大小,避免空间不足。例如处理n个区间时,离散化数组至少需要2n的空间。
2. 递归:化繁为简的艺术
递归是算法设计中的核心思想之一,它通过将问题分解为相似的子问题来简化求解过程。掌握递归思维对解决复杂问题至关重要。
2.1 递归的基本原理
递归函数有两个关键要素:
- 递归关系:如何将大问题分解为小问题
- 基线条件:最简单情况的直接解法
以经典的阶乘为例:
- 递归关系:n! = n × (n-1)!
- 基线条件:0! = 1
2.2 汉诺塔问题解析
汉诺塔问题是理解递归的绝佳案例。问题要求将n个盘子从A柱移动到B柱,期间可以借助C柱,且任何时候大盘子不能放在小盘子上面。
递归思路:
- 将n-1个盘子从A移到C(借助B)
- 将第n个盘子从A移到B
- 将n-1个盘子从C移到B(借助A)
cpp复制#include <iostream>
using namespace std;
void hanoi(int n, char from, char via, char to) {
if(n == 0) return;
hanoi(n-1, from, to, via);
printf("%c->%d->%c\n", from, n, to);
hanoi(n-1, via, from, to);
}
int main() {
int n;
char a, b, c;
cin >> n >> a >> b >> c;
hanoi(n, a, c, b);
return 0;
}
2.3 FBI树问题解析
FBI树要求根据01字符串构建二叉树并后序遍历输出节点类型。递归解法自然地反映了树的结构:
cpp复制#include <iostream>
using namespace std;
const int N = 11;
int f[1<<N]; // 前缀和数组
void dfs(int l, int r) {
if(l > r) return;
int sum = f[r] - f[l-1];
char ch = (sum == 0) ? 'B' : (sum == r-l+1) ? 'I' : 'F';
if(l == r) {
cout << ch;
return;
}
int mid = (l+r)/2;
dfs(l, mid);
dfs(mid+1, r);
cout << ch;
}
int main() {
int n; cin >> n;
n = 1 << n;
for(int i=1; i<=n; i++) {
char ch; cin >> ch;
f[i] = (ch == '1') + f[i-1];
}
dfs(1, n);
return 0;
}
2.4 递归编程技巧
-
明确函数定义:在写递归函数前,先明确这个函数要完成什么任务,接受什么参数,返回什么结果。
-
信任递归:不要试图在脑海中展开所有递归调用,相信递归函数能正确解决子问题。
-
边界处理:确保所有可能的输入都有明确的处理路径,特别是终止条件要完备。
-
避免重复计算:对于有重叠子问题的情况,考虑使用记忆化存储中间结果。
-
尾递归优化:当递归调用是函数的最后操作时,某些编译器会进行优化,减少栈空间使用。
3. 分治算法:复杂问题的分解策略
分治算法通过将问题分解为多个子问题,分别解决后再合并结果,是解决复杂问题的有效方法。许多经典算法如归并排序、快速排序都基于分治思想。
3.1 分治的基本步骤
- 分解:将原问题划分为若干子问题
- 解决:递归解决各子问题
- 合并:将子问题的解合并为原问题的解
3.2 逆序对问题解析
逆序对问题要求统计数组中逆序对的数量。使用分治思想,结合归并排序的过程可以高效解决:
cpp复制#include <iostream>
using namespace std;
typedef long long LL;
const int N = 5e5+10;
int a[N], tmp[N];
LL mergeSort(int l, int r) {
if(l >= r) return 0;
int mid = (l+r)/2;
LL res = mergeSort(l, mid) + mergeSort(mid+1, r);
int k=0, i=l, j=mid+1;
while(i<=mid && j<=r) {
if(a[i]<=a[j]) tmp[k++]=a[i++];
else {
tmp[k++]=a[j++];
res += mid-i+1;
}
}
while(i<=mid) tmp[k++]=a[i++];
while(j<=r) tmp[k++]=a[j++];
for(i=l,j=0; i<=r; i++,j++) a[i]=tmp[j];
return res;
}
int main() {
int n; cin >> n;
for(int i=0; i<n; i++) cin >> a[i];
cout << mergeSort(0, n-1) << endl;
return 0;
}
3.3 快速选择算法
快速选择算法用于在未排序数组中查找第k小的元素,是快速排序的变种,平均时间复杂度O(n):
cpp复制#include <iostream>
#include <ctime>
using namespace std;
const int N = 5e6+10;
int a[N];
int quickSelect(int l, int r, int k) {
if(l == r) return a[l];
int pivot = a[l + rand()%(r-l+1)];
int i=l, j=r;
while(i <= j) {
while(a[i] < pivot) i++;
while(a[j] > pivot) j--;
if(i <= j) swap(a[i++], a[j--]);
}
if(k <= j) return quickSelect(l, j, k);
if(k >= i) return quickSelect(i, r, k);
return a[k];
}
int main() {
srand(time(0));
int n, k;
scanf("%d%d", &n, &k);
for(int i=0; i<n; i++) scanf("%d", &a[i]);
printf("%d", quickSelect(0, n-1, k));
return 0;
}
3.4 分治算法优化技巧
-
平衡子问题:尽量将问题划分为规模相近的子问题,以获得更好的时间复杂度。
-
剪枝优化:在某些情况下可以提前终止不必要的递归分支。
-
并行计算:独立的子问题可以并行处理以提高效率。
-
记忆化:存储已解决的子问题结果,避免重复计算。
-
迭代实现:对于深度较大的递归,考虑使用迭代加栈的方式避免堆栈溢出。
4. 算法思想综合应用
在实际问题中,这些算法思想往往需要结合使用。例如:
- 离散化+线段树:处理大范围数据的区间查询问题
- 递归+记忆化:解决动态规划问题
- 分治+快速排序:优化选择算法
理解这些基础算法思想的内在联系,能够帮助我们在面对新问题时快速找到解决方案。建议通过大量练习来培养算法思维,特别注意每种算法的适用场景和时间复杂度分析。