基础算法篇(蓝桥杯常考)

算法除了基础的之外还有搜索,数据结构(之前发过),动态规划,图论等,这篇文章先给大家介绍12种基础算法和一些例题。

1.高精度

当数据的值特别⼤,各种类型都存不下的时候,此时就要⽤⾼精度算法来计算加减乘除:
• 先⽤字符串读⼊这个数,然后⽤数组逆序存储该数的每⼀位;
• 利⽤数组,模拟加减乘除运算的过程。
⾼精度算法本质上还是模拟算法,⽤代码模拟⼩学列竖式计算加减乘除的过程

1.1加法
洛谷⾼精度加法

  1. ⽤字符串读⼊数据;
  2. 将字符串的每⼀位拆分,逆序放在数组中;(我们是从最低位开始加)
  3. 模拟列竖式计算的过程:
    a. 对应位累加;
    b. 处理进位;
    c. 处理余数。
  4. 处理结果的位数。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e6;
int l1, l2, l3 = 0;
int arr1[N], arr2[N], arr3[N];
void add(int arr3[], int arr2[], int arr1[])
{
	for (int i = 0; i < l3; i++)
	{
		arr3[i] += arr2[i] + arr1[i];// 对应位相加,再加上进位
		arr3[i + 1] += arr3[i] / 10;// 处理进位 
		arr3[i] = arr3[i] % 10; // 处理余数 
	}
	if (arr3[l3])l3++;
}
int main()
{
	string a; string b;
	cin >> a >> b;
	l1 = a.size(); l2 = b.size(); l3 = max(l1, l2);
	for (int i = 0; i < l1; i++)
	{
		arr1[l1 - i - 1] = a[i] - '0';
	}
	for (int j = 0; j < l2; j++)
	{
		arr2[l2 - j - 1] = b[j] - '0';
	}

	add(arr3 ,arr1, arr2);

	for (int i = l3-1; i >=0; i--)
	{
		cout << arr3[i];
	}
	return 0;
}

1.2 减法
洛谷高精度减法
1. ⽤字符串读⼊数据;
2. 判断两个数的⼤⼩,让较⼤的数在前。注意字典序vs数的⼤⼩
a. 位数相等:按字典序⽐较;
b. 位数不等:按照字符串的⻓度⽐较。
3. 将字符串的每⼀位拆分,逆序放在数组中;
4. 模拟列竖式计算的过程:
a. 对应位求差;
b. 处理借位;
5. 处理前导零。

#include<iostream>
using namespace std;
const int N = 1e6;
int a[N], b[N], c[N];
int la, lb, lc;
bool cmp(string& x,string& y)
{
	if (x.size() != y.size())return x.size()<y.size();
    return x < y;
}

void sub(int a[], int b[], int c[])
{
	for(int i = 0; i < lc; i++)
	{
		c[i] += a[i] - b[i];
		if (c[i] < 0)
		{
			c[i + 1] -= 1;
			c[i] += 10;
		}
	}
	while (lc > 1 && c[lc-1] == 0)lc--;
}
int main()
{
	string x, y;
	cin >> x >> y;
	if (cmp(x, y))
	{
		swap(x, y);
		cout << '-';
	}
	la = x.size(); lb = y.size(); lc = max(la, lb);
	for (int i = 0; i < la; i++)
	{
		a[la - i - 1] = x[i]-'0';
	}
	for (int j = 0; j < lb; j++)
	{
		b[lb - j - 1] = y[j]-'0';
	}
	sub(a, b, c);

	for (int i = lc - 1; i >= 0; i--)
	{
		cout << c[i];
	}
	return 0;
}

1.3乘法
洛谷高精度乘法
模拟⽆进位相乘再相加的过程:
a. 对应位求乘积;
b. 乘完之后处理进位;
c. 处理余数;

#include<iostream>
using namespace std;
const int N = 1e6;
int a[N], b[N], c[N];
int la, lb, lc;
void mul(int a[], int b[], int c[])
{
	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[la - i - 1] = x[i] - '0';
	}
	for (int j = 0; j < lb; j++)
	{
		b[lb - j - 1] = y[j] - '0';
	}
	mul(a, b, c);
	for (int i = lc - 1; i >= 0; i--)
	{
		cout << c[i];
	}
	return 0;
}

