[AcWing] 算法基础课(一)学算法强推哦

第一讲 基础算法


本文题目及代码全部来自AcWing,强推!
(因为没有接触过C++所以一开始学起来不是很容易,慢慢听下去边查边学就好啦)

1. 排序

1.1 快速排序

快速排序基础算法:

题目:

在这里插入图片描述

#include <iostream>
using namespace std;

const int N = 1000010;

int n;
int q[N];

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

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++) cin >> q[i];
    
    quick_sort(q, 0, n-1);
    
    for (int i = 0; i < n; i ++) cout << q[i] << ' ';
    
    return 0;
}

思路:

  • 确定分界点x,定义双指针i在最左边,j在最右边;
  • 按照单调性分成两个区间,分界点左边为小于等于x的,右边为大于等于x的
  • 递归排序左边,递归排序右边

注意:

  • 指针都要往外扩一位,因为先做了++操作;
  • 边界问题,这里x的取值与后面递归用i还是j有直接关系,保险起见取中间值l + r >> 1,向右进位1表示除以2;

第k个数

题目:

在这里插入图片描述

#include <iostream>
using namespace std;

const int N = 1000010;

int n, k;
int q[N];

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

int main()
{
    cin >> n >> k;
    for (int i = 0; i < n ; i ++) cin >> q[i];
    
    cout << quick_sort(0, n-1, k) << ' ';
    
    return 0;
}

思路:

  • 与快排模版相同;
  • 这里会多出一个s1用来判断第k个数在哪个区间中

注意:

  • 因为j作为分界点,所以用j-i+1来判断k在左半区间还是在右半区间;
  • 当l=r的时候即找出夹出来的第k个数返回即可

1.2 归并排序

归并排序基础算法

题目:
在这里插入图片描述

#include<iostream>
using namespace std;

const int N = 1000010;

int n;
int q[N], tmp[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);
    
    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 ++];
    }
    while (i <= mid) tmp[k ++] = q[i ++];
    while (j <= r) tmp[k ++] = q[j ++];
    
    for (int i = l, j = 0; i <= r; i ++, j ++) q[i] = tmp[j];
}

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

思路:

  • 找中点,递归排序左边,递归排序右边;
  • 分成一半 再分一半 再再分一半···直到分成每一个单位为1的区间;
  • 定义双指针i为左边第一位,j为中点后第一位,即分为两个数组进行比较;
  • 如果i里面的数比j小则加入tmp数组中否则就将j加入tmp,此时tmp中的下标k随之增加;
  • 将排好序的tmp再赋值给q

注意:

  • i要从l开始而不是从0开始;
  • 记得把没排完的前一个i里的数组or后一个j里的数组再循环以下把剩下的数字全部加到后面

逆序对的数量

题目:

在这里插入图片描述

#include <iostream>
using namespace std;

const int N = 1000010;

int n;
int q[N], tmp[N];

long merge_sort(int l, int r)
{
    if (l >= r) return 0;
    
    int mid = l + r >> 1;
    
    long res = merge_sort(l, mid) + merge_sort(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 ++];
            res += mid - i + 1;
        }
    }
    while (i <= mid) tmp[k ++] = q[i ++];
    while (j <= r) tmp[k ++] = q[j ++];
    
    for (int i = l, j = 0; i <= r; i ++, j ++) q[i] = tmp[j];
    
    return res;
}

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++) cin >> q[i];
    
    cout << merge_sort(0, n-1) << endl;
    
    return 0;
}

思路:

  • 与归并排序模版大同小异;
  • 加了res用来计算逆序对的个数,因为返回的是数值所以两个merge可以直接相加;
  • 如果q[i] > q[j]了并且i < j所以这时就产生了逆序对,逆序对的个数为mid-i+1

注意:

  • 因为数据大小原因,这里res存的是long类型,所以merge方法也要记得设为long;
  • 当l>=r的时候记得返回0而不是空,因为是要加到res里的

2. 二分

2.1 整数二分(较麻烦)

题目:
在这里插入图片描述

#include <iostream>
using namespace std;

const int N = 1000010;

int n, m;
int q[N];

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i ++) scanf("%d", &q[i]);
    
    while (m --)
    {
        int x;
        scanf("%d",&x);
        
        int l = 0, r = n-1;
        
        while(l < r)
        {
            int mid = l + r >> 1;
            if (q[mid] >= x) r = mid;
            else l = mid + 1;
        }
        
        if (q[l] != x) cout << "-1 -1" << endl;
        
        else
        {
            cout << l << ' ';
            int l = 0, r = n-1;
            while (l < r)
            {
                int mid = l + r + 1>> 1;
                if (q[mid] <= x) l = mid;
                else r = mid - 1;
            }
            cout << l << endl;
        }
    }
    
    return 0;
}

