算法基础课
零.学习方法
(一)理解思想
(二)理解默写代码模板
1.背诵方法
(1)先看主要思想,在背过(不是一个字母一个字母背,而是能把代码写出来并调试通过即可),再找到对应题目练习
(2)提高熟练度:一个题目写完,写完后删掉,再重新写一遍,重复3-5次
一.基础算法
(一)排序算法
1.快排(不稳定)(O(nlogn))
Tips:让快排稳定方法:可以把元素ai变成双关键字<ai,i>
(1)原理:基于分治
①确定分界点:
左边界q[l]或右边界q[r]或中间界q[(l+r)/2]或随机确定
将区间一分为二,调整后使得第一个区间中数都<=x,第二个区间中数都>=x,注意分界点上数不一定为x
(2)例子解释:
最终实现得第一个区间中数都<=x,第二个区间中数都>=x
(3)模板:
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int n;
int q[N];
// 快速排序
void quick_sort(int q[],int l,int r) {
if(l >= r) return; // 判边界只有一个数时返回空
int x = q[l + r >> 1], i = l - 1, j = r + 1; // 取分界点x,两头指针i,j
while(i < j) { // 循环迭代,只要i,j未相遇,i往右走,j往左走
do i++; while(q[i] < x);
do j--; while(q[j] > x);
if(i < j) swap(q[i],q[j]);
// 若所用语言没有swap
// {
// int t = q[i];
// q[i] = q[j];
// q[j] = t;
// }
}
/*这种情况只能int x = q[l]去左边界,若int x = q[r]取右边界也会造成无限递归死循环
*/
quick_sort(q,l,j); // 左边界排序
quick_sort(q,j + 1,r); // 右边界排序
/*此处换成i也可以,但要注意对称
且上面int x = q[l]处要改为int x = q[(l + r +1) / 2]
(原因是若保持int x = q[l]会造成无限递归死循环)
quick_sort(q,l,i - 1); // 左边界排序
quick_sort(q,i,r); // 右边界排序
*/
}
int main() {
scanf("%d",&n);
for(int i = 0; i < n; i++) {
scanf("%d",&q[i]);
}
quick_sort(q, 0, n - 1);
for(int i = 0; i < n; i++) {
printf("%d ",q[i]);
}
return 0;
}
4.区别:快速选择算法(O(n))
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int n,k;
int q[N];
int quick_sort(int l, int r, int k) {
if(l >= r) return q[l];
int x = q[(l + r) >> 1],i = l - 1, j = r + 1;
while(i < j){
do i++;while(q[i] < x);
do j--;while(q[j] > x);
if(i < j) swap(q[i],q[j]);
}
int sl = j - l + 1;
if(k <= sl) {
return quick_sort(l,j,k);
}
return quick_sort(j + 1,r,k - sl);
}
int main() {
scanf("%d%d",&n,&k);
for(int i = 0; i < n; i++) {
scanf("%d",&q[i]);
}
cout<<quick_sort(0, n - 1, k);
}
2.归并排序(稳定)(O(nlogn))--基于分治
(1)原理:基于分治
①确定分界点:mid=(l+r)/2
②先递归排序左边和右边
③归并:把两个有序的数组合并为一个有序数组(O(n))
(2)例子解释
注:排序算法中稳定指原序列中两数值相同,排完序后位置不变-->稳定
(3)模板
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int n;
int q[N], temp[N];
// 归并排序
void merge_sort(int q[],int l,int r) {
if(l >= r) return;
int mid = l + r >> 1; // >>相当于/2
merge_sort(q,l,mid),merge_sort(q,mid + 1, r); //递归排序,分别递归左右边
int k = 0,i = l,j = mid + 1;
// 归并
while(i <= mid && j <= r) { // 先在中点两边分别比较并升序
if(q[i] <= q[j]) temp[k++] = q[i++];
else temp[k++] = q[j++];
}
// 防止一边已经排序完成,另一边还没排序完,所以把另一边继续排序
while(i <= mid) temp[k++] = q[i++];
while(j <= r) temp[k++] = q[j++];
// 将排好序的值再赋给q[]
for(i = l, j = 0; i <= r; i++,j++) q[i] = temp[j];
}
int main() {
scanf("%d",&n);
for(int i = 0; i < n; i++){
scanf("%d",&q[i]);
}
merge_sort(q,0,n - 1);
for(int i = 0; i < n; i++){
printf("%d ",q[i]);
}
return 0;
}
4.例题:逆序对数量求解
逆序对:当i < j时,q[i] > q[j]
题目:
输入格式
第一行包含整数n,表示数列的长度。
第二行包含 n 个整数,表示整个数列。
输出格式
输出一个整数,表示逆序对的个数。
数据范围
1≤n≤100000
输入样例:
6
2 3 4 5 6 1
输出样例:
5
思路:
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
typedef long long LL;
int n;
int q[N],temp[N];
LL count;
LL merge_sort(int q[],int l,int r) {
if(l >= r) return 0;
int mid = (l + r) >> 1;
count = merge_sort(q,l,mid) + merge_sort(q,mid + 1,r);
int k = 0, i = l, j = mid + 1;
while(i <= mid && j <= r) {
if(q[i] <= q[j]) temp[k++] = q[i++];
else {
temp[k++] = q[j++];
count += mid - i + 1;
}
}
while(i <= mid) {
temp[k++] = q[i++];
}
while(j <= r) {
temp[k++] = q[j++];
}
for(i = l,j = 0; i <=r; i++,j++) q[i] = temp[j];
return count;
}
int main() {
scanf("%d",&n);
for(int i = 0; i < n; i++) {
scanf("%d",&q[i]);
}
cout<<merge_sort(q, 0, n - 1);
}
(二)二分查找
1 整数二分
有单调性的题目一定可以二分,可以二分的题目不一定需要单调性
单调性是二分的充分不必要条件
二分的本质并不是单调性
(1)原理
mid = l + r >> 1时
①寻找中间值
(2)模板
给定一个按照升序排列的长度为n的整数数组,以及 q 个查询。
对于每个查询,返回一个元素k的起始位置和终止位置(位置从0开始计数)。
如果数组中不存在该元素,则返回“-1 -1”。
输入格式 第一行包含整数n和q,表示数组长度和询问个数。
第二行包含n个整数(均在1~10000范围内),表示完整数组。
接下来q行,每行包含一个整数k,表示一个询问元素。
输出格式 共q行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回“-1 -1”。
数据范围
1≤n≤100000
1≤q≤10000
1≤k≤10000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int n,m,x;
int q[N];
int main() {
scanf("%d%d",&n,&m);
// ★注意:数组进行二分查找前题是已升序排好序了
for(int i = 0; i < n; i ++ ) {
scanf("%d",&q[i]);
}
while(m--) {
scanf("%d",&x);
// ★取左区间方法,取左边界值
int l = 0, r = n - 1;
while(l < r) {
// 定义mid值在左侧,取左区间
int mid = l + r >> 1;
// 判断要查找值x是否在左区间内,并改变边界值
// 在左区间,改变右边界值,不在左区间,改变左边界值,到右区间查找
if(q[mid] >= x) r = mid;
else l = mid + 1;
}
// 如果查找不到x,则退出
if(q[l] != x) cout<<"-1 -1"<<endl;
else {
cout<< l <<" ";
// ★取右区间方法,取右边界值
int l = 0, r = n - 1;
while(l < r ) {
// 定义mid,取右区间
int mid = l + r + 1 >> 1;
// 判断要查找值x是否在右区间内,并改变边界值
// 在右区间,改变左边界值,不在右区间,改变右边界值,到左区间查找
if(q[mid] <= x) l = mid;
else r = mid - 1;
}
cout<< l <<endl;
}
}
return 0;
}
2 浮点数二分
(1)模板
给定一个浮点数n,求它的三次方根。
输入格式 共一行,包含一个浮点数n。
输出格式 共一行,包含一个浮点数,表示问题的解。
注意,结果保留6位小数。
数据范围
−10000≤n≤10000
输入样例:
1000.00
输出样例:
10.000000
#include<iostream>
#include<cmath>
using namespace std;
// ★注意:题目要求保留n位小数,那么N就要取1e-(n+2)
// ★N的类型要视题目而定,浮点数则为double
const double N = 1e-8;
int main() {
double x;
cin>>x;
// l与r分别为题目提供的x的范围
double l = -10000, r = 10000;
while(r - l > N) {
double mid = (l + r) / 2 ;
// 3次方根
if(pow(mid,3) >= x) r = mid;
else l = mid;
}
printf("%lf",l);
return 0;
}
(三)高精度
1.高精度加法
(1)原理
①大整数存储
②运算
2.模板
给定两个正整数,计算它们的和。
输入格式 共两行,每行包含一个整数。
输出格式 共一行,包含所求的和。
数据范围
1≤整数长度≤100000
输入样例:
12
23
输出样例:
35
#include<iostream>
#include<vector>
using namespace std;
const int N = 1e6 + 10;
// C = A + B
vector<int> add(vector<int> &A, vector<int> &B) {
vector<int> C;
int t = 0;
// A + B
for(int i = 0; i < A.size() || i < B.size(); i++) {
if(i < A.size()) t += A[i];
if(i < B.size()) t += B[i];
// 把余数放入C
C.push_back(t % 10);
// 进位保留到下一次继续运算
t /= 10;
}
// 最后一次运算,判断是否进位
if(t) C.push_back(1);
return C;
}
int main() {
// 以字符串形式输入两个数
string a,b;
vector<int> A,B;
cin >> a >> b;
// 倒着各自存放两个数的每位数
for(int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');
for(int i = b.size() - 1; i >= 0; i -- ) B.push_back(b[i] - '0');
// 计算好的结果放入C中
auto C = add(A,B);
for(int i =C.size() - 1; i >= 0; i--) printf("%d",C[i]);
return 0;
}
2.高精度减法
(1)原理
(2)模板
给定两个正整数,计算它们的差,计算结果可能为负数。
输入格式 共两行,每行包含一个整数。
输出格式 共一行,包含所求的差。
数据范围
1≤整数长度≤105
输入样例:
32
11
输出样例:
21
#include<iostream>
#include<vector>
using namespace std;
const int N = 1e6 + 10;
// 判断是否A >= B
bool cmp(vector<int> &A, vector<int> &B) {
// 判断A,B长度来比较A,B谁大
// return A.size() > B.size()是依据判断结果决定返回true/false
if(A.size() != B.size()) return A.size() > B.size();
for(int i = A.size() - 1; i >= 0; i -- ) {
if(A[i] != B[i]) {
// return A[i] > B[i]是依据判断结果决定返回true/false
return A[i] > B[i];
}
return true;
}
}
// C = A - B
vector<int> sub(vector<int> &A, vector<int> &B) {
vector<int> C;
for(int i = 0,t = 0; i <= A.size() - 1; i ++ ) {
// t代表借位
t = A[i] - t;
// 保证B还有时进行减法操作
if(i < B.size()) t -= B[i];
// 把余数放入C,此种情况对t >= 0或t < 0皆成立
C.push_back((t + 10) % 10);
// 此时t表示借位
if(t < 0) t = 1;
else t = 0;
}
// 清除数字前多余的0,类似于0001,把1前的0清掉
while(C.size() > 1 && C.back() == 0) {
C.pop_back();
}
return C;
}
int main() {
// 以字符串形式输入两个数
string a,b;
vector<int> A,B;
cin >> a >> b;
// 倒着各自存放两个数的每位数
for(int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');
for(int i = b.size() - 1; i >= 0; i -- ) B.push_back(b[i] - '0');
// 计算好的结果放入C中
if(cmp(A,B)) {
auto C = sub(A,B);
for(int i =C.size() - 1; i >= 0; i--) printf("%d",C[i]);
}
else {
auto C = sub(B,A);
printf("-");
for(int i =C.size() - 1; i >= 0; i--) printf("%d",C[i]);
}
return 0;
}
3.高精度乘法
(1)原理
(2)模板
#include<iostream>
#include<vector>
using namespace std;
// C = A * B
vector<int> mul(vector<int> &A,int b) {
vector<int> C;
// t为进位值
int t = 0;
for(int i = 0; i < A.size() || t; i ++ ) {
if(i < A.size()) {
t += A[i] * b;
}
C.push_back(t % 10);
t /= 10;
}
// 清除数字前多余的0,类似于0001,把1前的0清掉
while(C.size() > 1 && C.back() == 0) {
C.pop_back();
}
return C;
}
int main() {
string a;
int b;
cin>> a >> b;
vector<int> A;
for(int i = a.size() - 1; i >= 0; i -- ) {
A.push_back(a[i] - '0');
}
auto C = mul(A,b);
for(int i = C.size() - 1; i >= 0; i -- ) {
printf("%d",C[i]);
}
return 0;
}
4.高精度除法
(1)原理
(2)模板
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
// C = A / b
vector<int> div(vector<int> &A,int b,int &r) { // r通过引用传递到main函数
vector<int> C;
r = 0;
for(int i = A.size() - 1; i >= 0; i -- ) {
r = r * 10 + A[i];
// 注意:除法是存储商而非像前面几个余数
C.push_back(r / b);
// r就是商
r %= b;
}
// 因为除法C存储商本应该从前往后,但是为了和前面几种统一,上面没有变,reverse是把它重新反转过来,符合除法的商从前往后排的规律
reverse(C.begin(),C.end());
while(C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
int main() {
string a;
int b;
vector<int> A;
cin >> a >> b;
for(int i = a.size() - 1; i >= 0; i -- ) {
A.push_back(a[i] - '0');
}
int r;
auto C = div(A,b,r);
for(int i = C.size() - 1; i >= 0; i -- ) {
printf("%d",C[i]);
}
cout<<endl;
printf("%d",r);
return 0;
}
(四)前缀与差分
1.前缀和:子序列求和
(1)原理
①一维前缀和
②二维前缀和:子矩阵的和
2.差分:操作序列某一部分加减操作
①一维差分
②二维差分
(2)模板
1.前缀和
①一维前缀和
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int s[N];
int a[N];
int main() {
// ios::sync_with_stdio(false); // 让cin与scanf()异步,提高cin访问速度,用了后速度还是慢于scanf(),但用了之后scanf()无效
int n,m;
scanf("%d%d",&n,&m);
for(int i = 1; i <= n; i ++ ) scanf("%d",&a[i]);
// 前缀和初始化
for(int i = 1; i <= n; i ++ ) s[i] = s[i - 1] + a[i];
while(m -- ) {
int l,r;
scanf("%d%d",&l,&r);
// 区间和计算
printf("%d\n",s[r] - s[l - 1]);
}
return 0;
}
②二维前缀和:子矩阵的和
#include<iostream>
using namespace std;
// 注意N要在题目规定范围内,不要超过
const int N =1000 + 10;
int a[N][N];
int s[N][N];
int n,m,q;
int main() {
scanf("%d%d%d",&n,&m,&q);
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
scanf("%d",&a[i][j]);
// 求前缀和
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
s[i][j] = s[i - 1][j] + s[i][j - 1] -s[i - 1][j - 1] + a[i][j];
while(q -- ) {
int x1,y1,x2,y2;
scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
// 求子矩阵和
printf("%d\n",s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1-1][y1-1]);
}
return 0;
}
2.差分
①一维差分
#include<iostream>
using namespace std;
const int N = 1e6+10;
int n,m;
int a[N],b[N];
// 将序列中[l, r]之间的每个数加上c
int insert(int l,int r,int c) {
b[l] += c;
b[r + 1] -= c;
}
int main() {
scanf("%d%d",&n,&m);
for(int i = 1; i <= n; i++) scanf("%d",&a[i]);
// 把a[i]输入到b[i]
for(int i = 1; i <= n; i++) insert(i,i,a[i]);
while(m--) {
int l,r,c;
scanf("%d%d%d",&l,&r,&c);
insert(l,r,c);
}
// 将b数组变成a数组的前缀和
for(int i = 1; i <= n; i++ ) b[i] += b[i - 1];
for(int i = 1; i <= n; i++) printf("%d ",b[i]);
return 0;
}
②二维差分
#include<iostream>
using namespace std;
const int N = 1010;
int n,m,q;
int a[N][N],b[N][N];
void insert(int x1,int y1,int x2,int y2,int c) {
b[x1][y1] += c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y1] -= c;
b[x2 + 1][y2 + 1] += c;
}
int main() {
scanf("%d%d%d",&n,&m,&q);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
scanf("%d",&a[i][j]);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
insert(i,j,i,j,a[i][j]);
while(q--) {
int x1,y1,x2,y2,c;
scanf("%d%d%d%d%d",&x1,&y1,&x2,&y2,&c);
insert(x1,y1,x2,y2,c);
}
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++) {
a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + b[i][j];
}
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++)
printf("%d ",a[i][j]);
printf("\n");
}
return 0;
}
(五)双指针算法:优化朴素算法时间复杂度位o(n)
1.原理
2.模板
(1)输出含空格单词问题
输入一个字符串把其中每个单词输出来,单词间只有一个空格
// 输入一个字符串把其中每个单词输出来,单词间只有一个空格
#include<iostream>
#include<cstring>
using namespace std;
int main() {
char str[1000];
gets(str);
int n = strlen(str);
for(int i = 0; i < n; i ++ ) {
// 让i指向单词开头,j指向单词间空格处
int j = i;
while(j < n && str[i] != ' ') j++;
// 输出每个单词且每个单词占一行
for(int k = i; k < j; k++) cout<<str[k];
cout<<endl;
// 把i指向第二个单词开头
i = j;
}
return 0;
}
(2)最长连续不重复子序列
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int n;
int a[N];
int s[N];
int main() {
scanf("%d",&n);
for(int i = 0; i < n; i ++ ) {
cin >> a[i];
}
int res = 0;
for(int i = 0,j = 0; i < n; i ++ ) {
// i向后寻找是否有重复元素,若无,继续向前,若有,j向前到i处并把之前存储的元素删除
s[a[i]]++;
while(j <= i && s[a[i]] > 1) { // s[a[i]] > 1判断是否有重复元素,也就是如果有重复元素s[a[i]]==1
s[a[j]]--;
j++;
}
res = max(res,i - j + 1);
}
cout << res;
return 0;
}
(六)位运算
1.原理
(1)lowbit运算:返回x中最后一个1,统计x中1的个数
(2)n >> k & 1:求n的第k位数字
2.模板
(1)lowbit运算:返回x中最后一个1,统计x中1的个数
#include<iostream>
using namespace std;
int lowbit(int x) {
return x & -x;
}
int main() {
int n;
cin >> n;
while(n -- ) {
int x;
cin >> x;
int res = 0;
// 每次减去x的最后一位1
while(x) x -= lowbit(x), res++;
cout << res << " ";
}
return 0;
}
(2)n >> k & 1:求n的第k位数字
返回输入的数的负数的补码:
#include<iostream>
using namespace std;
int main() {
int n = 10;
unsigned int x = -n;
for(int i = 30; i >= 0; i -- ) {
cout << (x >> i & 1) << " ";
}
return 0;
}
(七)离散化(特指整数保序离散化):个数有限但值域很大,有时需要把值域当作下标使用,则需要将值域各个元素分别映射到0....n-1
1.原理
2.模板
(八)区间合并
1.原理
(1)两个区间端点相交->合并为一个区间
(2)一个区间包含另一个区间-->采用较大的那个区间
(3)一个区间与其他区间无交集:无法合并,独成一个区间
2.模板:
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int,int> PII; // 分别存储左右端点
const int N = 1e6 + 10;
int n; // n个区间
vector<PII> segs;
void merge(vector<PII> &segs) {
vector<PII> res;
// 对区间排序
sort(segs.begin(),segs.end());
// 区间为-1e9~1e9,故端点都从-2e9开始
int start = -2e9, end = -2e9;
for(auto seg:segs) {
// 区间之间无交集时
if(end < seg.first) {
if(start != -2e9) {
res.push_back({start,end});
}
start = seg.first,end = seg.second;
}
// 区间之间有交集时
else {
end = max(end,seg.second);
}
}
// 防止区间为空
if(start != -2e9) {
res.push_back({start,end});
}
segs = res;
}
int main() {
cin >> n;
for(int i = 0; i < n; i ++ ) {
int l,r; // 每个区间左右端点
cin >> l >> r;
segs.push_back({l,r}); // 将左右端点存储
}
merge(segs); // 合并区间
cout << segs.size() << endl;
return 0;
}
二.数据结构
(一)链表与邻接表:树与图的存储:用数组模拟链表
1.原理
(1)单链表
单链表-->邻接表-->存储图和树
①插入头结点:算法题中大多数情况都是把新结点插入到头结点
②插入结点到k结点后面
③删除k后面的结点
(2)双链表:优化某些问题
①在第k个点的右边插入一个点
②在第k个点的左边插入一个点
void add(int l[k],x) {
// 给插入的结点赋值
e[idx] = x;
// 让插入节点的右端点连接k节点的右边一个点
r[idx] = r[k];
// 让插入节点的左端点连接k节点
l[idx] = k;
// 让k结点的右边那个点的左端点连接插入节点
l[r[k]] = idx;
// 让k结点的右右端点连接插入节点
r[k] = idx;
}
③删除第k个节点
2.模板
(1)单链表
#include<iostream>
using namespace std;
const int N = 1e6+10;
// 定义
// head头指针
// val[N]存储结点具体值
// next_one[N]存储下一个节点地址
// index存储当前用到哪个结点
int head,val[N],next_one[N],index;
// 初始化
void init() {
head = -1;
index = 0;
}
// 将x插入到头结点
void insert_to_head(int x) {
// 把值存入当前节点
val[index] = x,
// 把当前结点与头结点下一个结点相连
next_one[index] = head,
// 把头结点与当前结点相连
head = index,
// 当前节点往后移动一格
index++;
}
// 将x插入到下标为k的结点后面
void insert(int k, int x) {
// 把值存入当前节点
val[index] = x,
// 把当前结点与第k个结点的下一个结点相连
next_one[index] = next_one[k],
// 把第k个结点的下一个结点与当前结点相连
next_one[k] = index,
// 当前节点往后移动一格
index++;
}
// 将下标为k的点的后面的点删掉
void remove(int k) {
// 用k的下一个结点的下一个结点覆盖k的下一个节点
next_one[k] = next_one[next_one[k]];
}
int main() {
int m;
cin >> m;
init();
int x,k;
while(m --) {
char option;
cin >> option;
if(option == 'H') {
cin >> x;
insert_to_head(x);
} else if(option == 'D') {
cin >> k;
// 鲁棒性:注明k=0时删除头结点
if(!k) head = next_one[head];
// k - 1是因为题目中k是从0开始的
remove(k - 1);
} else {
cin >> k >> x;
insert(k - 1,x);
}
}
for(int i = head; i != -1; i = next_one[i]) {
cout << val[i] << ' ';
}
return 0;
}
(2)双链表
(二)栈与队列:单调队列、单调栈
1.原理
(1)栈的模拟与相关操作
// ****** ********栈
// stake:栈本身,top:栈顶指针:永远指向新进入栈的元素,即栈顶元素
int stake[N], top = 0;
// 入栈
stake[ ++top ] = x;
// 出栈
top -- ;
// 判断栈是否为空
if( top >= 0) not empty
else empty
// 栈顶元素
stake[top];
(2)队列的模拟与相关操作
// *************模拟队列
// seq[N]:模拟队列,队列元素从队尾进入,队首出去
// top队列尾指针:永远指向新进队列元素,即队尾元素
// pop_one:队列首指针:永远指向最先进队列的元素即队首元素
int seq[N], top = -1, pop_one = 0;
// 插入元素
seq[ ++top ] = x;
// 出队列
pop_one++; // 队列尾指针往队尾移动,代表队首元素被释放
// 判断队列是否为空
if(pop_one <= top) not empty
else empty
// 取出队首元素
seq[pop_one];
// 取出队尾元素
seq[top];
(3)单调栈(用的情况较少):,给定序列中每一个数左边离他最近的数在什么地方,降低一个问题的时间复杂度(O(n))
暴力做法
单调栈优化
(4)单调队列(用的情况较少)
思路:先用暴力,在删去无用元素,在考虑有没有单调性,若有单调性,在思考如何优化
2.模板
(1)栈的模拟与相关操作
(2)队列的模拟与相关操作
(3)单调栈(用的情况较少)
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int n;
int stk[N];
int tt = 0;
int main() {
scanf("%d",&n);
for(int i = 0; i < n; i ++ ) {
int x;
scanf("%d",&x);
while(tt && stk[tt] >= x) tt -- ;
if(tt) printf("%d ",stk[tt]);
else printf("-1 ");
stk[ ++ tt ] = x;
}
return 0;
}
(4)单调队列(用的情况较少)
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int n,k;
// a[N]输入的数组,q[N]滑动窗口所在的数组
int a[N],q[N];
int main() {
scanf("%d%d",&n,&k);
for(int i = 0; i < n; i ++ ) scanf("%d",&a[i]);
//定义队头队尾
int front = 0,rear = -1;
// 每个滑动窗口最小值
for(int i = 0; i < n; i ++ ) {
// 判断队头是否划出窗口
while(front <= rear && i - k + 1 > q[front]) front ++ ;
while(front <= rear && a[q[rear]] >= a[i]) rear -- ;
q[ ++ rear] = i;
if(i >= k - 1) printf("%d ", a[q[front]]);
}
puts("");
// 每个滑动窗口最大值
front = 0,rear = -1;
for(int i = 0; i < n; i ++ ) {
// 判断队头是否划出窗口
while(front <= rear && i - k + 1 > q[front]) front ++ ;
while(front <= rear && a[q[rear]] <= a[i]) rear -- ;
q[ ++ rear] = i;
if(i >= k - 1) printf("%d ", a[q[front]]);
}
puts("");
return 0;
}
(三)kmp:除了循环节其余的建议使用字符串哈希法
1.原理
思路:先思考暴力怎么做,在思考如何去优化
暴力做法
next[j]与i是否匹配
(1)next数组构造
(2)kmp匹配过程
2.模板(此版本O(n))
#include<iostream>
using namespace std;
const int N = 100000 + 10, M = 1000000 + 10;
int n,m;
char p[N],s[M];
int next_one[N];
int main() {
cin >> n >> p + 1 >> m >> s + 1;
//求next过程
for(int i = 2, j = 0; i <= n; i ++ ) {
while(j && p[i] != p[j + 1]) j = next_one[j];
if(p[i] == p[j + 1]) j ++ ;
next_one[i] = j;
}
// kmp匹配过程
for(int i = 1, j = 0; i <= m; i ++ ) {
while(j && s[i] != p[j + 1]) j = next_one[j];
if(s[i] == p[j + 1]) j++;
if(j == n) {
printf("%d ", i - n);
j = next_one[j];
}
}
return 0;
}
(四)Trie树:高效的快速存储和查找字符串集合。
类型:全部小写字母/全部大写字母/全部数字,长度不会太长
1.原理
trie树存储
trie树查找
2.模板
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
// 下标为0的点既是根节点也是空结点
// 下标为x的点,x结点所有的儿子存到了son[x]中,son[x][i]就是x的第i个儿子
// count[x]存储以x结尾的单词数量多少个
// index存储当前下标
int son[N][26],count[N],index;
char str[N];
// 插入
void insert(char str[]) {
int p = 0;
for(int i = 0; str[i]; i ++ ) {
int u = str[i] - 'a';
if(!son[p][u]) son[p][u] = ++ index;
p = son[p][u];
}
count[p] ++ ;
}
//查找
int query(char str[]) {
int p = 0;
for(int i = 0; str[i]; i ++ ) {
int u = str[i] - 'a';
if(!son[p][u]) return 0;
p = son[p][u];
}
return count[p];
}
int main() {
int n;
scanf("%d",&n);
while(n--) {
char op[10];
scanf("%s%s",op,str);
if(op[0] == 'I') {
insert(str);
}
else {
printf("%d\n",query(str));
}
}
return 0;
}
(五)并查集:容易考,代码短,思路精巧
功能:快速的处理栈问题,合并两个集合/询问两个元素是否在一个集合当中(近乎O(1)时间完成操作)
1.原理
初版:
优化:路径压缩
2.模板
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int p[N];
int n,m;
// 返回x的祖宗节点,路径压缩法
int find(int x) {
// 利用递归的方法寻找根节点
if(p[x] != x) p[x] = find(p[x]); // p[x] == x =>p[x]为根节点
return p[x];
}
int main() {
scanf("%d%d",&n,&m);
// 初始化:生成集合中数
for(int i = 1; i <= n; i ++ ) p[i] = i;
while(m -- ) {
char op[2]; // op:操作类型
int a,b;
scanf("%s%d%d",op,&a,&b);
if(op[0] == 'M') {
// 把a连接到b上,所以所有连通块都在b上
p[find(a)] = find(b); // 合并a,b集合
} else {
// 查询a,b是否在同一集合中
if(find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
(六)堆(一般O(nlogn),特殊操作可以O(n))
1.原理
功能
注意:需要删除修改堆中任意一个元素需要映射操作(见下面模拟堆)
小根堆
小根堆存储方式
时间复杂度O(n)方法
2.模板
(1)堆排序
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int heap[N],length;
int n,m;
// 往下走(较大的值)
void down(int u) {
int t = u; // u为根节点,放入t中,t表示三个点中最小值
if(u * 2 <= length && heap[u * 2] < heap[t]) t = u * 2; // 左儿子存在且左儿子的值小于根节点,最小值变为左儿子
if(u * 2 + 1 <= length && heap[u * 2 + 1] < heap[t]) t = u * 2 + 1; // 右儿子存在且右儿子的值小于根节点,最小值变为右儿子
if(u != t) { // 若根节点不为最小值,则交换根节点与最小值使得根节点变为最小值
swap(heap[u],heap[t]);
down(t);
}
}
/*// 向上走(较小的值)
void up(int u) {
// 如果父亲结点比当前结点大,交换二者位置
while(u / 2 && heap[u / 2] > heap[u]) {
swap[heap[u / 2],heap[u]];
u /= 2;
}
}
*/
int main() {
scanf("%d%d",&n,&m);
// 构造二叉树
for(int i = 1; i <= n; i ++ ) scanf("%d",&heap[i]);
length = n;
// 优化步骤,时间复杂度由O(nlogn)变为O(n),从n/2开始是因为最下面一列的n/2个不可能在down,所以上面的n/2个才能down
for(int i = n / 2; i; i -- ) down(i);
while(m -- ) {
printf("%d ",heap[1]);
// 删除最小值
heap[1] = heap[length];
length -- ;
down(1);
}
return 0;
}
(2)模拟堆:需要删除修改堆中任意一个元素,需要映射操作
原理
题目
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int N = 1e5 + 10;
int heap[N],length;
int n,m;
// hp[N]从堆到下标的映射,ph[N]从下标映射到堆
int hp[N],ph[N];
void heap_swap(int a,int b) {
// 相互交换堆与下标的映射关系,变为交叉对称关系
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a],hp[b]);
// 交换a,b两点
swap(heap[a],heap[b]);
}
// 往下走(较大的值)
void down(int u) {
int t = u; // u为根节点,放入t中,t表示三个点中最小值
if(u * 2 <= length && heap[u * 2] < heap[t]) t = u * 2; // 左儿子存在且左儿子的值小于根节点,最小值变为左儿子
if(u * 2 + 1 <= length && heap[u * 2 + 1] < heap[t]) t = u * 2 + 1; // 右儿子存在且右儿子的值小于根节点,最小值变为右儿子
if(u != t) { // 若根节点不为最小值,则交换根节点与最小值使得根节点变为最小值
heap_swap(u,t);
down(t);
}
}
// 向上走(较小的值)
void up(int u) {
// 如果父亲结点比当前结点大,交换二者位置
while(u / 2 && heap[u / 2] > heap[u]) {
heap_swap(u / 2, u);
u /= 2;
}
}
int main() {
scanf("%d",&n);
while(n -- ) {
char op[10];
int k,x;
scanf("%s",op);
if(!strcmp(op,"I")) {
scanf("%d",&x);
length ++;
m ++ ;
ph[m] = length, hp[length] = m;
heap[length] = x;
up(length);
}
else if(!strcmp(op,"PM")) printf("%d\n", heap[1]);
else if(!strcmp(op,"DM")) {
heap_swap(1,length);
length --;
down(1);
}
else if(!strcmp(op,"D")) {
scanf("%d",&k);
k = ph[k];
heap_swap(k,length);
length -- ;
down(k),up(k);
}
else {
scanf("%d%d",&k,&x);
k = ph[k];
heap[k] = x;
down(k),up(k);
}
}
return 0;
}
(七)Hash表:将-1e9~1e9的数映射到0~1e5/1e6的序列中
1.原理
拉链法
开放寻址法
2.模板
(1)模拟散列表
拉链法
#include<iostream>
#include<cstring>
using namespace std;
// 开放寻址法
/*
// 求质数方法
for(int i = 200000; ;i ++ ) {
bool flag = true;
for(int j = 2; j * j <= i; j ++ ) {
if(i % j == 0) {
flag = false;
break;
}
}
if(flag) {
cout << i << endl;
break;
}
}
*/
// 开放寻址法N需要2倍于链地址法再求质数
const int N =200003, null = 0x3f3f3f3f; // null为<-1e9,>1e9的地址,防止与内部值冲突
int h[N];
int find(int x) {
int k = (x % N + N) % N;
//找的位置一定可以找到
while(h[k] != null && h[k] != x) { // k位置不为空并且k位置中的值不是要查找的x
k ++ ; // 找下一个位置
if(k == N) k = 0; // 如果找到最后了,返回第一个继续找
}
return k; // 找到了,k为x所在下标值,没找到,k就是x在哈希表中存储的位置
}
/*链地址法
// 1e5 + 3是因为1e5 + 3是1e5这个等级的最小质数,散列表的N最好取同等级最小质数
// 求质数方法
for(int i = 100000; ;i ++ ) {
bool flag = true;
for(int j = 2; j * j <= i; j ++ ) {
if(i % j == 0) {
flag = false;
break;
}
}
if(flag) {
cout << i << endl;
break;
}
}
const int N = 1e5 + 3;
// h[k]存放链表第一个结点的下标,e[i]当前结点的值为多少,下一个点的下标ne[i],
int h[N],e[N],ne[N],idx;
void insert(int x) {
int k = (x % N + N) %N; //除留余数法,+N目的是把负数转换为正数
// 单链表插入
e[idx] = x;
ne[idx] = h[k];
h[k] = idx ++ ;
}
bool find(int x) {
int k = (x % N + N) % N;
// 当前这个点的下标变到下一个点的坐标i = ne[i],空指针下标-1时停止
for(int i = h[k]; i != -1; i = ne[i]) {
if(e[i] == x) {
return true;
}
}
return false;
}
*/
int main() {
int n;
scanf("%d",&n);
int x;
// 开放地址法
memset(h,0x3f,sizeof h); //死记吧,按字节memset,一个字节8位
/*// 链地址法
memset(h,-1,sizeof h);
*/
while(n -- ) {
char op[10];
scanf("%s%d",op,&x);
//开放寻址法
/*// 优化代码
int k = find(x);
if(op[0] == 'I') {
h[k] = x; //把x插入到k处
}
else {
if(h[k] != null) puts("Yes");
else puts("No");
}
*/
if(op[0] == 'I') {
int k = find(x);
h[k] = x; //把x插入到k处
}
else {
int k = find(x);
if(h[k] != null) puts("Yes");
else puts("No");
}
/*链地址法
if(op[0] == 'I') insert(x);
else {
if(find(x)) puts("Yes");
else puts("No");
}
*/
}
return 0;
}
(2)字符串前缀哈希法:★★★处理字符串利器,可以解决很多KMP无法解决的问题,快速判断两个字符串是否相等,除了循环节外,其余建议使用该种方法
原理:把任一字符串映射成从0-Q-1之间的数,核心:用k进制角度把字符串看成数字
题目
#include<iostream>
using namespace std;
typedef unsigned long long ULL; //由于前缀值的值会很大 所以应该将数组中的数据定义为ULL型
// 核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
// 小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
const int N = 1e5 + 10;
const int P = 13331; //P为权重
//131为经验值 即P=131或13331时 哈希冲突的可能性最小
int n,m;
char str[N];
ULL h[N]; //h[]存放字符串的前缀值
ULL p[N]; //p[]存放各个位数的相应权值
ULL get(int l, int r) {
return h[r] - h[l - 1] * p[r- l + 1]; // 这步其实是将h[l-1]左移
//其目的事实上是为了将h[l-1]的高位与h[r]相对齐从而才可以未完成计算
}
int main() {
scanf("%d%d%s",&n,&m,str + 1);
p[0] = 1; //注意这步千万不要忘了 最开始的权值必须赋值为1 否则接下来就会出错
for(int i = 1; i <= n; i ++) {
p[i] = p[i - 1] * P; //计算每个位上的相应权值
h[i] = h[i - 1] * P + str[i]; //计算字符串前缀值
//最新加入的数的权值为p的0次 所以直接加上str[i]即可
}
while(m -- ) {
int l1,r1,l2,r2;
scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
if(get(l1,r1) == get(l2,r2)) puts("Yes");
else puts("No");
}
return 0;
}
(八)C++ STL使用技巧
1.原理
vector
pair
string
string方法
queue
队列方法
清空队列
priority_queue:默认大根堆
大根堆方法
小根堆的构造
stack
栈方法
deque:双端队列(用的不是太多,效率太低)
双端队列方法
set/map/multimap,multiset
bitset:省8倍空间
2.模板