Acwing - 算法基础课 - 笔记(基础算法 · 三)

基础算法(三)

这节讲的是双指针算法,位运算,离散化,区间合并

双指针

  • 2个指针指向不同的序列

    比如归并排序

  • 2个指针指向同一个序列

    比如快速排序

对于形如

for(int i = 0; i < n; i++) {
    for(int j = 0; j < n; j++) {
        
    }
}

这一类的双层循环,可能可以使用双指针来进行优化,从而能够把时间复杂度从O(n2)降低到O(n),比如求一个数组中最长的连续不重复的子序列的长度

最容易想到的暴力解法是:枚举 i = 0~n,对于每个i,将其看作右端点,尝试找到其左边最远的左端点。

int maxLen = 0;
for(int i = 0; i < n; i++) {
    for(int j = 0; j <= i; j++) {
        // 若[j, i]区间内不包含重复元素
        if(noRepeat(j, i)) maxLen = max(maxLen, i - j + 1);
    }
}

经过简单的推算,可以得知,当某个[j, i]区间内有重复元素,那么对于所有i + 1之后的数作为右端点,其左边的最远端点,最多取到j + 1。也就是说,随着i从左往右移动,每个i对应的最远的左端点j,也只能单向地从左往右移动。所以可以采用双指针算法,两个指针ij最多只需要各自从0走到n(共2n次操作),即可找到答案,时间复杂度为O(n)。

int maxLen = 1;
for(int i = 0, j = 0; i < n; i++) {
    while(j <= i && hasRepeat(j, i)) j++;  // 若[j, i]区间内包含重复元素, 右移j
    maxLen = max(maxLen, i - j + 1); // 找到当前i作为右端点, 最长的子序列
}
// 其中 hasRepeat 函数检查是否有重复元素, 可以用哈希表,或者开一个数组用计数排序的思路

练习题:Acwing - 799: 最长连续不重复子序列

#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int n;
int q[N], c[N];  // 这里对于判断重复, 采用了计数排序的思想, 若数的范围较大, 或者数不是整数, 可以考虑用哈希表

int main() {
    scanf("%d", &n);
    for(int i = 0; i < n; i++) scanf("%d", &q[i]);
    
    int res = 0;
    for(int i = 0, j = 0; i < n; i++) {
        c[q[i]]++; // i往后移动一位, 计数加一, 将q[i]这个数纳入判重的集合
        while(c[q[i]] > 1) {  // 若在[j, i]区间内有重复元素的话, 只可能重复新加入的这个q[i], 只需判断q[i]的个数大于1
            // 有重复, j 往右移动一位
            c[q[j]]--; // 将j这个位置的数的计数减1, 即把q[j]从判重的集合中移除
            j++;
        }
        res = max(res, i - j + 1); // 针对该i, 找到最远的左端点j
    }
    printf("%d", res);
    return 0;
}

练习题:Acwing - 800: 数组元素的目标和

#include<iostream>
using namespace std;

const int N = 1e5 + 10;

int a[N], b[N];

int main() {
	int n, m, x;
	scanf("%d%d%d", &n, &m, &x);
	for(int i = 0; i < n; i++) scanf("%d", &a[i]);
	for(int i = 0; i < m; i++) scanf("%d", &b[i]);
	int i = 0, j = m - 1;
	while(i < n && j >= 0) {
		int sum = a[i] + b[j];
		if(sum < x) i++;
		else if(sum > x) j--;
		else break;
	}
	printf("%d %d\n", i, j);
	return 0;
}

练习题:Acwing - 2816: 判断子序列

#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], b[N];

int main() {
	int n, m;
	scanf("%d%d", &n, &m);
	for(int i = 0; i < n; i++) scanf("%d", &a[i]);
	for(int i = 0; i < m; i++) scanf("%d", &b[i]);

	int i = 0, j = 0;
	while(i < n && j < m) {
		if(a[i] == b[j]) {
			i++;
			j++;
		} else j++;
	}
	if(i < n) printf("No\n");
	else printf("Yes\n");
	return 0;
}
小结

双指针算法,通常是适用于有两层循环的情况(循环变量分别为ij)。首先是写一个暴力的解法,然后观察一下ij之间是否存在单调性的关系(即ij是否都往着同一个方向移动,不回头)。若ij之间存在着这种单调性关系,则我们可以用双指针,来将时间复杂度从O(n2)降低到O(n)。

双指针算法模板

for(int i = 0, j = 0; i < n; i++) {
    while(j < i && check(i, j)) j++;
    // 具体逻辑
}

