学习笔记,仅供参考。
如有错误,欢迎探讨。
To Me
1.例题说明
1.整篇笔记的例题都是AcWing上的。
2.例题模块的第一行为例题题号和名称,随后的注释为题目内容,再下面是正确的可以运行的代码。
2.模板说明
1.y总给的模板,或者有我自己的一些修改,但是是正确的。
2.都是伪代码。
3.做题细节总结
1.2分中求mid的时候要用mid = l + (r - l) / 2来做。
2.当输入字符的时候可以用字符数组来代替,因为输入有的时候会有空格,而%c严格读入一个字符,所以有可能把空格读入,导致输入结果不对。%s可以过滤掉空格和回车。
3.memset是以字节赋值的,取数的后八位二进制数,然后把数组里所有数的每个字节都赋值成这样。一般为0或-1。
4.让cin和scanf一样快的方法
cin.tie(0);
ios::sync_with_stdio(false);
5.INT_MAIX一般为0x3f3f3f3f。
一、基础算法
排序
1.快速排序——分治
1.找分界点,一般为数组最左边,最右边,中间,或者随机点。
2.使分界点左边的数都小于等于分界点,右边的数都大于等于分界点。
3.递归
模板:
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;
int i = l - 1, j = r + 1, x = q[l];
while (i < j)
{
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
else break;
}
quick_sort(q, l, j), quick_sort(q, j + 1, r);
}
例题:
785 快速排序
//给定你一个长度为 n 的整数数列。
//请你使用快速排序对这个数列按照从小到大进行排序。
//并将排好序的数列按顺序输出。
#include<iostream>
using namespace std;
int n;
const int N = 1e6 + 10;
int q[N];
void quick_sort(int l, int r){
if(l >= r) return;
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]);
}
quick_sort(l, j);
quick_sort(j+1, r);
}
int main(){
scanf("%d", &n);
for(int i = 0; i < n; i++){
scanf("%d", &q[i]);
}
quick_sort(0, n-1);
for(int i = 0; i < n; i++){
printf("%d ", q[i]);
}
return 0;
}
2.归并排序——分治
1.找分界点,为数组中间的那个点。
2.递归排序:L->mid,mid+1->R
3.归并——合二为一。
模板:
void merge_sort(int q[], int l, int r)
{
if (l >= r) return;
int mid = l + r >> 1;
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]) tmp[k ++ ] = q[i ++ ];
else tmp[k ++ ] = q[j ++ ];
while (i <= mid) tmp[k ++ ] = q[i ++ ];
while (j <= r) tmp[k ++ ] = q[j ++ ];
for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}
例题:
787 归并排序
//给定你一个长度为 n 的整数数列。
//请你使用归并排序对这个数列按照从小到大进行排序。
//并将排好序的数列按顺序输出。
#include<iostream>
using namespace std;
int n;
const int N = 1e6 + 10;
int q[N], temp[N]
void merge_sort(int l, int r){
if(l >= r) return;
int mid = l + r >> 1;
merge_sort(l, mid);
merge_sort(mid + 1, r);
int i = l, j = mid + 1;
int k = 0;
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++];
for(int 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(0, n-1);
for(int i = 0; i < n; i++){
printf("%d ", q[i]);
}
}
二分法
1.整数二分
基础的mid写法为:mid = (l + r) / 2
但这种写法当l和r特别大的时候mid会得到一个负数,所以求mid时一般写成:
mid = l + (r - l) / 2,上取整为mid = l + (r - l + 1) / 2
模板1:
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
模板2:
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
例题:
789 数的范围
//给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。
//对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。
//如果数组中不存在该元素,则返回 -1 -1。
#include<iostream>
using namespace std;
int n, q;
const int N = 1e6 + 10;
int arr[N];
int main(){
cin>>n>>q;
for(int i = 0; i < n; i++){
cin>>arr[i];
}
while(q--){
int k;
cin>>k;
int l = 0, r = n - 1;
while(l < r){
int mid = l + (r - l) / 2;
if(arr[mid] >= k) r = mid;
else l = mid + 1;
}
if(k != arr[l]) cout<<"-1 -1"<<endl;
else{
cout<<l<<" ";
int l = 0, r = n - 1;
while(l < r){
int mid = l + (r - l + 1) / 2;
if(arr[mid] <= k) l = mid;
else r = mid - 1;
}
cout<<l<<endl;
}
}
return 0;
}
2.实数(浮点数)二分
一般eps取比题目要求精度多乘0.01就行
模板:
double bsearch_3(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
例题:
790 数的三次方根
//给定一个浮点数 n,求它的三次方根。
#include<bits/stdc++.h>
using namespace std;
int main(){
double n;
cin>>n;
double l = -10000, r = 10000;
while(r - l > 1e-8){
double mid = l + (r - l) / 2;
if(mid*mid*mid >= n) r = mid;
else l = mid;
}
printf("%lf", l);
return 0;
}
高精度
1.高精度加法
1.用vector去储存两个数,把两个数定义成String,然后拆分每一位给vector。
2.由于在vector后添加数据更为简单,所以在存储时将数的低位放到数组前面。
模板:
vector<int> add(vector<int> &A, vector<int> &B){
vector<int> C;
int temp = 0; //进位数
for(int i = 0; i < A.size() || i < B.size(); i++){ //只要A,B长度有一个没加完,就继续加
if(i < A.size()) temp += A[i];
if(i < B.size()) temp += B[i];
C.push_back(temp % 10); //要用push_back,不能数组用数组下标赋值
temp /= 10;
}
if(temp) C.push_back(temp); //最后一个进位肯定为1
return C;
}
例题:
791 高精度加法
//给定两个正整数(不含前导 0),计算它们的和。
#include<iostream>
#include<vector>
using namespace std;
vector<int> add(vector<int> &A, vector<int> &B){
vector<int> C;
int temp = 0; //进位数
for(int i = 0; i < A.size() || i < B.size(); i++){ //只要A,B长度有一个没加完,就继续加
if(i < A.size()) temp += A[i];
if(i < B.size()) temp += B[i];
C.push_back(temp % 10); //要用push_back,不能数组用数组下标赋值
temp /= 10;
}
if(temp) C.push_back(temp); //最后一个进位肯定为1
return C;
}
int main(){
string a, b;
cin>>a>>b;
vector<int> 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');
auto C = add(A, B);
for(int i = C.size() - 1; i >= 0; i--) cout<<C[i];
return 0;
}
2.高精度减法
默认给的两个整数都是正整数。
A, B, t(下一位找本位借位的数)
t = 0表示没有借位,t = 1表示借位了
t也表示当前位的值。
1.先判断A和B谁大,如果A - B >= 0,那就是A - B;如果A - B < 0,那就是B - A。
2.如果A - B - t >= 0,那结果就是A - B - t;如果A - B - t < 0,那结果就是A - B - t + 10。
模板:
// 判断A和B谁大
bool cmp(vector<int> &A, vector<int> &B){
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];
}
return true;
}
// 高精度减法
// C = A - B, 满足A >= B, A >= 0, B >= 0
vector<int> sub(vector<int> &A, vector<int> &B)
{
vector<int> C;
for (int i = 0, t = 0; i < A.size(); i ++ )
{
t = A[i] - t;
if (i < B.size()) t -= B[i];
C.push_back((t + 10) % 10);
if (t < 0) t = 1;
else t = 0;
}
while (C.size() > 1 && C.back() == 0) C.pop_back();// 去掉前导0
return C;
}
例题:
792 高精度减法
//给定两个正整数(不含前导 0),计算它们的差,计算结果可能为负数。
#include<iostream>
#include<vector>
using namespace std;
bool cmp(vector<int> &A, vector<int> &B){
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];
}
return true;
}
vector<int> sub(vector<int> &A, vector<int> &B){
vector<int> C;
for(int i = 0, t = 0; i < A.size(); i++){
t = A[i] - t;
if(i < B.size()) t -= B[i];
C.push_back((t + 10) % 10);
if(t < 0) t = 1;
else t = 0;
}
while(C.size() != 1 && C.back() == 0) C.pop_back();
return C;
}
int main(){
string a, b;
cin>>a>>b;
vector<int> 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');
if(cmp(A, B)){
auto C = sub(A, B);
for(int i = C.size() - 1; i >= 0; i--) cout<<C[i];
}else{
auto C = sub(B, A);
cout<<"-";
for(int i = C.size() - 1; i >= 0; i--) cout<<C[i];
}
return 0;
}
3.高精度乘法
高精度*低精度
1.用A的每一位去乘以b,得出来的数%10是当前位的值,/10是进位的值,用一个t既代表了进位,又代表了当前的值,每一轮循环最后再更新t的值,t /= 10。
模板:
// 高精度乘低精度
// C = A * b, A >= 0, b > 0
vector<int> mul(vector<int> &A, int b)
{
vector<int> C;
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;
}
while (C.size() > 1 && C.back() == 0) C.pop_back(); //处理前导0
return C;
}
例题:
793 高精度乘法
//给定两个非负整数(不含前导 0) A 和 B,请你计算 A×B 的值。
#include<iostream>
#include<vector>
using namespace std;
vector<int> mul(vector<int> &A, int b){
vector<int> C;
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;
}
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--) cout<<C[i];
return 0;
}
4.高精度除法
高精度/低精度
1.除法与其它三种算法不同,它不需要倒序存储,因为除法是从高位开始除的,但是为了保持和那三种算法的一致,我们还是用倒序存储高精度的数。
2.在处理商C的时候,需要将其反转一下,因为这样可以更好的消去它的前导0,并且不会在主函数输出的时候写法和前三种不同。
模板:
// 高精度除以低精度
// A / b = C ... r, A >= 0, b > 0
vector<int> div(vector<int> &A, int b, int &r)
{
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 %= b;
}
reverse(C.begin(), C.end());
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
例题:
794 高精度除法
//给定两个非负整数(不含前导 0) A,B,请你计算 A/B 的商和余数。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<int> div(vector<int> &A, int b, int &r){
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 %= b;
}
reverse(C.begin(), C.end());
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');
int r;
auto C = div(A, b, r);
for(int i = C.size() - 1; i >= 0; i--) cout<<C[i];
cout<<endl<<r;
return 0;
}
前缀和与拆分
1.一维前缀和
模板:
// 一维前缀和
// S[i] = a[1] + a[2] + ... a[i]
// a[l] + ... + a[r] = S[r] - S[l - 1]
例题:
795 前缀和
// 输入一个长度为 n 的整数序列。
//接下来再输入 m 个询问,每个询问输入一对 l,r。
//对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int a[N], S[N];
int main(){
int n, m;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]); // 这里都从下标1开始,因为题目给的l,r都是第几个数,而不是数组下标
for(int i = 1; i <= n; i++) S[i] = S[i - 1] + a[i];
while(m--){
int l, r;
scanf("%d%d", &l, &r);
cout<<S[r] - S[l - 1]<<endl;
}
return 0;
}
2.二维前缀和
模板:
// 二维前缀和
// S[i, j] = 第i行j列格子左上部分所有元素的和
// 以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为 S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
例题:
796 子矩阵的和
//输入一个 n 行 m 列的整数矩阵,再输入 q 个询问,每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角坐标。
//对于每个询问输出子矩阵中所有数的和。
#include<iostream>
using namespace std;
const int N = 1e3 + 10;
int a[N][N], 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);
cout<<S[x2][y2] - S[x2][y1 - 1] - S[x1 - 1][y2] + S[x1 - 1][y1 - 1]<<endl;
}
return 0;
}
3.一维差分
一个思想就是一开始认为a数组和b数组都为0,后面只需要插入一遍就可以了。
to me:看的时候如果看不明白就列一下递推式子看。bn = an - an-1
模板:
// 一维前缀和
// S[i] = a[1] + a[2] + ... a[i]
// a[l] + ... + a[r] = S[r] - S[l - 1]
例题:
797 差分
//输入一个长度为 n 的整数序列。
//接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。
//请你输出进行完所有操作后的序列。
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int a[N], b[N];
void insert(int l, int r, int c){
b[l] += c; // 下标为l的差分数组+c就可以改变其前缀和数组l及以后的数了
b[r + 1] -= c; // 对应的要在区间范围外的数减去c
}
int main(){
cin>>n>>m;
for(int i = 1; i <= n; i++) scanf("%d", &a[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);
}
for(int i = 1; i <= n; i++) b[i] += b[i - 1]; // 这一步其实就是求a数组,即b的前缀和数组
for(int i = 1; i <= n; i++) cout<<b[i]<<" ";
return 0;
}
4.二维差分
参考一位差分和二维前缀和的思想。
模板:
// 二维差分
// 给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
// S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c
例题:
798 差分矩阵
//输入一个 n 行 m 列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1,y1,x2,y2,c,其中 (x1,y1) 和 (x2,y2) 表示一个子矩阵的左上角坐标和右下角坐标。
//每个操作都要将选中的子矩阵中的每个元素的值加上 c。
//请你将进行完所有操作后的矩阵输出。
#include<iostream>
using namespace std;
const int N = 1e3 + 10;
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[x2 + 1][y1] -= c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y2 + 1] += c;
}
int main(){
cin>>n>>m>>q;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
scanf("%d", &a[i][j]);
// 把数组视作0,然后往里面插入数据,做完以后就是一个差分矩阵了
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++)
b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1]; //其实就是求a[i][j]
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++)
printf("%d ", b[i][j]);
cout<<endl;
}
return 0;
}
双指针算法
1.先用暴力解法写出来,然后再去优化。
2.找题的单调性,比如下面的例题800,两个序列都是升序的有序数组,且有唯一解,当i增大时,只有j减小才能保证值不会一直增大而错过x,同时也可以找到内层循环的判别条件。
模板:
// 双指针算法
for (int i = 0, j = 0; i < n; i ++ )
{
while (j < i && check(i, j)) j ++ ;
// 具体问题的逻辑
}
常见问题分类:
(1) 对于一个序列,用两个指针维护一段区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
例题:
799 最长连续不重复子序列
//给定一个长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], S[N];
int n;
int res = 0;
int main(){
cin>>n;
for(int i = 0; i < n; i++) scanf("%d", &a[i]);
for(int i = 0, j = 0; i < n; i++){
S[a[i]]++;
while(j < i && S[a[i]] > 1){
S[a[j]]--;
j++;
}
res = max(res, i - j + 1);
}
cout<<res<<endl;
return 0;
}
800 数组元素的目标和
// 给定两个升序排序的有序数组 A 和 B,以及一个目标值 x。
//数组下标从 0 开始。
//请你求出满足 A[i]+B[j]=x 的数对 (i,j)。
//数据保证有唯一解。
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m, x;
int a[N], b[N];
int main(){
cin>>n>>m>>x;
for(int i = 0; i < n; i++) scanf("%d", &a[i]);
for(int i = 0; i < m; i++) scanf("%d", &b[i]);
int i = 0;
int j = m - 1;
for(i = 0; i < n; i++){
while( j >= 0 && a[i] + b[j] > x){
j--;
}
if(a[i] + b[j] == x) break;
}
cout<<i<<" "<<j;
return 0;
}
位运算
1.lowbit方法 : 计算出来以后,n和-n总是最后一个1位置相同,1往后都是0,1往前全相反,所以&了以后就是1及以后的数,也就知道最后一位1在哪。
2.看不明白的话就写个数的原码和其负数的原码比一下就知道了。
3.负数的原码就是其正数的补码
模板:
//位运算
//求n的第k位数字: n >> k & 1
//返回n的最后一位1:lowbit(n) = n & -n
例题:
801 二进制中1的个数
//给定一个长度为 n 的数列,请你求出数列中每个数的二进制表示中 1 的个数。
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int n;
int lowbit(int x){
return x & -x;
}
int main(){
cin>>n;
for(int i = 0; i < n; i++) cin>>a[i];
int res = 0;
for(int i = 0; i < n; i++){
int x = a[i];
while(x){
x -= lowbit(x);
res++;
}
cout<<res<<" ";
res = 0;
}
return 0;
}
离散化
1.映射以后的数组中可能存在重复的元素,要先去重。
2.用二分法算出x离散后的值。
3.太难了。
模板:
//离散化
vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end()); // 去掉重复元素
// 二分求出x对应的离散化的值
int find(int x)
{
int l = 0, r = alls.size() - 1;
while (l < r)
{
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1;
}
例题:
802 区间和
//假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。
//现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c。
//接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r] 之间的所有数的和。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 3e5 + 10;
int a[N], S[N];
vector<int> alls; //存放的是原数组或者说原坐标轴上的位置,alls每一个下标对应一个具体的位置
vector<PII> add;//存放的是具体位置和要加的数
vector<PII> query;//询问
int find(int x){
int l = 0, r = alls.size() - 1;
while(l < r){
int mid = l + (r - l) / 2;
if(alls[mid] >= x) r = mid;
else l = mid + 1;
}
return l + 1;//这里返回l+1是为了让数组下标从1开始映射
}
int main(){
int n, m;
cin>>n>>m;
for(int i = 0; i < n; i++){
int x,c;
cin>>x>>c;
add.push_back({x, c});
alls.push_back(x);
}
for(int i = 0; i < m; i++){
int l, r;
cin>>l>>r;
query.push_back({l, r});
alls.push_back(l);
alls.push_back(r);
}
//去重
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
//处理插入
for(auto item : add){
int x = find(item.first);
a[x] += item.second;
}
//前缀和数组
for(int i = 1; i <= alls.size(); i++) S[i] = S[i - 1] + a[i];
//处理询问
for (auto item : query)
{
int l = find(item.first), r = find(item.second); //这里调用find是因为去重和排序后,下标发生了一些变化
cout << S[r] - S[l - 1] << endl;
}
return 0;
}
区间合并
合并的时候有三种情况:
1.完全包含
2.有交集
3.无交集
在写代码实现的时候可以就分成有无交集两种情况去判断。在遍历整个区间的时候,把所有有交集的区间合并起来。
模板:
区间合并
// 将所有存在交集的区间合并
void merge(vector<PII> &segs){
vector<PII> res;
sort(segs.begin(), segs.end());//先排序,pair的排序是先第一个数,后第二个数
int st = -2e9, ed = -2e9;//初始区间设置成负的无穷大
for(auto seg : segs)
//如果上一个区间的终点小于新一个区间的起点,那么上个区间已经维护完,现在又有了一个新的区间
if(ed < seg.first){
if(st != -2e9) res.push_back({st, ed});//只要不是初始区间,就加入res
st = seg.first; ed = seg.second;//更新正在维护区间的起点和终点
}else ed = max(ed, seg.second);//如果不是新的区间那么就代表有交集,求二者最远的终点,就是求并集
if(st != -2e9) res.push_back({st, ed});//因为代码缘故,还要把最后一个区间添加进去
segs = res;
}
例题:
803 区间合并
//给定 n 个区间 [li,ri],要求合并所有有交集的区间。
//注意如果在端点处相交,也算有交集。
//输出合并完成后的区间个数。
//例如:[1,3] 和 [2,6] 可以合并为一个区间 [1,6]。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int, int> PII;
int n;
void merge(vector<PII> &segs){
vector<PII> res;
sort(segs.begin(), segs.end());//先排序,pair的排序是先第一个数,后第二个数
int st = -2e9, ed = -2e9;//初始区间设置成负的无穷大
for(auto seg : segs)
//如果上一个区间的终点小于新一个区间的起点,那么上个区间已经维护完,现在又有了一个新的区间
if(ed < seg.first){
if(st != -2e9) res.push_back({st, ed});//只要不是初始区间,就加入res
st = seg.first; ed = seg.second;//更新正在维护区间的起点和终点
}else ed = max(ed, seg.second);//如果不是新的区间那么就代表有交集,求二者最远的终点,就是求并集
if(st != -2e9) res.push_back({st, ed});//因为代码缘故,还要把最后一个区间添加进去
segs = res;
}
int main(){
cin>>n;
vector<PII> segs;
for(int i = 0; i < n; i++){
int l, r;
cin>>l>>r;
segs.push_back({l, r});
}
merge(segs);
cout<<segs.size();
return 0;
}
二、数据结构
链表
1.单链表
模板:
1. 单链表
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 在链表头插入一个数a
void insert(int a)
{
e[idx] = a, ne[idx] = head, head = idx ++ ;
}
// 将头结点删除,需要保证头结点存在
void remove()
{
head = ne[head];
}
例题:
826 单链表
//实现一个单链表,链表初始为空,支持三种操作:
//向链表头插入一个数;
//删除第 k 个插入的数后面的数;
//在第 k 个插入的数后插入一个数。
//现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。
//注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int e[N], ne[N];
int head, idx;
void init(){
head = -1;
idx = 0;
}
// 表头插入一个节点
void add_to_head(int x){
e[idx] = x;
ne[idx] = head;
head = idx++;
}
// 删除k后面的那个节点
void remove(int k){
ne[k] = ne[ne[k]];
}
// 向k后面插入一个数
void add(int k, int x){
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx++;
}
int main(){
init();
int m;
cin>>m;
while(m--){
char op;
int k, x;
cin>>op;
if(op == 'H'){
cin>>x;
add_to_head(x);
}else if(op == 'D'){
cin>>k;
if(!k) head = ne[head];
else remove(k - 1);// 第k个数对应的数组下标应为k - 1
}else{
cin>>k>>x;
add(k - 1, x);// 第k个数对应的数组下标应为k - 1
}
}
for(int i = head; i != -1; i = ne[i]){
cout<<e[i]<<" ";
}
return 0;
}
2.双链表
copy的题解评论:
- 之所以在 “D”, “IL”, “IR” 要用 k+1 的原因是 双链表的起始点是2. 所以,每个插入位置k的真实位置应该为 k-1+2 = k+1 (在单链表中为 k-1)。
- 0, 1 节点的作用是边界。0为左边界,1为右边界。他俩在这里有点类似保留字的作用。正因如此,我们的idx也是从2开始
- 最后遍历输出结果的 for (int i = rn[0]; i != 1; i = rn[i])。从 rn[0] 开始是因为 0 为左边界,而终止条件 i==1是因为1为右边界(如果碰到,说明已经遍历完毕)
模板:
双链表
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;
// 初始化
void init()
{
//0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
}
// 在节点a的右边插入一个数x
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx ++ ;
}
// 删除节点a
void remove(int a)
{
l[r[a]] = l[a];
r[l[a]] = r[a];
}
例题:
827 双链表
//实现一个双链表,双链表初始为空,支持 5 种操作:
//1.在最左侧插入一个数;
//2.在最右侧插入一个数;
//3.将第 k 个插入的数删除;
//4.在第 k 个插入的数左侧插入一个数;
//5.在第 k 个插入的数右侧插入一个数
//现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int e[N], l[N], r[N], idx;
// 初始化
void init(){
r[0] = 1; l[1] = 0;
idx = 2;
}
// 在第k个点的右侧插入一个数
void add(int k, int x){
e[idx] = x;
r[idx] = r[k];
l[idx] = k;
l[r[k]] = idx;
r[k] = idx;
idx++;
}
// 删除第k个点
void remove(int k){
r[l[k]] = r[k];
l[r[k]] = l[k];
}
int main(){
init();
int m;
cin>>m;
while(m--){
string op;
cin>>op;
int k, x;
if(op == "L"){
cin>>x;
add(0, x);
}else if(op == "R"){
cin>>x;
add(l[1], x);
}else if(op == "D"){
cin>>k;
remove(k + 1);
}else if(op == "IL"){
cin>>k>>x;
add(l[k + 1], x);
}else{
cin>>k>>x;
add(k + 1, x);
}
}
for(int i = r[0]; i != 1; i = r[i]) cout<<e[i]<<" ";
return 0;
}
栈与队列
1.栈
模板:
int stk[N], tt = 0;
// 在栈顶插入一个元素,我的栈顶指针tt是从1开始的
void push(int x){
stk[++tt] = x;
}
// 从栈顶退出一个元素
void pop(){
tt--;
}
// 判空
void empty(){
if(tt >= 1) cout<<"NO"<<endl;
else cout<<"YES"<<endl;
}
// 查询栈顶元素
void query(){
cout<<stk[tt]<<endl;
}
例题:
828 模拟栈
//实现一个栈,栈初始为空,支持四种操作:
//push x – 向栈顶插入一个数 x;
//pop – 从栈顶弹出一个数;
//empty – 判断栈是否为空;
//query – 查询栈顶元素。
//现在要对栈进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int stk[N], tt = 0;
// 在栈顶插入一个元素,我的栈顶指针tt是从1开始的
void push(int x){
stk[++tt] = x;
}
// 从栈顶退出一个元素
void pop(){
tt--;
}
// 判空
void empty(){
if(tt >= 1) cout<<"NO"<<endl;
else cout<<"YES"<<endl;
}
// 查询栈顶元素
void query(){
cout<<stk[tt]<<endl;
}
int main(){
int m;
cin>>m;
while(m--){
string op;
cin>>op;
if(op == "push"){
int x;
cin>>x;
push(x);
}else if(op == "pop"){
pop();
}else if(op == "empty"){
empty();
}else{
query();
}
}
return 0;
}
2.队列
模板:
int q[N], hh = 1, tt = 0;// 还是以1这个下标作为队列的一开始指针的位置
// 在队尾插入一个元素
void push(int x){
q[++tt] = x;
}
// 从对头弹出一个元素
void pop(){
hh++;
}
// 判空
void empty(){
if(hh <= tt) cout<<"NO"<<endl;
else cout<<"YES"<<endl;
}
// 查询队头元素
void query(){
cout<<q[hh]<<endl;
}
例题:
829 模拟队列
//实现一个队列,队列初始为空,支持四种操作:
//1.push x – 向队尾插入一个数 x;
//2.pop – 从队头弹出一个数;
//3.empty – 判断队列是否为空;
//4.query – 查询队头元素。
//现在要对队列进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N], hh = 1, tt = 0;// 还是以1这个下标作为队列的一开始指针的位置
// 在队尾插入一个元素
void push(int x){
q[++tt] = x;
}
// 从对头弹出一个元素
void pop(){
hh++;
}
// 判空
void empty(){
if(hh <= tt) cout<<"NO"<<endl;
else cout<<"YES"<<endl;
}
// 查询队头元素
void query(){
cout<<q[hh]<<endl;
}
int main(){
int m;
cin>>m;
while(m--){
string op;
cin>>op;
if(op == "push"){
int x;
cin>>x;
push(x);
}else if(op == "pop"){
pop();
}else if(op == "empty"){
empty();
}else{
query();
}
}
return 0;
}
3.单调栈
1.保持当前栈顶元素小于要插入的元素,如果栈顶元素大于等于要插入的元素,那么就可以弹出栈顶元素。
2.当一直遵守1的规律去操作,那么整个栈就是一个单调递增的栈了。
模板:
单调栈
//常见模型:找出每个数左边离它最近的比它大/小的数
while(n--){
int x;
scanf("%d", &x);
while(tt && stk[tt] >= x) tt--;
if(tt) cout<<stk[tt]<<" ";
else cout<<-1<<" ";
stk[++tt] = x;
}
例题:
830 单调栈
//给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int stk[N], tt = 0;
int main(){
int n;
cin>>n;
while(n--){
int x;
scanf("%d", &x);
while(tt && stk[tt] >= x) tt--; // 当栈不为空,且当前栈顶的数大于等于要插入的数,则把栈顶的数弹出
// 如果栈不为空,则输出栈顶这个数
if(tt) cout<<stk[tt]<<" ";
else cout<<-1<<" ";
// 往栈顶加入这个数
stk[++tt] = x;
}
return 0;
}
4.单调队列
1.先暴力思考怎么做。
2.把没有用的元素删掉。
3.看有没有单调性。
4.有单调性再去优化问题。
模板:
单调队列
//常见模型:找出滑动窗口中的最大值/最小值
int hh = 1, tt = 0;
for(int i = 0; i < n; i++){
if(hh <= tt && i - k + 1 > q[hh]) hh++;// 判断队头是否滑出窗口
while(hh <= tt && a[i] <= a[q[tt]]) tt--;// 判断当前元素是否比队尾元素要小
q[++tt] = i; // 往队尾插入新元素
if(i - k + 1 >= 0) cout<<a[q[hh]]<<' ';// 只有窗口中有k个元素的时候才去输出
}
例题:
解题思路:
最大值和最小值类似,只有判队尾元素大于还是小于不同。
1.先看队首是否滑出窗口。
2.判断当前要入队元素a[i]与队尾元素的大小问题,使队列具有单调性。
3.将当前元素a[i]加入队尾。
4.满足条件输出。
注意:
1.一定要先步骤3,再4,因为新加入的元素有可能是这个窗口的最小值(最大值)。
2.q[N]存的是a数组下标,a[N]是元素值。
154 滑动窗口
//给定一个大小为 n≤10^6 的数组。
//有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
//你只能在窗口中看到 k 个数字。
//每次滑动窗口向右移动一个位置。
//以下是一个例子:
//该数组为 [1 3 -1 -3 5 3 6 7],k 为 3。
//窗口位置 最小值 最大值
//[1 3 -1] -3 5 3 6 7 -1 3
//1 [3 -1 -3] 5 3 6 7 -3 3
//1 3 [-1 -3 5] 3 6 7 -3 5
//1 3 -1 [-3 5 3] 6 7 -3 5
//1 3 -1 -3 [5 3 6] 7 3 6
//1 3 -1 -3 5 [3 6 7] 3 7
//你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int a[N], q[N];// 这题队列里存放的是数组下标,a里面存放的才是值
int main(){
int n, k;
scanf("%d%d", &n, &k);
for(int i = 0; i < n; i++) scanf("%d", &a[i]);
int hh = 1, tt = 0;
for(int i = 0; i < n; i++){
if(hh <= tt && i - k + 1 > q[hh]) hh++;// 判断队头是否滑出窗口
while(hh <= tt && a[i] <= a[q[tt]]) tt--;// 判断当前元素是否比队尾元素要小
q[++tt] = i; // 往队尾插入新元素
if(i - k + 1 >= 0) cout<<a[q[hh]]<<' ';// 只在窗口中有k个元素的时候才去输出
}
cout<<endl;
hh = 1, tt = 0;
for(int i = 0; i < n; i++){
if(hh <= tt && i - k + 1 > q[hh]) hh++;
while(hh <= tt && a[i] >= a[q[tt]]) tt--;// 其它步骤与判最小相同,只有比较队尾元素是不同的,这个需要大于
q[++tt] = i;
if(i - k + 1 >= 0) cout<<a[q[hh]]<<' ';
}
cout<<endl;
return 0;
}
KMP
不太会,等二刷了。
模板:
KMP
求Next数组:
// s[]是模式串,p[]是模板串, n是s的长度,m是p的长度
for (int i = 2, j = 0; i <= m; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++ ;
ne[i] = j;
}
// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == m)
{
j = ne[j];
// 匹配成功后的逻辑
}
}
例题:
831 KMP字符串
//给定一个模式串 S,以及一个模板串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
//模板串 P 在模式串 S 中多次作为子串出现。
//求出模板串 P 在模式串 S 中所有出现的位置的起始下标。
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
const int M = 1e6 + 10;
char s[M], p[N];
int m, n;
int ne[N];
int main(){
cin>>n>>p + 1>>m>>s + 1;
for(int i = 2, j = 0; i <= n; i++){
while(j && p[i] != p[j + 1]) j = ne[j];
if(p[i] == p[j + 1]) j++;
ne[i] = j;
}
for(int i = 1, j = 0; i <= m; i++){
while(j && s[i] != p[j + 1]) j = ne[j];
if(s[i] == p[j + 1]) j++;
if(j == n){
cout<<i - n<<' ';
j = ne[j];
}
}
return 0;
}
Trie树
copy的第一个大佬的题解
Trie树中有个二维数组 son[N][26],表示当前结点的儿子,如果没有的话,可以等于++idx。Trie树本质上是一颗多叉树,对于字母而言最多有26个子结点。所以这个数组包含了两条信息。比如:son[1][0]=2表示1结点的一个值为a的子结点为结点2;如果son[1][0] = 0,则意味着没有值为a子结点。这里的son[N][26]相当于链表中的ne[N]。
模板:
Trie树
int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量
// 插入一个字符串
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] = ++ idx;
p = son[p][u];
}
cnt[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 cnt[p];
}
例题:
835 Trie字符串统计
//维护一个字符串集合,支持两种操作:
//I x 向集合中插入一个字符串 x;
//Q x 询问一个字符串在集合中出现了多少次。
//共有 N 个操作,输入的字符串总长度不超过 105,字符串仅包含小写英文字母。
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int son[N][26], cnt[N], idx;
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] = ++idx;
p = son[p][u];
}
cnt[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 cnt[p];
}
int main(){
int n;
scanf("%d", &n);
while(n--){
char op[2];
scanf("%s%s", op, str);
if(*op == 'I') insert(str);
else printf("%d\n", query(str));
}
return 0;
}
并查集
并查集思想:1.将两个集合合并在一起。2.询问两个元素是否在一个集合中。
解题的基本原理:每个集合用一颗树来表示。树根的编号就是整个集合的编号。每个节点表示它的父节点,p[x]表示x的父节点。
步骤1:如何判断树根:if(p[x] == x)
步骤2:如何求x的集合编号:while(p[x] != x) x = [px];
步骤3:如何合并两个集合:px是x的集合编号,py是y的集合编号,px = y;
朴素并查集,维护size的并查集,维护到祖宗节点距离的并查集
模板:
并查集
(1)朴素并查集:
int p[N]; //存储每个点的祖宗节点
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) p[i] = i;
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
(2)维护size的并查集:
int p[N], size[N];
//p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
size[i] = 1;
}
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
size[b] += size[a];
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
(3)维护到祖宗节点距离的并查集:
int p[N], d[N];
//p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x)
{
int u = find(p[x]);
d[x] += d[p[x]];
p[x] = u;
}
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
d[I] = 0;
}
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量
例题:
836 合并集合
//一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。
//现在要进行 m 个操作,操作共有两种:
//M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
//Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int p[N];// 表示当前节点的父亲节点
int n, m;
int find(int x){
if(p[x] != x) p[x] = find(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];
int a, b;
scanf("%s%d%d", op, &a, &b);
if(*op == 'M') p[find(a)] = find(b);
else{
if(find(a) == find(b)) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
}
return 0;
}
堆
1.存储的数据结构为一维数组,逻辑结构为完全二叉树。
思路:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PaeFF9gr-1660545578268)(…/Study_images/image-20220810134659043.png)]
模板:
堆
// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;
// 交换两个点,及其映射关系
void heap_swap(int a, int b)
{
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}
void down(int u)
{
int t = u;
if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t)
{
heap_swap(u, t);
down(t);
}
}
void up(int u)
{
while (u / 2 && h[u] < h[u / 2])
{
heap_swap(u, u / 2);
u >>= 1;
}
}
// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);
例题:
838 堆排序
//输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int h[N], cnt;
void down(int u)
{
int t = u;
if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t)
{
swap(h[u], h[t]);
down(t);
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
cnt = n;
for (int i = n / 2; i; i -- ) down(i);
while (m -- )
{
printf("%d ", h[1]);
h[1] = h[cnt -- ];
down(1);
}
return 0;
}
哈希表
1.一般哈希
拉链法
1.开一个一维数组存储,每个下标相当于一个头节点,然后指向一个链表,每个节点插入的时候都是插在数组后面。
2.哈希函数的N一般取N及N以后最小的一个质数。
模板:
拉链法
int h[N], e[N], ne[N], idx;
// 向哈希表中插入一个数
void insert(int x)
{
int k = (x % N + N) % N;
e[idx] = x;
ne[idx] = h[k];
h[k] = idx ++ ;
}
// 在哈希表中查询某个数是否存在
bool find(int x)
{
int k = (x % N + N) % N;
for (int i = h[k]; i != -1; i = ne[i])
if (e[i] == x)
return true;
return false;
}
例题:
840 模拟散列表
//维护一个集合,支持如下几种操作:
//I x,插入一个数 x;
//Q x,询问数 x 是否在集合中出现过;
//现在要进行 N 次操作,对于每个询问操作输出对应的结果。
#include<iostream>
#include<cstring>
using namespace std;
const int N =1e5 + 1;
int h[N], e[N], ne[N], idx = 1;
void insert(int x){
int k = ( (x % 100003) + N ) % N;
e[idx] = x;
ne[idx] = h[k];
h[k] = idx++;
}
bool find(int x){
int k = ( (x % 100003) + N) % N;
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);
memset(h, -1, sizeof(h));
while(n--){
char op[2];
int x;
scanf("%s%d", op, &x);
if(*op == 'I'){
insert(x);
}else{
if( find(x) ) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
}
return 0;
}
开放寻址法
1.N一般开要求的2倍到3倍。
2.只用开一个数组,数组里每个值初始化为一个很大的数null,然后再去比较,如果这个位置的数不等于null,那么就代表这个位置有数,插入操作的话就需要按顺序往后面找,查找操作的话看x是否等于这个位置的数,不等于就接着往后,如果这个位置的值等于null的话就为空。
模板:
开放寻址法
int h[N];
// 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
int find(int x)
{
int t = (x % N + N) % N;
while (h[t] != null && h[t] != x)
{
t ++ ;
if (t == N) t = 0;
}
return t;
}
例题:
840 模拟散列表
//维护一个集合,支持如下几种操作:
//I x,插入一个数 x;
//Q x,询问数 x 是否在集合中出现过;
//现在要进行 N 次操作,对于每个询问操作输出对应的结果。
#include<iostream>
#include<cstring>
using namespace std;
const int N = 2e5 + 3;
int h[N], null = 0x3f3f3f3f;
int find(int x){
int k = (x % N + N) % N;
while(h[k] != null && h[k] != x){
k++;
if(k == N) k = 0;
}
return k;
}
int main(){
memset(h, 0x3f, sizeof h);
int n;
scanf("%d", &n);
while(n--){
char op[2];
int x;
scanf("%s%d", op, &x);
int k = find(x);
if(*op == 'I') h[k] = x;
else{
if(h[k] == null) puts("No");
else puts("Yes");
}
}
return 0;
}
2.字符串哈希
841第一个题解:
(字符串哈希) O(n)+O(m)O(n)+O(m)
全称字符串前缀哈希法,把字符串变成一个p进制数字(哈希值),实现不同的字符串映射到不同的数字。
对形如 X1X2X3⋯Xn−1XnX1X2X3⋯Xn−1Xn 的字符串,采用字符的ascii 码乘上 P 的次方来计算哈希值。
映射公式 (X1×Pn−1+X2×Pn−2+⋯+Xn−1×P1+Xn×P0)modQ
前缀和公式 h[i+1]=h[i]×P+s[i]h[i+1]=h[i]×P+s[i] i∈[0,n−1]i∈[0,n−1] h为前缀和数组,s为字符串数组
区间和公式 h[l,r]=h[r]−h[l−1]×Pr−l+1h[l,r]=h[r]−h[l−1]×Pr−l+1
区间和公式的理解: ABCDE 与 ABC 的前三个字符值是一样,只差两位,
乘上 P2P2 把 ABC 变为 ABC00,再用 ABCDE - ABC00 得到 DE 的哈希值。
模板:
字符串哈希
核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
注意: 1.不能映射成0
2.不存在冲突。
typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64
// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
h[i] = h[i - 1] * P + str[i];
p[i] = p[i - 1] * P;
}
// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
例题:
841 字符串哈希
给定一个长度为 n 的字符串,再给定 m 个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1] 和 [l2,r2] 这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
#include <iostream>
using namespace std;
typedef unsigned long long ULL;
const int N = 1e5 + 10, P = 131;
char str[N];
ULL h[N], p[N];
ULL query(int l, int r){
return h[r] - h[l - 1] * p[r - l + 1];
}
int main(){
int n, m;
scanf("%d%d", &n, &m);
scanf("%s", str + 1);
p[0] = 1;//如果为0,那么所有的权重就都为0了
for(int i = 1; i <= n; i++){
p[i] = p[i - 1] * P;// 处理权重的数组
h[i] = h[i - 1] * P + str[i];// 前缀和数组,之前的每一项都乘以P,相当于左移1位,再加上新的一个字符
}
while(m--){
int l1, r1, l2, r2;
scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
if(query(l1, r1) == query(l2, r2)) puts("Yes");
else puts("No");
}
return 0;
}
C++STL
模板:
C++ STL简介
vector, 变长数组,倍增的思想
size() 返回元素个数
empty() 返回是否为空
clear() 清空
front()/back()
push_back()/pop_back()
begin()/end()
[]
支持比较运算,按字典序
pair<int, int>
first, 第一个元素
second, 第二个元素
支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
string,字符串
szie()/length() 返回字符串长度
empty()
clear()
substr(起始下标,(子串长度)) 返回子串
c_str() 返回字符串所在字符数组的起始地址
queue, 队列
size()
empty()
push() 向队尾插入一个元素
front() 返回队头元素
back() 返回队尾元素
pop() 弹出队头元素
priority_queue, 优先队列,默认是大根堆
push() 插入一个元素
top() 返回堆顶元素
pop() 弹出堆顶元素
定义成小根堆的方式:priority_queue<int, vector<int>, greater<int>> q;
stack, 栈
size()
empty()
push() 向栈顶插入一个元素
top() 返回栈顶元素
pop() 弹出栈顶元素
deque, 双端队列
size()
empty()
clear()
front()/back()
push_back()/pop_back()
push_front()/pop_front()
begin()/end()
[]
set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
size()
empty()
clear()
begin()/end()
++, -- 返回前驱和后继,时间复杂度 O(logn)
set/multiset
insert() 插入一个数
find() 查找一个数
count() 返回某一个数的个数
erase()
(1) 输入是一个数x,删除所有x O(k + logn)
(2) 输入一个迭代器,删除这个迭代器
lower_bound()/upper_bound()
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器
map/multimap
insert() 插入的数是一个pair
erase() 输入的参数是pair或者迭代器
find()
[] 时间复杂度是 O(logn)
lower_bound()/upper_bound()
unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
和上面类似,增删改查的时间复杂度是 O(1)
不支持 lower_bound()/upper_bound(), 迭代器的++,--
bitset, 圧位
bitset<10000> s;
~, &, |, ^
>>, <<
==, !=
[]
count() 返回有多少个1
any() 判断是否至少有一个1
none() 判断是否全为0
set() 把所有位置成1
set(k, v) 将第k位变成v
reset() 把所有位变成0
flip() 等价于~
flip(k) 把第k位取反
三、搜索与图论
DFS
1.一直搜到头。
模板:
(1) 深度优先遍历
int dfs(int u)
{
st[u] = true; // st[u] 表示点u已经被遍历过
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) dfs(j);
}
}
例题:
842 排列数字
//给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。
//现在,请你按照字典序将所有的排列方法输出。
#include<iostream>
using namespace std;
const int N = 10;
int path[N];// 记录其中一条路径
bool st[N];// 状态数组,判断某个数是否被用过了
int n;
void dfs(int u){
if(u == n){
for(int i = 0; i < n; i++) printf("%d ", path[i]);
puts("");
return;
}
for(int i = 1; i <= n; i++){
if(!st[i]){
path[u] = i;
st[i] = true;
dfs(u + 1);
st[i] = false;// 状态恢复在每一次深搜以后
}
}
}
int main(){
scanf("%d", &n);
dfs(0);// u要从0开始,因为dfs是先判断u是否是叶子节点,如果从1开始,那么n就永远遍历不到
return 0;
}
BFS
模板:
宽度优先遍历
手写队列的模板
queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);
while (q.size())
{
int t = q.front();
q.pop();
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (!s[j])
{
st[j] = true; // 表示点j已经被遍历过
q.push(j);
}
}
}
例题:
844 走迷宫
//给定一个 n×m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。
//最初,有一个人位于左上角 (1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
//请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。
//数据保证 (1,1) 处和 (n,m) 处的数字为 0,且一定至少存在一条通路。
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 1e2 + 10;
int g[N][N], d[N][N];// d代表走到当前位置已经用了多少步了
int n, m;
int bfs(){
queue<PII> q;
q.push({0, 0});
memset(d, -1, sizeof d);// 初始化为-1,代表没有走到过这里
d[0][0] = 0;// 给起点初始化为0, 代表走到起点用了0步
int dx[4] = {0, 1, 0, -1}, dy[4] = {1, 0, -1, 0};
while(q.size()){
auto t = q.front();
q.pop();
for(int i = 0; i < 4; i++){
int x = t.first + dx[i], y = t.second + dy[i];
if(x >= 0 && x < n && y >= 0 && y < m && d[x][y] == -1 && g[x][y] == 0){
d[x][y] = d[t.first][t.second] + 1;
q.push({x, y});
}
}
}
return d[n - 1][m - 1];
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
scanf("%d", &g[i][j]);
cout<< bfs() << endl;
return 0;
}
树与图的存储
树是一种特殊的图,与图的存储方式相同。
对于无向图中的边ab,存储两条有向边a->b, b->a。
因此我们可以只考虑有向图的存储。
邻接表中,点的存储顺序是没关系的,只要存储每个点能到哪个点就行。
遍历的时候,一层循环其实是遍历某一个顶点能到的所有点。
模板:
树与图的存储
(1) 邻接矩阵:g[a][b] 存储边a->b
(2) 邻接表:
// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx;
// 添加一条边a->b
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
树与图的遍历
深度优先遍历
模板:
深度优先遍历
int dfs(int u)
{
st[u] = true; // st[u] 表示点u已经被遍历过
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) dfs(j);
}
}
例题:
846 树的重心
给定一颗树,树中包含 n 个结点(编号 1∼n)和 n−1 条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N =1e5 + 10, M = 2 * N;
int h[N], e[M], ne[M], idx;
bool st[N];
int n;
int ans = N;
// 在a, b间插入一条边,a是根,b是链表
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 返回以u为根节点的子树中节点的个数
int dfs(int u){
st[u] = true;
int res = 0;// 存储删掉某个节点后,最大的连同子图节点数
int sum = 1;// 存储以u为根的子树的节点数,包括u
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if(!st[j]){
int s = dfs(j);// u的单棵子树的节点数
res = max(res, s);// 最大联通子图节点数
sum += s;// 以j为根的子树的节点数,包括j
}
}
res = max(res, n - sum);
ans = min(ans, res);
return sum;
}
int main(){
scanf("%d", &n);
memset(h, -1, sizeof h);
for(int i = 0; i < n - 1; i++){
int a, b;
cin>>a>>b;
add(a, b), add(b, a);
}
dfs(1);
cout << ans << endl;
return 0;
}
宽度优先遍历
就用bfs的框架。
模板:
宽度优先遍历
queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);
while (q.size())
{
int t = q.front();
q.pop();
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (!s[j])
{
st[j] = true; // 表示点j已经被遍历过
q.push(j);
}
}
}
例题:
847 图中点的层次
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环。
所有边的长度都是 1,点的编号为 1∼n。
请你求出 1 号点到 n 号点的最短距离,如果从 1 号点无法走到 n 号点,输出 −1。
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e5 + 10;
queue<int> q;
int h[N], e[N], ne[N], idx;
int d[N];
int n, m;
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int bfs(){
memset(d, -1, sizeof d);
d[1] = 0;
q.push(1);
while(q.size()){
int t = q.front();
q.pop();
for(int i = h[t]; i != -1; i = ne[i]){
int j = e[i];
if(d[j] == -1){
d[j] = d[t] + 1;
q.push(j);
}
}
}
return d[n];
}
int main(){
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
while(m--){
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
}
cout << bfs() << endl;
return 0;
}
拓扑排序
1.有向无环图一定有拓扑序,有向有环图没有拓扑序。
2.将入度为0的点加入队列,再删去该点指向的所有点;当有新的点入度为0,将该点入队。
3.队列入队的顺序就是拓扑序。
模板:
拓扑排序
bool topsort()
{
int hh = 0, tt = -1;
// d[i] 存储点i的入度
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (-- d[j] == 0)
q[ ++ tt] = j;
}
}
// 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。
return tt == n - 1;
}
例题:
848 有向图的拓扑序列
给定一个 n 个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。
请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1。
若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1e5 + 10;
int h[N], e[N], ne[N], idx;
int n, m;
int q[N];
int d[N];
void add(int x, int y){
e[idx] = y, ne[idx] = h[x], h[x] = idx++;
}
bool topsort(){
int hh = 0, tt = -1;
for(int i = 1; i <= n; i++){
if(!d[i]) q[++tt] = i; // 如果入度为0,则入队
}
while(hh <= tt){
int t = q[hh++];
for(int i = h[t]; i != -1; i = ne[i]){
int j = e[i];
--d[j];// 删除t这个点指向j的边
if(d[j] == 0)// 如果点j入度为0则入队
q[++tt] = j;
}
}
return tt == n - 1;// 如果只有n-1个点就代表没有环,则可以输出
}
int main(){
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 0; i < m; i++){
int x, y;
scanf("%d%d", &x, &y);
add(x, y);
d[y]++;// d数组表示入度,因为是x指向y,所以y的入度加1
}
if(topsort()){
for(int i = 0; i < n; i++) cout << q[i] << ' ';
puts(" ");
}else{
puts("-1");
}
return 0;
}
最短路算法
1.约定n为点数,m为边数,当m和n^2是一个数量级的时候,就说这张图是稠密图;如果m和n是一个数量级的话就说明这张图是稀疏图。
2.单源最短路,没有负权边,且是稠密图,用朴素Dijkstra算法,稀疏图用堆优化Dijkstra算法。
3.单源最短路,有负权边,一般用spfa算法;如果求不超过k条边的题,用bellman—ford算法.
4.多源最短路,Floyd算法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rvRUAy2u-1662635838719)(…/Study_images/image-20220823181141554.png)]
1.朴素Dijkstra算法
1.循环n次,在未确定最短路的点中找出距离起点最短的点,让它确定,然后再用这个点去更新其它还未确定的点到起点的最短距离。
2.Dijkstra算法不能被用于有负权边的情况,是因为这个算法是基于贪心的一种算法,每次都会遍历当前点的最小距离的点,而如果另外一条路径上,后面的点有负权边,那么就有可能使其不是最短路。
模板:
朴素dijkstra算法
int g[N][N]; // 存储每条边
int dist[N]; // 存储1号点到每个点的最短距离
bool st[N]; // 存储每个点的最短路是否已经确定
// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < n - 1; i ++ )
{
int t = -1; // 在还未确定最短路的点中,寻找距离最小的点
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
// 用t更新其他点的距离
for (int j = 1; j <= n; j ++ )
dist[j] = min(dist[j], dist[t] + g[t][j]);
st[t] = true;
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
例题:
849 Dijkstra求最短路 I
//给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
//请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510;
int g[N][N];// 稠密图,用邻接矩阵表示
int dis[N];// 代表每个点到起点的最短距离
bool st[N];// 代表当前这个点是否已经有了最小值
int n, m;
int dijkstra(){
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;// 将起点初始化为0,其它点为正无穷
for(int i = 0; i < n; i++){// 一共n个点,要迭代n轮
int t = -1;// 用t来存储当前点
for(int j = 1; j <= n; j++){
if(!st[j] && (t == -1 || dis[t] > dis[j]))// 从还未确定最短路的点中,找出来距离最小的点
t = j;
}
st[t] = true;
for(int j = 1; j <= n; j++)// 用t来更新其它还未确定最短路的点的距离
if(!st[j])
dis[j] = min(dis[j], dis[t] + g[t][j]);
}
if(dis[n] == 0x3f3f3f3f) return -1;
return dis[n];
}
int main(){
memset(g, 0x3f, sizeof g);
scanf("%d%d", &n, &m);
while(m--){
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
g[x][y] = min(g[x][y], z);// 有重边的可能性,要选出最短的那条边
}
cout << dijkstra() << endl;
return 0;
}
2.堆优化Dijkstra算法
用优先队列来维护,本质上是小根堆。
这道题相对于朴素版算法优化的原理理解的不好。
模板:
堆优化版dijkstra
typedef pair<int, int> PII;
int n; // 点的数量
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N]; // 存储所有点到1号点的距离
bool st[N]; // 存储每个点的最短距离是否已确定
// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1}); // first存储距离,second存储节点编号
while (heap.size())
{
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > distance + w[i])
{
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
例题:
850 Dijkstra求最短路 II
//给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
//请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
typedef pair<int, int> PII;
const int N = 150010;
int h[N], e[N], ne[N], idx;
int w[N];
int dis[N];// 存储每个点到起点的最短距离
bool st[N];
int n, m;
// 邻接表插入
void add(int x, int y, int z){
w[idx] = z;
e[idx] = y;
ne[idx] = h[x];
h[x] = idx++;
}
int dijkstra(){
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;// 起点的距离为0
priority_queue<PII, vector<PII>, greater<PII>> heap;// 用优先队列存储,第一个点存储距离,第二点存储时哪个点,这个代码来看的话不需要用到距离的点,但是不这么写会wa
heap.push({0, 1});
while(heap.size()){
auto t = heap.top();
heap.pop();
int ver = t.second;
if(st[ver]) continue;
st[ver] = true;
// 用当前距离最短点更新所有它能到的点
for(int i = h[ver]; i != -1; i = ne[i]){
int j = e[i];
if(dis[j] > dis[ver] + w[i]){
dis[j] = dis[ver] + w[i];
heap.push({dis[j], j});
}
}
}
if(dis[n] == 0x3f3f3f3f) return -1;
return dis[n];
}
int main(){
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
while(m--){
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z);
}
cout<< dijkstra() << endl;
return 0;
}
3.bellman-ford算法
n为点数,m为边数,k < m
1.路径里如果存在负环,那么最短路就不一定存在了,因为有可能会一直减权重,直到负无穷。
2.那么存在负环但最短路还是存在的情况也是有的,比如整个路径不需要经过这个负环,那么最短路还是存在的。
3.负环:权重和为负数的环。
4.bellman-ford算法用于存在负权边,且限制最多经过k条边的情况下。
5.若在n - 1次松弛(不知道啥意思)后还能更新,那就说明存在负环。因为n个点,就有n-1条边,n-1次循环的话一定能到达n,没到达就说明存在负环。
6.因为限制最多经过k条边,也就是最多松弛k次,要开一个last数组,记录上一次循环时候各个点的最短距离。
如果再看的时候不知道为什么要开last数组的话,可以csdn搜一下,策马奔腾向前冲这个博主写的挺好。
模板:
Bellman-Ford算法
int n, m; // n表示点数,m表示边数
int dist[N]; // dist[x]存储1到x的最短路距离
struct Edge // 边,a表示出点,b表示入点,w表示边的权重
{
int a, b, w;
}edges[M];
// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
// 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
for (int i = 0; i < n; i ++ )
{
for (int j = 0; j < m; j ++ )
{
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
if (dist[b] > dist[a] + w)
dist[b] = dist[a] + w;
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
例题:
853 有边数限制的最短路
//给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
//请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。
//注意:图中可能 存在负权回路。
#include<iostream>
#include<cstring>
using namespace std;
const int N = 510, M = 10010;
int dis[N], last[N];
int n, m, k;
struct Edge{
int a, b, w;
}edge[M];
void bellman(){
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;
for(int i = 1; i <= k; i++){
memcpy(last, dis, sizeof dis);
for(int j = 1; j <= m; j++){
auto e = edge[j];
dis[e.b] = min(dis[e.b], last[e.a] + e.w);
}
}
}
int main(){
scanf("%d%d%d", &n, &m, &k);
for(int i = 1; i <= m; i++){
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edge[i].a = a, edge[i].b = b, edge[i].w = w;
}
bellman();
if(dis[n] > 0x3f3f3f3f / 2) puts("impossible");
else cout << dis[n] << endl;
return 0;
}
4.spfa算法
1.Bellman_ford算法会遍历所有的边,但是有很多的边遍历了其实没有什么意义,我们只用遍历那些到源点距离变小的点所连接的边即可,只有当一个点的前驱结点更新了,该节点才会得到更新;因此考虑到这一点,我们将创建一个队列每一次加入距离被更新的结点。(抄的点赞数最多的题解)
2.spfa同样可以运用于大部分dijkstra算法的情况,但有的时候会被卡。
3.spfa不可以有负权回路。
模板:
spfa 算法(队列优化的Bellman-Ford算法)
int n; // 总点数
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N]; // 存储每个点到1号点的最短距离
bool st[N]; // 存储每个点是否在队列中
// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while (q.size())
{
auto t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if (!st[j]) // 如果队列中已存在j,则不需要将j重复插入
{
q.push(j);
st[j] = true;
}
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
例题:
851 spfa求最短路
//给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
//请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible。
//数据保证不存在负权回路。
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 1e5 + 10;
bool st[N];
int dis[N];
int e[N], ne[N], w[N], h[N], idx;
int n, m;
void add(int a, int b, int c){
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int spfa(){
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while(q.size()){
int t = q.front();
q.pop();
st[t] = false;
for(int i = h[t]; i != -1; i = ne[i]){
int j = e[i];
if(dis[j] > dis[t] + w[i]){
dis[j] = dis[t] + w[i];
if(!st[j]){
q.push(j);
st[j] = true;
}
}
}
}
return dis[n];
}
int main(){
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for(int i = 0; i < m; i++){
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
add(a, b, w);
}
int t = spfa();
if(t == 0x3f3f3f3f) puts("impossible");
else cout << t << endl;
return 0;
}
5.spfa判断是否存在负环
模板:
spfa判断图中是否存在负环
int n; // 总点数
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N], cnt[N]; // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
bool st[N]; // 存储每个点是否在队列中
// 如果存在负环,则返回true,否则返回false。
bool spfa()
{
// 不需要初始化dist数组
// 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。
queue<int> q;
for (int i = 1; i <= n; i ++ )// 不是从源点开始,是所有点都要加入队列去判断
{
q.push(i);
st[i] = true;
}
while (q.size())
{
auto t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true; // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
例题:
852 spfa判断负环
//给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
//请你判断图中是否存在负权回路。
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N = 2010;
const int M = 10010;
bool st[N];
int dis[N];
int cnt[N];
int e[M], ne[M], h[N], w[M], idx;
int n, m;
void add(int a, int b, int c){
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
bool spfa(){
queue<int> q;
for(int i = 1; i <= n; i++){
q.push(i);
st[i] = true;
}
while(q.size()){
int t = q.front();
q.pop();
st[t] = false;
for(int i = h[t]; i != -1; i = ne[i]){
int j = e[i];
if(dis[j] > dis[t] + w[i]){
dis[j] = dis[t] + w[i];
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n) return true;
if(!st[j]){
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main(){
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
for(int i = 0; i < m; i++){
int a, b, c;
scanf("%d%d%d", &a, &b ,&c);
add(a, b, c);
}
if(spfa()) puts("Yes");
else puts("No");
return 0;
}
6.floyd算法
没太懂算法的思想是啥,但是挺好背的,忘了就看下floyd的模板就行,做题的时候注意例题里的两个注释。
模板:
floyd算法
初始化:
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
if (i == j) d[i][j] = 0;
else d[i][j] = INF;
// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
for (int k = 1; k <= n; k ++ )
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
例题:
854 Floyd求最短路
//给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。
//再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y 的最短距离,如果路径不存在,则输出 //impossible。
数据保证图中不存在负权回路。
#include<iostream>
using namespace std;
const int N = 210, M = 20010, INF = 1e9;
int dist[N][N];
int n, m, k;
void floyd(){
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
int main(){
scanf("%d%d%d", &n, &m, &k);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(i == j) dist[i][j] = 0;
else dist[i][j] = INF;
while(m--){
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
dist[x][y] = min(dist[x][y], z);// 有重边存在,所以要注意保留最小边
}
floyd();
while(k--){
int x, y;
scanf("%d%d", &x, &y);
// 因为有负边的存在,所以不能到的点的边权有可能会有减小
if(dist[x][y] > INF / 2) cout << "impossible" << endl;
else cout << dist[x][y] << endl;
}
return 0;
}
最小生成树与二分图
稠密图用朴素版Prim,稀疏图用Kruskal,堆优化基本上用不到。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6dIQzuQ0-1662635838720)(…/Study_images/image-20220905224044485.png)]
1.朴素版Prim算法
与朴素版dijkstra算法类似。
1.先找出距离集合最近的点。
2.将这个点加入集合。
3.用这个点去更新其它点到集合的距离。
摘自示例代码评论:
1.与dijkstra不同,prim需要迭代n次
2.最小生成树是针对无向图的,所以在读入边的时候,需要赋值两次
3.要先累加再更新,避免t有自环,影响答案的正确性。后更新不会影响后面的结果么?不会的,因为dist[i]为i到集合S的距离,当t放入集合后,其dist[t]就已经没有意义了,再更新也不会影响答案的正确性。
4.需要特判一下第一次迭代,在我们没有做特殊处理时,第一次迭代中所有点到集合S的距离必然为无穷大,而且不会进行更新(也没有必要),所以不需要将这条边(第一次迭代时,找到的距离集合S最短的边)累加到答案中,也不能认定为图不连通。
5.如果需要设置起点为i的话,在初始化dist数组之后,dist[i] = 0即可,这样也可以省去每轮迭代中的两个if判断。
模板:
prim算法
int n; // n表示点数
int g[N][N]; // 邻接矩阵,存储所有边
int dist[N]; // 存储其他点到当前最小生成树的距离
bool st[N]; // 存储每个点是否已经在生成树中
// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim()
{
memset(dist, 0x3f, sizeof dist);
int res = 0;
for (int i = 0; i < n; i ++ )
{
int t = -1;
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
if (i && dist[t] == INF) return INF;
if (i) res += dist[t];
st[t] = true;
for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
}
return res;
}
例题:
858 Prim算法求最小生成树
//给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。
//求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
//给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。
//由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int n, m;
const int N = 510, INF = 0x3f3f3f3f;
bool st[N];
int g[N][N];
int dist[N];// 点到集合的最短距离
int prim(){
memset(dist, 0x3f, sizeof dist);
int res = 0;
for(int i = 0; i < n; i++){
int t = -1;// 表示这一轮迭代要加入集合的点
for(int j = 1; j <= n; j++) // 找出还没有加入集合,距离集合距离最短的点
if(!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
// 需要对第一次迭代进行特判,因为第一次加入集合无论如何那个点都是无穷大,所以不需要更新
if(i && dist[t] == INF) return INF;// 判断孤立点
if(i) res += dist[t];// 将权重加起来
st[t] = true;// 将点加入集合
for(int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]);// 用新加入集合的点来更新其它点到集合的距离
}
return res;
}
int main(){
memset(g, 0x3f, sizeof g);
scanf("%d%d", &n, &m);
while(m--){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
g[a][b] = g[b][a] = min(g[a][b], c);// 注意是无向图,且有重边
}
int t = prim();
if(t == INF) puts("impossible");
else cout << t;
return 0;
}
2.Kruskal算法
这个算法的实现用到了并查集。
先对整个边的集合进行排序,如果两个点不在同一个集合,则把他们连起来,否则就不要这条边。
第一次学到这的时候把并查集忘得差不多了,第二遍的时候好好看看,也不是太难。
模板:
Kruskal算法
int n, m; // n是点数,m是边数
int p[N]; // 并查集的父节点数组
struct Edge // 存储边
{
int a, b, w;
bool operator< (const Edge &W)const
{
return w < W.w;
}
}edges[M];
int find(int x) // 并查集核心操作
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int kruskal()
{
sort(edges, edges + m);
for (int i = 1; i <= n; i ++ ) p[i] = i; // 初始化并查集
int res = 0, cnt = 0;
for (int i = 0; i < m; i ++ )
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if (a != b) // 如果两个连通块不连通,则将这两个连通块合并
{
p[a] = b;
res += w;
cnt ++ ;
}
}
if (cnt < n - 1) return INF;
return res;
}
例题:
859 Kruskal算法求最小生成树
//给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。
//求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
//给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。
//由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。
#include<iostream>
#include<algorithm>
#include <cstring>
using namespace std;
const int N = 100010, M = 200010, INF = 0x3f3f3f3f;
int p[N];
int n, m;
struct Edge{
int a, b, w;
bool operator< (const Edge &W){// 这里的运算符重载也可以写一个cmp函数,加在sort的第三个参数
return w < W.w;
}
}edges[M];
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int kruskal(){
sort(edges, edges + m);
for(int i = 1; i <= n; i++) p[i] = i;// 初始化并查集
int res = 0, cnt = 0;
for(int i = 0; i < m; i++){
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if(a != b){
p[a] = b;
cnt++;
res += w;
}
}
if(cnt < n - 1) return INF;
return res;
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 0; i < m; i++){
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i].a = a;
edges[i].b = b;
edges[i].w = w;
}
int t = kruskal();
if(t == INF) puts("impossible");
else cout << t;
return 0;
}
3.染色法
奇数环:环中边的数量为奇数。
二分图定义:一个顶点集合可以分为两个不相邻的子集,两个子集的内部没有边,并且图中每条边依附的两个顶点分别属于这两个子集。
二分图性质:当且仅当图中没有奇数环。
dfs判断一条边的两个顶点颜色是否相同,不相同是正确的,相同就直接判断为不是二分图。
模板:
染色法判别二分图
int n; // n表示点数
int h[N], e[M], ne[M], idx; // 邻接表存储图
int color[N]; // 表示每个点的颜色,-1表示为染色,0表示白色,1表示黑色
// 参数:u表示当前节点,father表示当前节点的父节点(防止向树根遍历),c表示当前点的颜色
bool dfs(int u, int father, int c)
{
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (color[j] == -1)
{
if (!dfs(j, u, !c)) return false;
}
else if (color[j] == c) return false;
}
return true;
}
bool check()
{
memset(color, -1, sizeof color);
bool flag = true;
for (int i = 1; i <= n; i ++ )
if (color[i] == -1)
if (!dfs(i, -1, 0))
{
flag = false;
break;
}
return flag;
}
例题:
860 染色法判定二分图
//给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环。
//请你判断这个图是否是二分图。
#include<iostream>
#include<cstring>
using namespace std;
const int N = 100010, M = 200010;// 无向图
int h[N], e[M], ne[M], idx;// 邻接表
int color[N];// 0代表没访问过,1和2分别代表一种颜色
int n, m;
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool dfs(int u, int c){
color[u] = c;
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if(!color[j]){// 如果没染色过
if(!dfs(j, 3 - c)) return false;// 染和相邻节点不一样的色
}
else if(color[j] == c) return false;// 染色过就判断这个点颜色是否和相邻点颜色相同,如果相同则代表有奇数环
}
return true;
}
int main(){
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
while(m--){
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a);// 无向图
}
bool flag = true;
for(int i = 1; i <= n; i++){
if(!color[i]){
if(!dfs(i, 1)){
flag = false;
break;
}
}
}
if(flag) puts("Yes");
else puts("No");
return 0;
}
4.匈牙利算法
规定图为一个二分图,然后拿左边的点集去匹配右边的点集,任意两条匹配的边没有公共顶点。
摘自点赞数最多的题解:
要了解匈牙利算法必须先理解下面的概念:
匹配:在图论中,一个「匹配」是一个边的集合,其中任意两条边都没有公共顶点。
最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。
模板:
匈牙利算法
int n; // n表示点数
int h[N], e[M], ne[M], idx; // 邻接表存储所有边
int match[N]; // 存储每个点当前匹配的点
bool st[N]; // 表示每个点是否已经被遍历过
bool find(int x)
{
for (int i = h[x]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true;
if (match[j] == 0 || find(match[j]))
{
match[j] = x;
return true;
}
}
}
return false;
}
// 求最大匹配数
int res = 0;
for (int i = 1; i <= n; i ++ )
{
memset(st, false, sizeof st);
if (find(i)) res ++ ;
}
例题:
861 二分图的最大匹配
//给定一个二分图,其中左半部包含 n1 个点(编号 1∼n1),右半部包含 n2 个点(编号 1∼n2),二分图共包含 m 条边。
//数据保证任意一条边的两个端点都不可能在同一部分中。
//请你求出二分图的最大匹配数。
//二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E} 中的任意两条边都不依附于同一个顶点,则称 M //是一个匹配。
//二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 510, M = 100010;
bool st[N];// 表示当前这个男生的匹配轮次下,这个女生被男生预定了 预约数组
int match[N];// match[j]表示第j个女生的男朋友是谁,0的话代表还没有男朋友 匹配数组
int e[M], ne[M], h[N], idx;
int n1, n2, m;
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool find(int u){
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if(!st[j]){
st[j] = true;// 女孩j被预约了
// 当j这个女孩没有匹配对象或者匹配对象有备胎的时候,这时候匹配成功
if(match[j] == 0 || find(match[j])){
match[j] = u;
return true;
}
}
}
return false;
}
int main(){
memset(h, -1, sizeof h);
scanf("%d%d%d", &n1, &n2, &m);
while(m--){
int a, b;
scanf("%d%d", &a, &b);
add(a, b);// 虽然是无向图,但只需要保存男生指向女生的那部分就行了
}
int res = 0;// 保存匹配的边数
for(int i = 1; i <= n1; i++){
// 每一轮重置一次st数组,相当于每一次查询第i个男生的匹配对象的时候,女孩j被男孩i预约了,是否匹配还需要再看
// 如果不重置的话,那这个女孩被预约就相当于匹配上了,就不存在找备胎的情况了
// 也就是说达不到最大匹配数
memset(st, false, sizeof st);
if(find(i)) res ++;
}
cout << res;
return 0;
}