acwing算法基础课模版分析

文章目录

  • 前言:
  • 一.基础算法
    • 1. 快排模版
    • 2. 归并排序
    • 3. 整数二分算法
    • 4. 浮点数二分算法
    • 5. 一维前缀和数组
    • 6. 二维前缀和数组
    • 7. 一维差分数组
    • 8. 二维差分数组
    • 9. 位运算
    • 10. 离散化
    • 11. 区间和并
  • 二 .数据结构
    • 1. 静态单链表
      • 模版
      • 例题:
    • 2. 静态双向链表
      • 模版
      • 例题:
    • 3. 栈
      • 模版
      • 例题:
    • 4. 队列
      • 普通队列
        • 模版
        • 例题:
      • 循环队列
        • 模版:
    • 5. 单调栈
      • 模版:
    • 6. 单调队列
      • 模型:
      • 题目:[滑动窗口模型/单调队列](https://www.luogu.com.cn/problem/P1886)
    • 7. KMP算法
      • 模版
      • 题目:[KMP模版题](https://www.luogu.com.cn/problem/P3375)
    • 8. Trie树(字典树/前缀树)
      • 模版
    • 9. 并查集
      • 模版:
    • 10. 堆
      • 模版
      • 例题:[模拟堆](https://www.luogu.com.cn/problem/P3378)
    • 11. 一般哈希表
      • 模版1:拉链法
      • 模版二:开放寻址法
    • 12.字符串哈希
      • 模版
  • 三. 搜索与图论
    • 1.深度优先搜索(dfs)
      • 例题:[n皇后问题](https://icpc.qlu.edu.cn/p/1760)
      • 代码:
    • 2.宽度优先搜索(bfs)
      • 例题:[走迷宫](https://icpc.qlu.edu.cn/p/1863)
      • 代码:
    • 3. 树与图的存储
      • 存储模版(邻接表法):
    • 4. 树与图的遍历
      • dfs模版
        • 例题:[树的重心](https://icpc.qlu.edu.cn/p/2162)
        • 代码:
      • bfs模版
      • 例题:[图点的层次](https://icpc.qlu.edu.cn/p/2163)
      • 代码:
    • 5. 拓扑排序
      • 例题:[拓扑序列](https://icpc.qlu.edu.cn/p/1763)
      • 代码:
    • 6.最短路算法
      • 1. 朴素版dijkstra算法
      • 2.堆优化的dijkstra算法
      • 3. Bellman-Ford算法
        • 题目:
        • 代码:
      • 4. spfa 算法(队列优化的Bellman-Ford算法)
        • 例题:`最短路题目均可
        • 代码:
      • 5. spfa判断图中是否存在负环
        • 例题:[负环](https://www.luogu.com.cn/problem/P3385)
        • 代码:
      • 6. floyd算法
        • 模版:
    • 9. prim算法
      • 例题:[Prim算法求最小生成树](https://www.luogu.com.cn/problem/P3366)
      • 代码
    • 10. Kruskal算法
      • 例题:[Kruskal算法求最小生成树](https://www.luogu.com.cn/problem/P3366)
      • 代码:
    • 11. 染色法判别二分图
    • 12. 匈牙利算法
      • 例题:[二分图最大匹配](https://www.luogu.com.cn/problem/P3386)
      • 代码:
  • 四. 数学知识
    • 1.试除法判定质数
    • 2.试除法分解质因数
    • 3.朴素筛法求素数<埃氏筛法>
    • 4.线性筛法求素数<欧拉筛法>
    • 5.试除法求所有约数
    • 6.约数个数和约数之和
      • 例题:[约数个数](https://www.dotcpp.com/oj/problem1933.html?sid=9084378&lang=1#editor)
      • 代码:
    • 7.欧几里得算法<辗转相除法>

前言:

本博客借鉴acwing的模版分析,这里的大多数算法都是用数组模拟的,比如数组模拟指针!!

一.基础算法

1. 快排模版

void quick_sort(int q[], int l, int r)
{
	if (l >= r)return;
	int i = l - 1, j = r + 1, x = q[(r + l) >> 1];
	while (i < j)
	{
		do i++; while (q[i] < x);
		do j--; while (q[j] > x);
		if (i < j)swap(q[i], q[j]);
	}
	quick_sort(q, l, j), quick_sort(q, j + 1, r);
}

模版分析:

i = r -1, j = l + 1//这步相当于双指针,至于为什么要往两边移一格呢?
这是为了指针移动的时候第一次就到达边界并且判断,
还有就是交换后,重新循环必须得先移动指针i和j才行,故这样写简直完美!

do i++; while (q[i] < x);
do j- -; while (q[j] > x);
i是找到比x大的停止,j是找到比x小的停止

2. 归并排序

int tmp[50010] = { 0 };//辅助数组大小根据题目大小而定
void merge_sort(vector<int>&q, int l, int r)//归并模版
{
	if (l >= r)return;
	int mid = (r + l) >> 1;
	merge_sort(q, l, mid), merge_sort(q, mid + 1, r);//分治
	int i = l, j = mid+1, k = 0;
	while (i <= mid && j <= r)//和并到辅助数组
	{
		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++];
	for (int i = l, k = 0; i <= r; i++, k++)q[i] = tmp[k];//将排好序的辅助数组拷贝到原数组中,注意起始位置和终点位置
}

3. 整数二分算法

int bsearch_1(int q[],int l,int r,int x)
{
	while (l<r)
	{
		int mid = (l + r+1) >> 1;
		if (q[mid] <= x)l = mid;//这是找数组中最右边的x
		else r = mid - 1;
	}
	return l;
}
int bsearch_2(int q[], int l, int r, int x)
{
	while (l < r)
	{
		int mid = (l + r) >> 1;
		if (q[mid] >= x)r = mid;//这是找数组中最左边的x
		else l = mid + 1;
	}
	return l;
}

模版分析:

先分析这里的mid为什么有两种取法!
acwing算法基础课模版分析_第1张图片
由该图可知,mid的取法可以有这两种,而这两种分别对应着分割数组的边界!!!
先看当if(q[mid]<=x) l=mid;那么x应该在蓝色区域,所以我们的l应该等于(L+R+1)/2,否则就是在绿色区域,所以就是r=mid-1,所以我们这的mid应该为(L+R+1)/2,同理:当if(q[mid]>=x),那么x应该是在绿色区域,那么r应该为(L+R)/2,否则l应该为mid+1

为什么说返回的位置是x在数组中的最左边或者最右边呢??
我们单看if(q[mid]>=x)这一个条件就能懂了,就算q[mid]==x了,他还有往右缩小范围,所以只有当缩小到最后一个x的位置他就不往右缩小了,那么就只能往左缩小了,因为我们知道指针只能我那个中间缩小,是单调的!

4. 浮点数二分算法

这个算法一般是用来求开方后的结果的!!

用一个例题来理解!
给你一个数字x,请你求根号x的结果,保留六位小数!

double bsearch_3(double l, double r,double x)//l是从0开始,r就是x的值
{
	const double eps = 1e-6;
	while (r - l > eps)
	{
		double mid = (l + r) / 2;
		if (mid * mid >= x)r = mid;//这里不能用l代替x,因为l是会变的,x是固定的
		else l = mid;
	}
	return l;
}

模版分析:

图解:acwing算法基础课模版分析_第2张图片

5. 一维前缀和数组

举个例子你就知道什么叫前缀和了,已知原数组是a(下标从1开始,0位置数值为0),前缀和数组是s,s[i]=a[0]+a[1]+…+a[i]
,这就是前缀和!!!
前缀和的好处是当你频繁计算区间和时,支持O(1)复杂度计算!

for(int i = 1; i < size(); ++i)
	s[i] = s[i - 1] + a[i];//a数组是原数组,s数组是前缀和数组

性质:
求[L,R]区间和=>s[R]-S[L-1]

6. 二维前缀和数组

看图分析:
acwing算法基础课模版分析_第3张图片

for(int i=1;i<a.size();++i)
	for(int j=1;j<a[0].size();++j])
	{
		s[i][j]=s[i-1][j]+s[i][j-1]+s[i-1][j-1]+a[i][j];
	}

性质:
求以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为: S[x2][y2]-S[x2][y1-1]-S[x1-1][y2]+S[x1][y1]

7. 一维差分数组

举例说明什么是差分数组
原数组是a(下标从1开始,0位置数值为0),差分数组是b,
那么b[1]=a[1]-a[0],b[2]=a[2]-a[1], … ,b[i]=a[i]-a[i-1]
我们发现一个规律差分数组b的前缀和就是a数组
满足这样一个关系:
acwing算法基础课模版分析_第4张图片
我们利用这个性质可以得出一个结论:
我们想在一个区间[L,R]都加上c,我们只需b[L]+=c,b[R+1]-=c;然后对b取前缀和得出所需要的结果!
为什么我们b[L]+=c,b[R+1]-=c只经过这个操作就可以在一个区间上加上c呢?

因为差分数组其实就是存的相邻原数组之间的差值d,我们要求原数组只不过是将这些d加起来而已,也就是前缀和(a[i]=d1+d2+d3+d4+d5+…+di),你在某一个d上加上c后,那么后面的a不都是加上c了吗,同理你在某一个d上减去c后,不就是都减去c了吗!!

我们用过这个原理可以直接构造出差分数组,我们假设原数组都是0,那么差分数组也都是0,我们输入一个a[i],就就相当于在差分数组的i位置加上a[i],输入完a数组后,差分数组也就构造完成了!

void insert(int l, int r, int c)
{
	b[l] += c;
	b[r + 1] -= c;
}

8. 二维差分数组

有了一维差分的概念,二维差分我们看图应该可以理解!
我们假设在左上角(x1,y1),到右下角(x2,y2)这个区域加上c!
看图:
acwing算法基础课模版分析_第5张图片
那么根据该图得到的公式为:
b[x1][y1]+=c,b[x2+1][y1]-=c,b[x1][y2+1]-=c,b[x2+1][y2+1]+=c

void insert(int x1, int y1,int x2,int y2 int c)
{
	b[x1][y1] += c;
	b[x1][y2 + 1] -= c;
	b[x2 + 1][y1] -= c;
	b[x1 + 1][y1 + 1] += c;
}

输入一个 nn 行 mm 列的整数矩阵,再输入 qq 个操作,每个操作包含五个整数 x1x1,y1y1,x2x2,y2y2,cc,其中 (x1,y1)(x1,y1) 和 (x2,y2)(x2,y2) 表示一个子矩阵的左上角坐标和右下角坐标。
每个操作都要将选中的子矩阵中的每个元素的值加上 cc。
请你将进行完所有操作后的矩阵输出。
Input
第一行包含整数 nn,mm,qq。
接下来 nn 行,每行包含 mm 个整数,表示整数矩阵。
接下来 qq 行,每行包含 55 个整数 x1x1,y1y1,x2x2,y2y2,cc,表示一个操作。
数据范围:
1≤n,m≤10001≤n,m≤1000,
1≤q≤1000001≤q≤100000,
1≤x1≤x2≤n1≤x1≤x2≤n,
1≤y1≤y2≤m1≤y1≤y2≤m,
−1000≤c≤1000−1000≤c≤1000,
−1000≤−1000≤矩阵内元素的值≤1000≤1000
Output
共 nn 行,每行 mm 个整数,表示所有操作进行完毕后的最终矩阵。

#include
#include
using namespace std;
const int N = 1010, M = 1010;
 
int a[N][M], b[N][M];
 
void insert(int x1, int y1, int x2, int y2, int c)//插入操作
{
    b[x1][y1] += c;
    b[x1][y2 + 1] -= c;
    b[x2 + 1][y1] -= c;
    b[x2+1][y2+1] += c;
}
 
int main()
{
    int n, m, q;
    cin >> n >> m >> q;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
        {
            scanf("%d", &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, y1, x2, y2, c;
        cin >> x1 >> y1 >> x2 >> y2 >> c;
        insert(x1, y1, x2, y2, 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];//求前缀和
        }
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
            printf("%d ", b[i][j]);
        puts("");
    }
 
 
 
    return 0;
}

9. 位运算

求n的第k位数字: n >> k & 1
返回n的最后一位1:lowbit(n) = n & -n

10. 离散化

离散化就是一种哈希思想,对于一些连续的数据,其实也是映射的,比如数组中的数据,他是连续的,他是通过下边映射到对应的数据的,但是对于一些不连续的数据呢??? 那我们怎么办呢?如果通过数组下边映射存储的话,对应那些范围跨度小一点的还行,对应那些跨度非常大或者出现用负数映射的怎么办呢?我们可以用过一个离散化数组来存储映射的下标,然后用一个普通数组来存储对应的数据,但是我们怎么存呢?怎么通过离散化映射找到我们对应的数据呢? 我们这里先说说怎么存储吧,我们先将离散化数组排序,去重,然后通过find(二分查找)找到对应的下标,然后在普通数组中在该下标存储其数据! 所以我们想查找某个映射的数据,我们可以先find映射的下标,通过下标去查询数据!可能文字看不懂,我画个图帮助你理解!!!
acwing算法基础课模版分析_第6张图片
a是val数组,alls是key数组,alls先将key排序,去重,然后通过其在数组中的下标,去a中对应的下标存储对应的val

看一道离散化经典题目:
acwing算法基础课模版分析_第7张图片

	#include
	#include
	#include 
	using namespace std;
	typedef pair<int, int> PII;
	const int N = 300010;
	int a[N], s[N];
	vector<int> alls;//离散化数组
	vector<PII> add, query;//add是插入的x,c集合数组,query是询问结果区间的集合

	int find(int x)//二分查找
	{
		int l = 0, r = alls.size() - 1;
		int mid = (l + r) >> 1;
		while (l < r)
		{
			mid = (l + r) >> 1;
			if (alls[mid] >= x) r = mid;
			else l = mid + 1;
		}
		return l + 1;
	}

	int main()
	{
		int n, m;
		cin >> n >> m;
		while (n--)
		{
			int x, c;
			cin >> x >> c;
			add.push_back({ x,c });//将插入的x,c放到add中,这就是map映射关系
			alls.push_back(x);//插入key
		}
		while (m--)
		{
			int l, r;
			cin >> l >> r;
			query.push_back({ l,r });
			alls.push_back(l);//插入key
			alls.push_back(r);//插入key
		}
		sort(alls.begin(), alls.end());//排序
		alls.erase(unique(alls.begin(), alls.end()), alls.end());//去重
		for (auto& e : add)
		{
			int x = find(e.first);//得到key在数组中的下标
			a[x] += e.second;通过该下标去存储对应的val
		}
		for (int i = 1; i <= alls.size(); ++i)
			s[i] = s[i - 1] + a[i];//求前缀和数组
		for (auto& q : query)
		{
			int l = find(q.first), r = find(q.second);
			cout << s[r] - s[l - 1] << endl;
		}
		return 0;
	}

11. 区间和并

acwing算法基础课模版分析_第8张图片
分析:
先将区间排序,这个应该容易想的,区间之间的关系无非就相交,不相交,包含,这三种关系,可以先用两个指针st,ed指向前一个区间的两边,然后去更新他,当有区间交集的时候,就将当前区间和前一个区间合并,这里就是将指针ed移动而已,当没有交集就加入到新区间数组中去,以此类推!!要注意的是,我们遍历第一个区间的时候,我们可以将st,ed指向一个更小的区间(并不存在)

#include
#include
#include 

using namespace std;
typedef pair<int, int> PII;
vector<PII> segs;
vector<PII> tmp;
void merge(vector<PII>& segs)
{
	sort(segs.begin(), segs.end());//将所以的区间排序
	int st = -2e9, ed = -2e9;//先设置一个无穷小区间,在题目范围外的

	for (auto& seg : segs)
	{
		if (ed < seg.first)//没有交集时
		{
			if (ed != -2e9)tmp.push_back({ st,ed });//如果不是无穷小区间就将前一个区间加入到新区间集合中
			st = seg.first;//然后指针指向新区间
			ed = seg.second;
		}
		else
			ed = max(ed, seg.second);//如果有交集,就更新ed
	}
	if (ed != -2e9)tmp.push_back({ st,ed });//这是防止只有一个区间
}
int main()
{
	int n;
	cin >> n;
	while (n--)
	{
		int l, r;
		cin >> l >> r;
		segs.push_back({ l,r });
	}
	merge(segs);
	cout<<tmp.size();
	return 0;
}

二 .数据结构

1. 静态单链表

动态单链表请看C语言版单链表实现,本小结主要是为了实现静态单链表,有人会问,有了动态了,为什么要静态呢? 我想说的是在算法竞赛中,如果我们把时间都花在链表的开辟而超时了,那么不得可惜死了吗?

静态链表的实现思路是用数组模拟,我们创建一个e[N]和ne[N],e数组用来存元素,ne用来存下一个结点的索引,有人可能疑问,那么我们怎么对应某个元素和他的next指针呢?? 还记得我们上面的离散化思路吗,我们通过数组下标来一一对应他们!!

模版

#include
#include
using namespace std;

const int N = 100010;
//head是用来记录头结点的索引的,默认为-1
//e数组是用来存元素的值的
//ne数组用来存下一个结点的索引
//idx用来存新节点的索引
int head, e[N], ne[N], idx;
void init()
{
	head = -1;//定义一个头节点指向一个不存在的索引-1代表null
	idx = 0;//idx结点存在数组中的位置
}
void push_front(int x)//头插
{
	e[idx] = x;//存入元素
	ne[idx] = head;//改变指向,让新的结点指向头结点指向的结点
	head = idx++;//头结点再指向新节点的位置
}
void push(int k, int x)//在k位置后面插入结点
{
	e[idx] = x;//存入元素
	ne[idx] = ne[k];//改变指向,让新的结点指向k位置结点指向的结点
	ne[k] = idx++;//头结点再指向新节点的位置
}
void pop(int k)//删除k位置后面那个结点
{
	ne[k] = ne[ne[k]];//这步直接让k指向k后面的后面的结点,不需要考虑内存泄漏,毕竟是算法题,过了就行
}
void remove()//删除头结点
{
	head = ne[head];//头指向头结点指向的结点
}

例题:

acwing算法基础课模版分析_第9张图片
代码:

#include
#include
using namespace std;

const int N = 100010;
int head, e[N], ne[N], idx;
void init()
{
	head = -1;
	idx = 0;
}
void push_front(int x)
{
	e[idx] = x;
	ne[idx] = head;
	head = idx++;
}
void push(int k, int x)
{
	e[idx] = x;
	ne[idx] = ne[k];
	ne[k] = idx++;
}
void pop(int k)
{
	ne[k] = ne[ne[k]];
}
void remove()
{
	head = ne[head];
}
int main()
{
	int w, x, k;
	char i;
	cin >> w;
	init();
	while (w--)
	{
		cin >> i;
		if (i == 'H')
		{
			cin >> x;
			push_front(x);
		}
		else if (i == 'D')
		{
			cin >> k;
			if (!k)
				remove();
			else
				pop(k - 1);
		}
		else
		{
			cin >> k >> x;
			push(k - 1, x);
		}
	}
	for (int i = head; i != -1; i = ne[i])printf("%d ", e[i]);
	return 0;
}

2. 静态双向链表

没有双链表经验的可以先看看我的C语言版双链表,有了静态单链表的基础后我们看静态双向链表就轻松了许多

acwing算法基础课模版分析_第10张图片

模版

#include
using namespace std;

const int N = 100010;

//e数组用来存元素
//l数组用来存左边结点的索引
//r数组用来存右边结点的索引
//idx用来存目前用到哪个结点的索引
int e[N], l[N], r[N], idx;
void init()//初始化要说明一下,我们让0和1这两个结点作为双链表的两边边结点,看我注释的两行,因为我们都知道头尾的索引了,所以不需要向平常的双链表一样,通过头结点去找尾结点!!!!所以可以注释掉,写了也没关系
{
	r[0] = 1;
	l[1] = 0;
	//l[0] = 1;
	//r[1] = 0;
	idx = 2;
}
void insert(int k, int x)//在k结点的右边边插入
{
	e[idx] = x;
	l[idx] = k;
	r[idx] = r[k];
	l[r[k]] = idx;
	r[k] = idx;
	idx++;
}
void pop(int k)//删除k节点
{
	l[r[k]] = l[k];
	r[l[k]] = r[k];
}

例题:

acwing算法基础课模版分析_第11张图片
代码:

#include
using namespace std;

const int N = 100010;

//e数组用来存元素
//l数组用来存左边结点的索引
//r数组用来存右边结点的索引
//idx用来存目前用到哪个结点的索引
int e[N], l[N], r[N], idx;
void init()
{
	r[0] = 1;
	l[1] = 0;
	//l[0] = 1;
	//r[1] = 0;
	idx = 2;
}
void insert(int k, int x)//在k结点的右边边插入
{
	e[idx] = x;
	l[idx] = k;
	r[idx] = r[k];
	l[r[k]] = idx;
	r[k] = idx;
	idx++;
}
void pop(int k)//删除k节点
{
	l[r[k]] = l[k];
	r[l[k]] = r[k];
}


int main()
{
	int M, k, x;
	cin >> M;
	init();
	while (M--)
	{
		string op;
		cin >> op;
		if (op == "L")
		{
			cin >> x;
			insert(0, x);
		}
		else if (op == "R")
		{
			cin >> x;
			insert(l[1], x);
		}
		else if (op == "D")
		{
			cin >> k;
			pop(k + 1);
		}
		else if (op == "IL")
		{
			cin >> k >> x;
			insert(l[k + 1], x);
		}
		else
		{
			cin >> k >> x;
			insert(k + 1, x);
		}


	}
	for (int i = r[0]; i != 1; i = r[i])cout << e[i] << ' ';
	return 0;
}

3. 栈

模拟栈很容易,用一个st数组维护即可,用top代表着栈顶位置!

模版

#include
#include

using namespace std;

const int N = 100010;
int st[N], top;
//这里的top默认是0,我们压栈的时候是++top,说明是将数据压入1这个位置的,这只是按照我们现实模拟来的,你也可以按照自己喜好改变
void push(int x)//压栈
{
	st[++top] = x;
}
void pop()//出栈
{
	top--;
}
bool empty()//判空
{
	return top == 0;
}
int query()//栈顶元素
{
	return st[top];
}

例题:

acwing算法基础课模版分析_第12张图片
代码:


#include
#include

using namespace std;

const int N = 100010;
int st[N], top;
void push(int x)
{
	st[++top] = x;
}
void pop()
{
	top--;
}
bool empty()
{
	return top == 0;
}
int query()
{
	return st[top];
}
int main()
{
	int M, x;
	cin >> M;
	while (M--)
	{
		string s;
		cin >> s;
		if (s == "push")
		{
			cin >> x;
			push(x);
		}
		else if (s == "pop")
		{
			pop();
		}
		else if (s == "empty")
		{
			empty() == true ? (cout << "YES" << endl) : (cout << "NO" << endl);
		}
		else
		{
			cout << query() << endl;
		}

	}
	return 0;
}

4. 队列

普通队列

只要题目的操作次数明确了,你把这个队列数组的大小定义大一点,普通队列完全够用!!

模版
#include
#include
using namespace std;

const int N = 100010;
//q是队列数组
//h是头
//t是尾
int q[N], h, t;
void push(int x)
{
	q[t++] = x;
}
void pop()
{
	h++;
}
int front()
{
	return q[h];
}
int back()
{
	
	return q[t - 1];
}
bool empty()//判空
{
	return h>=t;//这个要不要大于要看具体题目,有的题目队列是空也是可是删除(也就是非法操作)
}
int query()//取元素
{
	return q[h];
}
例题:

acwing算法基础课模版分析_第13张图片

#include
#include
using namespace std;

const int N = 100010;
//q是队列数组
//h是头
//t是尾
int q[N], h, t;
void push(int x)
{
	q[t++] = x;
}
void pop()
{
	h++;
}
int front()
{
	return q[h];
}
int back()
{
	
	return q[t - 1];
}
bool empty()//判空
{
	return h>=t;//这个要不要大于要看具体题目,有的题目队列是空也是可是删除(也就是非法操作),本题就会非法操作,非常的坑!!!
}
int query()//取元素
{
	return q[h];
}

int main()
{
	int M, x;
	cin >> M;
	while (M--)
	{
		string s;
		cin >> s;
		if (s == "push")
		{
			cin >> x;
			push(x);
		}
		else if (s == "pop")
		{
			pop();
		}
		else if (s == "empty")
		{
			empty() == true ? (cout << "YES" << endl) : (cout << "NO" << endl);
		}
		else
		{
			cout << query() << endl;
		}

	}
	return 0;
}


循环队列

循环队列是为了充分使用队列数组而存在的,其本质只需控制边界!!!

思考方式:

一般情况下,循环队列的判空是判断头尾指针是否相等,判满是看尾指针+1是否等于头指针,也就是说数组大小为N,那么存储的数量只能是N-1个,但是我现在用普通队列的方式去思考循环队列呢?我们让头指针和尾指针只管++,通过取模的方式去映射到队列数组中去!!!

重点思考一下取模的原理:
acwing算法基础课模版分析_第14张图片

模版:
#include
#include
using namespace std;

const int N = 50 ;
//q是队列数组
//h是头
//t是尾
int q[N], h, t;
//h,t就当普通队列一样去迭代,通过取余就可以映射到数组中的位置
bool isFull()
{
	return t-h==N;
}
void push(int x)
{
	if (!isFull())//判断是否满
	{
		q[t++%N] = x;
	}

}
void pop()
{

	h++;
}
bool empty()
{
	return h >= t;
}

int query()
{
	return q[h%N];
}

5. 单调栈

单调栈用于处理一个数组中某个元素左边/右边第一个大于/小于的方案,可以将暴力解发的O(n^2)的复杂度优化到O(n)!

模版:

常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(st[tt], i)) tt -- ;
    st[ ++ tt] = i;
}

直接上题目分析:
链接:单调栈模版题
acwing算法基础课模版分析_第15张图片
分析:
acwing算法基础课模版分析_第16张图片
acwing算法基础课模版分析_第17张图片
这个用文字来讲不方便解释,有条件的同学可以去搜索一下视频看一下!!!
看代码:

#include
#include
#include

using namespace std;

const int N = 3e6 + 10;
int st[N], top, a[N];
vector<int> ans;
int main()
{
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++)
	{
		cin >> a[i];
	}
	for (int i = n; i >=1; i--)
	{
		while (top && a[st[top]] <= a[i])top--;

		if (top)
			ans.push_back(st[top]);
		
		else ans.push_back(0);
		st[++top] = i;
	}
	for (int i = ans.size() - 1; i >= 0; i--)
	{
		cout << ans[i] << ' ';
	}
	return 0;
}

6. 单调队列

模型:

常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
    while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头是否滑出窗口
    while (hh <= tt && check(q[tt], i)) tt -- ;
    q[ ++ tt] = i;
}

题目:滑动窗口模型/单调队列

acwing算法基础课模版分析_第18张图片
分析:

想要找区间的最小值,就是每入队一个数就让他和队尾比较,如果比队尾小,就删除队尾,一直循环,直到刚加入的元素比队尾大即可,这有就可以保证队头一直是最小的,因为队列的大小是固定的,像一个窗口,而每次入列是遍历数组入列的,每次只入列一个,所以像滑动一样,所以叫滑动窗口模型!!!,我们发现这个队列是会删除队尾的,所以他本质是一个双端队列!!!

#include
using namespace std;

const int N = 1e6 + 10;
int a[N];
int q[N], h, t=-1;
//队列,如果t初始值默认为-1的话,那么t就是指向队尾的那个元素,如果是0的话就是指向队尾后面的那个元素,这个看个人习惯
int main()
{
	int n, k;
	cin >> n >> k;
	for (int i = 0; i < n; i++)
		cin >> a[i];
	for (int i = 0; i < n; i++)
	{
		while (h <= t && i - k + 1 > q[h])h++;//判断队头是否滑出窗口(是窗口移动一格看队头是否滑出),这里要注意的是如果队头没有滑出的话,就得自己亲自使其滑出
		while (h <= t && a[i] < a[q[t]])//这步是看预加入的元素和队尾比较,如果加入之后破坏原来的单调性就得删除队尾元素,直到添加后依然现成单调性为止!!!
			t--;
		q[++t] = i;//插入元素
		if (i >= k - 1)cout << a[q[h]] << ' ';//这是保证第一次窗口元素全部入列
	}
	cout << endl;
	h = 0, t = -1;
	for (int i = 0; i < n; i++)
	{
		while (h <= t && i - k + 1 > q[h])h++;
		while (h <= t && a[i] > a[q[t]])
			t--;
		q[++t] = i;
		if (i >= k - 1)cout << a[q[h]] << ' ';
	}

	return 0;
}

7. KMP算法

模版

// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:

//本模版p数组的下标必须从1开始,而s数组是否从1开始不强求
for (int i = 2, j = 0; i <= m; i ++ )
{
    while (j && p[i] != p[j + 1]) j = ne[j];//下一个匹配不相等就回调,直到相等为止或者j==0
    if (p[i] == p[j + 1]) j ++ ;//相等那么就匹配下一个元素,所以j得++
    ne[i] = j;//每次迭代都要记录next值
}

// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
    while (j && s[i] != p[j + 1]) j = ne[j];//下一个匹配不相等就回调,直到相等为止或者j==0
    if (s[i] == p[j + 1]) j ++ ;//相等那么就匹配下一个元素,所以j得++
    if (j == m)//这就是匹配成功了
    {
    	// 匹配成功后的逻辑
        j = ne[j];//这是继续往下匹配第二个串了,如果只需第一次匹配成功,那么就直接break即可!
    }
}

匹配的逻辑分析图:
acwing算法基础课模版分析_第19张图片
next回调数组分析图
acwing算法基础课模版分析_第20张图片

题目:KMP模版题

s下标从1开始代码:

#include
using namespace std;
const int N=1e6+10;
char s[N],p[N];
int ne[N];
int main()
{
    cin>>(s+1)>>(p+1);
    int slen=strlen(s+1);
    int plen=strlen(p+1);
    for(int i=2,j=0;i<=slen;i++)
    {
        while(j&&p[i]!=p[j+1])j=ne[j];
        if(p[i]==p[j+1])j++;
        ne[i]=j;
    }
    
    for(int i=1,j=0;i<=slen;i++)
    {
        while(j&&s[i]!=p[j+1])j=ne[j];//如果j没回到起点,并且下一个匹配不相等就回退
        if(s[i]==p[j+1])j++;
        if(j==plen)
        {
            cout<<i-j+1<<endl;
            j=ne[j];//匹配下一个
        }
    }
    for(int i=1;i<=plen;i++)
    cout<<ne[i]<<' ';
    return 0;
}

s下标从0开始:

#include
using namespace std;
const int N = 1e6 + 10;
char s[N], p[N];
int ne[N];
int main()
{
	cin >> (s) >> (p + 1);
	int slen = strlen(s);
	int plen = strlen(p + 1);
	for (int i = 2, j = 0; i <= plen; i++)
	{
		while (j && p[i] != p[j + 1])j = ne[j];
		if (p[i] == p[j + 1])j++;
		ne[i] = j;
	}

	for (int i = 0, j = 0; i < slen; i++)
	{
		while (j && s[i] != p[j + 1])
			j = ne[j];//如果j没回到起点,并且下一个匹配不相等就回退
		if (s[i] == p[j + 1])j++;
		if (j == plen)
		{
			cout << i - j + 2 << endl;
			j = ne[j];//匹配下一个
		}
	}
	for (int i = 1; i <= plen; i++)
		cout << ne[i] << ' ';
	return 0;
}

8. Trie树(字典树/前缀树)

模版

//N是最大结点个数
int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量

// 插入一个字符串
void insert(char *str)
{
    int p = 0;
    for (int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';
        if (!son[p][u]) son[p][u] = ++ idx;//每个结点存储的是自己映射到一维数组的下标
        p = son[p][u];//自己变成儿子,这里是往下迭代
        //cnt[p] ++ ;前缀树就写这
    }
    cnt[p] ++ ;//求字符串个数写这
}

// 查询字符串出现的次数
int query(char *str)
{
    int p = 0;
    for (int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';
        if (!son[p][u]) return 0;//没有这个儿子什么,不存在这个字符串
        p = son[p][u];//往下迭代,迭代到最后找到字符串就提取到了自己在一维数组的下标
    }
    return cnt[p];//通过下标找到数量
}

前缀树是查找字符串是否存在和存在的个数/存在前缀的个数,也可以高效的排序字符串
解释模版:N是最大结点个数,一般题目都会说总字符串不大于N,如果说单个字符串不大于N,就得求一下总字符串长度来代替原来的N!正是因为这个我想了这个模版半天!!!
son用来记录当前结点儿子的,结点位置用idx来维护,N是最大节点数,所以son[N][26],只有当只有一个字符串的时候才会到底,这时就像链表一样的
acwing算法基础课模版分析_第21张图片

9. 并查集

并查集的功能:合并两个集合,询问两个元素是否在同一个集合中,也可以查询每个集合中的元素个数,也可以查询元素到祖宗结点的距离!

模版:

:find非递归版,建议使用递归版本,代码简介!!!
int find(int x)
{
	int st[N],top=0;
	while(p[x]!=x)
	{
		st[++top]=x;//将所有的子节点全部压入栈中(路径压缩)
		x=p[x];//向上遍历
	}
	while(top)p[st[top--]]=x;//将所有的子节点全部指向根节点
	return x;//返回根节点
	
}
(1)朴素并查集:

    int p[N]; //存储每个点的祖宗节点

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);


(2)维护size的并查集:

    int p[N], size[N];
    //p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量,合并的时候也要将size的值加上去

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        size[i] = 1;
    }

    // 合并a和b所在的两个集合:
    size[find(b)] += size[find(a)];//先计算size再合并千万反了
    p[find(a)] = find(b);


(3)维护到祖宗节点距离的并查集:

    int p[N], d[N];
    //p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x)
        {
            int u = find(p[x]);
            d[x] += d[p[x]];
            p[x] = u;
        }
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        d[i] = 0;
    }

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);
    d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量

acwing算法基础课模版分析_第22张图片

10. 堆

模版

// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;

// 交换两个点,及其映射关系
void heap_swap(int a, int b)//这边的交换是为了有些题目是要求操作第k个插入的结点
{
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a], hp[b]);
    swap(h[a], h[b]);
}

void down(int u)
{
    int t = u;//t为父节点
    if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;//让父节点和左儿子比较
    if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;//和左儿子比较后和右儿子比较
    if (u != t)//不是叶子结点
    {
        heap_swap(u, t);//交换
        down(t);//往下递归
    }
}

void up(int u)
{
    while (u / 2 && h[u] < h[u / 2])u是孩子,u/2是父亲
    {
        heap_swap(u, u / 2);
        u >>= 1;//向上遍历
    }
}

// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);//n/2是从下往上第一个非叶子结点


例题:模拟堆

#include
using namespace std;
const int N = 1e6 + 10;
int h[N], size;

void down(int u)
{
	int p = u;
	if (u * 2 <= ::size && h[u * 2] < h[p])p = 2 * u;
	if (u * 2 + 1 <= ::size && h[u * 2 + 1] < h[p])p = 2 * u + 1;
	if (u != p)
	{
		swap(h[u], h[p]);
		down(p);
	}
}
void up(int u)
{
	int p = u;
	while (u / 2 && h[u / 2] > h[u])
	{
		swap(h[u / 2], h[u]);
		u /= 2;
		
	}	
}
int main()
{
	int op, n, x;
	cin >> n;
	while (n--)
	{

		cin >> op;
		if (op == 1)
		{
			cin >> x;
			h[++::size] = x;

			up(::size);
		}
		else if (op == 2)
		{
			cout << h[1] << endl;
		}
		else
		{
			swap(h[1], h[::size--]);
			down(1);
		}

	}
	return 0;
}

11. 一般哈希表

模版1:拉链法

思路就是用过映射到一个数组h上,但是如果同时有很多的都是映射到同一位置,改怎么办呢,我们可以创建一个链表去接在h数组上!
长这个样子!acwing算法基础课模版分析_第23张图片

const int N = 10003;//这个最好取大于数据个数的第一个质数,这样能有效减少冲突
int h[N], e[N], ne[N], idx;
void insert(int x)
{
	int k = (x % N + N) % N;//因为负数mod还是负数,所以+N变成正数
	e[idx] = x;
	ne[idx] = h[k];
	h[k] = idx++;
}
bool find(int x)
{
	int k = (x % N + N) % N;//因为负数mod还是负数,所以+N变成正数
	for (int i = h[k]; i != -1; i = ne[i])//遍历链表
	{
		if (e[i] == x)return true;
	}
	return false;
}

模版二:开放寻址法

这个方法是通过一个数组来完成所有数据的映射的,他是将数据放入到映射位置往后的第一个不为空的位置(包括映射位置),就像上厕所一样,你找的第一个坑位有人,那么就去下一个…找到最后一个还有人就再去找第一个!
acwing算法基础课模版分析_第24张图片

const int N = 20003;//是数据范围的2~3倍的第一个质数

int h[N],null=0x3f3f3f3f;//null代表是空
int find(int x)
{
	int k = (x % N + N) % N;
	while (h[k] != null && h[k] != x)//如果这个位置不为空而且还不是x就往下找
	{
		k++;
		if (k == N)k = 0;//从头遍历
	}
	return k;//返回null位置或者是x所在的下标
}

12.字符串哈希

字符串哈希可以快速判断一个字符串中两个区域的子字符串是否相等的!!
字符串哈希该怎么实现呢?
我们可以将字符串看成是一个P进制的数!!
我们先用十进制来解释一下然后推广到P进制:
acwing算法基础课模版分析_第25张图片

模版

typedef unsigned long long ULL;
ULL h[1510], p[1510], P = 131;
// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
    h[i] = h[i - 1] * P + str[i];//计算前缀哈希值
    p[i] = p[i - 1] * P;//计算p的指数值
}

// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}	

三. 搜索与图论

1.深度优先搜索(dfs)

深度优先搜索就是沿着一条路走到黑再返回!

例题:n皇后问题

acwing算法基础课模版分析_第26张图片
分析:有题意可知,任意的皇后都不能在同一行或者同一列或者同一斜线上,故皇后肯定是每行都有一个的,那么我们只需要判断皇后在每行的那个位置即可,也就是按行遍历,那么我们怎么知道某一行或者某一列或者某一斜线上有皇后呢??
那么我们只能标记一下了,我们创建一个col表示竖向,创建一个dg表示左斜向,创建一个udg表示右斜向,那么我们的纵坐标就可以表示某一列了,这里有个难点就是如何表示某一条斜线,这里我们可以通过截距来表示,同一斜线的截距是相同的,假设右斜线是y=x+b那么b=y-x,由于y-x可能是负数,那么我们加个n确保是正数即可同理左斜线b=x+y

代码:

#include
using namespace std;
const int N = 11;
char q[N][N];
int n;
bool col[N], dg[2*N], udg[2*N];//斜线的截距范围是正方形边长的两倍
void dfs(int u)
{
	if (u == n)
	{
		for (int i = 0; i < n; i++)
		{
			for (int j = 0; j < n; j++)
			{
				cout << q[i][j];
			}
			cout << endl;
		}
		cout << endl;
		return;

	}
	for (int i = 0; i < n; i++)
	{
		if (!col[i] && !dg[u + i] && !udg[n - u + i])//剪枝,如果该列/左右斜向有皇后就跳过
		{
			q[u][i] = 'Q';//将皇后放入
			col[i] = dg[u + i] = udg[n - u + i] = true;//将该皇后的列和斜向改为true
			dfs(u + 1);//往下一行迭代
			col[i] = dg[u + i] = udg[n - u + i] = false;//回溯上来了就要恢复现场!!!
			q[u][i] = '+';
		}
	}

}
int main()
{

	cin >> n;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			q[i][j] = '+';
		}
	}

	dfs(0);
	return 0;
}

2.宽度优先搜索(bfs)

宽度优先搜索就是按层遍历,有了这个特性那么就可以求最短路问题(仅限权重相同的边才行),因为按层遍历,就是第一次找到答案的时候深度肯定是最小的.

例题:走迷宫

acwing算法基础课模版分析_第27张图片
分析:
我们从坐标(0,0)这个位置开始bfs,用一个数组d表示该点是起点的走了多少步来的,我们搜索上下左右四个方向的点,只要满足不越界,没走过,走得通即可,将找到的点的对于在d数组的值改成前一个位置+1即可

代码:

#include
#include
using namespace std;
const int N = 110;
typedef pair<int, int> PII;
int n, m;
int g[N][N];//地图
int d[N][N];//标记这个点是走多少步到的
queue<PII> q;//队列
void bfs(int i, int j)
{
	q.push({ i,j });
	d[0][0] = 0;
	int dx[] = { -1,0,1,0 }, dy[] = { 0,-1,0,1 };//通过改变坐标实现上下左右移动
	while (!q.empty())
	{
		auto t = q.front();//出队列
		q.pop();
		for (int i = 0; i < 4; i++)
		{

			int x = t.first + dx[i];
			int y = t.second + dy[i];
			if (x >= 0 && x < n && y >= 0 && y < m&&g[x][y] == 0 &&d[x][y]==0 )//满足条件的就入列
			{
				d[x][y] = d[t.first][t.second] + 1;
				q.push({ x,y });

			}
		}
	}
	cout << d[n - 1][m - 1];
}
int main()
{
	cin >> n >> m;
	for (int i = 0; i < n; i++)
		for (int j = 0; j < m; j++)
		{
			cin >> g[i][j];
		}
	bfs(0, 0);
	return 0;
}

3. 树与图的存储

树是中特殊的图,所以我们就讨论图即可,图分为有向图和无向图,无向图的每条边可以看为互相指着的有向图,故我们只需讨论有向图即可!!!

存储模版(邻接表法):

邻接表法就是说每个结点可以到达的点的集合!
acwing算法基础课模版分析_第28张图片

// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx;

// 添加一条边a->b
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

// 初始化
idx = 0;
memset(h, -1, sizeof h);

4. 树与图的遍历

dfs模版

int dfs(int u)
{
    st[u] = true; // st[u] 表示点u已经被遍历过

    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j]) dfs(j);
    }
}
例题:树的重心

acwing算法基础课模版分析_第29张图片

代码:
#include
#include
#include
using namespace std;
const int N = 1e5 + 10;
int h[N], e[2 * N], ne[2 * N], idx;

int ans = N;
int n, a, b;
bool st[N];//用来标记结点是否遍历过
void add(int a, int b)
{
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx++;
}

int dfs(int u)//删除节点u,求得到的连通块的最大值
{
	st[u] = true;
	int sum = 1; int ret = 0;//ret用来返回子树的最大值
	for (int i = h[u]; i != -1; i = ne[i])
	{
		int j = e[i];
		if (!st[j])
		{
			int s = dfs(j);//s是得到该子树的大小
			ret = max(s, ret);//ret来得到子树的最大值
			sum += s;//sum用来记录这棵树大小
		}
	}
	ret = max(ret, n - sum);//这边就记录一下删除节点u后,连通块的最大值
	ans = min(ans, ret);//这里更新连通块最大值中的最小值
	return sum;//返回树的大小
}

int main()
{

	cin >> n;
	memset(h, -1, sizeof h);
	for(int i=0;i<n-1;i++)
	{
		cin >> a >> b;
		add(a, b);
		add(b, a);
	}
	dfs(1);
	cout << ans;
	return 0;
}

bfs模版

queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);

while (q.size())
{
    int t = q.front();
    q.pop();

    for (int i = h[t]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true; // 表示点j已经被遍历过
            q.push(j);
        }
    }
}

例题:图点的层次

acwing算法基础课模版分析_第30张图片

代码:

#include
#include
#include
using namespace std;
const int N = 1e5 + 10;
int h[N], e[N], ne[N], idx;//存储图
int n, m, a, b;
queue<int> q;
int d[N];//存每个结点离1的距离
void add(int a, int b)
{
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx++;
}
void bfs()
{
	q.push(1);//先将1入列
	d[1] = 0;//自己离自己为0
	while (!q.empty())
	{
		int t = q.front();//出列
		q.pop();
		for (int i = h[t]; i != -1; i = ne[i])//将所有的连接点入列
		{
			int j = e[i];
			if (d[j]==-1)//如果没有遍历过
			{
				
				q.push(j);//就入列
				d[j] = d[t] + 1;//在前一层上+1表示该层里1的距离
			}
		}

	}
}
int main()
{
	cin >> n >> m;
	memset(h, -1, sizeof h);
	memset(d, -1, sizeof d);//因为题目说找不到就返回-1
	for (int i = 0; i < m; i++)
	{
		cin >> a >> b;
		add(a, b);
	}
	bfs();
	cout << d[n];
	return 0;
}

5. 拓扑排序

什么是拓扑排序?拓扑排序有什么用?
拓扑排序的结果就是出现在前面的点肯定是指向他后面的点的!
acwing算法基础课模版分析_第31张图片
则满足条件的拓扑排序可以为[1, 2, 3, 4, 5]
注意:带环的图是不存在拓扑排序的,假设排序好了,但是带环的图肯定有节点往前指的与拓扑排序的定义矛盾!!!
拓扑排序有什么用??
一般用于排序具有依赖关系的任务,比如a完成任务需要bc,b完成任务需要d…

例题:拓扑序列

acwing算法基础课模版分析_第32张图片

代码:

#include
#include
#include 
using namespace std;
const int N = 1e5 + 10;
int h[N], ne[N], e[N], idx;//邻接表存储图
int n, m, a, b;
int du[N];//用来定义每个结点的入度情况
queue<int> q;
int cnt;//用来记入队列结点的个数
void add(int a, int b)
{
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx++;
}
bool topsort()
{
	for (int i = 1; i <=n; i++)
		if (!du[i])//将所有入度为0的全部入列
		{
			cnt++;
			q.push(i);
		}
	while (!q.empty())
	{
		int t = q.front();
		q.pop();
		for (int i = h[t]; i != -1; i = ne[i])//将临界点入度-1为0的全部入列
		{
			int j = e[i];
			if (--du[j] == 0)
			{
				q.push(j);
				cnt++;
			}
		}
	}
	return cnt == n;//如果==n说明所有结点都入列了,否则就不存在拓扑序列
}



int main()
{
	cin >> n >> m;
	memset(h, -1, sizeof h);
	for (int i = 0; i < m; i++)
	{
		cin >> a >> b;
		add(a, b);
		du[b]++;//加边的时候就记录入度情况
	}
	if (topsort())puts("YES");
	else puts("NO");
	return 0;
}

6.最短路算法

acwing算法基础课模版分析_第33张图片

1. 朴素版dijkstra算法

稠密图我们用邻接矩阵来存储!
主体思想:就是找到当前最短路径的结点,用该节点再优化它能到达的下一个结点,然后将该节点设为遍历过了,重复至所有结点全部遍历完成!
例题:Dijkstra求最短路 I
代码讲解:

#include
#include
#include
using namespace std;

const int N = 510;
int g[N][N];//存边的权重
int dist[N];//最短路径的集合
bool st[N];//是否找到最短路径

int dijkstra(int n)
{
	memset(dist, 0x3f3f3f3f, sizeof dist);
	dist[1] = 0;//起始点的最短路径就是0
	//st[1] = true;//这是不对的,因为我们要通过最短路t更新下一次的结点,如果不遍历1,那么我们无法更新1的下一次位置的路径,重复下去就都推断不出来
	for (int i = 0; i < n - 1; i++)//遍历接下来的n-1给点
	{
		int t = -1;//用t代表当前离起点最近的点,-1就是没找到!
		for (int j = 1; j <= n; j++)//再所有点中遍历出里当前里起点最近的点
		{
			if (!st[j] && (t == -1 || dist[t] > dist[j]))//在j这个点没遍历过的情况下,看最短路找到没,没找到就将自己设为最短路,找到的话就和最新的最短路比较
				t = j;
		}

		for (int j = 1; j <= n; j++)//用刚刚得到的最短路径t,来更新t能到达的下一个结点
		{
			dist[j] = min(dist[j], dist[t] + g[t][j]);
		}
		st[t] = true;//本次循环t结点找到最短路径

	}
	if (dist[n] == 0x3f3f3f3f)return -1;
	return dist[n];
}

int main()
{
	int n, m;
	cin >> n >> m;
	memset(g, 0x3f3f3f3f, sizeof g);//用0x3f3f3f3f代表无穷
	for (int i = 0; i < m; i++)
	{
		int x, y, z;
		cin >> x >> y >> z;
		g[x][y] = min(g[x][y], z);//保留重边的最小值
	}
	int t = dijkstra(n);
	cout << t;
	return 0;
}

我们先分析一下代码复杂度,外面一层循环是n,里面一层循环是n+m,由于n远远大于m,所以复杂度大约是O(n^2)

2.堆优化的dijkstra算法

我们分析一下上面代码内部的for循环看看能不能优化,第一个for循环是用来查询当前没被遍历过的最短路径的,第二个for循环是用来更新当前遍历到的最短路的下一个结点的最短路的,
因此我们可以使用一个堆来维护第一个for,用堆来存储所有的最短路,所以查询的时候就是O(1),第二个for,我们就是向堆中添加更新好的最短路一共加m次,因为有m条边,复杂度为O(m*logn)

题目:【模板】单源最短路径(标准版)

代码:

#include
using namespace std;
const int N = 1e5 + 10;
typedef pair<int, int> PII;
int g[N], e[2*N], ne[2*N], idx, w[2*N];
bool st[N];
int dist[N];
int n, m, s, U, V, W;

void add(int a, int b, int c)
{
	e[idx] = b;
	w[idx] = c;
	ne[idx] = g[a];
	g[a] = idx++;
}

void dijkstra()
{
	memset(dist, 0x3f3f3f3f, sizeof dist);
	dist[s] = 0;//起始点是s
	priority_queue<PII, vector<PII>, greater<PII>> heap;
	heap.push({ 0,s });//second存编号,first存距离,因为Pair默认比较大小的原因
	while (heap.size())
	{
		auto t = heap.top();
		heap.pop();
		int ver = t.second, distance = t.first;//找到最短路
		if (st[ver])continue;//发现最短路已经记录过了,就继续
		st[ver] = true;//否则记录最短路

		for (int i = g[ver]; i != -1; i = ne[i])//更新后面能走到的结点
		{
			int j = e[i];//获取结点
			if (dist[j] > distance + w[i])//如果发现更小就加入更小的
			{
				dist[j] = distance + w[i];//更新最短路
				heap.push({ distance + w[i], j });
			}
		}
	}
}
int main()
{
	cin >> n >> m >> s;
	memset(g, -1, sizeof g);
	for (int i = 0; i < m; i++)
	{
		cin >> U >> V >> W;
		add(U, V, W);
	}
	dijkstra();
	for (int i = 1; i <= n; i++)
	{
		if (dist[i] == 0x3f3f3f3f)cout << INT32_MAX << ' ';
		else cout << dist[i] << ' ';
	}
}

3. Bellman-Ford算法

bellman-ford算法是一种整体更新的算法,只要更新n次即可最短路就可求出来了!
与dijkstra算法的区别:

    1. dijkstra算法是借助贪心思想,每次选取一个未处理的最近的结点,去对与他相连接的边进行松弛操作;贝尔曼福特算法是直接对所有边进行N-1遍松弛操作。

    2. dijkstra算法要求边的权值不能是负数;贝尔曼福特算法边的权值可以为负数,并可检测负权回路。
题目:

acwing算法基础课模版分析_第34张图片

代码:
#include
using namespace std;
const int N = 510,M=10010;
int dist[N], backup[N];
int n, m, k;
struct Edge//由于bellman_ford算法不需要有序的更新最短路,所以我们存储边权关系可以用一个结构体来维护
{
	int a, b, w;
}edge[M];

int bellman_ford()
{
	memset(dist, 0x3f3f3f3f, sizeof dist);
	dist[1] = 0;
	for (int i = 0; i < k; i++)//这个就是对多走k步的最短路,所以可以用于有限制步数的最短路题目,如果求整体最短路的话就设为n即可
	{
		memcpy(backup, dist, sizeof dist);//将上一次的最短路拷贝下来,因为下面更新最短路的时候可能对后面的更新造成影响
		for (int j = 0; j < m; j++)
		{
			int a = edge[j].a, b = edge[j].b, w = edge[j].w;
			dist[b] = min(dist[b], backup[a] + w);//有最短路就更新
		}
	}
	if (dist[n] > 0x3f3f3f3f / 2)return -1;//这里/2是因为我们这个是无序的更新的,所以后面没更新的结点就是无穷,如果用无穷的边权更新一下还是无穷,并没什么太大影响,但是值还是变了
	return dist[n];
}
int main()
{
	cin >> n >> m >> k;
	for (int i = 0; i < m; i++)
	{
		int a, b, w;
		cin >> a >> b >> w;
		edge[i] = { a,b,w };
	}
	int t = bellman_ford();
	cout << t;
	return 0;
}

4. spfa 算法(队列优化的Bellman-Ford算法)

bellman-Ford中更新最短路径的时候我们发现dist[b] = min(dist[b], backup[a] + w)这一步,在a有更新最短路后,那么b一定也会更新,所以我们用队列来存储更新的最短路,然后用每个更新过的最短路来更新下一次能到达的结点的最短路

例题:`最短路题目均可
代码:
#include
using namespace std;
const int N = 1e5 + 10;
typedef pair<int, int> PII;
int g[N], e[2 * N], ne[2 * N], idx, w[2 * N];
bool st[N];
int dist[N];
int n, m, s, U, V, W;

void add(int a, int b, int c)
{
	e[idx] = b;
	w[idx] = c;
	ne[idx] = g[a];
	g[a] = idx++;
}

void spfa()
{
	memset(dist, 0x3f3f3f3f, sizeof dist);
	dist[s] = 0;
	queue<int> q;//q中存储的是当前发现的最短路结点,当并不代表这这些结点的最短路就是最终结果
	q.push(s);
	st[s] = true;//存入队列,就是说明当前s就有较短短路了,后面可能有更优选择,但是现在还没发现
	while (!q.empty())
	{
		int t = q.front();//取出队列元素
		q.pop();
		st[t] = false;//设为false是为了接下来可以更新它
		for (int i = g[t]; i != -1; i = ne[i])//取出这个最短路来更新他接下来能到达的点
		{
			auto j = e[i];
			if (dist[j] > dist[t] + w[i])//如果所有结点都不能更新了,那么就一直出列,那么就得出最终结果了
			{
				dist[j] = dist[t] + w[i];//如果可以更新,那么就要加入队列中,因为它又找到的当前的最短路
				if (!st[j])
				{
					q.push(j);
					st[j] = true;
				}
			}

		}

	}
}
int main()
{
	cin >> n >> m >> s;
	memset(g, -1, sizeof g);
	for (int i = 0; i < m; i++)
	{
		cin >> U >> V >> W;
		add(U, V, W);
	}
	spfa();
	for (int i = 1; i <= n; i++)
	{
		if (dist[i] > 0x3f3f3f3f)cout << INT32_MAX << ' ';
		else cout << dist[i] << ' ';
	}
}

5. spfa判断图中是否存在负环

我们知道一个n个结点的无环最短路单位长度是n-1,如果这个最短路有环,那么单位长度就会大于等于n,我们可以通过这个来判断

例题:负环
代码:
#include
using namespace std;
const int N = 2010, M = 6010;
int h[N], e[M], ne[M], w[M], idx;
int dist[N];
int cnt[N];
bool st[N];
int U, V, W,n,m;
void add(int a, int b, int c)
{
	e[idx] = b;
	w[idx] = c;
	ne[idx] = h[a];
	h[a] = idx++;
}
bool spfa()
{
	memset(dist, 0x3f3f3f3f, sizeof dist);//如果题目给了起点就要初始化最大值,如果题目只问图中存不存在负环,那么就要将所有的点全部入列,这时就不要初始化!
	memset(st,0,sizeof st);
	memset(cnt, 0, sizeof cnt);

	dist[1] = 0;
	queue<int> q;
	q.push(1);
	st[1] = true;
	while (q.size())
	{
		int t = q.front();
		q.pop();
		st[t] = false;
		for (int i = h[t]; i != -1; i = ne[i])
		{
			int j = e[i];
			if (dist[j] > dist[t] + w[i])
			{
				dist[j] = dist[t] + w[i];
				cnt[j] = cnt[t] + 1;
				if (cnt[j] >= n)return true;
				if (!st[j])
				{
					q.push(j);
					st[j] = true;
				}
			}
		}
	}
	return false;
}


int main()
{
	int T;
	cin >> T;

	while (T--)
	{
			memset(h, -1, sizeof h);
			for(int i=0;i<idx;i++)
			{
			    e[i]=0,w[i]=0,ne[i]=0;
			}
			idx = 0;
			cin >> n >> m;

			for (int i = 0; i < m; i++)
			{
				cin >> U >> V >> W;
				add(U, V, W);
				if(W>=0)add(V, U, W);
			}
			if (spfa())cout << "YES"<<endl;
			else cout << "NO"<<endl;
	}


	return 0;
}

6. floyd算法

此方法基于动态规划,记住即可!
用于多次查询最短路!

模版:
初始化:
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;

// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

9. prim算法

prim算法适用于稠密图求最小生成树问题,思路是我们假设最小生成树的结点在一个集合中,一开始为空,我们放入距离集合最近的一个结点,然后用这个结点去更新下一个结点到达集合的最短距离.复杂度O(n^2+m)

例题:Prim算法求最小生成树

acwing算法基础课模版分析_第35张图片

代码

#include
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;//INF是正无穷
int g[N][N];//邻接矩阵存边点关系
int dist[N];//每个点到最小生成树集合的最短距离
bool st[N];//判断点是否在集合中
int n, m;
int a, b, c;
int prim()
{
	memset(dist, INF, sizeof dist);//初始距离为正无穷
	int res = 0;//权重和
	for (int i = 0; i < n; i++)//遍历n次,也就是将所有的点存入集合
	{
		int t = -1;//记录距离集合最短的点
		for (int j = 1; j <= n; j++)//遍历所有结点
			if (!st[j] && (t == -1 || dist[t] > dist[j]))t = j;//更新出里集合最近的点
		//这里的if(i)是第一次加入集合不要下面两步操作
		if (i && dist[t] == INF)return INF;//如果最近的点是正无穷,那么不存在最小生成树
		if (i)res += dist[t];
		st[t] = true;//设置存在集合中
		for (int j = 1; j <= n; j++)//用刚刚距离集合最短的点更新下一个结点到达集合最短距离
		{
			dist[j] = min(dist[j], g[t][j]);
		}
	}
	return res;
}
int main()
{
	cin >> n >> m;
	memset(g, INF, sizeof g);//初始边设为正无穷
	for (int i = 0; i < m; i++)
	{
		cin >> a >> b >> c;
		g[a][b] = g[b][a] = min(g[a][b], c);//无向图
	}
	int t = prim();
	if (t == INF)cout << "orz";
	else cout << t;
	return 0;
}

10. Kruskal算法

克鲁斯卡尔算法的核心思想是将所有的边小到大排序,然后从前往后利用并查集判断如果两个结点不在一个区域就加上那条边的权重.

例题:Kruskal算法求最小生成树

acwing算法基础课模版分析_第36张图片

代码:

#include
using namespace std;
const int N = 5010, M = 2 * 1e5 + 10, INF = 0x3f3f3f3f;
int n, m, a, b, c;
int p[N];//并查集
struct edge//存边
{
	int a, b, c;
	bool operator<(edge& w)
	{
		return c < w.c;
	}
}edges[M];
int find(int x)
{
	if (p[x] != x)p[x] = find(p[x]);
	return p[x];
}
int main()
{
	cin >> n >> m;
	for (int i = 0; i < m; i++)//存边
	{
		cin >> a >> b >> c;
		edges[i] = { a,b,c };
	}
	sort(edges, edges + m);//排序,核心
	for (int i = 0; i < n; i++)p[i] = i;
	int res = 0, cnt = 0;
	for (int i = 0; i < m; i++)
	{
		int a = edges[i].a, b = edges[i].b, c = edges[i].c;
		a = find(a), b = find(b);
		if (a != b)//不在一个区域就合并
		{
			p[a] = b;
			res += c;
			cnt++;
		}
	}
	if (cnt < n - 1)cout << "orz";
	else cout << res;
	return 0;
}

11. 染色法判别二分图

二分图的充要条件是不存在奇数结点的环

int n;      // n表示点数
int h[N], e[M], ne[M], idx;     // 邻接表存储图
int color[N];       // 表示每个点的颜色,-1表示未染色,1表示白色,2表示黑色

// 参数:u表示当前节点,c表示当前点的颜色
bool dfs(int u, int c)//dfs遍历判断是否是二分图,返回值就是返回是否为二分图
{
    color[u] = c;//染色
    for (int i = h[u]; i != -1; i = ne[i])//遍历该节点的所有子节点
    {
        int j = e[i];
        if (color[j] == -1)//如果没染过色
        {
            if (!dfs(j, 3-c)) return false;//往下遍历,如果返回值为false说明不是二分图,那么返回false
        }
        else if (color[j] == c) return false;//如果染过色看和父亲结点颜色是否一样,一样就返回false
    }

    return true;
}

bool check()
{
    memset(color, -1, sizeof color);//默认所有结点颜色为-1
    bool flag = true;//用于判断是否是二分图
    for (int i = 1; i <= n; i ++ )//遍历所有节点
        if (color[i] == -1)
            if (!dfs(i, 1))//如果dfs后不是二分图就设置flag的返回值为false
            {
                flag = false;
                break;
            }
    return flag;
}

12. 匈牙利算法

用于判断二分图最大匹配个数

例题:二分图最大匹配

acwing算法基础课模版分析_第37张图片

代码:

#include
using namespace std;
const int N = 510, M = 50010;
int h[N], e[M], ne[M], idx;
bool st[N];//判断是否遍历过改心仪的女生
int match[N];//用于保存女生的对象
int n, m, w;
void add(int a, int b)
{
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx++;
}
bool find(int x)
{
	for (int i = h[x]; i != -1; i = ne[i])//遍历男生心仪的所有女生
	{
		int j = e[i];
		if (!st[j])//如果没遍历过
		{	
			st[j] = true;//本次遍历就要设为true,表示遍历过了
			if (match[j] == 0 || find(match[j]))//这是说自己心仪的女生没有对象或者心仪的女生的男朋友有备胎,让他让给我即可,否则只能单身了
			{
				match[j] = x;
				return true;
			}
		}
	}
	return false;
}
int main()
{
	cin >> n >> m >> w;
	memset(h, -1, sizeof h);
	for (int i = 0; i < w; i++)
	{
		int a, b;
		cin >> a >> b;
		add(a, b);
	}
	int res = 0;

	for (int i = 1; i <= n; i++)
	{
		memset(st, 0, sizeof st);//每次遍历前都要将st初始化一下,因为st是所有的男生用来记录是否遍历过某个女生的,所有每次都要初始化一下
		if (find(i))res++;//遍历所有男生能找到对象就++
	}
	cout << res;
	return 0;
}

四. 数学知识

1.试除法判定质数

bool is_prime(int x)
{
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
            return false;
    return true;
}

2.试除法分解质因数

一个正数肯定可以分解成素数的形式

void divide(int x)
{
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
        {
            int s = 0;
            while (x % i == 0) x /= i, s ++ ;
            cout << i << ' ' << s << endl;
        }
    if (x > 1) cout << x << ' ' << 1 << endl;
    cout << endl;
}

3.朴素筛法求素数<埃氏筛法>

int primes[N], cnt;     // primes[]存储所有素数
bool st[N];         // st[x]存储x是否被筛掉

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i])//如果没有筛过,就是素数
        {
            primes[cnt ++ ] = i;//添加到素数表中
        	for (int j = i + i; j <= n; j += i)//用该素数将自己的所有倍数的结果(合数)都筛去
            st[j] = true;
        }

    }
}

4.线性筛法求素数<欧拉筛法>

由于埃氏筛法可能会重复筛去同一个合数,我们可以利用每个数的最小质因子来筛掉某一个合数,这样就不会重复了

int primes[N], cnt;     // primes[]存储所有素数
bool st[N];         // st[x]存储x是否被筛掉

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++ ] = i;//没被筛过就是素数
        for (int j = 0; primes[j] <= n / i; j ++ )//将我们所得到素数都作为最小质因子来筛去合数
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;//如果一个数可以被一个素数整除,那么就可以结束了,接下来的素数也不会是最小质因子了
        }
    }
}