位运算

  • 获取一个数的二进制的第k位:x >> k & 1

    即,先将x右移k位,然后和1与运算

  • 获取一个数的二进制的最后一位1:lowbit(x) = x & -x

    如,10的二进制表示是1010,则对10做lowbit运算,得到10(二进制),转为十进制即为2

    lowbit运算的原理是,x & -x,由于-x采用补码表示,它等于对x的原码取反再加1,即-x = ~x + 1

    比如 x 的二进制表示是:

    100101000,对x取反得

    011010111,加1得

    011011000

    所以x & (~X + 1),则x的最后一位1,被保留了下来,这个位置后面,两个数全是0,这个位置前面,两个数是取反,做与运算后也全为0。

    lowbit的最简单的应用:统计x的二进制表示中,1的个数。具体的实现方式是:每次对x做lowbit运算,并将运算结果从x中减去。循环做下去,直到x被减为0,一共减了多少次,x中就有多少个1。

    练习题:Acwing - 801: 二进制中1的个数

    #include<iostream>
    using namespace std;
    
    const int N = 1e5 + 10;
    int n;
    
    int lowbit(int x) {
        return x & -x;
    }
    
    int main() {
        scanf("%d", &n);
        while(n--) {
            int x;
            scanf("%d", &x);
            int c = 0;
            while(x > 0) {
                x -= lowbit(x);
                c++;
            }
            printf("%d ", c);
        }
        return 0;
    }
    

离散化

有的数组,其元素的值域很大,比如数组中的元素取值都是[0, 10^9],但元素的个数很少,比如只有1000个元素。

有时(例如计数排序的思想),我们需要将元素的值,作为数组的下标来操作。此时不可能开一个10^9大小的数组。

此时我们把这些元素,映射为从0(或者从1)开始的自然数。(也可以理解为对稀疏数组进行压缩)

例子如下

有一个数组a,[1, 3, 100, 2000, 500000](已经排好序),我们把这个数组中的元素,映射为

[0, 1, 2, 3, 4],这个映射的过程,称之为离散化

离散化有2个要点:

  • 原数组a中若有重复元素,可能需要去重
  • 如何根据a[i],算出其离散化后的值:由于原数组已经排好序,故这里用二分查找即可

离散化的代码模板

// C++
vector<int> v; // 待离散化的数组
sort(v.begin(), v.end()); // 将数组先排序
v.erase(unique(v.begin(), v.end()), v.end()); // 对数组进行去重
// 进行离散化, 将数组的值依次映射到 0,1,2,3,4,5, ... 等自然数

