OK啊朋友们,也是很久没有更新博客了啊。这一个月专注于备战蓝桥杯省赛,最终也是拿下了C++组省一的一个成绩啊,虽然大家都说蓝桥杯水分大,但是我认为这也是一次不错的竞赛经验。今天和大家分享一下这一个月遇到的一些个人认为值得了解的一些算法思路
快速幂
首先一个就是基础的一个快速幂算法,这个算法属于是写起来简单,但是实际用途真的超乎想象的,能在竞赛中为我们节省很多很多很多的时间。
我们先来看看普通的求幂次的做法,当我们求n的x次方时需要遍历x次,每次将答案乘n,当x是一个相当大的数的时候,这个循环的次数可是千万甚至上亿的次数,程序要运行多久先不说,还可能异常退出,根本得不到答案
所以我们引入快速幂的算法,我们将幂次转换为二进制形式,如2的5次幂:32
幂数可以转换成:101,即5 = 4+0+1
从底数a开始,不断平方:a^1,a^2,a^4......(对应二进制位的权重)
当幂数的当前二进制位是1时,就将此时平方结果乘入最终答案。
当然,竞赛题中对于这么大的一个数一般会让我们对答案进行取余操作,我们就在每次计算时进行一个取余操作就行。
做法:
遍历b的每一个二进制位,不断将底数平方,同时根据当前位决定是否累乘
①逐位判断幂次的二进制是否为1
②如果是1,就将答案乘上n
③每次将n变成n*n,因为二进制每增加一位,要乘的数就变为原来的平方
④每次将m右移一位
代码实现:
cpp
long long func(long long n,long long x) {
int res = 1;
while (x) {
if ((x & 1) == 1) {//如果当前二进制位是1
res = (res * n) % N;//累乘当前数
}
n = (n * n) % N;//底数平方
x = x >> 1;//幂数右移一位
}
return res;
}
0-1BFS
0-1BFS绝对是大部分初学者没有学习过的一个进阶算法,对于普通的BFS来说,我们认为所有边的权值都是相同的(一般为1)。
而有的时候,会出现移动需要代价(即边权为1)和不需要代价(即边权为0)的两种情况,这个时候普通的BFS就无法满足我们的需求。
我们来看一个例题:
高桥君居住的城镇由 H 行 W 列的网格状区域构成,每个区域是道路或墙壁。
将从上往下第 i 行(1≤i≤H)、从左往右第 j 列(1≤j≤W)的区域表示为区域 (i,j)。
各区域的信息由 H 个长度为 W 的字符串 S1,S2,...,SH 给出。具体来说,当 Si 的第 j 个字符(1≤i≤H,1≤j≤W)为 . 时,区域 (i,j) 是道路;当为 # 时,区域 (i,j) 是墙壁。
高桥君可以按任意顺序重复执行以下两种操作:
- 移动到上下左右相邻的、位于城镇内且为道路的区域。
- 选择一个上下左右方向,进行前踢。当高桥君进行前踢时,可以将当前区域在该方向上 前 1 格 和 前 2 格 的区域(如果它们是墙壁)变为道路。注意:即使前 1 格或前 2 格位于城镇外,仍然可以进行前踢操作,但城镇外的区域不会发生变化。
高桥君最初位于区域 (A,B),想要到达位于区域 (C,D) 的鱼店。
保证高桥君初始所在的区域及鱼店所在的区域是道路。
请计算高桥君到达鱼店所需的最小前踢次数。
这题就是很经典的0-1BFS问题,每次行动都有移动(不需要代价,边权为0)和前踢(需要代价,边权为1)的两种情况。而我们要做的是找到两个点之间最小前踢次数的移动路线
思路:
①和普通BFS不同,这里我们需要使用到双端队列,将不需要代价的移动放入队头,优先取出处理,需要代价的前踢放入队尾,延迟处理。并保持层数的递增
②使用一个cnt[i][j]记录到达(i,j)这个点最小的前踢次数。
③对于每次取出的点,我们用cur记录该点的最小前踢次数,对于下一个点,如果可以直接移动,我们就判断cnt[i][j]和cur的大小(即判断最小前踢次数是否大于当前前踢次数)来决定是否进行更新;如果是需要前踢到达,我们就判断cnt[i][j] 和cur+1的大小(即判断最小前踢次数是否大于当前前踢次数+1)来决定是否更新。
代码实现:
cpp
#include<bits/stdc++.h>
using namespace std;
int a, b, c, d;
int h, w;
//mp数组记录图,cnt数组记录前踢次数
char mp[1005][1005];
int cnt[1005][1005];
//方向数组
int dx[] = { 1,-1,0,0 };
int dy[] = { 0,0,1,-1 };
void bfs(int x,int y) {
deque<pair<int, int>>q;//双端队列
cnt[x][y] = 0;//起点前踢次数为0
q.push_back({ x,y });
while (!q.empty()) {
int curx = q.front().first;
int cury = q.front().second;
q.pop_front();
//获取当前点的前踢次数
int cur = cnt[curx][cury];
//如果当前点就是终点,直接输出
if (curx == c && cury == d) {
cout << cur << endl;
return;
}
//找能直接移动到达的点
for (int i = 0;i < 4;i++) {
int nxtx = curx + dx[i];
int nxty = cury + dy[i];
//判断越界
if (nxtx<1 || nxtx>h || nxty<1 || nxty>w) {
continue;
}
//判断是否可以更新
//条件是该点是道路并且前踢次数大于当前前踢次数
if (mp[nxtx][nxty] == '.' && cnt[nxtx][nxty] > cur) {
cnt[nxtx][nxty] = cur;
q.push_front({ nxtx,nxty });
}
}
//找需要前踢到达的点
for (int i = 0;i < 4;i++) {
//因为前踢可以开拓1格或2格
for (int j = 1;j <= 2;j++) {
int nxtx = curx + dx[i] * j;
int nxty = cury + dy[i] * j;
//判断越界
if (nxtx<1 || nxtx>h || nxty<1 || nxty>w) {
continue;
}
//判断是否已经有次数更少的路径了
if (cnt[nxtx][nxty] <= cur + 1) {
continue;
}
//更新cnt数组,并入队
cnt[nxtx][nxty] = cur + 1;
q.push_back({ nxtx,nxty });
}
}
}
}
int main() {
cin >> h >> w;
for (int i = 1;i <= h;i++) {
string s;
cin >> s;
for (int j = 1;j <= w;j++) {
mp[i][j] = s[j - 1];
}
}
//将次数初始化为无穷大,方便更新最小次数
for (int i = 1;i <= h;i++) {
for (int j = 1;j <= w;j++) {
cnt[i][j] = 1e9;
}
}
cin >> a >> b >> c >> d;
bfs(a, b);
return 0;
}
当然,如果一个图中的边权不止这两种,而是有很多很多种边权,那我们就用Dijkstra最短路算法去找到达目标点的最短路径,感兴趣的可以看看主页的最短路算法。详细讲解了单源最短路和全源最短路算法
实现可删除的堆结构
C++中的堆结构大家应该都不陌生,使用大根堆 / 小根堆取一组数据中的最大值 / 最小值相信大家都是非常熟练的。
但是堆结构有一个致命的缺点就是无法直接删除堆中的某个元素,只能删除头部或尾部元素。这就使得堆在一些问题中的使用非常局限,例如给你一组乱序的数据,要求你每次删除最小值,并且把删除的最小值的左右相邻元素加上这个最小值。此时就遇到一个很尴尬的问题:如果不使用小根堆,每次都遍历找最小值,时间复杂度早就爆了,而只用一个小根堆又修改左右相邻元素。
所以我们可以使用其他数据结构相互配合,手动实现一个可删除的堆结构
思路:
①使用两个数组L和R模拟双向链表:
因为删除元素后,其他元素的索引会发生变化,所以需要用双向链表记录他们的相对位置
②使用标记数组记录哪些元素被删除或已经被修改:
因为更新左右相邻元素后,原本的旧数据还留在堆中,需要标记来识别。
③小根堆(优先队列)
需要直接访问得到当前的最小值,避免遍历操作,堆中元素是{值,索引}。
代码实现:
cpp
#include<bits/stdc++.h>
using namespace std;
int main() {
int n, k;
cin >> n >> k;
//使用的数据结构
vector<long long>vec(n + 1);
vector<int>L(n + 1), R(n + 1);
vector<bool>flag(n + 1, false);
priority_queue <pair<long long, int>,
vector<pair<long long, int>>,
greater<pair<long long, int>>>q;
//输入数据,并初始化
for (int i = 1;i <= n;i++) {
cin >> vec[i];
L[i] = i - 1;
if (i == n) {
R[i] = 0;
}
else {
R[i] = i + 1;
}
q.push({ vec[i],i });
}
while (k--) {
//找目标值
while (!q.empty()) {
long long val = q.top().first;
int index = q.top().second;
//如果已经被删除或者已经被修改,就在堆中删除该元素
if (flag[index] || vec[index] != val) {
q.pop();
}
else {
break;
}
}
//处理目标值,并获取他的值,索引和左右索引
//标记为已删除
long long val = q.top().first;
int index = q.top().second;
q.pop();
int l = L[index];
int r = R[index];
flag[index] = true;
//链表中删除元素
//如果不是最左边的元素,就让他左边元素的右边节点等于他的右边节点
//右边也同理
if (l != 0) {
R[l] = r;
}
if (r != 0) {
L[r] = l;
}
//将新元素入队
if (l != 0) {
vec[l] += vec[index];
q.push({ vec[l],l });
}
if (r != 0) {
vec[r] += vec[index];
q.push({ vec[r],r });
}
}
//输出最后答案,即数组中未被删除部分
for (int i = 1;i <= n;i++) {
if (!flag[i]) {
cout << vec[i] << " ";
}
}
return 0;
}
这就是今天我给大家分享的三种个人认为相对重要的算法结构。当然,算法的世界很大,肯定还有很多算法结构值得我们大家一起学习,欢迎大家给主播提提意见并一起分享你们遇到的算法难题。