小白也能学懂的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;
}

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

  • 19
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
### 回答1: 《高等数学 同济第七版7版 上册 习题全解指南 课后习题答案解析.pdf》 是一本涵盖了同济大学高等数学第七版上册全部习题答案和解析的辅导材料。本书共分为10章,每章都包含了大量的练习题,每道题目都有详细的解答和思路,非常适合同济大学高等数学第七版上册的学生进行课后复习和练习。 本书的特点如下: 第一,答案准确性高。本书的答案经过作者精心校对,确保其准确性,读者无需担心因答案错误而带来的误导或浪费时间的情况。 第二,解题思路清晰。本书提供了详细的解题思路和过程,能够帮助读者深入理解每一个练习题,掌握解题的方法和步骤。 第三,涵盖全面。本书涵盖了同济大学高等数学第七版上册的全部章节和练习题,几乎涵盖了所有考试题型,帮助读者全面掌握材内容。 第四,易于操作。本书采用简单明了的语言和图表,便于读者理解,操作简单实用。 综上所述,本书是一本非常优秀的同济大学高等数学第七版上册习题全解指南,对于同济大学高等数学第七版上册的学生来说是一份非常珍贵的学习资料。同时,本书也适合其他高校的同学进行参考、借鉴。 ### 回答2: 《高等数学 同济第七版7版 上册 习题全解指南 课后习题答案解析.pdf》是一本针对同济大学高等数学第七版上册课后习题的解答指南。这本指南由同济大学数学系的老师和研究生编写,内容全面,详尽,适合同济大学高等数学的学生参考和学习。 这本指南共包含了高等数学上册的全部习题,每道习题都有详细的解答和解析。解答中不仅给出了答案,而且还重点解释了解题方法和思路,有助于学生全面理解和掌握知识点。同时,该指南还包含了一些例题和总结,帮助学生深入了解高等数学上册的知识结构和难点。 本指南在内容上涵盖了高等数学上册的全部章节和难度层次,具有很高的参考价值。通过阅读本指南,学生可以更加深入地理解高等数学的知识点,提高解题能力,同时也可以帮助老师进行课程的优化和提高。 总之,《高等数学 同济第七版7版 上册 习题全解指南 课后习题答案解析.pdf》是一本十分有用的参考书,对同济大学高等数学的学生来说是一本必备的工具书。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值