Chapter 1 基础算法
1:快速排序
【1】确定分界点。x = q[l], q[r], q[(r+l)/2]
都可以
【2】调整区间,使得左段<=x
,右段>x
【3】递归处理左右两段
暴力:
【1】开辟
a[N], b[N]
【2】把小于等于x的数放在
a[N]
,大于x的数放在b[N]
【3】遍历
a[N], b[N]
,按大小顺序拷贝到q[N]
双指针:
# include <iostream>
using namespace std;
const int N = 1e6+10;
int n;
int q[N];
// basic quick sort
void quick_sort(int q[], int l, int r){
if(l >= r) return;
// judge the boundary
int i=l-1,j=r+1;
int x=q[r+l >> 1];
// initiate x, i, j
while(i < j){
do i++;
while(q[i]<x);
do j--;
while(q[j]>x);
if(i < j) swap(q[i],q[j]);
}
// 如果x=q[l],则必须写成j和j+1
quick_sort(q, l, j);
quick_sort(q, j+1, 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;
}
/*
5 3
2 4 1 5 3
3
*/
2:归并排序
分治法
【1】确定分界点mid = (l+r)/2
【2】递归排序left和right
【3】归并,然后合二为一
双指针:
#include <iostream>
using namespace std;
const int N=1e6+10;
int n;
int q[N], tmp[N];
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; // i是左侧数组的起点,j是右侧数组的起点
while(i <= mid && j <= r){ // i和j都在数组范围内
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++]; //右侧还有数
// 拷贝tmp到q(额外空间tmp开销)
for(i = l, j = 0; i <= r; i++, j++){
// i从左侧数组起点l开始,j从tmp数组起点0开始
q[i] = tmp[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;
}
3:整数二分
—0—0—0—0—0—x(0)—y(1)—1—1—1—1—1—1—1—
1:右侧都满足check
0:左侧都不满足check
y为1的边界
x为0的边界
方法1(得到x)
【1】确定分界点:mid = (l+r+1)/2
【2】检查mid是否符合条件(即取0),若true(取0了),则答案在[mid, r]
内,更新l = mid
;若false(取1了),则答案在[l, mid-1]
内,更新r = mid-1
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1; // 一定要加1,否则会出现边界问题!
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
方法2(得到y)
【1】确定分界点:mid = (l+r)/2
【2】检查mid是否符合条件(即取1),若true(取1了),则答案在[l, mid]
内,更新r = mid
;若false(取0了),则答案在[mid+1, r]
内,更新l = mid+1
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
题目:数的范围
给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。
对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。
如果数组中不存在该元素,则返回
-1 -1
。输入格式:
第一行包含整数 n 和 q,表示数组长度和询问个数。
第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。
接下来 q 行,每行包含一个整数 k,表示一个询问元素。
输出格式
共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。
#include <iostream>
using namespace std;
const int N=1e6+10;
int n,m;
int q[N];
int main(){
scanf("%d%d", &n, &m);
for (int i=0; i<n; i++) scanf("%d", &q[i]);
while(m--){
int x;
scanf("%d", &x);
int l = 0, r = n-1;
// 起始位置判定
while(l < r){
int mid = l+r >> 1;
if(q[mid] >= x) r = mid; // 如果mid位置的数,不小于询问元素k
else l = mid+1;
}
if(q[l] != x) cout<<"-1 -1"<<endl;
else{
cout<<l<<" ";
int l=0, r=n-1;
// 终止位置判定
while(l < r){
int mid = l+r+1 >> 1;
if(q[mid] <= x) l = mid;
else r = mid-1;
}
cout<<l<<endl;
}
}
return 0;
}
4:浮点二分
题目:大于1的数字的平方根
# include <iostream>
using namespace std;
int main(){
double x;
cin>>x;
double l=0, r=x;
while(r - l > 1e-8){
double mid = (l+r)/2;
if(mid * mid > x) r = mid;
else l = mid;
}
printf("%lf\n", l);
return 0;
}
5:高精度加法
【1】大整数存储(正整数),从个位开始(string to vector)
【2】Ai + Bi + 进位
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e6+10;
// 加法
vector<int> add(vector<int> &A, vector<int> &B){
vector<int> C;
int t=0;
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.push_back(t%10);
t /= 10;
}
if(t) C.push_back(1); //最高位有进位,例如9+2=11,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');
auto C = add(A, B);
for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
return 0;
}
6:高精度减法
【1】大整数存储(正整数),从个位开始(string to vector)
【2】Ai - Bi + 结位
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e6+10;
// 判断A>=B
bool cmp(vector<int> &A, vector<int> &B){
// 判断bit-size
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 1; // A == B
}
// 减法
vector<int> sub(vector<int> &A, vector<int> &B){
vector<int> C;
int t=0;
for(int i=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,例如01
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');
// A >= B
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(A, B);
cout<<"-";
for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
}
return 0;
}
7:高精度乘法
【1】大整数存储(正整数),从个位开始(string to vector)
【2】b看成一个整体,和A相乘(A是大整数,B是小整数)
#include <iostream>
#include <vector>
using namespace std;
// multiply
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;
}
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;
}
8:高精度除法
【1】大整数存储(正整数),从个位开始(string to vector)
【2】b看成一个整体,和A相除(A是大整数,B是小整数),从最高位开始算(一般会有前导0)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// divide
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());
// 因为除法是从高位开始,因此需要reverse
while(C.size()>1 && C.back()==0) C.pop_back();
// 去除前导0
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--) printf("%d",C[i]);
cout<<endl<<r<<endl;
return 0;
}
9:前缀和
作用:一次运算求出某一段区间的所有数据的和
【1】数组定义:原数组为A[]
,前缀和为S[]
【2】前缀和Si = a1 + a2 + ... + ai
,其中S0 = 0
题目:一维前缀和
求出区间[l, r]之间数的和
#include <iostream>
using namespace std;
const int N = 1e6+10;
int n, m;
int a[N], S[N];
int main(){
scanf("%d%d", &n, &m);
a[0]=0;
s[0]=0;
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];
// m次询问
while(m--){
int l, r;
scanf("%d%d", &l, &r);
print("%d\n", s[r] - s[l-1]);
}
return 0;
}
题目:二维前缀和
求出某个子矩阵内数的和,左上角坐标为(x1, y1),右下角坐标为(x2, y2)
=>容斥定理
#include <iostream>
using namespace std;
const int N=1010;
int a[N][N], s[N][N];
int main(){
int n,m,q;
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]);
// 2-dimensional matrix, n for rows, m for cols
// 初始化前缀和
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, x2, y1, y2; // 1是左上角,2是右下角
cin>>x1>>y1>>x2>>y2;
printf("%d\n",s[x2][y2] - s[x1-1][y2] - s[x2][y1 -1] + s[x1-1][y1-1]);
}
return 0;
}
10:差分
本质:前缀和的逆运算
【1】构造b[]
,使得ai = b1 + b2 + ... + bi
,即a是b的前缀和,b是a的差分
【2】一维计算公式:b[n] = a[n] - a[n-1]
【2】二维计算公式:b[i][j] = a[i][j] − a[i−1][j] − a[i][j−1] + a[i−1][j−1]
,容斥定理
【3】初始化:可以假定全数组都是0,然后在[i,i]
区间加上a[i]
用途:
【1】一维:在某个区间[l,r]
的多次操作都加上或减去一个数x
【2】二维:在一个二维矩阵中,有多块区间需要增加或减少一个数值,多次操作后求最终的矩阵内容
题目:一维差分
#include <iostream>
using namespace std;
const int N = 1e6+10;
int n,m;
int a[N],b[N];
void 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]); //数据读入
for(int i=1;i<=n;i++) insert(i,i,a[i]); //差分初始化
//a[0]=0
//b[0]=0
// 执行m次,对每一次输入的区间[l,r]的数加上c
while(m--){
int l,r,c;
cin>>l>>r>>c;
insert(l,r,c);
}
for(int i=1;i<=n;i++) b[i] += b[i-1]; // 还原a[i]
for(int i=1;i<=n;i++) cout<<b[i]<<" ";
return 0;
}
题目:二维差分
#include <iostream>
using namespace std;
const int N = 1010;
int n,m,q;
int a[N][N],b[N][N]; // a是原数组,b是差分数组
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(){
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]);
// 差分二维矩阵初始化,在空集子矩阵上插入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,x2,y1,y2,c;
cin>>x1>>y1>>x2>>y2>>c; //c是子矩阵需要加减的数
insert(x1,y1,x2,y2,c); // 在差分数组上,需要加减的子矩阵上插入数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] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + b[i][j]; // 从b还原到a
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++) printf("%d ",a[i][j]); // 输出
puts("");
}
return 0;
}
/*
input test
3 4 3
1 2 2 1
3 2 2 1
1 1 1 1
1 1 2 2 1
1 3 2 3 2
3 1 3 4 1
*/
/*
output test
2 3 4 1
4 3 4 1
2 2 2 2
*/
11:双指针
核心:将O(n^2)优化到O(n)
朴素算法:
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
function();
双指针算法:
for(int i=0,j=0;i<n;i++){
while(j<i && check(i,j)){
j++;
}
function();
}
题目:分割一句英文的每个单词,每个单词按空格隔开
#include <string.h>
#include <iostream>
using namespace std;
int main(){
char a[1000];
gets(a);
int n=strlen(a);
for(int i=0;i<n;i++){
int j=i;
while(j<n && a[j]!=' '){
j++;
}
for(int k=i;k<j;k++) cout<<a[k];
cout<<endl;
i=j;
}
return 0;
}
题目:最长连续不重复子序列
给定一个长度为n的整数序列,找出最长的不包含重复数字的连续子序列,输出其长度
#include <string.h>
#include <iostream>
using namespace std;
const int N=1e6+10;
int a[N],s[N];
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++) cin>>a[i];
int res=0;
for(int i=0, j=0;i<n;i++){
s[a[i]]++; // hash中,对应的数值计数+1
while(s[a[i]]>1){ // 检测到有重复的值
s[a[j]]--;
j++;
}
res=max(res,i-j+1);
}
cout<<res<<endl;
return 0;
}
/*
5
1 2 2 3 5
3
*/
12:位运算
作用:n的二进制表示中,第k位是几
【1】先把第k位移到最后一位,采用n>>k
【2】看个位是几,采用x&1
#include <string.h>
#include <iostream>
using namespace std;
int main(){
int n=10;
for(int k=3;k>=0;k--){
cout<<(n>>k&1);
}
return 0;
}
//n的二进制表示
low bit运算:返回x的最后一位1
本质:x & -x = x & (~x + 1)
如果x = 10000010
,则lowbit(x) = 10
题目:求每个十进制数,转化为二进制后1的个数
#include <string.h>
#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;
while(x) x -= lowbit(x),res++;
cout<<res<<endl;
}
return 0;
}
13:离散化
将稀疏的数据变为连续的整数值
例如下表,第一行是稀疏数据,第二行为离散化数据
本质:映射
a[0] | a[1] | a[2] | a[3] | a[4] |
---|---|---|---|---|
1 | 3 | 100 | 2000 | 50000 |
0 | 1 | 2 | 3 | 4 |
题目:区间和
有一个无限长数轴,每个坐标上的数都是0
进行n次操作,每次在位置x上的数加c
进行m次循环,求出区间[l, r]内的所有数的和
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
typedef pair<int,int> PII;
const int N=3e6+10;
int n,m;
int a[N],s[N];
vector<int> alls;
vector<PII> add,query;
// integer binary search
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;
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++){
int x,c;
cin>>x>>c;
add.push_back({x,c}); // append a key value
alls.push_back(x); // append location
}
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);
}
//去重,在位置数组alls中
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(),alls.end()),alls.end());
//处理插入
for(auto item:add){
int x = find(item.first); // 在alls中找到位置
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);
cout<<s[r]-s[l-1]<<endl;
}
return 0;
}
/*
3 3
1 2
3 6
7 5
1 3
4 6
7 8
8
0
5
*/
对于已经排序号的数组,unique的实现如下:
【1】当前是第一个数,即
i == 0
【2】
a[i] != a[i-1]
14:区间和并
作用:合并有交集的区间
【1】按区间左端点排序
【2】扫描每个区间,并维护当前区间;每个区间和当前区间有3种关系,①完全包含②部分交集③完全无交集
①当前维护区间[start, end]不变,类似[1,5]and[2,3]
②当前维护区间[start, end]更新为[start, new-end],类似[1,5]and[2,7]
③当前区间存放,将当前维护区间更新为[new-start, new-end],类似[1,5]and[7,8]
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
typedef pair<int,int> PII;
const int N=1e6+10;
int n;
vector<PII> segs;
// 区间合并
void merge(vector<PII> &segs){
vector<PII> res; // 临时变量
sort(segs.begin(),segs.end()); // 按照左端点排序
int bound=-2e9; // 负无穷边界值
int start=bound,end=bound; // 维护区间的初始化为[负无穷, 负无穷]
for(auto seg:segs){ // 遍历每一个区间
// 如果下一个区间完全不相交
if(end < seg.first){
if(start != bound) res.push_back({start,end}); // 如果不是一开始的初始区间
// 更新维护区间
start = seg.first;
end = seg.second;
}
// 有相交
else end = max(end,seg.second); // 维护右侧
}
if (start != bound) 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;
}
/*
5
1 2
2 4
5 6
7 8
7 9
3
*/
习题!
1:第k个数
给定一个长度为n的整数序列,以及一个整数k,用快排求出薯类的第k小的数是多少
输入:
row1是整数n和整数k
row2是n个整数
# include <iostream>
using namespace std;
const int N = 1e6+10;
int n,k;
int q[N];
// basic quick sort
void quick_sort(int q[], int l, int r){
if(l >= r) return;
// judge the boundary
int i=l-1,j=r+1;
int x=q[r+l >> 1];
// initiate x, i, j
while(i < j){
do i++;
while(q[i]<x);
do j--;
while(q[j]>x);
if(i < j) swap(q[i],q[j]);
}
// 如果x=q[l],则必须写成j和j+1
quick_sort(q, l, j);
quick_sort(q, j+1, r);
}
int main(){
cin>>n>>k;
for(int i=0; i<n; i++){
scanf("%d", &q[i]);
}
quick_sort(q, 0, n-1);
cout<<q[k-1];
return 0;
}
/*
5 3
2 4 1 5 3
3
*/
直接求解第k个数
# 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],i=l-1,j=r+1;
while(i<j){
while(q[++i]<x);
while(q[--j]>x);
if(i<j) swap(q[i],q[j]);
}
int len=j-l+1; // 左边数组的个数
if(k <= len) return quick_sort(l,j,k);
return quick_sort(j+1,r,k-len);
}
int main(){
cin>>n>>k;
for(int i=0; i<n; i++){
scanf("%d", &q[i]);
}
cout<<quick_sort(0,n-1,k); // left, right, k-th
return 0;
}
/*
5 3
2 4 1 5 3
3
*/
2:逆序对的数量
# include <iostream>
using namespace std;
const int N = 1e6+10;
int n,k;
int q[N],tmp[N];
typedef long long LL;
LL merge_sort(int l, int r){
if (l>=r) return 0;
int mid = l+r >> 1;
LL res = merge_sort(l,mid) + merge_sort(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++];
res += mid-i+1; // 和普通归并不一样的地方
// 此时说明左侧当前数 是大于 右侧当前数的
// 那么,逆序对的数量,就可以计算了
// 右侧当前数对应的逆序对数量,就是左侧当前数到最后一个数的个数
}
}
while(i<=mid){
tmp[k++]=q[i++];
}
while(j<=mid){
tmp[k++]=q[j++];
}
// copy tmp to q
i = l; // for q
j = 0; // for tmp
for(;i<=r;i++,j++){
q[i]=tmp[j];
}
return res;
}
int main(){
cin>>n;
for(int i=0;i<n;i++)
cin>>q[i];
cout<<merge_sort(0,n-1)<<endl;
return 0;
}
/*
6
2 3 4 5 6 1
5
*/
3:三次方根
#include <iostream>
using namespace std;
const int N = 1e6+10;
int main(){
double x;
cin>>x;
double l=-10000;
double r=-l;
while(r-l > 1e-8){
double mid=(l+r)/2;
if(mid * mid * mid >= x)
r = mid;
else
l=mid;
}
cout<<l;
return 0;
}