基础算法模板

基础算法

筛法

埃氏筛

核心思想:只要碰到素数,就将其倍数都筛掉,会有重启筛的情况
时间复杂度:\(O(nloglogn)\)

#include<bits/stdc++.h>
using namespace std;

int n = 0;
bool a[50000010] = {};

int main()
{
	scanf("%d", &n);
	for(int i=2; i<=n; i++)
	{
		if(a[i] == 0)
		{
			for(int j=i; j<=n/i; j++) a[i*j] = 1;
		}
	}
	for(int i=2; i<=n; i++)
		if(a[i] == 0) printf("%d\n", i);
} 

线性筛

核心思想:
任何合数都可以唯一表示为最小质因数与其他因数的乘积
基于以上原理,我们设计算法,使每个合数只被他的最小质因数筛掉
时间复杂度:\(O(n)\)

#include <bits/stdc++.h>
using namespace std;

/*
线性筛
核心思想:
任何合数都可以唯一表示为最小质因数与其他因数的乘积
基于以上原理,我们设计算法,使每个合数只被他的最小质因数筛掉
*/

int n = 0;
int prim[50000000] = {}, cnt = 0;	//prim存储所有素数,cnt为素数的数量
bool notprim[50000000] = {};	//notprim[i]标记i不是素数

int main()
{ 
	scanf("%d", &n);
	
	for(int i=2; i<=n; i++)
	{
		if(!notprim[i]) prim[++cnt] = i;
		for(int j=1; j<=cnt; j++)
		{
			if(i * prim[j] > n) break;
			notprim[i * prim[j]] = true;
			//prim[j]即为i的最小质因数
			//j后面的质数就没必要再乘了
			//比如:当i=9时,此时prim的值为2/3/5/7
			//当prim[j]=3时,跳出循环,后面的就不需要再处理了
			//假如再处理个5*9,标记上45,但45应该被3*15标记,即i=15时处理
			if(i % prim[j] == 0) break;	
		}
	}
	
	for(int i=1; i<=cnt; i++) 
	{
		printf("%d\n", prim[i]);
	}

	return 0;
}

分治

整数二分

bool check(int x)  //检查x是否满足某种性质

int l = 1, r = n, ans = 0;
while(l <= r)
{
  int mid = (l+r)>>1;
  if(check(mid)) { ans = mid; r = mid - 1; }
  else l = mid + 1;
}
printf("%d", ans);

浮点数二分

bool check(double x)  //检查x是否满足某种性质

double l = 1, r = n;
while(l + eps < r)
{
  double mid = (l+r)/2;
  if(check(mid)) r = mid; 
  else l = mid;
}
printf("%lf", l);

//或者写成这样
double l = 1, r = n;
for(int i=1; i<=100; i++)
{
  double mid = (l+r)/2;
  if(check(mid)) r = mid; 
  else l = mid;
}
printf("%lf", l);

蒙哥马利快速幂取模算法

void merge_sort(int q[], int l, int r)
{
  typedef long long ll;
  
  ll montgomery(ll a, ll b, ll c)
  {
  	ll ans = 1;
  	a = a % c;
  	while(b > 0)
  	{
  		if(b & 1) ans = (ans * a) % c;
  		b = b >> 1;
  		a = a * a % c;
  	}
  
	return ans;
}

排序算法

排序的稳定性是指排序完成后,相等数字的相对位置是否改变,不改变则是稳定排序,改变则是不稳定排序。
稳定排序:
插入排序
桶排序
归并排序
不稳定排序:
快速排序

快速排序

原理: 通过分治的方式将一个数组排序
快排分为三个过程:
1、将数列划分为两部分(要求保证相对大小关系)
2、递归到两个子序列中分别进行快速排序
3、不用合并,因为此时数列已经完全有序
时间复杂度: 快排的最优时间复杂度和平均时间复杂度为\(O(nlogn)\),最坏时间复杂度为\(O(n^2)\)
稳定性: 快排是一种不稳定的排序算法。
例题:
P1177 【模板】排序
https://www.luogu.com.cn/problem/P1177

#include <bits/stdc++.h>
using namespace std;
 
const int maxn = 1e5 + 10;

int n = 0; 
int a[maxn] = {};

void qsort(int q[], int l, int r)
{
	if(l >= r) return;
	
	int i = l - 1, j = r + 1;
	int x = q[(l + 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]);
	}
	qsort(q, l, j);
	qsort(q, j+1, r);
}

int main()
{       
	scanf("%d", &n);
	for(int i=1; i<=n; i++) scanf("%d", &a[i]);
	
	qsort(a, 1, n);
	for(int i=1; i<=n; i++) printf("%d ", a[i]);
 
	return 0;
} 

插入排序

原理: 将待排序元素划分为“已排序”和“未排序”两部分,每次从“未排序”元素中选择一个插入到“已排序”的元素中的正确位置。
时间复杂度:
最优时间复杂度为\(O(n)\),在数列几乎有序时效率很高。
最坏时间复杂度和平均时间复杂度都为\(O(n^2)\)
稳定性: 插入排序是一种稳定的排序算法。
例题:
该题用插入排序,有些点会TLE
P1177 【模板】排序
https://www.luogu.com.cn/problem/P1177