思路:

  • 按数组长度设置左右端点;
  • 拿左右端点中间值去寻找,与目标值x进行比较,夹出x的位置即为l

注意:

  • 当r=mid时找的是目标值x第一次出现的位置即最左面的值,当l=mid的时候则找的是最右面的值;
  • 当拿l = mid时记得取mid时多加1,否则会导致死循环,因为mid会向上取整

2.2 浮点数二分

题目:
在这里插入图片描述

#include <iostream>
using namespace std;

const int N = 100010;

int main()
{
    double x;
    cin >> x;
    
    double l = - 10000, r = 10000;
    while (r - l > 1e-8)
    {
        double mid = (l + r) / 2;
        if ((mid * mid * mid) >= x) r = mid;
        else l = mid ;
    }
    printf("%1f\n", l);
    return 0;
}

思路:

  • 按照数据范围设置左右边界;
  • 与二分法同理夹出目标值;
  • 因为保留六位小数,所以判断条件为大于1e-8,保险起见一般多往后取两位

注意:

  • 因为浮点数没有整除问题,所以此时l可以直接等于mid而不用mid+1;
  • 浮点数都用double类型

3. 前缀和与差分

3.1 前缀和

一维数组
题目:
在这里插入图片描述

#include<iostream>

using namespace std;

const int N = 100010;

int n, m;
int a[N], s[N];

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++) scanf("%d", &a[i]);
    
    for (int i = 1; i <= n; i ++) s[i] = s[i-1] + a[i];
    
    while (m --)
    {
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d\n", s[r] - s[l-1]);
    }
    return 0;
}

思路:

  • 数组s用来存放a数组的前缀和,前缀和公式:s[i] = s[i-1] + a[i]
  • 前缀和数组s的右端点值减去座端点-1的值即为所求的l到r的区间

注意点:

  • 因为查询的是第几个数,所以数组下标从1开始而不是0;
  • 记住公式就好了。。。

矩阵
题目:
请添加图片描述

#include <iostream>
using namespace std;

const int N = 1010;

int n, m, q;
int a[N][N], s[N][N];

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]);
            
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j ++)
            s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];
            
    while (q --)
    {
        int x1, y1, x2, y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        printf("%d\n", s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1]);
    }
    return 0;
}

思路:

  • 与前缀和模版类似,因为是二维数组所以要进行两次循环读入a[i][j];
  • 二维数组求前缀和公式为s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j],减去的为相加重和那一部分数值;

注意:

  • 数组下标从1开始,循环i,j;
  • 搞懂数据范围,另外x1或y1要多减1,类似于一维数组中l-1

3.2 差分

一维数组
题目:
请添加图片描述

#include<iostream>
using namespace std;

const int N = 100010;

int n, m;
int a[N], b[N];

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

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++) scanf("%d", &a[i]);
    
    for (int i = 1; i <= n; i ++) insert(i, i, a[i]);
    
    while (m --)
    {
        int l, r, c;
        scanf("%d%d%d", &l, &r, &c);
        insert(l, r, c);
    }
    
    for (int i = 1; i <= n; i ++) b[i] += b[i-1];
    
    for (int i = 1; i <= n; i ++) printf("%d ", b[i]);
    
    return 0;
}

思路:

  • 关键在于理解差分的概念,即建立一个是前缀和为原数组的数组;
  • 初始化差分数组用当前数值的下标即可;
  • 在差分数组中将要操作序列的起始点加上目标值=给目标值后的所有数加上了目标值,因为它是前缀和数组,此时记得把操作序列终点值+1减去目标值,因为只在操作序列中加了该值;
  • 将前缀和数组循环相加就得出了操作后的原数组

注意:


矩阵
题目:
请添加图片描述

#include <iostream>
using namespace std;

const int N = 1010;

int n, m, q;
int a[N][N], b[N][N];

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

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]);
            
    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;
    
}

思路:

  • 与一维数组差分模版类似;
  • 矩阵需循环两次,构建初始前缀和数组时应传入i,j,i,j四个点;
  • 操作时可以想像在一个正方形中,在左上顶点加上目标值就相当于在这个顶点延伸到横纵边界的所有值加上了目标值,这时只需减去左下顶点右上顶点延伸加上的值后,再加上多减去的那一部分右下顶点延伸的值即可;
  • 最后再循环用二维前缀和数组公式求出操作后的数组

注意:

  • 输入时总手误把y1输成x2。。。
  • 一个一个输出二维数组里的值时先将j里的数组全部打印出来,后面别忘加puts("")用来换行

4. 双指针

最长连续不重复子序列
题目:
请添加图片描述

#include <iostream>
using namespace std;

const int N = 100010;
int n;
int a[N], s[N];

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++) cin >> a[i];
    
    int res = 0;
    for (int i = 0, j = 0; i < n; i ++) 
    {
        s[a[i]] ++;
        while (s[a[i]] > 1)
        {
            s[a[j]] --;
            j ++;
        }
        res = max(res, i-j+1);
        
    }
    cout << res << endl;
    return 0;
}

