第一章 基础算法(三)

课程内容

  • 双指针算法
  • 位运算
  • 离散化
  • 区间合并

双指针算法类型

两大类:
1.两个指针指向同一个序列
2.两个指针指向不同的序列

通用模板

for(int i = 0, j = 0;i < n;i++){
	// check()不一定要明写出来,它只是表示满足某种性质
    while(i < j && check(i, j)) j++;
    
    // 每道题目的具体逻辑
    // 虽然说循环长成这样样子, 看起来是两重循环, 但是每一个指针在循环中移动次数不超过n, 两个指针移动次数不超过O(2n)
}

虽然说循环长成这样样子, 看起来是两重循环, 但是每一个指针在循环中移动次数不超过n, 两个指针移动次数不超过O(2n)

双指针最核心的用途就是优化

for(int i = 0;i < n;i++){
    for(int j = 0;j < n;j++){
        O(n^2)        
    }    
}
双指针可以将上面的朴素算法O(n^2) 优化到O(n)

上课举的例子: 输入一个字符串, 然后将每个单词输出出来, 每个单词间有空格(且只有1个空格)
样例输入:
abc def ghi
样例输出:
abc
def
ghi

#include <iostream>
#include <cstring>
using namespace std;

int main(){
	char str[1000];
	gets(str);
	int n = strlen(str);

	for (int i = 0; i < n; i ++ ){
	    int j  = i;
		while (j < n && str[j] != ' ') j ++;

		for (int k = i; k < j; k ++ ) cout << str[k];
		cout << endl;
		i = j;
	}

	return 0;
}

双指针相关习题

leetcode 3, 76

799. 最长连续不重复子序列

分析

红指针:i(后指针). 绿指针:j(前指针)
绿颜色指针的意思: 在红颜色指针固定后, 绿颜色指针最左能到的位置, 使得绿颜色指针和红颜色指针中间没有重复元素
每次将红颜色指针往后移动一位, 然后求下新的绿颜色指针最靠左可以到什么位置
可以发现一个**性质(单调性): 红颜色指针往后移动, 绿颜色指针不会往前移动 **

证明: 假设红颜色移动到图中的第2根红颜色位置, 绿颜色移动到水平位置靠上的那根指针上(向左移动), 那么与原先2号区间(见图2)是最长的区间,矛盾, 即:原先的区间可以扩展到3号区间的长度

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

check(j, i)

开一个100000个数组, 动态的记录下当前区间内, 每个数出现的次数.
s[N]
每一次i往后移动一格, 相当于在区间内部加入一个新的数, s[a[i]] ++;

如果新加的数字有重复数字, 那么重复的数字必定是a[i], 因为加的是a[i]
所以check(j, i) 可以简写成s[a[i]] > 1

每一次j往后移动一格, 相当于从[j,i]区间内少了一个数, s[a[j]]--;

code

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

int main(){
    cin >> n;
    for (int i = 0; i < n; i ++ ) scanf("%d", &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 ++ ]] --;
        res = max(res, i - j + 1);
    }
    
    cout << res << endl;
    
    return 0;
    
}

双指针第二种类型(两指针在两个字符串上)

AcWing 2816. 判断子序列

分析

思路:
从前往后扫描B这个数组, 每次扫描B这个数组的时候, 判断下B里面的当前数是不是和A里面的当前数一样,

  1. 如果一样的话, 将A的当前数匹配的B的当前数. 找到第一个与A里面相同的数的时候, 匹配到一块, 匹配完后, A指针往后走, B指针也往后走

    (yxc:其实类似于贪心的思想)

  2. 如果下一个数与A的当前指针的数不一样, B往后走

当我们遍历完B数组之后, 如果发现A数组里面每一个数都顺次匹配了B里面的数字, 那么意味着找到了B的子序列与A相等

注意

双指针: 只能说明可以找到B内的子序列与A匹配.(可以找到一种匹配方式)
反过来不一定正确. ⬅️

反过来需要证明: 如果A是B的子序列(B里面存在一个子序列与A相等), 那么通过双指针算法, 能够将A中与B内匹配的子序列找出来.(如果存在一种匹配方式, 双指针算法一定可以将这种匹配方式找出来) 需要证明:➡️

这样才能说明:A是B的子序列, 等价于双指针算法找出来匹配.

命题: 若存在一组匹配, 则上述算法必然可以找出一个匹配

证明: 假设存在一组匹配,
在这里插入图片描述
如果A中第一个元素就是 存在(已经给的)的匹配, 跳过, 直到找到第一个与存在匹配方式不同的点(虚线的匹配方式)
在这里插入图片描述
修改已经存在的匹配, 并不影响后面的匹配(因为双指针算法找的是第一个可以匹配的点), 因此整个过程可以将已经存在的匹配, 修改称为双指针匹配
在这里插入图片描述

bugfree题解
在这里插入图片描述
在这里插入图片描述

code

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

int main(){
    cin >> n >> m;
    for (int i = 0; i < n; i ++ ) cin >> a[i];
    for (int i = 0; i < m; i ++ ) cin >> b[i];
    // 双指针, 每次找到A中第一个可以与B匹配的元素
    int i = 0, j = 0;
    while (i < n && j < m){
        if (a[i] == b[j]) i ++; // 如果A字符与B字符匹配, 则A指针后移
        j ++; // 无论B指针能否与A指针匹配, 都需要后移
    }

    if (i == n) puts("Yes"); // 如果i到n了, 说明A字符串匹配完毕
    else puts("No");

    return 0;
}