#include <bits/stdc++.h>
using namespace std;

const int maxn = 1e5 + 10; 

int n = 0;
int a[maxn] = {};

//插入排序
void insort(int arr[], int cnt)
{
	for(int i=2; i<=cnt; i++)
	{
		int key = a[i];	//将a[i]的值记录下来
		int j = i - 1;
		//从i-1开始往前找,找到第一个小于a[i]值的位置j
		//如果大于a[i]则不断往后移
		while(j >= 1 && arr[j] > key)
		{
			arr[j+1] = arr[j];
			j--;
		}
		arr[j+1] = key;
	}
}
 
int main() 
{   
	scanf("%d", &n);
	for(int i=1; i<=n; i++) scanf("%d", &a[i]);
	
	insort(a, n);

	for(int i=1; i<=n; i++) printf("%d ", a[i]);

    return 0;
}  

桶排序

桶排序适用于待排序数据值域较大但分布比较均匀的情况
排序过程:
1、直接设置n个桶,求出每个桶可放元素的范围
2、遍历序列,将元素一个个放到对应的桶中
3、对每个不是空的桶进行排序(插入排序)
4、从不是空的桶里把元素再放回原来的序列中
时间复杂度:
桶排序的平均时间复杂度为\(O(n+n^2/k+k)\)(将值域平均分成\(n\)块+排序+重新合并元素),当k≈n时为\(O(n)\)
桶排序的最坏时间复杂度为\(O(n^2)\)
稳定性: 最后的桶排序是一种稳定的,如果桶内排序使用插入排序,则整体是稳定的。
ps: 分成\(n\)个桶是按值域分的,不是按元素的个数
例题:
P1177 【模板】排序
https://www.luogu.com.cn/problem/P1177

#include <bits/stdc++.h>
using namespace std;

/*
直接开n个桶就行,因为这样时间复杂度最低
*/

const int maxn = 1e5 + 10; 

int n = 0;
int a[maxn] = {};  
vector<int> b[maxn];  
 
//插入排序
void insort(vector<int> &v)
{
	for(int i=1; i<v.size(); i++)
	{
		int key = v[i];
		int j = i - 1;
		while(j >= 0 && v[j] > key)
		{
			v[j+1] = v[j];
			j--;
		}
		v[j+1] = key;
	}
}
 
void bucketsort()
{ 
	//直接按值域开n个桶
	//求出每个桶可以放的数量,方便后边求每个元素应该放到哪个桶中
	int size = 1e9 / n + 1;			
	
	//将每个元素放到不同的桶中
	for(int i=1; i<=n; i++) 
	{
		b[a[i] / size].push_back(a[i]);
	}
	
	int p = 0;
	for(int i=0; i<n; i++)
	{
		insort(b[i]);	//对每个桶进行插入排序
		for(int j=0; j<b[i].size(); j++)
		{
			a[++p] = b[i][j];	//将每个桶中的元素放回原数组
		}
	}
}
 
int main() 
{   
	int x = 0;

	scanf("%d", &n);
	for(int i=1; i<=n; i++) scanf("%d", &a[i]);  
	
	bucketsort(); 

	for(int i=1; i<=n; i++) printf("%d ", a[i]);

    return 0;
}  

归并排序

原理: 归并排序基于分治思想将数组分段排序后再合并,合并是核心
时间复杂度:
时间复杂度在最优、最坏与平均情况下均为\(O(nlogn)\),空间复杂度为\(O(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);

	//回溯时进行合并
	//将[l, r]这段区间分为[l, mid]和[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++];
    }

	//将前mid前或mid后的剩余元素加入临时数组
    while(i <= mid) tmp[k++] = q[i++];
    while(j <= r) tmp[k++] = q[j++];

	//将临时数组中的元素,放到原数组,位置是对应的,相当于对这一段进行了排序
    for (i=l, j=0; i<=r; i++) q[i] = tmp[j++];
}

前缀和&差分

一维前缀和

主要用于求数列区间和的问题,是一种重要的预处理方式

#include <bits/stdc++.h>
using namespace std;

//a为原数组,s为前缀和
int a[10] = {}, s[10] = {};

int main()
{
	for(int i=1; i<=5; i++)
	{
		scanf("%d", &a[i]);
		s[i] = s[i-1] + a[i];
	}

	int l = 2, r = 4;
	//求a数组区间[l,r]的和
	int ans = s[r] - s[l-1];
	printf("%d", ans);

	return 0;
}

二维前缀和

1、计算前缀和
基于容斥原理
给定大小为\(m*n\)的二维数组\(A\),要求出其前缀和\(S\)。那么,\(S\)同样是大小为\(m*n\)的二维数组