1.4除法
洛谷高精度除法
定义⼀个指针i 从「⾼位」遍历被除数,⼀个变量t 标记当前「被除的数」,记除数是b ;
• 更新⼀个当前被除的数t = t × 10 + a[i] ;
• t/b 表⽰这⼀位的商,t%b 表⽰这⼀位的余数;
• ⽤t 记录这⼀次的余数,遍历到下⼀位的时候重复上⾯的过程
被除数遍历完毕之后,t ⾥⾯存的就是余数,但是商可能存在前导0 ,注意清空。

#include<iostream>
using namespace std;
const int N = 1e6 + 10;
typedef long long LL;
int a[N], b, c[N];
int la, lc;
// ⾼精度除法的模板 - c = a / b (⾼精度 / 低精度) 
void sub(int c[], int a[], int b)
{
 LL t = 0; // 标记每次除完之后的余数 
 for(int i = la - 1; i >= 0; i--)
 {
 // 计算当前的被除数 
 t = t * 10 + a[i];
 c[i] = t / b;
 t %= b;
 }
 // 处理前导 0 
 while(lc > 1 && c[lc - 1] == 0) lc--;
}
int main()
{
 string x; cin >> x >> b;
 la = x.size();
 for(int i = 0; i < la; i++) a[la - 1 - i] = x[i] - '0';
 // 模拟除法的过程 
 lc = la;
 sub(c, a, b); // c = a / b
 for(int i = lc - 1; i >= 0; i--) cout << c[i];
 
 return 0;
}

2.枚举

顾名思义,就是把所有情况全都罗列出来,然后找出符合题⽬要求的那⼀个。因此,枚举是⼀种纯暴⼒的算法.使⽤枚举策略时,重点思考枚举的对象(枚举什么),枚举的顺序(正序还是逆序),以及枚举的⽅式(普通枚举?递归枚举?⼆进制枚举)。
⼀般情况下,枚举策略都是会超时的。此时要先根据题⽬的数据范围来判断暴⼒枚举是否可以通过。
如果不⾏的话,就要⽤后⾯要学的各种算法来进⾏优化(⽐如⼆分,双指针,前缀和与差分等),
下面会提到。
这里给大家介绍一下二进制枚举
力扣子集
枚举1~1<<n之间的数,每⼀个数的⼆进制中1的位置可以表⽰数组中对应位置选上该元
素。

class Solution 
{
public:
 vector<vector<int>> subsets(vector<int>& nums) 
 {
 vector<vector<int>> ret;
 int n = nums.size();
 
 // 枚举所有的状态 
 for(int st = 0; st < (1 << n); st++)
 {
 // 根据 st 的状态,还原出要选的数 
 vector<int> tmp; // 从当前选的⼦集 
 for(int i = 0; i < n; i++)
 {
 if((st >> i) & 1) tmp.push_back(nums[i]);
 }
 ret.push_back(tmp);
 }
 return ret;
 }
};

3.前缀和

前缀和与差分的核⼼思想是预处理,可以在暴⼒枚举的过程中,快速给出查询的结果,从⽽优化时间
复杂度。
是经典的⽤空间替换时间的做法。
3.1⼀维前缀和

原数组
arr[N]={1,2,3,4,5}
前缀和
f[N]={1,3,6,10,15}

创建前缀和数组:f[i] = f[i − 1] + a[i]
查询[l, r] 区间和:f[r] − f[l − 1]

3.2⼆维前缀和

  1. 创建前缀和矩阵:f[x1][y1] = f[x1][y1-1] + f[y1][x1 − 1] − f[x1− 1][y1 − 1] + a[x1][y1]
    在这里插入图片描述
  2. 查询以(x1 , y1 )为左上⻆(x2 , y2 )为右下⻆的⼦矩阵的和
    sum = f[x2][y2]-f[x2][y1-1]-f[x1-1][y2]+f[x1-1][y1-1]

激光炸弹洛谷