思路:

  • s数组用来存储数组a中数字出现的个数,用来判断重复值;
  • 双指针i和j都在数组下标起始0位置,i和j指向相同的数组,拿i来判断重复值个数,当出现重复即下标i对应的a[i]在s中存的个数大于2时进入循环,此时用到指针j,将前面存入的值减掉,减到当前i对应的值,此时在s里面存的个数又变成了1,开始了新一轮不重复区间循环;
  • res用来存最长连续不重复子序列

注意:

  • res值别忘了➕1

5. 位运算

二进制中1的个数
题目:
请添加图片描述

#include<iostream>
using namespace std;

int lowbit(int x)
{
    return x & -x;
}

int main()
{
    int n;
    cin >> n;
    while (n --)
    {
        int x;
        cin >> x;
        
        int res = 0;
        while (x) x -= lowbit(x), res ++;
        cout << res << " ";
    }
    
    return 0;
}

思路:

  • lowbit中返回的是x二进制最后一位1的位置,因为c++中存储的负数x为正数x的反码➕1,与x进行与运算后即返回最后一位1其他位均为0;
  • 循环x减去最后一位1,与此同时res ++即可算出二进制x中1的个数

注意:

  • lowbit返回值为int而不是void

6. 离散化

区间和
题目:
请添加图片描述

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

typedef pair<int, int> PII;

const int N = 300010;

int n, m;
int a[N], s[N];

vector<int> alls;
vector<PII> add, query;

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

int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++)
    {
        int x, c;
        cin >> x >> c;
        add.push_back({x, c});
        
        alls.push_back(x);
    }
    
    for (int i = 0; i < m; i ++)
    {
        int l, r;
        cin >> l >> r;
        query.push_back({l, r});
        
        alls.push_back(l);
        alls.push_back(r);
    }
    
    sort(alls.begin(), alls.end());
    alls.erase(unique(alls.begin(), alls.end()), alls.end());
    
    for (auto item : add)
    {
        int x = find(item.first);
        a[x] += item.second;
    }
    
    for (int i = 1; i <= alls.size(); i ++) s[i] = s[i-1] + a[i];
    
    for (auto item : query)
    {
        int l = find(item.first), r = find(item.second);
        cout << s[r] - s[l-1] << endl;
    }
    return 0;
}

在这里插入图片描述

思路:

  • 离散化思想即把一段无限长的数组映射到原数组中插入数值的数组
  • add中存入的是操作位置x和加入数c,query中存入的是查询区间和;
  • a中存入离散化的数组,alls中存入原数组插入值的下标以及查询的下标;
  • 要先对alls排序后去重,为了离散化数组;
  • find函数中用二分查找找出离散化后的下标返回;
  • 用前缀和公式求出前缀和数组s,再用离散后的查询坐标代入算出区间值

注意:

  • 因为会用到n次x操作和2m次查询操作所以总共的数据范围为300010,而不是数据范围109;
  • typedef pair<int, int> PII; vector<PII> add, query; 这样才可以用push_back存入两个值;
  • unique求得去重数组最后一个数下标的位置,再erase后面的数就得到去重后数组啦;
  • 迭代器auto item:add的运用,first second用来取值;
  • 查询时别忘记将l和r离散化

7. 区间合并

题目:
请添加图片描述

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

typedef pair<int, int> PII;

void merge(vector<PII> &segs)
{
    vector<PII> res;
    
    sort(segs.begin(), segs.end());
    
    int st = -2e9, ed = -2e9;
    
    for (auto seg : segs)
    {
        if (ed < seg.first)
        {
            if (st != -2e9) res.push_back({st, ed});
            st = seg.first, ed = seg.second;
        }
        else ed = max(ed, seg.second);
    }
    if (st != -2e9) res.push_back({st, ed});
    segs = res;
}

int main()
{
    int n;
    cin >> n;
    
    vector<PII> segs;
    for (int i = 0; i < n; i ++)
    {
        int l, r;
        scanf("%d%d", &l, &r);
        segs.push_back({l, r});
    }
    merge(segs);
    
    cout << segs.size() << endl;
    
    return 0;
    
}

思路:

  • 在segs中存入每个区间,对segs进行排序(按照左左端点值);
  • 设置初始值起点和终点,均为坐标左端点;
  • 迭代器遍历每一组区间,分为三种情况:1)区间在前区间中 2)前区间末端点在区间当中 3)与前区间毫无瓜葛,前两者成包含关系可放在一起考虑即求一遍max值,第三种情况则将区间放入res中,开辟新区间

注意:

  • 别忘了加入最后一组区间,前提条件是判断下是否为空数组;
  • 别忘了将res的值赋给segs后再输出segs的长度
  • 4
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值