算法基础笔记

根据时间复杂度选择算法

主体内容参考下面,

算法基础简介 - OI Wiki (oi-wiki.org)

AcWing《算法基础课》第2章 数据结构 - AcWing

AcWing 算法基础课笔记 3.搜索与图论(持续更新)_acwing搜索课件-CSDN博客

ACwing算法基础课全程笔记(2021年8月12日开始重写+优化)_acwing的csp辅导课更新到什么时候-CSDN博客

y式变量翻译对照表 - AcWing

AcWing基础语法课总结(完) - AcWing

AcWing个人笔记汇总 - AcWing

数据结构和算法_叫我小秦就好了的博客-CSDN博客

一维差分

 差分数组的前缀和数组是原数组
若f()为求前缀和,a为原数组,则a = f(b) ,b为差分数组
)题目:
标题

下面程序的主要功能是处理一个数组的区间加操作。首先,程序定义了两个数组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)的时间复杂度内完成子矩阵加操作

二分

349. 两个数组的交集

350. 两个数组的交集 II

高精度加法

实现一个加法器,使其能够输出 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树进行数据压缩。

什么是 Trie 树 - 知乎 (zhihu.com)

插入,删除操作如下:

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) 的时间内完成以下两个操作

  1. 将两个集合合并
  2. 询问两个元素是否在一个集合当中

哈希表

开放寻址法

#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;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值