#include<iostream>
using namespace std;
const int N = 5e3 + 10;
int arr[N][N];
int f[N][N];
int main()
{
	int n, r;
	cin >> n >> r;
	while (n--)
	{
		int x, y,v = 0;
		cin >> x >> y >> v;
		x++; y++;
		arr[x][y] += v;  
	}
	n = 5001;
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= n; j++)
		{
			f[i][j] = f[i - 1][j] + f[i][j-1] - f[i - 1][j - 1] + arr[i][j];
		}
	}
	int ret = 0;
	int x1, x2, y1, y2;
	r = min(r, n);
	for (int x2 = r; x2 <= n; x2++)
	{
		for (int y2 = r; y2 <= n; y2++)
		{
			x1 = x2 - r + 1; y1 = y2 - r + 1;
			ret = max(ret, f[x2][y2] - f[x2][y1 - 1] - f[x1 - 1][y2] + f[x1 - 1][y1 - 1]);
		}
	}
	cout << ret;
	return 0;
}

4.差分

前缀和与差分的核⼼思想是预处理,可以在暴⼒枚举的过程中,快速给出查询的结果,从⽽优化时间复杂度。
4.1⼀维差分
原数组
arr[N]={1,2,3,4,5}
差分
f[N]={1,1,1,1,1}
1.创建差分数组,根据定义:f[i] = a[i] − a[i − 1]
2.若原数组前4个数+1
arr[N]={2,3,4,5,5}
差分
f[N]={2,1,1,1,0}
根据差分数组的性质处理区间修改:f[L] + = c, f[R + 1] − = c
3.还原经过q 次询问之后的a 数组:对差分数组做⼀次「前缀和」,就可以还原出原数组
f[1]=a[1]______a[1]=f[1]
f[2]=a[2]-a[1] ______a[2]=f[2]+a[1]=f[2]+[1]
f[i]=a[i]-a[i-1] ______a[i]=f[i]+f[i-1]+…f[1]
海底高铁洛谷

