我是靠谱客的博主 英勇可乐,最近开发中收集的这篇文章主要介绍小白也能学懂的XCPC第三站——习题课+双指针+位运算+离散化+区间合并。手把手教,萌新也能看懂的基础算法教程!前言二、计算逆序对数量(归并排序的延伸)三、双指针四、位运算五、区间合并六、整数的保序离散化,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

在这里插入图片描述

前言

在本篇文章中,博主将讲解前面算法相关的练习题,还有双指针的相关方法。

一.快速选择算法(快速排序算法的延伸)

快速选择算法是一种与快速排序算法相似的算法,但是它的时间复杂度降为0(N),因为它每次都能根据实际情况只递归一边。下面让我们通过一道题来看看吧!
在这里插入图片描述
代码实现如下:

#include<iostream>
using namespace std;

const int N = 1e5+10;
int n,k;
int q[N];

int quick_sort(int l,int r,int k)
{
//这是base case,即最小规模子问题的处理
    if(l==r)    return q[l];
    int i = l-1,j = r+1,x = q[(l+r)/2];
    
    while(i<j)
    {
        do i++;while(q[i]<x);
        do j--;while(q[j]>x);
        if(i<j) swap(q[i],q[j]);
    }
    //计算左半边数组的长度。如果len>=k,说明我们要找的数在左半边,那么只要将左半边排序并返回第k小的值即可。如果len<k,说明我们要找的数在右半边,那么就要将右半边的数进行排序,并返回第(k-len)小的值。因为左半边的数<=右半边的数,因此这个值就是整个区间第k小的数。
    int len = j-l+1;
    if(k<=len)  return quick_sort(l,j,k);
    else    return  quick_sort(j+1,r,k-len); 
}

int main()
{
    scanf("%d%d",&n,&k);
    for(int i = 0;i<n;i++)  scanf("%d",&q[i]);
    
    cout<<quick_sort(0,n-1,k)<<endl;
}

我们可以发现,第一次我们需要处理的区间长度为n,第二次为n/2(这里的“/”表示除以,并不向下取整),第三次为n/4……那么加和后小于2n,因此该算法的时间复杂度为O(N)。

二、计算逆序对数量(归并排序的延伸)

逆序对是指:a、b两个数满足a>b,但b的位置在a之前。下面让我们学习一下如何求解逆序对的数量。
在这里插入图片描述
当左右两半部分排好序以后,逆序对的种类有三种:1.两个数都在左半边。2、两个数都在右半边。3.一个在左半边,一个在右半边。假设升级后(我们新定义)的归并排序函数能够返回逆序对的数量,那么对于1、2两种情况,只要分别递归左右部分并相加即可。那么第三种情况怎么处理呢?我们回忆一下归并排序的最后一步:把左右两部分数组按照大小顺序插入临时数组,再把临时数组的内容copy回原来的数组。我们的思路就可以是这样的:
在这里插入图片描述
这里的Sj表示与j指向的数构成逆序对的left中数的个数。
那么三种情况我们都已经处理完啦!接下来看看代码吧!

#include <iostream>

using namespace std;

typedef long long LL;

const int N = 1e5 + 10;

int a[N], tmp[N];

LL merge_sort(int q[], int l, int r)
{
    if (l >= r) return 0;

    int mid = l + r >> 1;
    //先处理第1、2种情况
    LL res = 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
        {
            res += mid - i + 1;
            tmp[k ++ ] = q[j ++ ];
        }
    //若还有某个数组剩下一部分,直接接上就行
    //扫尾时不需要再让res增加,因为此时已经不存在数对了
    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];

    for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
    

    return res;
}

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

    cout << merge_sort(a, 0, n - 1) << endl;

    return 0;
}

//作者:yxc
//链接:https://www.acwing.com/activity/content/code/content/39791/
//来源:AcWing
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

三、双指针

双指针算法是一种依靠两个指针对数据结构进行操作的算法,我们熟知的归并排序中就用到了双指针算法。下面让我们通过一些例题来感受一下双指针算法的使用方式吧!

例题三、1 输出单词

这里有一些单词,它们之间以一个空格隔开,如abc def ghi。要求输出各个单词,每个单词占一行。这是一个比较简单的题目,但是它很好地体现了双指针的思想:

#include <iostream>
#include<string.h>
using namespace std;


int main()
{
    char str[100];
    
    gets[char];
    
    int n = strlen(str);
    
    for(int i = 0;i<n;i++)
    {
        //从i开始,寻找空格。
        int j = i;
        while(j<n&&str[j]!=' ') j++;
        //此时退出循环,j已经指向空格
        
        for(int k = i;k<j;k++)  cout<<str[k];
        cout<<endl;
        //下一次循环之前把j给i,那么进入循环后i++,i就自动指向空格后一个字母了。
        i = j;
    }
}

例题三、2最长连续不重复子序列

在这里插入图片描述
这道题该如何思考呢?我们创造i、j两个指示器,i在右边负责遍历数组,j在左边负责寻找离i最远的且j、i之间无重复数字的数,那么i、j之间长度的最大值就是我们要求的结果。图示如下:
在这里插入图片描述
那么根据这个思路,我们就知道i、j两个指针这样运动:i一直往前走,j从i处往后走,遇到重复的数就停下,每次更新最大长度。博主写的代码如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

const int N = 1e5 + 10;

int n;
int q[N];

//判断是否有重复元素
int check(int q[], int l, int r)
{
    for (int i = l; i <= r; i++)
    {
        for (int j = l; j < i; j++)
        {
            if (q[i] == q[j])  return 0;
        }
    }
    return 1;
}

int getMaxSunString(int q[], int n)
{
    //用max记录不重复连续子序列的最大长度
    int max = 0;
    for (int i = 0; i < n; i++)
    {
        //一定要小于等于!防止出现只有一个不重复的数字的情况!
        for (int j = 1; j<=i`在这里插入代码片`; j--)
        {
            if (check(q, j, i))
            {
                //当只有一个不重复的数字时如 2 2 2 2 2 2 2 1仍然适用。因为max全局变量初始值为0,i-j+1是大于max的。
                if (i - j + 1 > max)
                {
                    max = i - j + 1;
                }
            }
        }
    }
    return max;
}


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

    int res = getMaxSunString(q, n);
    
    cout<<res<<endl;
}

这段代码在Acwing上会造成Tine Limit Exceed,但是VS上测试成功。博主猜测是用了太多循环的原因。让我们看看yxc大佬是怎么做的吧!

#include <iostream>

using namespace std;

const int N = 100010;

int n;
int q[N], s[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 ++ )
    {
        //把q[i]出现的次数存到s数组对应q[i]下标处(q[i]是几就存到s的几下标处),每出现一次s对应下标处的值加一。
        s[q[i]] ++ ;
        //s[q[i]]>1说明前面有重复,如 4 4,那么后一个4的时候s[4] == 2,在前一个4处s[4] == 1。
        //在两种情况下while循环会停止:第一种是j==i而s[q[i]]>1,那么此时已经有重复,计数作废,i - j + 1 ==1.恰好只有一个不重复,符合实际;第二种是j<i而s[q[i]]<=1,说说明此时还没有重复,j停在上一次重复最后数字的下一个位置,i - j + 1也是对于此刻的i来说往左的最长不重复连续子串长度,符合实际。可能会有人认为按照逻辑关系量词运算还应该有j == i 且s[q[i]]<=1的情况,但这是不可能发生的,因为j == i之前循环已经因为s[q[i]]<=1停下!!!      
        while (j < i && s[q[i]] > 1) 
        //这里不能只写j++!这样写是为了让在i位置前出现的与i位置重复的数字所对应的s数组下标的值自减,否则会造成错判。比如说s[3]本来已经是1了,后面若再含有3,如果不把这个1减掉,会造成s[2]>1,从而j++(j<i的情况下)。但是实际上这里的2是前面的3而不是现在这个3造成的,j不应该++!!!
            s[q[j++]]--;
        //比较原有res和当前区间长度,把大的那个赋给res。
        res = max(res, i - j + 1);
    }

    cout << res << endl;

    return 0;
}

如果你想了解双指针算法在链表中的运用,可以看看这篇文章:
带你刷穿Leetcode链表OJ!

四、位运算