// 根据数的值, 求出其离散化的值
int find(int x) {
    int l = 0, r = v.size() - 1;
    while(l < r) {  // 找到第一个大于等于x的离散化的值
        int mid = l + r >> 1;
        if(v[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return r + 1; // 是否加1, 跟题目相关, 若是前缀和差分等需要下标从1开始, 则需要加1
}

练习题:Acwing - 802: 区间和

// C++, 自己的解法
// 只针对插入操作涉及到的数组下标, 进行离散化
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

const int N = 1e5 + 10;

int index[N], value[N];

void init(vector<pair<int, int>> v) {
    for(int i = 0; i < v.size(); i++) {
        index[i + 1] = v[i].first; // 记录原坐标, 这样以来, 在index数组中, i是离散化后的坐标, index[i]是离散化前的坐标
        value[i + 1] = value[i] + v[i].second; // 记录这个坐标累加的值, 构造前缀和
        // 离散化的坐标从1开始
    }
}

int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    vector<pair<int, int>> insert;
    while(n--) {
        int x, c;
        scanf("%d%d", &x, &c);
        insert.push_back({x, c});
    }
    sort(insert.begin(), insert.end()); // 默认按照 pair 的 first 进行升序排列, 这里没有去重
    // 离散化
    init(insert);
    
    while(m--) {
        int l, r;
        scanf("%d%d", &l, &r);
        // 因为上面离散化时没有去重, 若对同一个位置x插入了多次, 则x会对应离散化后的多个连续坐标
        // 如 [5,6] [5,7] 表示对x=5的位置, 添加值6和7, 则在离散化后的下标中, 可能x=5会对应 1,2 两个
        // 所以下面取离散化的左边界时, 要取到 >= x 的第一个值
        int ll = 1, lr = insert.size();  // 左边界
        while(ll < lr) {
            int mid = ll + lr >> 1;
            if(index[mid] >= l) lr = mid;
            else ll = mid + 1;
        }
        // 同样因为没有去重, 这里取右边界时, 要取到 <= x 的最后一个值
        int rl = 1, rr = insert.size(); // 右边界
        while(rl < rr) {
            int mid = rl + rr + 1 >> 1;
            if(index[mid] <= r) rl = mid;
            else rr = mid - 1;
        }
        // 先预计算出一个结果
        int res = value[rl] - value[ll - 1];
        // 当 l 和 r 二分出来的位置是相同时, [l, r]区间内可能没有任何值
        // 仅当 l <= index[ll] 并且 r >= index[ll] 时, [l, r] 之间才有值, 否则 [l, r]之间没有任何值, 结果应当为0
        if(ll == rl && (l > index[ll] || r < index[ll])) res = 0; 
        printf("%d\n", res);
    }
    return 0;
}
// C++, 按照yxc的思路的解法
// 将所有用到的下标先存起来, 对全部下标进行离散化(所有的x, l, r)
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

typedef pair<int, int> PII;

const int N = 3e5 + 10; // 用到的下标总数为  n + 2m , 300000 量级

vector<PII> add; // 插入操作
vector<PII> query; // 询问操作
vector<int> index; //所有待离散化的下标

int s[N]; // 记录前缀和
int a[N]; // 记录值

// 进行离散化
int find(int x) {
    int l = 0, r = index.size() - 1;
    while(l < r) {
        int mid = l + r >> 1;
        if(index[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return l + 1; // 离散化后的下标从1开始, 因为需要计算前缀和
}

vector<int>::iterator unique(vector<int> &v) {
    // 去除有序数组的重复元素
    int j = 0;
    for(int i = 0; i < v.size(); i++) {
        if( i == 0 || v[i] != v[i - 1]) v[j++] = v[i];
    }
    
    // j 最后的位置是, 去重后的第一个位置
    return v.begin() + j; // 
}

int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    // 读入全部数据
    while(n--) {
        int x, c;
        scanf("%d%d", &x, &c);
        add.push_back({x, c});
        index.push_back(x); // 添加待离散化的下标
    }
    while(m--) {
        int l, r;
        scanf("%d%d", &l, &r);
        query.push_back({l, r});
        index.push_back(l);// 添加待离散化的下标
        index.push_back(r);
    }
    // 对所有的下标点进行离散化
    sort(index.begin(), index.end()); // 先对 index 排序
    index.erase(unique(index), index.end()); // 对 index 数组进行去重
    
    // 处理插入
    for(int i = 0; i < add.size(); i++) {
        int p = find(add[i].first); // 找到待插入的位置(离散化后的位置)
        a[p] += add[i].second; // 插入
    }
    
    // 计算前缀和
    for(int i = 1; i <= index.size(); i++) {
        s[i] = s[i - 1] + a[i]; // 对所有用到的下标, 计算前缀和
    }
    
    // 处理查询
    for(int i = 0; i < query.size(); i++) {
        int l = find(query[i].first);
        int r = find(query[i].second);
        printf("%d\n", s[r] - s[l - 1]);
    }
    return 0;
}

值域跨度很大,但是实际用到的数字的个数不多,这种场景适合用离散化

练习题:美团笔试题: 格子染色

区间合并

给定很多个区间,若2个区间有交集,将二者合并成一个区间

做法思路:

  1. 先按照区间的左端点进行排序
  2. 然后遍历每个区间,进行合并即可(类似双指针的思想)

练习题:Acwing - 8023: 区间合并

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

using namespace std;

typedef pair<int, int> PII;

int main() {
    
    int n;
    vector<PII> segments;
    scanf("%d", &n);
    while(n--) {
        int l, r;
        scanf("%d%d", &l, &r);
        segments.push_back({l, r});
    }
    // 数据读入完毕
    // 排序
    sort(segments.begin(), segments.end());
    int cnt = 0; // 开始计数
    for(int i = 0; i < segments.size(); i++) {
        if(i == segments.size() - 1) {
            // 当前的区间是最后一个区间
            cnt++;
            break;
        } else {
            // 存在后续区间, 进行合并
            int r = segments[i].second; // 当前区间的右边界
            // 当存在后续区间时, 进行循环合并
            while(i + 1 < segments.size()) {
                if(segments[i + 1].first > r) break;  // 后一个区间和该区间不存在交集
                else {
                    // 存在交集, 更新右边界
                    r = max(r, segments[i + 1].second);
                    i++;
                }
            }
            cnt++; // 当前区间合并完毕, 计数 + 1
        }
    }
    printf("%d", cnt);
    return 0;
}

—2023/02/01更新,上面的区间合并代码写的复杂了,看下面更简洁的

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
int n, l, r;
vector<PII> seg;

int main() {
	std::ios::sync_with_stdio(false);
	cin >> n;
	while (n--) {
		cin >> l >> r;
		seg.push_back({l, r});
	}
	sort(seg.begin(), seg.end());
	int cnt = 1;
	l = seg[0].first, r = seg[0].second;
	for (int i = 1; i < seg.size(); i++) {
		PII s = seg[i];
		if (s.first <= r) r = max(r, s.second); // 可以合并
		else {
			cnt++;
			l = s.first, r = s.second;
		}
	}
	printf("%d\n", cnt);
	return 0;
}
  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值