#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int n, m;
LL f[N]; // 差分数组 
int main()
{
 cin >> n >> m;
 // x->y
 int x; cin >> x;
 for(int i = 2; i <= m; i++)
 {
 int y; cin >> y;
 // x -> 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 - 1];
 // 直接求结果 
 LL ret = 0;
 for(int i = 1; i < n; i++)
 {
 LL a, b, c; cin >> a >> b >> c;
 ret += min(a * f[i], c + b * f[i]);
 }
 cout << ret << endl;
 return 0;
}

⼆维差分
在这里插入图片描述
f[y1][x1]+=k
但求前缀和之后会把其余地方也加上看,因此需要进行处理
f[y2+1][x1]-=k
f[y1][x2+1]-=k
f[y2+1][x2+1]+=k
利⽤前缀和还原出修改之后的数组
f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + f[i][j];

5.双指针

双指针算法有时候也叫尺取法或者滑动窗⼝,是⼀种优化暴⼒枚举策略的⼿段:
• 当我们发现在两层for循环的暴⼒枚举过程中,两个指针是可以不回退的,此时我们就可以利⽤
两个指针不回退的性质来优化时间复杂度。• 因为双指针算法中,两个指针是朝着同⼀个⽅向移动的,因此也叫做同向双指针。
唯一的雪花洛谷

#include <iostream>
#include <unordered_map>
using namespace std;
const int N = 1e6 + 10;
int n;
int a[N];
int main()
{
 int T; cin >> T;
 while(T--)
 {
 cin >> n;
 for(int i = 1; i <= n; i++) cin >> a[i];
 // 初始化 
 int left = 1, right = 1, ret = 0;
 unordered_map<int, int> mp; // 维护窗⼝内所有元素出现的次数 
 while(right <= n)
 {
 // 进窗⼝ 
 mp[a[right]]++;
 // 判断 
 while(mp[a[right]] > 1)
 {
 // 出窗⼝ 
 mp[a[left]]--;
 left++;
 }
 // 窗⼝合法,更新结果 
 ret = max(ret, right - left + 1);
 right++;
 }
 cout << ret << endl;
 }
 return 0;
}

6.⼆分算法

当我们的解具有⼆段性时,就可以使⽤⼆分算法找出答案:
• 根据待查找区间的中点位置,分析答案会出现在哪⼀侧;
• 接下来舍弃⼀半的待查找区间,转⽽在有答案的区间内继续使⽤⼆分算法查找结果。

// ⼆分查找区间左端点 
int l = 1, r = n;
while(l < r)
{
 int mid = (l + r) / 2;
 if(check(mid)) r = mid;
 else l = mid + 1;
}
// ⼆分结束之后可能需要判断是否存在结
// ⼆分查找区间右端点 
int l = 1, r = n;
while(l < r)
{
 int mid = (l + r + 1) / 2;
 if(check(mid)) l = mid;
 else r = mid - 1;
}
// ⼆分结束之后可能需要判断是否存在结

7.贪心

贪⼼算法,或者说是贪⼼策略:企图⽤局部最优找出全局最优。

  1. 把解决问题的过程分成若⼲步;
  2. 解决每⼀步时,都选择"当前看起来最优的"解法;
  3. "希望"得到全局的最优解。
    因为贪心算法简单的很简单难的很困难,之后另开一篇详说

8.倍增思想

倍增,顾名思义就是翻倍。它能够使线性的处理转化为对数级的处理,极⼤地优化时间复杂度

#include <iostream>
using namespace std;
typedef long long LL;
LL a, b, p;
// 快速幂的模板 
LL qpow(LL a, LL b, LL p)
{
 LL 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.离散化

当题⽬中数据的范围很⼤,但是数据的总量不是很⼤。此时如果需要⽤数据的值来映射数组的下标时,就可以⽤离散化的思想先预处理⼀下所有的数据,使得每⼀个数据都映射成⼀个较⼩的值。之后再⽤离散化之后的数去处理问题。
⽐如:[99, 9, 9999, 999999] 离散之后就变成[2, 1, 3, 4]

// 离散化模板:排序 + 去重 + ⼆分查找离散之后的结果 
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N];
int pos; // 标记去重之后的元素个数
int disc[N]; // 帮助离散化 
// ⼆分 x 的位置 
int find(int x)
{
 int l = 1, r = pos;
 while(l < r)
 {
 int mid = (l + r) / 2;
 if(disc[mid] >= x) r = mid;
 else l = mid + 1;
 }
 return l;
}
int main()
{
 cin >> n;
 for(int i = 1; i <= n; i++) 
 {
 cin >> a[i];
 disc[++pos] = a[i];
 }
 // 离散化 
 sort(disc + 1, disc + 1 + pos); // 排序 
 pos = unique(disc + 1, disc + 1 + pos) - (disc + 1); // 去重 
 for(int i = 1; i <= n; i++)
 {
 cout << a[i] << "离散化之后:" << find(a[i]) << endl;
 }
 return 0;
}

10.递归

  1. 先找到相同的⼦问题->确定函数的功能以及函数头的设计;
  2. 只关⼼某⼀个⼦问题是如何解决的->函数体
  3. 不能继续拆分的⼦问题->递归出口

11.分治

分治,字⾯上的解释是「分⽽治之」,就是把⼀个复杂的问题分成两个或更多的相同的⼦问题,直到
最后⼦问题可以简单的直接求解,原问题的解即⼦问题的解的合并。
一般分为左中右

12.模拟

模拟,顾名思义,就是题⽬让你做什么你就做什么,考察的是将思路转化成代码的代码能⼒。
一般都是简单题,但也有例外。
这里就给大家介绍一道比较经典的例题。
洛谷蛇形方阵
可以定义两个方向数组。

#include<iostream>
using namespace std;
const int N = 15;
int tmp[N][N];
int main()
{
	int x = 1;
	int y = 1;
	int pos = 0;
	int a = 0; int b = 0;
	int dx[] = { 0,1,0,-1 };
	int dy[] = { 1,0,-1,0 };
	int n;
	cin >> n;
	for(int i=1;i<=n*n;i++)
	{
		tmp[x][y] = i;
		a = x + dx[pos]; b = y + dy[pos];
		if (a > n || a<1 || b>n || b < 1||tmp[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", tmp[i][j]);
		}
		printf("\n");
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值