四、1:获得n二进制表示中第k位的方法

:(n>>k)&1(我们记最低位为第一位)

四、2:统计n二进制表示中1的个数

首先我们介绍一下lowbit,它可以返回二进制表示中最低位的1所表示的值。例如若某二进制数n为101010000,则lowbit(n)== 10000。lowbit(x)的实现方式如下:
x&(-x)
为什么这样就可以返回二进制中最低位的1所表示的值呢?我们来看看下面这张图就能理解了(这里需要原码、反码和补码的相关知识)——
在这里插入图片描述
在这里插入图片描述

#include<iostream>
using namespace std;

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

int main()
{
    int n = 0;
    cin>>n;
    
    while(n--)
    {
        int x = 0;
        cin>>x;
        
        int count = 0;
        
        while(x--)
        {
        //每次减去最低位的1表示的数值,知道减为零。
            x-= lowbit(x);
            count++;
        }
        
        cout<<count<<' ';
    }
    
    return 0;
}

五、区间合并

这是一种合并交集不为空的区间的算法。如下题:
在这里插入图片描述

在这里插入图片描述

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

using namespace std;
//用于存储两个数
typedef pair<int, int> PII;

void merge(vector<PII> &segs)
{
    //res是存储结果区间各个左右端点的数组
    vector<PII> res;
    //优先按照左端点排序,若左端点相同,再按照右端点排序。
    sort(segs.begin(), segs.end());
    //初始化,把范围扩大一些
    int st = -2e9, ed = -2e9;
    //遍历存左右端点的vector数组
    for (auto seg : segs)
    //交集为空的情况
        if (ed < seg.first)
        {
            if (st != -2e9) 
            //不要把最开始那个区间放进去!
                res.push_back({st, ed});
                //更新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;
    scanf("%d", &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;
}

// 作者:yxc
// 链接:https://www.acwing.com/activity/content/code/content/40108/
// 来源:AcWing
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

六、整数的保序离散化

考虑这样一些整数,它们的范围分布很大如1~1e9,但是个数较少,如1 ~ 1e5。离散化就是这样一个过程:把这些数映射到连续的数中(一般是从1开始的自然数,即数据对应的下标+1。当然从零开始也OK)。如1 300 200000 4102030离散化后的结果是1 2 3 4。但是原数组中可能有重复元素,因此我们要去重。
在这里插入图片描述
对于这道题,我们要先离散化,再做前缀和(因为原始数据并不是紧挨着放置,直接前缀和的话无法确定步长)。

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

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/40105/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

bonus:unique函数的实现(对于已经排好序的序列,假设是升序)

1 2 2 3 5 5 5 6 7 9
我们开一个新数组来存放不重复的元素。这个不重复的元素应该满足一下两条中的一条:
1.它是第一个元素
2.a【i】! = a【i-1】。
只要满足其中之一,我们就把它存起来。为了达到这个目的,我们用双指针算法。一个指针用来遍历,另一个指针用来追踪。
代码实现(出自——):
Acwing yxc

vector<int>::interator unique(vector<int>&a)
{
	//j用于记录新数组中已经有多少个元素
	int j = 0;
	for(int i = 0;i<a.size();i++)
	{
		if(!i||a[i]!=a[i-1])
		a[j++] = a[i];
	}
	return a.begin()+j;
}

好啦,以上就是本篇文章的全部内容。如果你感觉对你有帮助的话,请多多支持博主,你们的支持是我更新的最大动力哦!

最后

以上就是英勇可乐为你收集整理的小白也能学懂的XCPC第三站——习题课+双指针+位运算+离散化+区间合并。手把手教,萌新也能看懂的基础算法教程!前言二、计算逆序对数量(归并排序的延伸)三、双指针四、位运算五、区间合并六、整数的保序离散化的全部内容,希望文章能够帮你解决小白也能学懂的XCPC第三站——习题课+双指针+位运算+离散化+区间合并。手把手教,萌新也能看懂的基础算法教程!前言二、计算逆序对数量(归并排序的延伸)三、双指针四、位运算五、区间合并六、整数的保序离散化所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(55)

评论列表共有 0 条评论

立即
投稿
返回
顶部