\(S_{i,j}=\sum_{i'\leqslant i}\sum_{j'\leqslant j}A_{i',j'}\)
如下图:
image
\(S_{i-1,j}\)\(S_{i,j-1}\)会有\(S_{i-1,j-1}\)的重合,因此,这两部分相加后要减去重合的部分
所以二维前缀和的计算公式为:
\(S_{i,j}=S_{i-1,j}+S_{i,j-1}-S_{i-1,j-1}+A_{i,j}\)
2、计算子矩阵和
在已经预处理出二维前缀和后,要查询左上角为\((i_1, j_1)\)、右下角为\((i_2, j_2)\)的子矩阵的和,方式如下:
\(S_{i_2,j_2} - S_{i_1-1,j_2} - S_{i_2,j_1-1} + S_{i_1-1,j_1-1}\)
这可以在\(O(1)\)时间内完成。
在二维的情形,以上算法的时间复杂度可以简单认为是\(O(mn)\),即与给定数组的大小成线性关系。
例题:[HNOI2003]激光炸弹
https://h.hszxoj.com/d/hztg/p/HNOI2003?tid=67734b28c12f64c045802fa8

#include <bits/stdc++.h>
using namespace std;
 
/*
二维前缀和
*/
 
const int maxn = 1e4 + 10;

int n = 0, R = 0;
int a[5010][5010] = {}, s[5010][5010] = {};

int main()
{   
	int x = 0, y = 0, v = 0;
	int ans = 0;
	     
	scanf("%d%d", &n, &R);
	for(int i=1; i<=n; i++)
	{
		scanf("%d%d%d", &x, &y, &v);
		//让数组下标从1开始,方便计算前缀和
		a[x+1][y+1] = v;
	}
	 
	//求二维前缀和
	for(int i=1; i<=5001; i++)
	{
		for(int j=1; j<=5001; j++)
		{
			s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];
		}
	}
	
	//求边长为R的正方形的和的最大值
	for(int i=R; i<=5001; i++)
	{
		for(int j=R; j<=5001; j++)
		{
			ans = max(ans, s[i][j] - s[i-R][j] - s[i][j-R] + s[i-R][j-R]);
		}
	}
	
	printf("%d", ans);

	return 0;
} 

树上前缀和

一维前缀和还可以推广到有根树(树根为1)的情形。通过预处理前缀和,可以快速求解树上一段路径的权值和。

点权的情形

权值存储在结点处,设结点\(x\)处有权值\(a_x\),可以通过递推关系
\(S_1=a_1\),\(S_x=S_{\text{fa}(x)}+a_x\)
求出从根结点到结点\(x\)的路径上的节点的权值和,其中,\(fa(x)\)表示\(x\)的父节点。预处理完前缀和后,就可以通过
\(S_x + S_y - S_{\text{lca}(x,y)} - S_{\text{fa}(\text{lca}(x,y))}\)
计算连接节点\(x\)\(y\)的路径上的节点权值和。其中\(lca(x, y)\)表示节点\(x\)\(y\)最近公共祖先

边权的情形

权值储存在边上的情形几乎可以转化为点权的情形。
对于所有非根节点\(x \neq 1\),记\(edge(x)\)表示连接节点\(x\)和它的父节点\(fa(x)\)的边,那么可以假设边权存储在离根远的节点上,也就是说,节点\(x\)处存储的是边\(edge(x)\)上的边权,根节点处存储的权值是\(0\)
可以预处理出根节点到节点\(x\)的路径经过的所有边的权值和\(S_x\),此时,连接节点\(x\)\(y\)的路径上的节点权值和可以通过
\(S_x + S_y - 2 * S_{\text{lca}(x, y)}\)
进行查询。
注意:与点权的情形不同,所查询的权值和不包括\(lca(x, y)\)处的权值,因为它存储的边权不在所求路径中。

子树和

一般情况下,树上前缀和指的是自上而下计算的前缀和。
这里将自下而上计算的前缀和称为子树和
以节点\(x\)为根的子树的点权权值和,即相应的子树和,就是
\(T_x = \sum_{y \in \text{desc}(x)} a_x\)
其中,\(desc(x)\)表示\(x\)的所有子孙节点(包括其自身)的集合。
与树上前缀和不同,子树和并不能应用于\(O(1)\)求路径权值和,但是可以用于理解下文的树上差分

差分数组

1、解释
差分是一种和前缀和相对的策略,可以当做是求和的逆运算,定义方式如下:

\[b_i = \begin{cases} a_1 & i = 1 \\ a_i - a_{i - 1} & i \in [2, n] \end{cases} \]

2、性质
\(a_i\)\(b_i\)的前缀和,即$$a_j = \sum_{i=1}^{j} b_i$$
3、应用
通过差分数组,可以将原数组的区间修改操作转变为差分数组的单点修改操作,时间复杂度由\(O(n)\)降为\(O(1)\)
4、代码示例

b[1] = a[1];
for(int i=2; i<=n; i++)
{
	b[i] = a[i] - a[i-1]; 
}

树上差分

点差分

边差分

posted @ 2024-06-15 14:54  毛竹259  阅读(49)  评论(0)    收藏  举报