5.试除法求所有约数

vector<int> get_divisors(int x)
{
    vector<int> res;
    for (int i = 1; i <= x / i; i ++ )
        if (x % i == 0)
        {
            res.push_back(i);
            if (i != x / i) res.push_back(x / i);//相等就不要重复加入了,比如2*2=4
        }
    sort(res.begin(), res.end());
    return res;
}

6.约数个数和约数之和

一个数可以拆解成他的所有质因数的组合,比如20=2 * 2 * 5=22 * 51 约数有1 2 4 5 10 20

如果 N = p1^c1 * p2^c2 * ... *pk^ck
约数个数: (c1 + 1) * (c2 + 1) * ... * (ck + 1)
约数之和: (p1^0 + p1^1 + ... + p1^c1) * ... * (pk^0 + pk^1 + ... + pk^ck)

例题:约数个数

代码:

#include
#include
using namespace std;
int main()
{
	unordered_map<int, int> primes;
	int n = 0;
	cin >> n;
	for (int i = 2; i <= n / i; i++)
	{
		while (n % i == 0)
		{
			n /= i;
			primes[i]++;
		}
	}
	if (n > 1)primes[n]++;//特判一下,如果这个数就是素数,那么它的最小质因子就是自己
	long long res = 1;
	for (auto prime : primes)
	{
		res = res * (prime.second + 1);
	}
	cout << res;
	return 0;
}

7.欧几里得算法<辗转相除法>

求最大公约数

int gcd(int a,int b)
{
	return b?gcd(b,a%b):a:
}

你可能感兴趣的:(算法,算法)