习题课1

1.第k个数

image-20230310095200944

这道题虽我做出来了,并且通过了,但是你老师永远是你老师,我还是太弱了。

我的想法很简单,先快排,然后直接输出那个位置即可,时间复杂度是O( nlogn)

但是老师提出了一个新思路,可以说叫做快选,时间复杂度是O(n)

我们快排思路是每次将区间分成两个子区间然后递归处理。其实我们可以记录下k的位置,那么我们每次只需要递归k所在的那个区间就可以了,另一个区间中的数一定是集体大于或者小于k的,不影响结果。

这样处理,我们第一次时间复杂度为n,第二次递归时间复杂度为n/2(理想状态下,也就是期望下),第三次n/4....知道区间长度为1,这时候这个区间内的数就是我们要找的k。综上,总体时间复杂度为n (1+1/2+1/4+1/8+......) < 2n,所以时间复杂度就是O(n)

代码如下

#include <iostream>

using namespace std;
const int N=1e6+10;
int a[N];

int quick_sort(int l, int r, int k) {
    // 两指针相遇时,说明已经部分快排完毕,di
    if(l >= r) return a[k];
	// 初始化
    int x = a[l], i = l - 1, j = r + 1;
    while (i < j) {
        // 另外一种写法,可以代替do-while,也是至少会进行一次
        while(a[++i] < x);
        while(a[--j] > x);
        if (i < j) swap(a[i], a[j]);
    }
    // 判断排好序后第k个数在哪个区间里,只递归那个区间
    if (k <= j) return quick_sort(l, j, k);
    else return quick_sort(j + 1, r, k);
}

int main() {
    int n, k;
    cin >> n >> k;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }
    // 我们默认处理a数组,所以可以不用写,但是参数要记录第k个数
    //第k个数在数组里的位置其实是第k-1位,所以传参为k-1
    cout << quick_sort(0, n - 1, k - 1) << endl;
    return 0;
}

2.逆序对的数量

image-20230310122251426

写这道题之前,一定要先明白,归并排序是先递归后处理

我们先一直递归到区间长度为1或0的时候,这时候return 0,这个值会给到上一层的res

每一层的res值都是他下面两层的res结果之和,倒数第一层的res都是0,所以这样就相当于初始化res=0

我们先不管递归的过程,来分析一下逆序对的情况,如果我们此时把大区间分为了两个小区间,那么逆序对两个数位置可能的情况有:

  1. 两个都在左区间
  2. 两个都在右区间
  3. 一个在左一个在右

看似有三种情况,其实在真正递归处理时,我们只需要处理第三种情况:我们知道,归并排序是分治的过程,在传递完后开始处理,我们的处理其实是从最后一层往第一层走的。所以每一层我们都是拿到了下一层两个已经处理好的区间,他们已经按照从小到大的顺序排列好了,所以就不会存在情况一和二。

其实可以这样理解,如果拿到的两个区间没有被处理,那么情况一和情况二是有可能存在的,但是现在被处理好了,也就意味着他们其实对于下面的某一层来说属于情况三,底下的那一层把他们已经作为逆序数记录下来了,所以现在就没有必要记录他们了

所以,我们只需要处理情况三即可,任何情况一或二都能在他下面的某一层中作为情况三被处理:

先说一下最简单的暴力方法:左区间遍历,每一个数都去遍历右区间,去看看是否满足,时间复杂度O(n^2)

但这样其实很傻,因为两个区间都是有序而且从小到大的,如果左区间中a大于右区间中的b,那么其实这时候已经表明a后面的所有数(左区间内)都大于b,这样会快很多。

因此,我们先使用if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];,当他停止的时候,说明我们在左区间找到了一个大于右区间的数,那么此时这个数以及他后面所有左区间的数总和为mid - i + 1(左区间的右边界为mid,当前指到i),然后再使用tmp[k ++ ] = q[j ++ ];去看看右区间的下一个数如何。如此往复。

我们从最底层开始传res,不断地把当前层的两个(一层一般是两个子区间)res传回给上层,最后在第一层完成汇总,这就是答案了。

代码如下:

#include <iostream>

using namespace std;
// 给long long起别名,方便使用
typedef long long LL;

const int N = 1e5 + 10;
int a[N], tmp[N];

LL merge_sort(int q[], int l, int r)
{
    // 如果区间为1,那么必然此时无法得出逆序对,返回0
    // 这一步也是res的初始化
    if (l >= r) return 0;
	
    int mid = l + r >> 1;
	// 归并排序先进行递归
    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 ++ ];
        }
    // 扫尾工作
    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];
	// 将处理好的结果给到a数组
    for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
	//返回res
    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;
}

3.数的三次方根

image-20230310150429211

这个简单,直接二分。注意这里即使没有负数,也不能将初始区间定为[ 0 ,x ],因为小数开完根会更大,所以右边界应该为max( 1 , x )。还有一点补充:一般要求小数精度为x,那么我们应该至少让他运行到x+2精度再停止,这样的数据精度更高

#include <iostream>

using namespace std;

int main()
{
    double x;
    cin >> x;
	// 这里直接根据答案范围写区间,无脑但简单
    double l = -100, r = 100;
    while (r - l > 1e-8)
    {
        double mid = (l + r) / 2;
        if (mid * mid * mid >= x) r = mid;
        else l = mid;
    }

    printf("%.6lf\n", l);
    return 0;
}
posted @ 2023-03-18 10:29  Zaughter  阅读(23)  评论(0编辑  收藏  举报