基础算法总结
基础算法总结
- 1、模拟
- 1.1 什么是模拟算法
- 1.2 算法题
- 1.2.1 多项式输出
- 1.2.2 蛇形方阵
- 2 高精度算法
- 2.1 什么是高精度算法
- 2.2 算法题
- 2.2.1 高精度加法
- 2.2.2 高精度乘法
- 3 普通枚举
- 3.1 算法题
- 3.1.1 铺地毯
- 3.1.2 回文日期
- 4 前缀和算法
- 4.1 什么是前缀和
- 4.2 算法题
- 4.2.1 最大子段和
- 4.2.2 二维前缀和
- 5 差分
- 5.1 什么是差分
- 5.2 算法题
- 5.2.1 海底高铁
- 5.2.2 地毯
- 6、双指针
- 6.1 双指针算法是什么
- 6.2 算法题
- 6.2.1 唯一的雪花
- 6.2.2 逛画展
- 7 二分算法
- 7.1 什么是二分算法
- 7.2 算法题
- 7.2.1 牛可乐和魔法封印
- 7.2.2 A-B数对
- 8、贪心算法
- 8.1 什么是贪⼼算法?
- 8.2 贪⼼算法的特点
- 8.3. 如何学习贪⼼?
- 8.4 相关算法题(2道)
- 1.4.1 货仓选址
- 8.4.2 最大子段和
- 9、倍增算法
- 9.1 倍增,顾名思义就是翻倍。它能够使线性的处理转化为对数级的处理,极⼤地优化时间复杂度。
- 9.2.1 快速幂
- 9.2.2 64位整数乘法
- 10 离散化
- 10.1 算法:
- 10.2 离散模版
- 10.3 算法题
- 10.3.1 火烧赤壁
- 10.3.2
- 11、递归初阶
- 11.1 算法概念
- 11.2 算法题
- 11.2.1 汉诺塔
- 11.2.2 占卜diy
- 12 、分治算法
- 12.1 算法概念
- 12.2 算法题
- 12.2.1 逆序对
- 12.2.2 求第k小的数
1、模拟
1.1 什么是模拟算法
模拟,顾名思义,就是题⽬让你做什么你就做什么,考察的是将思路转化成代码的代码能⼒。
这类题⼀般较为简单,属于竞赛⾥⾯的签到题(但是,万事⽆绝对,也有可能会出现让⼈⾮常难受的
模拟题),我们在学习语法阶段接触的题,⼤多数都属于模拟题。
1.2 算法题
1.2.1 多项式输出
https://www.luogu.com.cn/problem/P1067
#include<iostream>
#include<cmath>
using namespace std;
int main()
{
int n; cin >> n;
for (int i = n; i >= 0; i--)
{
int a; cin >> a;
if (a == 0) continue;
if (a < 0) cout << '-';
if (a > 0)
{
if (i != n) cout << '+';
}
//符号
a = abs(a);
if (a != 1 || (a == 1 && i == 0)) cout << a;
//数字
if (i > 1) cout << "x^" << i;
if (i == 1) cout << "x";
//指数
}
return 0;
}
1.2.2 蛇形方阵
https://www.luogu.com.cn/problem/P5731
#include<iostream>
using namespace std;
const int N = 15;
//右,下,左,上
int dx[] = { 0,1,0,-1 };
int dy[] = { 1,0,-1,0 };
//向量数组
int arr[N][N];
int main()
{
int n; cin >> n;
int x = 1, y = 1;
int pos = 0;
int cnt = 1;
while (cnt <= n * n)
{
arr[x][y] = cnt;
cnt++;
int a = x + dx[pos], b = y + dy[pos];
if (a<1 || b>n || a > n || b < 1 || arr[a][b])
{
pos = (pos + 1) % 4;
a = x + dx[pos], b = y + dy[pos];
}
x = a, y = b;
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
printf("%3d", arr[i][j]);
}
cout << endl;
}
return 0;
}
2 高精度算法
2.1 什么是高精度算法
当数据的值特别⼤,各种类型都存不下的时候,此时就要⽤⾼精度算法来计算加减乘除:
• 先⽤字符串读⼊这个数,然后⽤数组逆序存储该数的每⼀位;
• 利⽤数组,模拟加减乘除运算的过程。
⾼精度算法本质上还是模拟算法,⽤代码模拟⼩学列竖式计算加减乘除的过程。
2.2 算法题
2.2.1 高精度加法
https://www.luogu.com.cn/problem/P1601
#include<iostream>
using namespace std;
const int N = 1e5;
int a[N], b[N], c[N];
int la, lb, lc;
void add(int c[], int a[], int b[])
{
for (int i = 0; i < lc; i++)
{
c[i] += a[i] + b[i];
c[i + 1] += c[i] / 10;
c[i] %= 10;
}
if (c[lc]) lc++;
}
int main()
{
string x, y; cin >> x >> y;
la = x.size(), lb = y.size(); lc = max(la, lb);
for (int i = 0; i < la; i++) a[i] = x[x.size() - i - 1] - '0';
for (int i = 0; i < lb; i++) b[i] = y[y.size() - i - 1] - '0';
add(c, a, b);
for (int i = lc - 1; i >= 0; i--)
cout << c[i];
return 0;
}
2.2.2 高精度乘法
https://www.luogu.com.cn/problem/P1303
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int a[N], b[N], c[N];
int la, lb, lc;
void mul(int c[], int a[], int b[])
{
for (int i = 0; i < la; i++)
for (int j = 0; j < lb; j++)
c[i + j] += a[i] * b[j];
for (int i = 0; i < lc; i++)
{
c[i + 1] += c[i] / 10;
c[i] %= 10;
}
while (lc > 1 && c[lc - 1] == 0) lc--;
}
int main()
{
string x, y; cin >> x >> y;
la = x.size(), lb = y.size(), lc = la + lb;
for (int i = 0; i < la; i++) a[i] = x[x.size() - i - 1] - '0';
for (int i = 0; i < lb; i++) b[i] = y[y.size() - i - 1] - '0';
mul(c, a, b);
for (int i = lc - 1; i >= 0; i--) cout << c[i];
return 0;
}
3 普通枚举
3.1 算法题
3.1.1 铺地毯
https://www.luogu.com.cn/problem/P1003
#include <iostream>
using namespace std;
const int N=1e5;
int a[N],b[N],g[N],k[N];
int n,x,y;
int judge()
{
for(int i=n;i>=1;i--)
{
if(x>=a[i]&&y>=b[i]&&x<=a[i]+g[i]&&y<=b[i]+k[i])
return i;
}
return -1;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i]>>b[i]>>g[i]>>k[i];
cin>>x>>y;
cout<<judge();
return 0;
}
3.1.2 回文日期
https://www.luogu.com.cn/problem/P2010
#include<iostream>
using namespace std;
int x,y;
int day[]={0,31,29,31,30,31,30,31,31,30,31,30,31};
int main()
{
cin>>x>>y;
int cnt=0;
for(int i=1;i<=12;i++)
{
for(int j=1;j<=day[i];j++)//11 22
{
int k=j%10*1000+j/10*100+i%10*10+i/10;
int num=k*10000+i*100+j;
if(x<=num&&num<=y) cnt++;
}
}
cout<<cnt;
return 0;
}
4 前缀和算法
4.1 什么是前缀和
前缀和与差分的核⼼思想是预处理,可以在暴⼒枚举的过程中,快速给出查询的结果,从⽽优化时间
复杂度。是经典的⽤空间替换时间的做法。
4.2 算法题
4.2.1 最大子段和
https://www.luogu.com.cn/problem/P1115
/*#include<iostream>
using namespace std;
const int N=2e5+10;
long long dp[N];
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int x; cin>>x;
dp[i]=dp[i-1]+x;
}
long long ret=-1e6;
long long prevmin=0;
for(int i=1;i<=n;i++)
{
ret=max(ret,dp[i]-prevmin);
prevmin=min(prevmin,dp[i]);
}
cout<<ret;
return 0;
}*/
4.2.2 二维前缀和
https://ac.nowcoder.com/acm/problem/226333
#include<iostream>
using namespace std;
const int N=1e3+10;
long long dp[N][N];
int n,m,q;
int main()
{
cin>>n>>m>>q;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
int x; cin>>x;
dp[i][j]=dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]+x;
}
}
while(q--)
{
int x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
cout<<dp[x2][y2]-dp[x2][y1-1]-dp[x1-1][y2]+dp[x1-1][y1-1]<<endl;
}
return 0;
}
5 差分
5.1 什么是差分
前缀和与差分的核⼼思想是预处理,可以在暴⼒枚举的过程中,快速给出查询的结果,从⽽优化时间
复杂度。是经典的⽤空间替换时间的做法。
学完差分之后,⼤家会发现,前缀和与差分是⼀对互逆的运算。
5.2 算法题
5.2.1 海底高铁
https://www.luogu.com.cn/problem/P3406
#include <iostream>
using namespace std;
int n,m;
const int N=1e5+10;
long long f[N];
int main()
{
cin>>n>>m;
int x; cin>>x;
for(int i=2;i<=m;i++)
{
int y; cin>>y;
if(x>y) f[y]++,f[x]--;
else f[x]++,f[y]--;
x=y;
}
for(int i=1;i<=n;i++)
f[i]=f[i]+f[i-1];
long long ret=0;
for(int i=1;i<n;i++)
{
long long a,b,c; cin>>a>>b>>c;
ret+=min(a*f[i],c+b*f[i]);
}
cout<<ret<<endl;
return 0;
}
5.2.2 地毯
https://www.luogu.com.cn/problem/P3397
#include<iostream>
using namespace std;
const int N=1010;
int n,m;
int f[N][N];
void add(int x1,int y1,int x2,int y2,int k)
{
f[x1][y1]+=k;
f[x1][y2+1]-=k;
f[x2+1][y1]-=k;
f[x2+1][y2+1]+=k;
}
int main()
{
cin>>n>>m;
while(m--)
{
int x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
add(x1,y1,x2,y2,1);
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
f[i][j]=f[i][j-1]+f[i-1][j]+f[i][j]-f[i-1][j-1];
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
cout<<f[i][j]<<' ';
}
cout<<endl;
}
return 0;
}
6、双指针
6.1 双指针算法是什么
双指针算法有时候也叫尺取法或者滑动窗⼝,是⼀种优化暴⼒枚举策略的⼿段:
• 当我们发现在两层 for 循环的暴⼒枚举过程中,两个指针是可以不回退的,此时我们就可以利⽤
两个指针不回退的性质来优化时间复杂度。
• 因为双指针算法中,两个指针是朝着同⼀个⽅向移动的,因此也叫做同向双指针。
注意:希望⼤家在学习该算法的时候,不要只是去记忆模板,⼀定要学会如何从暴⼒解法优化成双指
针算法。不然往后遇到类似题⽬,你可能压根都想不到⽤双指针去解决。
6.2 算法题
6.2.1 唯一的雪花
https://www.luogu.com.cn/problem/UVA11572
#include <iostream>
#include<unordered_map>
using namespace std;
const int N = 1e6 + 10;
int t, arr[N];
int main()
{
cin >> t;
while (t--)
{
int n; cin >> n;
for (int i = 1; i <= n; i++) cin >> arr[i];
int left = 1, right = 1;
unordered_map<int, int> mp;
int ret = 0;
while (right <= n)
{
mp[arr[right]]++;
while (mp[arr[right]] > 1)
{
mp[arr[left++]]--;
}
ret = max(ret, right - left + 1);
right++;
}
cout << ret << endl;
}
return 0;
}
6.2.2 逛画展
https://www.luogu.com.cn/problem/P1638
#include<iostream>
using namespace std;
int n,m,kinds;
const int N=1e6+10;
int a[N];
int mp[N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
int left=1,right=1;
int ret=n,begin=1;
while(right<=n)
{
if(mp[a[right]]++==0) kinds++;
while(kinds==m)
{
int len=right-left+1;
if(len<ret)
{
begin=left;
ret=len;
}
if(mp[a[left]]--==1) kinds--;
left++;
}
right++;
}
cout<<begin<<' '<<begin+ret-1<<endl;
return 0;
}
7 二分算法
7.1 什么是二分算法
⼆分算法是我觉得在基础算法篇章中最难的算法。⼆分算法的原理以及模板其实是很简单的,主要的
难点在于问题中的各种各样的细节问题。因此,⼤多数情况下,只是背会⼆分模板并不能解决题⽬,
还要去处理各种乱七⼋糟的边界问题。
7.2 算法题
7.2.1 牛可乐和魔法封印
https://ac.nowcoder.com/acm/problem/235558
#include <iostream>
using namespace std;
const int N=1e5+10;
long long a[N],n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
int q; cin>>q;
while(q--)
{
int l,r; cin>>l>>r;
int left=1,right=n;
while(left<right)
{
int mid=(left+right)/2;
if(a[mid]>=l) right=mid;
else left=mid+1;
}
int lefti=left;
if(a[lefti]<l)
{
cout<<0<<endl;
continue;
}
left=1,right=n;
while(left<right)
{
int mid=(left+right+1)/2;
if(a[mid]<=r) left=mid;
else right=mid-1;
}
int righti=right;
if(a[righti]>r)
{
cout<<0<<endl;
continue;
}
cout<<righti-lefti+1<<endl;
}
return 0;
}
7.2.2 A-B数对
https://www.luogu.com.cn/problem/P1102
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 2e5 + 10;
long long a[N], c;
int n;
int main()
{
cin >> n >> c;
for (int i = 1; i <= n; i++) cin >> a[i];
sort(a+1,a+n+1);
long long ret = 0;
for (int i = n; i > 1; i--)
{
long long b = a[i] - c;
ret += upper_bound(a + 1, a + i, b) - lower_bound(a + 1, a + i, b);
}
cout << ret;
return 0;
}
8、贪心算法
8.1 什么是贪⼼算法?
贪⼼算法,或者说是贪⼼策略:企图⽤局部最优找出全局最优。
- 把解决问题的过程分成若⼲步;
- 解决每⼀步时,都选择"当前看起来最优的"解法;
- "希望"得到全局的最优解。
8.2 贪⼼算法的特点
- 对于⼤多数题⽬,贪⼼策略的提出并不是很难,难的是证明它是正确的。因为贪⼼算法相较于暴⼒枚举,每⼀步并不是把所有情况都考虑进去,⽽是只考虑当前看起来最优的情况。但是,局部最优并不等于全局最优,所以我们必须要能严谨的证明我们的贪⼼策略是正确的。⼀般证明策略有:反证法,数学归纳法,交换论证法等等。
- 当问题的场景不同时,贪⼼的策略也会不同。因此,贪⼼策略的提出是没有固定的套路和模板
的。我们后⾯讲的题⽬虽然分类,但是⼤家会发现具体的策略还是相差很⼤。
因此,不要妄想做⼏道贪⼼题⽬就能遇到⼀个会⼀个。有可能做完50道贪⼼题⽬之后,第51
道还是没有任何思路。
8.3. 如何学习贪⼼?
先有⼀个认知:做了⼏⼗道贪⼼的题⽬,遇到⼀个新的⼜没有思路,这时很正常的现象,把⼼态放
平。
- 前期学习的时候,重点放在各种各样的策略上,把各种策略当成经验来吸收;
- 在平常学习的时候,我们尽可能的证明⼀下这个贪⼼策略是否正确,这样有利于培养我们严谨的思维。但是在⽐赛中,能想出来⼀个策略就已经不错了,如果再花费⼤量的时间去证明,有点得不偿失。这个时候,如果根据贪⼼策略想出来的若⼲个边界情况都能过的话,就可以尝试去写代码了。
8.4 相关算法题(2道)
1.4.1 货仓选址
https://www.luogu.com.cn/problem/P10452
#include <iostream>
#include<algorithm>
using namespace std;
const int N=1e5+10;
int n;
int a[N];
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+1+n);
long long ret=0;
for(int i=1;i<=n;i++)
{
ret+=abs(a[i]-a[(1+n)/2]);
}
cout<<ret;
return 0;
}
选中间位置即可
8.4.2 最大子段和
https://www.luogu.com.cn/problem/P1115
#include<iostream>
using namespace std;
const int N=2e5+10;
int a[N],n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
long long ret=-1e10;
long long sum=0;
for(int i=1;i<=n;i++)
{
sum+=a[i];
ret=max(sum,ret);
if(sum<0) sum=0;
}
cout<<ret<<endl;
return 0;
}
9、倍增算法
9.1 倍增,顾名思义就是翻倍。它能够使线性的处理转化为对数级的处理,极⼤地优化时间复杂度。
9.2.1 快速幂
https://www.luogu.com.cn/problem/P1226
#include<iostream>
using namespace std;
long long a,b,p;
long long qpow(long long a,long long b,long long p)
{
long long ret=1;
while(b)
{
if(b&1) ret=ret*a%p;
a=a*a%p;
b>>=1;
}
return ret;
}
int main()
{
cin>>a>>b>>p;
printf("%lld^%lld mod %lld=%lld",a,b,p,qpow(a,b,p));
return 0;
}
9.2.2 64位整数乘法
https://www.luogu.com.cn/problem/P10446
#include<iostream>
using namespace std;
typedef long long=LL;
LL a,b,p;
LL qmul(LL a,LL b,LL p)
{
LL sum=0;
while(b)
{
if(b&1) sum=(sum+a)%p;
a=(a+a)%p;
b>>=1;
}
return sum;
}
int main()
{
cin>>a>>b>>p;
cout<<qmul(a,b,p)<<endl;
return 0;
}
10 离散化
10.1 算法:
当题⽬中数据的范围很⼤,但是数据的总量不是很⼤。此时如果需要⽤数据的值来映射数组的下标
时,就可以⽤离散化的思想先预处理⼀下所有的数据,使得每⼀个数据都映射成⼀个较⼩的值。之后再⽤离散化之后的数去处理问题。
⽐如:[99, 9, 9999, 999999] 离散之后就变成[2, 1, 3, 4] 。
10.2 离散模版
离散化模板 排序+去重+⼆分离散化之后的值
模版一:
10.3 算法题
10.3.1 火烧赤壁
https://www.luogu.com.cn/problem/P1496
10.3.2
11、递归初阶
11.1 算法概念
11.2 算法题
11.2.1 汉诺塔
http://ybt.ssoier.cn:8088/problem_show.php?pid=1205
#include<iostream>
using namespace std;
int n;
char a,b,c;
void dfs(int n,char x,char y,char z)//把x上的盘子借助y的帮助全部放到z上
{
if(n==0) return;
dfs(n-1,x,z,y);
printf("%c->%d->%c\n",x,n,z);
dfs(n-1,y,x,z);
}
int main()
{
cin>>n>>a>>b>>c;
dfs(n,a,c,b);
return 0;
}
11.2.2 占卜diy
https://www.luogu.com.cn/problem/P10457
#include<iostream>
using namespace std;
int n=13,m=4;
int a[14][5];
int cnt[14];
void dfs(int x)
{
if(x==13) return;
int t=a[x][cnt[x]];
cnt[x]--;
dfs(t);
}
int main()
{
for(int i=1;i<=n;i++)
{
cnt[i]=4;
for(int j=1;j<=m;j++)
{
char x; cin>>x;
if(x>='2'&&x<='9') a[i][j]=x-'0';
else if(x=='A') a[i][j]=1;
else if(x=='J') a[i][j]=11;
else if(x=='Q') a[i][j]=12;
else if(x=='K') a[i][j]=13;
else a[i][j]=10;
}
}
for(int i=1;i<=m;i++)
{
dfs(a[n][i]);
}
int ret=0;
for(int i=1;i<=n;i++)
if(cnt[i]==0) ret++;
cout<<ret;
return 0;
}
12 、分治算法
12.1 算法概念
12.2 算法题
12.2.1 逆序对
https://www.luogu.com.cn/problem/P1908
#include <iostream>
using namespace std;
const int N=5e5+10;
int n;
int a[N],b[N];
long long merge(int left,int right)
{
if(left>=right) return 0;
long long ret=0;
int mid=(left+right)/2;
ret+=merge(left,mid);
ret+=merge(mid+1,right);
int cur1=left,cur2=mid+1,i=left;
while(cur1<=mid&&cur2<=right)
{
if(a[cur1]<=a[cur2]) b[i++]=a[cur1++];
else
{
ret+=mid-cur1+1;
b[i++]=a[cur2++];
}
}
while(cur1<=mid) b[i++]=a[cur1++];
while(cur2<=right) b[i++]=a[cur2++];
for(int j=left;j<=right;j++) a[j]=b[j];
return ret;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
cout<<merge(1,n);
return 0;
}
12.2.2 求第k小的数
https://www.luogu.com.cn/problem/P1923
#include<iostream>
#include<ctime>
using namespace std;
const int N = 5e6 + 10;
int n, k;
int a[N];
int getrand(int left, int right)
{
return a[rand() % (right - left + 1) + left];
}
int quick_select(int left, int right, int k)
{
if (left >= right) return a[left];
// 1. 选择基准元素
int p = getrand(left, right);
// 2. 数组分三块
int l = left - 1, i = left, r = right + 1;
while (i < r)
{
if (a[i] < p) swap(a[++l], a[i++]);
else if (a[i] == p) i++;
else swap(a[--r], a[i]);
}
// 3. 选择存在最终结果的区间
// [left, l] [l + 1, r - 1] [r, right]
int a = l - left + 1, b = r - 1 - l, c = right - r + 1;
if (k <= a) return quick_select(left, l, k);
else if (k <= a + b) return p;
else return quick_select(r, right, k - a - b);
}
int main()
{
scanf("%d%d", &n, &k);
k++;
srand(time(0));
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
printf("%d\n", quick_select(1, n, k));
return 0;
}