位运算

位运算
只讲最常用的两种操作

  1. 一个整数 n 的二进制表示中第 k 位是几(最低位是第0位)
    (n >> k) & 1 即可
    右移几位就是取第几位

  2. lowbit(x)
    返回 x 的最后一位 1---------> lowbit(x) = x&(-x)
    比如 :
    x = ( 1010 ) 2 , 则 l o w b i t ( x ) = ( 10 ) 2 = ( 2 ) 10 x = ( 101000 ) 2 , 则 l o w b i t ( x ) = ( 1000 ) 2 = ( 6 ) 10 x = (1010)_2,则 lowbit(x) = (10)_2 = (2)_{10}\\ x = (101000)_2,则 lowbit(x) = (1000)_2 = (6)_{10} x=(1010)2lowbit(x)=(10)2=(2)10x=(101000)2lowbit(x)=(1000)2=(6)10

原理:
-x = ~x+1
所以 x&(-x) = x&(~x + 1)
例子:
x = 1 0 1 0 … 1 0 0 … 0
~x= 0 1 0 1… 0 1 1… 1
~x+1=0 1 0 1… 1 0 0 … 0
x&(~x+1)= 1 0 0 … 0


801. 二进制中1的个数

分析

直接当前数进行lowbit操作, 能够减掉几次, 就表示当前数二进制中有几个1

code

#include <iostream>
using namespace std;
int n;

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

int main(){
    cin >> n;
    while (n -- ){
        int res = 0;
        int x;
        cin >> x;
        while (x) x -= lowbit(x), res ++;
        cout << res << ' ';
    }
    cout << endl;
    return 0;
}
x = 1010
原码00…0…1010
反码11…1…0101
补码11…1…0110

离散化

特指整数的离散化
一个整数序列:值域比较大,个数比较小
比如有1万个数,最小是 0,最大是 1亿,我们开不了长度一亿的数组,就需要对这 1 万个数进行离散化,把这 1 万个数映射到 0—n-1的连续的自然数(下标)上,就是把值映射成了下标。
例子:
a[ ] : 1, 3, 100, 2000, 500000 (a 是有序的)
把这5个数分别映射到 0, 1, 2, 3, 4(下标)
问题:
1.a 中可能有重复元素------>需要去重
2.如何算出序列中某个值 x 离散化后的值是多少 ?因为序列有序,所以二分

802. 区间和

在这里插入图片描述

分析

如果数据范围比较小,就可以用前缀和来做
本题坐标总共有 2 ∗ 1 0 9 2 * 10^9 2109 个, 但是只用到了 n + 2 * m 个坐标,最多也就是 3 ∗ 1 0 5 3 * 10^5 3105 个坐标。所以通过离散化,把数据范围缩小到 3 ∗ 1 0 5 3 * 10^5 3105 ,就可以用前缀和了。
离散化方式: 直接将数组映射成当前数字所在的下标
每个操作都是两个数,所以用一个Pair

在这里插入图片描述

uniqe函数实现思路(leetcode 26. 删除排序数组中的重复项)

在这里插入图片描述

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        int k = 0;
        for (auto x : nums){
            while (!k || nums[k - 1] != x)
                nums[k ++ ] = x;
        }
        return k;
    }
};

code

将add, query需要做的位置, 重新离散化后, 在离散化后的位置上进行增加, 前缀和, 查询操作

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

vector<int>::iterator unique(vector<int> &a)
{
    int j = 0;
    for (int i = 0; i < a.size(); i ++ )
        if (!i || a[i] != a[i - 1])
            a[j ++ ] = a[i];
    // a[0] ~ a[j - 1] 所有a中不重复的数

    return a.begin() + j;
}

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

区间合并

803. 区间合并

[st, ed]表示当前所维护的区间
思路:
1.按区间左端点排序
2.扫描整个区间
上个区间,我们用 st 表示左端点,ed 表示右端点

不会出现当前区间是图中红色区间的状况, 因为是按左端点排序, 因此 最次的情况 当前区间的起点与st相同
在这里插入图片描述

分别判断1,2,3种情况该如何去更新[st, ed].

  1. 更新完后, [st, ed]不变
  2. 将维护的区间变长
  3. [st, ed]与当前区间无交集, 因此可以将[st, ed] 加入到答案中, 然后将[st, ed]更新成当前的区间

实际代码中先做第3中情况, else {1,2可以合并做}
在这里插入图片描述

code

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
typedef pair<int, int> PII;
vector<PII> segs;
int n;

vector<PII> merge(){
    vector<PII> res;
    sort(segs.begin(), segs.end());
    
    int st = -2e9, ed = -2e9;
    for (auto item : segs){
        if (ed < item.first){
            if (st != -2e9) res.push_back({st, ed}); // 第3种情况
            st = item.first, ed = item.second;
        }else ed = max(ed, item.second); // 1, 2情况同时处理, 按左端点排序, 因此左边可以不处理
    }
    if (st != -2e9) res.push_back({st, ed});// 将最后维护的区间加入到答案
    return res;
}

int main(){
    cin >> n;
    for (int i = 0; i < n; i ++ ){
        int l, r;
        cin >> l >> r;
        segs.push_back({l, r});
    }
    

    
    segs = merge();
    
    cout << segs.size() << endl;
    
    return 0;
        
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值