根据时间复杂度选择算法
主体内容参考下面,
算法基础简介 - OI Wiki (oi-wiki.org)
AcWing《算法基础课》第2章 数据结构 - AcWing
AcWing 算法基础课笔记 3.搜索与图论(持续更新)_acwing搜索课件-CSDN博客
ACwing算法基础课全程笔记(2021年8月12日开始重写+优化)_acwing的csp辅导课更新到什么时候-CSDN博客
一维差分
下面程序的主要功能是处理一个数组的区间加操作。首先,程序定义了两个数组a和b,其中a数组用于存储原始数据,b数组用于存储每个位置的累加值(前缀和)。然后,程序使用insert函数在b数组上执行区间加操作。在执行完所有操作后,程序输出b数组的每个位置的值,即为每个位置从1到当前位置的和。
程序的实现思路是使用差分数组的方法来处理区间加操作。差分数组是一种用于处理区间加操作的数据结构,可以在O(1)的时间复杂度内完成区间加操作。具体来说,对于数组b,b[i]表示a[i]与a[i-1]的差值。当我们需要在区间[l, r]内加上c时,只需要在b[l]上加上c,在b[r+1]上减去c即可。这样,我们就可以在O(1)的时间复杂度内完成区间加操作。
最后,程序使用前缀和的方法来输出每个位置的值。前缀和是一种用于处理数组区间和的数据结构,可以在O(1)的时间复杂度内完成数组区间和的查询。具体来说,对于数组b,b[i]表示从1到i的和。当我们需要查询区间[l, r]的和时,只需要计算b[r] - b[l-1]即可。这样,我们就可以在O(1)的时间复杂度内完成数组区间和的查询。
#include<iostream>
using namespace std;
const int N = 100010;
int a[N], b[N]; //全局数组,默认初始化为全0
// insert函数用于在区间[l, r]内加上c
void insert(int l, int r, int c) {
b[l] += c; // 在l位置加上c
b[r + 1] -= c; // 在r+1位置减去c,这样可以保证区间内加上c,区间外不受影响
}
int main() {
int n, q; // 定义两个整数n和q,分别表示数组大小和操作次数
cin >> n >> q; // 输入n和q
for (int i = 1; i <= n; i++) { // 循环输入原始数组a的元素
cin >> a[i]; // 输入数组a的元素
insert(i, i, a[i]); // 初始化b[i],此时为差分数组,b[i]后缀和数组为a[i]
}
while (q--) { // 循环执行q次操作
int a, b, c;
cin >> a >> b >> c;
insert(a, b, c); // 在区间[a, b]内加上c,区间加操作时间辅助度为O(1)
}
for (int i = 1; i <= n; i++) { // 循环输出数组b的元素
b[i] += b[i - 1]; // 将b[i]加上b[i-1],这样可以保证每个位置上的b[i]都表示从1到i的和
cout << b[i] << " "; // 输出b[i]
}
return 0;
}
注意:
在C++中,全局变量的数组如果没有显式初始化,其元素会自动初始化为0。这是因为全局变量和静态局部变量(在函数内部声明为static的变量)会被默认初始化为它们的默认值,对于整数类型(包括数组)来说,默认值是0。
对于自动变量(函数内部未声明为static的局部变量),C++不会进行默认初始化,它们的值是未定义的,直到被赋予一个明确的值。因此,如果你需要在函数内部使用一个初始化为0的数组,你需要显式地进行初始化:
二维差分
#include <iostream>
using namespace std;
const int N = 1010;
int n, m, q;
int a[N][N], b[N][N]; //全局数组,默认初始化为全0
// insert函数用于在子矩阵(x1, y1)到(x2, y2)的范围内每个元素的值增加c
void insert(int x1, int y1, int x2, int y2, int c) {
b[x1][y1] += c; // 在(x1, y1)位置加上c
b[x2 + 1][y1] -= c; // 在(x2+1, y1)位置减去c,这样可以保证只有(x1, y1)到(x2, y2)的范围内加上c
b[x1][y2 + 1] -= c; // 在(x1, y2+1)位置减去c
b[x2 + 1][y2 + 1] += c; // 在(x2+1, y2+1)位置加上c,这样可以抵消对(x2+1, y1)和(x1, y2+1)的影响
}
int main() {
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
{ scanf("%d", &a[i][j]); // 输入矩阵a的元素
insert(i, j, i, j, a[i][j]); // 初始化差分矩阵b,b[i][j]的前缀和对应a[i][j]
}
while (q--) { // 循环执行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]; // 计算前缀和
printf("%d ", b[i][j]); // 输出矩阵的每个元素
puts(""); // puts("Hello, World!") 会输出 Hello, World! 和一个新行,而 puts("") 只会输出一个新行。}
}
return 0;
}
这个程序的主要功能是处理一个矩阵的子矩阵加操作。首先,程序定义了两个二维数组a和b,其中a数组用于存储原始矩阵,b数组用于存储差分矩阵。然后,程序使用insert函数在b数组上执行子矩阵加操作。在执行完所有操作后,程序输出b数组的每个位置的值。
程序的实现思路是使用差分矩阵的方法来处理子矩阵加操作。差分矩阵是一种用于处理子矩阵加操作的数据结构,可以在O(1)的时间复杂度内完成子矩阵加操作。
二分
高精度加法
实现一个加法器,使其能够输出 a+b的值:
https://www.acwing.com/problem/content/description/3599/
法一:用数组,即可用C实现
int main(){
string a1,b1;
while(cin>>a1>>b1){
int a[1001]={},b[1001]={},c[1001]={},lenc=0; //数组先全部初始化为0
for(int i=0;i<a1.size();i++)a[i]=a1[a1.size()-i-1]-'0';
for(int i=0;i<b1.size();i++)b[i]=b1[b1.size()-i-1]-'0';
while(lenc<a1.size()||lenc<b1.size()){
c[lenc]+=a[lenc]+b[lenc];
if(c[lenc]>=10){
c[lenc]-=10;//用-比%快
c[lenc+1]++;
}
lenc++;
}
if(c[lenc]==0)lenc--;
for(int i=lenc;i>=0;i--)cout<<c[i];
cout<<endl;
}
法二:我的第一思路
vector<int> add(vector<int> a, vector<int> b)
{
int i = 0, c = 0, s;
vector<int> C;
while (i < a.size() && i < b.size()) {
s = a[i] + b[i];
s += c;
if (s >= 10) c = 1;
else c = 0;
C.push_back( s % 10);
i++;
}
// 处理a和b中剩余的数字
while (i < a.size()) {
s = a[i] + c;
c=s/10;//这样更快
C.push_back( s % 10);
i++;
}
while (i < b.size()) {
s = b[i] + c;
c=s/10;
C.push_back(s % 10);
i++;
}
// 如果最后有进位,需要将进位添加到结果中
if (c ) C.push_back(c);
return C;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
string a, b;
vector<int> A, B;
int i;
while (cin >> a >> b) {
// 将字符串转换为向量
A.clear();//注意每轮清空向量
B.clear();
for (i = a.size() - 1; i >= 0; i--) {
A.push_back(a[i] - '0');
}
for (i = b.size() - 1; i >= 0; i--) {
B.push_back(b[i] - '0');
}
vector<int> C = add(A, B);
for (i = C.size() - 1; i >= 0; i--) {
cout << C[i];
}
cout << endl;
}
return 0;
}
加法函数可以实现得更简洁:
vector<int> add(vector<int>& A, vector<int>& B) {
vector<int> C;
int t = 0;
for (int i = 0; i < A.size() || i < B.size(); i++) {
if (i < A.size()) t += A[i];
if (i < B.size()) t += B[i];
C.push_back(t % 10);
t /= 10;
}
if (t) C.push_back(1);
return C;
}
Trie树
用来高效的存储和查找字符串集合的数据结构(题目一定会限制字符的种类)
Trie树,又称字典树、前缀树或搜索树,是一种用于处理字符串匹配的数据结构。Trie树是一种有序树,用于处理字符串的查找、插入和删除操作,特别是用于自动补全、拼写检查和IP路由等场景。
Trie树的优点:
查询效率高:对于长度为L的字符串,Trie树的查询时间复杂度为O(L)。
空间利用合理:相比其他字符串查找方法,Trie树在空间上更为节省,因为公共前缀只存储一次。
支持高效的前缀匹配:可以快速找到具有相同前缀的所有字符串
Trie树的应用:
字符串检索:在文本编辑器中,用于实现单词补全、拼写检查等功能。
自动补全:搜索引擎、输入法中的自动补全功能。
IP路由:在网络路由中,使用Trie树进行IP地址最长前缀匹配。
压缩算法:如Burrows-Wheeler变换,使用Trie树进行数据压缩。
插入,删除操作如下:
int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
//son[x][]存储每个点的子结点,idx指示当前用到的下标
// cnt[x]存储以节点x结尾的单词数量
// 插入一个字符串
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] ++ ;
}
// 查询字符串出现的次数
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];
}
求解问题 最大异或对
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=32*N+5;
int son[N][2],idx;
int a[N];
void insert(int x){
int p=0;
for(int i=30;i>=0;i--){
int u=x>>i &1;//取出整数的第i位
if(!son[p][u]) son[p][u]=++idx;
p=son[p][u];
}
}
int query(int x){
int p=0;
int t=0;
for(int i=30;i>=0;i--){
int u=x>>i &1;
if(son[p][!u]){
p=son[p][!u];
t=t*2+!u;
}else{
p=son[p][u];
t=t*2+u;
}
}
return t;
}
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++) cin>>a[i];
int res=0;
for(int i=0;i<n;i++){
insert(a[i]);
int t=query(a[i]);
res=max(res,a[i]^t);
}
cout<<res<<endl;
return 0;
}
并查集
并查集可以在近乎 O(1) 的时间内完成以下两个操作
- 将两个集合合并
- 询问两个元素是否在一个集合当中
哈希表
开放寻址法
#include<iostream>
#include<cstring>
using namespace std;
const int N=100003,null=0x3f3f3f3f; //0x3f3f3f3f>1e9,用一个数据范围之外的值来作为空(null)
// N=100003,用质数作为长度可以使得冲突最少
int h[3*N]; //开2~3倍,来保证哈希表一定有null且较快就能找到null
int find(int x){
int t=(x%N+N)%N; 计算x的哈希值k,为了避免负数,先加上N再取模N
while(h[t]!=null&&h[t]!=x){
t++;
if(t==N) t=0;
}
return t;
}
int main(){
int n;
scanf("%d",&n);
memset(h,0x3f,sizeof h);// 给h的每个字节初始化成0x3f,使得每个元素的值都是null
while(n--){
char op[2];
int x;
scanf("%s%d",op,&x);
int k=find(x);
if(op[0]=='I'){
h[k]=x;
}else{
if(h[k]==null) puts("No");
else puts("Yes");
}
}
}
字符串哈希
很多字符串问题可以用哈希来做,不一定要用KMP。(字符串循环节只能KMP,其它都可以用字符串哈希且更快)
用来快速判断两个字符串是否相等。
DFS
//dfs框架
int dfs(int u) //u为图节点编号
{
st[u] = true; // st[u] 表示点u已经被遍历过
for (int i = h[u]; i != -1; i = ne[i]) //h[]存得是头结点的idx(数组下标)
//可通过idx,访问到结点的data域(e[idx])、next域(ne[idx])
{
int j = e[i]; //获取图节点编号(结点data)
if (!st[j]) dfs(j); //若该节点未被访问过,则递归下去
}
}
红线为搜索顺序
#include<bits/stdc++.h>
using namespace std;
const int N=7;
// 定义常量N为7,这里n的最大值为7
int n;
// 定义变量n,表示排列的数字个数
int path[N];
// 定义数组path,用于存储当前路径(即一种排列)
bool st[N]; //初始化为全为0即全为false
// 定义数组st(state),用于标记数字是否已经在当前路径中
void dfs(int u){
// 定义深度优先搜索函数dfs,参数u表示当前路径的长度
if(u==n){ //递归出口
// 如果当前路径长度等于n,说明找到了一个完整的排列
for(int i=0;i<n;i++) printf("%d ",path[i]);//下标0~n-1存序列
// 遍历当前路径,输出排列
printf("\n");
// 输出换行符,准备输出下一个排列
return;
// 返回上一层递归
}
for(int i=1;i<=n;i++){
// 每次递归进来都要遍历数字1到n
if(!st[i]){
// 如果数字i没有在当前路径中
path[u]=i;
// 将数字i加入当前路径
st[i]=1;
// 标记数字i已经在当前路径中
dfs(u+1);
// 路径长度加1,递归搜索下一个数字
st[i]=0;
// 回溯,将数字i从当前路径中移除(不需要真正去移除,直接被下一次存放覆盖即可),标记为未使用
}
}
}
// dfs函数结束
int main(){
cin>>n;
// 输入n的值
dfs(0);
// 从路径长度为0开始搜索,看上面的图
}
伪代码:
dfs函数:
如果当前路径长度等于n,则:
输出当前路径
返回
否则:
对于每个数字i从1到n,如果i不在当前路径中,则:
将i加入当前路径
标记i为已使用
递归调用dfs函数,参数为当前路径长度加1
将i从当前路径中移除
标记i为未使用
例题:树的重心
给定一颗树,树中包含n个结点(编号1~n)和n-1条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
如图,重心为4,将重心删除后,剩余各个连通块中点数的最大值为5.
思路:dfs,该题蕴含解求树的节点数、最大子树的节点数的方法,再这样基本步骤的基础上解出此题。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1e5 + 10; //数据范围是10的5次方
const int M = 2 * N; //以有向图的格式存储无向图(树就是特殊的无向图),所以每个节点至多对应2n-2条边,这里多开两个空间。
int h[N]; //邻接表存储树,有n个节点,所以需要n个头节点,数组h每个元素存的是头结点的idx(数组下标)
int e[M]; //存储元素,这里存节点编号
int ne[M]; //存储指向下一个节点的数组下标idx (底层是根据下标获得数组元素的逻辑(虚拟)地址),(指针本质也是逻辑(虚拟)地址)故可通俗的理解为元素地址以便访问
int idx; //数组下标,邻接表中的所有链表,用一个大数组来模拟(这个数组可以模拟多个链表)
int n; //题目所给的输入,n个节点0
int ans = N; //表示重心的所有的子树中,最大的子树的结点数目(最后迭代为答案)
bool st[N]; //记录节点是否被访问过,访问过则标记为true
//a所对应的单链表中插入b 即编号为a的节点插入子节点
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
//返回以u为根的子树中节点的个数,包括u节点
int dfs(int u) {
int res = 0; //存储 删掉u节点之后,最大的连通子图节点数
st[u] = true; //标记访问过u节点
int sum = 1; //存储 以u为根的树 的节点数
//访问u的每个子节点
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
//每个节点的编号都是不一样的,用编号为下标 来标记是否被访问过
if (!st[j]) {
int s = dfs(j); // 以u节点的某个孩子节点为根的树的节点数即某个子树节点数
res = max(res, s); // 求出最大子树节点数
sum += s; //以u为根的树的节点数
}
}
res = max(res, n - sum); // 选择u节点为重心,最大的 连通子图节点数
//把以u为根的树删除,剩下一个连通子图,该节点数与最大子树节点数相比,得出最大的连通子图节点数
ans = min(res, ans); //得出遍历过的假设重心中,最小的最大联通子图的节点数
return sum;
}
int main() {
memset(h, -1, sizeof h); //初始化h数组 -1表示尾节点
cin >> n; //表示树的结点数
// 题目接下来会输入,n-1行数据,
// 树中是不存在环的,对于有n个节点的树,必定是n-1条边
for (int i = 0; i < n - 1; i++) {
int a, b;
cin >> a >> b;
//add(a, b), add(b, a); //把树作为无向图。
//add(a, b); 作为有向图,也可得出正确答案。
}
dfs(1); //可以任意选定一个节点开始遍历(相当于枚举重心在每个节点上,来迭代出答案)
cout << ans << endl;
return 0;
}
BFS
走迷宫
给定一个n*m的二维整数数组,用来表示一个迷宫,数组中只包含0或1,其中0表示可以走的路,1表示不可通过的墙壁。
最初,有一个人位于左上角(1, 1)处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角(n, m)处,至少需要移动多少次。
数据保证(1, 1)处和(n, m)处的数字为0,且一定至少存在一条通路。
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
// 定义一个类型别名PII,表示一个整数对,用于存储坐标
const int N=110;
// 定义常量N为110,这里假设地图的大小不会超过10x10
int n,m;
// 定义变量n和m,分别表示地图的行数和列数
int g[N][N];//地图
// 定义二维数组g,用于存储地图的每个位置的状态(0或1)
int d[N][N];//距离
// 定义二维数组d,用于存储从起点到每个位置的最短路径长度
PII q[N*N];//模拟队列
// 定义一个PII类型的数组q,用于模拟队列操作,存储待处理的坐标
int bfs(){
int hh=0,tt=0;
// 初始化队头指针hh和队尾指针tt为0
q[0]={0,0};
// 将起点(0,0)加入队列
memset(d,-1,sizeof(d));
// 初始化距离数组d的所有元素为-1,表示所有位置都未被访问过
d[0][0]=0;
// 起点到起点的距离设置为0
int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};
// 定义方向数组,表示上、右、下、左四个方向
while(hh<=tt){
// 当队列不为空时,继续搜索
auto t=q[hh++];
// 取出队头元素t,并更新队头指针hh
for(int i=0;i<4;i++){ //试探四个方向
int x=t.first+dx[i],y=t.second+dy[i];
// 计算下一个方向的坐标(x,y)
if(x>=0&&x<n&&y>=0&&y<m&&g[x][y]==0&&d[x][y]==-1){
// 如果坐标(x,y)在地图范围内,且是空地,且未被访问过
d[x][y]=d[t.first][t.second]+1;
// 更新坐标(x,y)的距离为当前坐标的距离加1
q[++tt]={x,y};
// 将坐标(x,y)加入队列,并更新队尾指针tt
}
}
}
return d[n-1][m-1];
// 返回终点(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];
// 输入地图的每个位置的状态
cout<<bfs()<<endl;
// 输出从左上角到右下角的最短路径长度
return 0;
}
拓展:并求出最短路径
开一个prev数组,存放每一个点是由哪个点拓展过来的。
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
const int N=110;
int n,m;
int g[N][N];
int d[N][N];
PII q[N*N];
PII Prev[N][N];
int bfs(){
int hh=0,tt=0;
q[0]={0,0};
memset(d,-1,sizeof(d));
d[0][0]=0;
int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};
while(hh<=tt){
auto t=q[hh++];
for(int i=0;i<4;i++){
int x=t.first+dx[i],y=t.second+dy[i];
if(x>=0&&x<n&&y>=0&&y<m&&g[x][y]==0&&d[x][y]==-1){
/*注意d[x][y]==-1代表该点没有更新过距离
也就是没有走过
*/
d[x][y]=d[t.first][t.second]+1;
//下面是重点!!!
Prev[x][y]=t;
q[++tt]={x,y};
}
}
}
//求路径的过程
int x=n-1,y=m-1;
while(x||y){//只要x,y不同时为0,就继续向前转移
cout<<x<<" "<<y<<endl;
auto t=Prev[x][y];
x=t.first,y=t.second;
}
return 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];
cout<<bfs()<<endl;
return 0;
}