二、 常用技巧
1. 散列
原理是把一个元素key通过散列函数H转换为H(key)。
最常见的散列法:把key作为数组下标。
散列函数:直接定址法,平方取中法,除留余数法。
解决冲突的方法:线性探测法,平方探测法,链地址法。
字符串hash:把26个字母看做26进制(0~25)的数,将26进制转换为10进制,如BCD看做731。
2. 递归
- 递归边界:返回最简单底层的结果。
- 递归式:减少数据规模并向下一次递归。
2.1 全排列问题
#include <cstdio>
const int maxn = 11;
//输出1~n的全排列,P为当前排列,hash记录整数x是否已经在P中
int n, P[maxn], hash[maxn] = {false};
void fullP(int index) { //当前处理排列的第index号位
if (index == n + 1) { //递归边界,已经处理完排列的1~n位
for (int i = 1; i <= n; i++) {
printf("%d", P[i]); //输出当前排列
}
printf("\n");
return;
}
for (int x = 1; x <= n; x++) {
if (hash[x] == false) {
P[index] = x;
hash[x] = true;
fullP(index + 1); //递归式
hash[x] = false;
}
}
}
int main() {
n = 3; //输出1~3的全排列
fullP(1); //从P[1]开始填
return 0;
}
2.2 n皇后问题
#include <cstdio>
#include <cmath>
const int maxn = 11;
// n皇后问题,P为当前排列,hash记录整数x是否已经在P中,count记录合法排列个数
int n, P[maxn], hash[maxn] = {false}, count = 0;
void fullP(int index) { //当前处理排列的第index号位
if (index == n + 1) { //递归边界,已经处理完排列的1~n位
bool flag = true;
for (int i = 1; i <= n; i++) { //遍历任意两个皇后
for (int j = i + 1; j <= n; j++) {
if (abs(i - j) == abs(P[i] - P[j])) { //在一条对角线上
flag = false;
}
}
}
if (flag) count++;
return;
}
for (int x = 1; x <= n; x++) {
if (hash[x] == false) {
P[index] = x;
hash[x] = true;
fullP(index + 1); //递归式
hash[x] = false;
}
}
}
int main() {
n = 8; //八皇后问题
fullP(1); //从P[1]开始填
printf("%d", count);
return 0;
}
2.3 回溯法优化n皇后问题
#include <cstdio>
#include <cmath>
const int maxn = 11;
// n皇后问题,P为当前排列,hash记录整数x是否已经在P中,ans数组记录每种情况,cnt记录合法排列个数
int n, P[maxn], hash[maxn] = {false}, ans[1000] = {0}, cnt = 0;
void fullP(int index) { //当前处理排列的第index号位
if (index == n + 1) { //递归边界,能到达这里的一定是合法的
for (int i = 1; i <= n; i++) {
ans[cnt] = ans[cnt] * 10 + P[i]; //将结果数组转化为整数
}
cnt++;
return;
}
for (int x = 1; x <= n; x++) {
if (hash[x] == false) {
bool flag = true;
for (int pre = 1; pre < index; pre++) { //遍历之前的皇后
//第index列皇后的行号为x,第pre列皇后的行号为P[pre]
if (abs(index - pre) == abs(x - P[pre])) {
flag = false;
break;
}
}
if (flag) {
P[index] = x;
hash[x] = true;
fullP(index + 1); //递归式
hash[x] = false;
}
}
}
}
int main() {
n = 8; //八皇后问题
fullP(1); //从P[1]开始填
printf("%d\n", cnt);
for (int i = 0; i < cnt; i++) {
printf("%d\n", ans[i]);
}
return 0;
}
3. 贪心
局部最优推全局最优。
3.1 简单贪心
3.2 区间贪心
N个开区间,选择尽可能多的开区间,使这些开区间两两没有交集。
- 若开区间1被开区间2包含,则选开区间2。
- 去除开区间包含的情况,先选所有开区间左端点最大的区间。
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 110;
struct Inteval {
int x, y; //开区间左右段点
} I[maxn];
bool cmp(Inteval a, Inteval b) {
if (a.x != b.x)
return a.x > b.x; //先按左端点从大到小排序
else
return a.y < b.y; //左端点相同的按右端点从小到大
}
int main() {
int n;
while (scanf("%d", &n), n != 0) {
for (int i = 0; i < n; i++) {
scanf("%d%d", &I[i].x, &I[i].y);
}
sort(I, I + n, cmp);
// ans记录不相交区间个数,lastX记录上一个被选中区间的左端点
int ans = 1, lastX = I[0].x;
for (int i = 1; i < n; i++) {
if (I[i].y <= lastX) { //如果该区间右端点在lastX左边
lastX = I[i].x; //以I[i]作为新选中的区间
ans++;
}
}
printf("%d\n", ans);
}
return 0;
}
4. 二分
4.1 二分查找
严格单调递增数列中查找元素
int binarySearch(int arr[], int left, int right, int key) {
while (left <= right) {
//下列句子可以优化为mid = left + (right - left) / 2,防止溢出
int mid = (left + right) / 2; //取中点
if (arr[mid] == key){ //找到
return mid;
} else if (arr[mid] > key) { //中间的数大于key
right = mid - 1;
} else { //中间的数小于key
left = mid + 1;
}
}
return -1; //查找失败,返回-1
}
查找第一个满足条件C的元素
int binarySearch(int arr[], int left, int right, int key) {
while (left < right) { //对[left,right]来说,left=right意味着到唯一位置
int mid = left + (right - left) / 2;
//下面if的括号内填入找的条件,如下是找第一个大于等于key的元素
if (arr[mid] >= key) { //条件成立,第一个满足条件的元素的位置<=mid
right = mid;
} else { //条件不成立,则第一个满足条件的元素的位置>mid
left = mid + 1;
}
}
return left; //返回夹出的位置
}
查找最后一个满足条件C的元素
先求第一个满足**条件"!C"**的元素,然后将该位置减1。
4.2 二分法拓展(以求根号2近似值为例)
#include <cstdio>
const double eps = 1e-5;
double f(double x) {
return x * x;
}
double calSqrt() {
double left = 1, right = 2, mid;
while (right - left > eps) {
mid = (left + right) / 2;
if (f(mid) > 2) {
right = mid;
} else
left = mid;
}
return mid;
}
int main() {
printf("%f", calSqrt());
return 0;
}
4.3 快速幂
给定三个正整数 a 、 b 、 m ( a < 1 0 9 , b < 1 0 6 , 1 < m < 1 0 9 ) a、b、m(a<10^9,b<10^6,1<m<10^9) a、b、m(a<109,b<106,1<m<109),求解 a b % m a^b\%m ab%m
递归写法
- 若 b b b是奇数,则 a b = a ∗ a b − 1 a^b =a*a^{b-1} ab=a∗ab−1
- 若 b b b是偶数,则 a b = a b / 2 ∗ a b / 2 a^b=a^{b/2}*a^{b/2} ab=ab/2∗ab/2
typedef long long LL;
//求a^b%m,递归写法
//若初始时a大于m,则在进入函数前先让a对m取模
//若m=1,则直接判定为0
LL binaryPow(LL a, LL b, LL m) {
if (b == 0) return 1;
// b为奇数,转换为b-1
if (b % 2 == 1) { //可用if (b & 1)替代
return a * binaryPow(a, b - 1, m) % m;
} else { // b为偶数,转换为b/2
LL mul = binaryPow(a, b / 2, m);
return mul * mul % m;
}
}
迭代写法
可以证明,任意的 a b a^b ab都可以表示为 a 2 k 、 … a 4 、 a 2 、 a 1 a^{2k}、…a^4、a^2、a^1 a2k、…a4、a2、a1中若干项的乘积。若 b b b的二进制的 i i i位为1,则 a 2 i a^{2i} a2i就选中,注意到 a 2 k 、 … a 4 、 a 2 、 a 1 a^{2k}、…a^4、a^2、a^1 a2k、…a4、a2、a1序列中的前一项总是等于后一项的平方。
typedef long long LL;
//求a^b%m,递归写法
LL binaryPow(LL a, LL b, LL m) {
LL ans = 1;
while (b > 0) {
if (b & 1) { //如果b的二进制末尾为1,相当于b为奇数
ans = ans * a % m; //令ans累积上a
}
a = a * a % m; //令a平方,相当于a^2i序列
b >>= 1; // b二进制右移一位,相当于b/=2
}
return ans;
}
5. two pointers(双指针)
5.1 求递增数列中的A+B=M
void add(int arr[], int n, int m) {
int i = 0, j = n - 1;
while (i < j) {
if (arr[i] + arr[j] == m) { //找到满足的情况
printf("%d+%d=%d\n", arr[i], arr[j], m);
i++;
j--;
} else if (arr[i] + arr[j] < m) { // i往右移动
i++;
} else // j往左移动
j--;
}
}
5.2 序列合并问题
将两个递增序列A与B合并为一个递增序列C。
int merge(int A[], int B[], int C[], int n, int m) {
int i = 0, j = 0, index = 0; // i指向A[0],j指向B[0]
while (i < n && j < m) {
if (A[i] <= B[j]) { //选择序列A和B中最小的元素加入序列C
C[index++] = A[i++];
} else {
C[index++] = B[j++];
}
}
while (i < n) C[index++] = A[i++]; //将序列A中剩余元素直接加入序列C
while (j < m) C[index++] = B[j++]; //将序列B中剩余元素直接加入序列C
return index; //返回序列C的长度
}
5.3 [归并排序](#4. 归并排序)
5.4 [快速排序](#5. 快速排序)
6. 打表
- 在程序中一次性计算出所有需要的结果,之后的查询直接取这些结果,如素数表。
- 在程序B中分一次或多次计算出所有需要的结果,手工把结果写在程序A的数组中,然后在程序A中就可以直接使用这些结果,如提前计算出n皇后问题的方案数。
- 对没有思路的题,先用暴力程序计算小范围内的结果,然后找规律。
7. 活用递推
找到F(n)与F(n-1)的递推关系,简化计算。
8. 随机选择算法
找到一个无序数组中第K大的数(数组中元素各不相同)
int randPartition(int A[], int left, int right) {
//生成[left,right]内的随机数
int p = (int)(round(1.0 * rand() / RAND_MAX * (right - left) + left));
swap(A[p], A[left]); //交换A[p]和A[left]
int temp = A[left];
while (left < right) {
while (left < right && A[right] > temp) right--; //反复左移right
A[left] = A[right]; //将A[right]移到A[left]
while (left < right && A[left] < temp) left++; //反复右移left
A[right] = A[left]; //将A[left]移到A[right]
}
A[left] = temp; //把temp放到left和right相遇的地方
return left; //返回相遇的下标
}
int randSlect(int A[], int left, int right, int K) {
if (left == right) return A[left]; //递归边界
int p = randPartition(A, left, right); //划分后主元的最终位置为p
int M = p - left + 1; // A[p]是A[left,right]中的第M大
if (K == M) return A[p]; //找到第K大数
if (K < M) { //第K大数在主元左侧
return randSlect(A, left, p - 1, K);
} else //第K大数在主元右侧
return randSlect(A, p + 1, right, K - M);
}
应用举例
给定一个由整数组成的集合,集合中的整数各不相同,现在要将它分为两个子集合,使得这两个子集合的并为原交集、交为空集,同时在两个子集合的元素个数 n 1 n_1 n1与 n 2 n_2 n2之差的绝对值 ∣ n 1 − n 2 ∣ \lvert n_1-n_2 \rvert ∣n1−n2∣尽可能小的前提下,要求它们各自的元素之和 S 1 S_1 S1与 S 2 S_2 S2之差的绝对值 ∣ S 1 − S 2 ∣ \lvert S_1-S_2 \rvert ∣S1−S2∣尽可能大,并求 ∣ S 1 − S 2 ∣ \lvert S_1-S_2 \rvert ∣S1−S2∣的最大值。
该问题等价于求原集合中第n/2大元素,同时根据这个元素把集合划分为两部分,使得其中一个子集合中的元素都不小于这个数,而另一个子集合中的元素都大于这个数。因此使用随机选择算法较为简便。
#include <cstdio>
#include <ctime>
#include <cstdlib>
#include <cmath>
#include <algorithm>
using namespace std;
const int maxn = 100010;
int A[maxn], n;
int randPartition(int A[], int left, int right) {
//生成[left,right]内的随机数
int p = (int)(round(1.0 * rand() / RAND_MAX * (right - left) + left));
swap(A[p], A[left]); //交换A[p]和A[left]
int temp = A[left];
while (left < right) {
while (left < right && A[right] > temp) right--; //反复左移right
A[left] = A[right]; //将A[right]移到A[left]
while (left < right && A[left] < temp) left++; //反复右移left
A[right] = A[left]; //将A[left]移到A[right]
}
A[left] = temp; //把temp放到left和right相遇的地方
return left; //返回相遇的下标
}
int randSlect(int A[], int left, int right, int K) {
if (left == right) return A[left]; //递归边界
int p = randPartition(A, left, right); //划分后主元的最终位置为p
int M = p - left + 1; // A[p]是A[left,right]中的第M大
if (K == M) return A[p]; //找到第K大数
if (K < M) { //第K大数在主元左侧
return randSlect(A, left, p - 1, K);
} else //第K大数在主元右侧
return randSlect(A, p + 1, right, K - M);
}
int main() {
//初始化随机数种子
srand((unsigned)time(NULL));
// sum和sum1记录所有整数之和与切分后前n/2个元素之和
int sum = 0, sum1 = 0;
scanf("%d", &n); //整数个数
for (int i = 0; i < n; i++) {
scanf("%d", &A[i]);
sum += A[i];
}
randSlect(A, 0, n - 1, n / 2);
for (int i = 0; i < n / 2; i++) {
sum1 += A[i];
}
printf("%d", (sum - sum1) - sum1); //求两个子集合的元素和之差
return 0;
}
三、 排序算法
1. 冒泡排序
void bubbleSort(int arr[], int n) {
int flag = 0; //标记本轮是否发生元素交换
for (int i = 1; i <= n - 1; i++) {
for (int j = 0; j < n - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = 1;
}
}
if (!flag) break;
}
}
2. 选择排序
void selectSort(int arr[], int n) {
for (int i = 0; i < n; i++) {
int min = i; //标记最小元素的下标
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[min]) {
min = j;
}
}
int temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
}
3. 插入排序
void insertSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int temp = arr[i], j = i;
while (j > 0 && temp < arr[j - 1]) { //元素后移
arr[j] = arr[j - 1];
j--;
}
arr[j] = temp; //插入到对应的位置
}
}
4. 归并排序
4.1 二路归并(递归)
const int maxn = 100;
void merge(int A[], int L1, int R1, int L2, int R2) {
int i = L1, j = L2; // i指向A[L1],j指向A[L2]
int temp[maxn], index = 0; // temp临时存放合并后的序列,index为其元素个数
while (i <= R1 && j <= R2) {
if (A[i] <= A[j]) {
temp[index++] = A[i++];
} else {
temp[index++] = A[j++];
}
}
while (i <= R1) temp[index++] = A[i++]; //将[L1,R1]中剩余元素直接加入序列C
while (j <= R2) temp[index++] = A[j++]; //将[L2,R2]中剩余元素直接加入序列C
for (i = 0; i < index; i++) {
A[L1 + i] = temp[i]; //将合并后的序列赋值返回序列A
}
}
void mergeSort(int A[], int left, int right) {
if (left < right) {
int mid = (left + right) / 2; //取[left,right]中点
mergeSort(A, left, mid); //递归,对左子区间[left,mid]归并排序
mergeSort(A, mid + 1, right); //递归,对右子区间[mid+1,right]归并排序
merge(A, left, mid, mid + 1, right); //将左子区间和右子区间合并
}
}
4.2 二路归并(非递归)
const int maxn = 100;
void merge(int A[], int L1, int R1, int L2, int R2) {
int i = L1, j = L2; // i指向A[L1],j指向A[L2]
int temp[maxn], index = 0; // temp临时存放合并后的序列,index为其元素个数
while (i <= R1 && j <= R2) {
if (A[i] <= A[j]) {
temp[index++] = A[i++];
} else {
temp[index++] = A[j++];
}
}
while (i <= R1) temp[index++] = A[i++]; //将[L1,R1]中剩余元素直接加入序列C
while (j <= R2) temp[index++] = A[j++]; //将[L2,R2]中剩余元素直接加入序列C
for (i = 0; i < index; i++) {
A[L1 + i] = temp[i]; //将合并后的序列赋值返回序列A
}
}
void mergeSort(int A[], int n) {
// step为组内元素个数,step/2为左子区间元素个数
for (int step = 2; step / 2 <= n; step *= 2) {
//每step个元素一组,组内前step/2和后step/2个元素进行合并
for (int i = 0; i < n; i += step) { //对每一组
int mid = i + step / 2 - 1;
if (mid + 1 <= n) { //右子区间存在元素则合并,注意最后一个右子区间
//左子区间为[i,mid],右子区间为[mid+1,min(i+step-1,n)]
merge(A, i, mid, mid + 1, min(i + step - 1, n));
}
}
}
}
4.3 用sort函数替代merge实现
void mergeSort(int A[], int n) {
// step为组内元素个数,step/2为左子区间元素个数
for (int step = 2; step / 2 <= n; step *= 2) {
//每step个元素一组,组内前step/2和后step/2个元素进行合并
for (int i = 0; i < n; i += step) { //对每一组
sort(A + i, A + min(i + step, n + 1));
}
}
}
5. 快速排序
5.1 总是以A[left]为主元
//区间划分
int partition(int A[], int left, int right) {
int temp = A[left]; //以A[left]为主元
while (left < right) {
while (left < right && A[right] > temp) right--; //反复左移right
A[left] = A[right]; //将A[right]移到A[left]
while (left < right && A[left] < temp) left++; //反复右移left
A[right] = A[left]; //将A[left]移到A[right]
}
A[left] = temp; //把temp放到left和right相遇的地方
return left; //返回相遇的下标
}
//快速排序
void quickSort(int A[], int left, int right) {
if (left < right) { //当前区间的长度大于1
int pos = partition(A, left, right); //划分左右子区间
quickSort(A, left, pos - 1); //对左子区间递归进行快速排序
quickSort(A, pos + 1, right); //对右子区间递归进行快速排序
}
}
5.2 随机主元
int randPartition(int A[], int left, int right) {
//生成[left,right]内的随机数
int p = (int)(round(1.0 * rand() / RAND_MAX * (right - left) + left));
swap(A[p], A[left]); //交换A[p]和A[left]
int temp = A[left];
while (left < right) {
while (left < right && A[right] > temp) right--; //反复左移right
A[left] = A[right]; //将A[right]移到A[left]
while (left < right && A[left] < temp) left++; //反复右移left
A[right] = A[left]; //将A[left]移到A[right]
}
A[left] = temp; //把temp放到left和right相遇的地方
return left; //返回相遇的下标
}
void quickSort(int A[], int left, int right) {
if (left < right) { //当前区间的长度大于1
int pos = randPartition(A, left, right); //划分左右子区间
quickSort(A, left, pos - 1); //对左子区间递归进行快速排序
quickSort(A, pos + 1, right); //对右子区间递归进行快速排序
}
}