courese-day-4-序列算法-分治思想-前缀和-差分-two-pointer的学习

本文实际上是我于2022/7/29补写的

分治

逆序对(洛谷P1908)

我们直接打暴力,枚举每一个树a[i],在a[1~n]直接寻找比a[i]小的数即可。

但是这样很明显不能通过本题,他的时间复杂度是 O ( n 2 ) O(n^{2}) O(n2)

我们可以用线段树维护权值数组来加速,但是今天的重点是分治。

我们可以观察到,对于逆序对*(x,y)*,只有3种情况:

  • x,y在左边
  • x,y在右边
  • x左边y右边

所以我们能实现一个函数 calc(l, r):

  • 调用 calc(l, mid) 获得左边块内的逆序对个数
  • 调用 calc(mid, r) 获得右边块内的逆袭对个数
  • 亲自去统计跨块的逆序对个数

即为在归并的过程中统计个数。

#include <bits/stdc++.h>
using namespace std;

int a[500005], n;

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

long long ans;


// 统计 a[l, r) 区间内的逆序对的个数
void calc(int l, int r) {
    if(l == r - 1)
        return;

    int mid = (l + r) / 2;

    calc(l, mid);   // 统计左边块内的逆序对个数
    calc(mid, r);   // 统计右边块内的逆序对个数

    // 亲自统计跨块逆序对个数:x在左边,y在右边

    for(int x=l; x<mid; x++)        // 枚举左边块的 x
        for(int y=mid; y<r; y++)    // 枚举右边块的 y
            if(a[x] > a[y]) ans++;  // 发现一个跨块逆序对
}

void work() {
    calc(0, n);
    
    printf("%lld\n", ans);
}



int main() {
    input();
    work();

    return 0;
}

但是我们用Master定理一算,嗯?还是 O ( n 2 ) O(n^{2}) O(n2)啊!

实现一个函数 calc(l, r)

  • 调用 calc(l, mid)
  • 调用 calc(mid, r)
  • 将左边、右边的块排序
  • O(n) 亲自统计跨块的逆序对个数
#include <bits/stdc++.h>
using namespace std;

int a[500005], n;

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

long long ans;

int tmp[500005];

// 统计 a[l, r) 区间内的逆序对的个数
// 顺便给 a[l, r) 排序
void calc(int l, int r) {
    if(l == r - 1)
        return;

    int mid = (l + r) / 2;

    calc(l, mid);   // 统计左边块内的逆序对个数
    calc(mid, r);   // 统计右边块内的逆序对个数

    // 现在 a[l, mid) 和 a[mid, r) 都是有序的

    // 亲自统计跨块逆序对个数:x在左边,y在右边
    int y = mid;
    for(int x=l; x<mid; x++) {  // 枚举左边的 x
        while(y < r && a[y] < a[x]) y++;
        ans += y - mid;     // a[mid, y) 全都小于 a[x],所以统计进答案
    }

    // 以下是合并左右两个有序数组,保证 a[l, r) 在调用结束后有序
    // 归并排序
    int p=l, q=mid, cur=l;

    while(p < mid && q < r){
        if(a[p] < a[q])
            tmp[cur++] = a[p++];
        else
            tmp[cur++] = a[q++];
    }

    while(p < mid)
        tmp[cur++] = a[p++];
    while(q < r)
        tmp[cur++] = a[q++];
    
    for(int i=l; i<r; i++)
        a[i] = tmp[i];
}

void work() {
    calc(0, n);
    
    printf("%lld\n", ans);
}



int main() {
    input();
    work();

    return 0;
}

省了2个sort,就降下来了时间复杂度:
T ( n ) = 2 T ( n / 2 ) + O ( n ) = O ( n l o g n ) T(n) = 2T(n/2) +O(n) = O(nlog n) T(n)=2T(n/2)+O(n)=O(nlogn)

快速幂

因为取模可以在运算过程中取,所以我们可以不爆空间。

计算 x a m o d p x_{a} mod p xamodp 时,分两种情况:
若 2 ∣ a ,则答案为 ( x a / 2 ) 2 m o d p 若 2 ∤ a ,则答案为 ( x ⌊ a / 2 ⌋ ) 2 ⋅ x m o d p 若 2 | a,则答案为 (x^{a/2})2 mod p\\ 若 2 ∤ a,则答案为 (x^{⌊a/2⌋})2 · x modp 2∣a,则答案为(xa/2)2modp2a,则答案为(xa/2)2xmodp

ll quickPow(ll a, ll x, ll p) {
    if(x == 0) return 1;
    if(x == 1) return a % p;

    ll tmp = quickPow(a, x/2, p);

    if(x % 2 == 0)
        return tmp * tmp % p;
    else
        return tmp * tmp % p * a % p;
}

二分

平方根

题目描述
n \sqrt n n 的整数部分。

输入格式

仅一行,一个正整数,表示 n n n

输出格式

仅一行,一个正整数,表示$\lfloor \sqrt n\rfloor $。

样例 #1

样例输入 #1

12345

样例输出 #1

111

提示

对于 100 % 100\% 100%的数据, n ⌊ 1 0 18 ⌋ n\lfloor 10^{18}\rfloor n1018

可以转换为求最大的x使 x 2 ≤ n x^{2}\leq n x2n

暴力

可以直接暴力

for(ll i=1;i*i<=n;i++);
	printf("%lld",x-1);

但是x达到了 1 18 1^{18} 118级别,无法通过本题。

二分

我们可以通过猜来解决问题。猜一个数x如果 x 2 x^{2} x2大于n就说明我们猜大了,如果小于就说明猜小了。

1~1e9猜数即可:

void work() {
    ll l = 0, r = 1000000000;
    // 在 [l, r] 范围内寻找答案

    // l, r 差距大于 5 的时候,执行二分
    while(r - l > 5) {
        ll mid = (l + r) / 2;
        // 猜中点,判断答案与之的大小关系

        if(mid * mid > n)   // mid 猜大了,答案比 mid 小
            r = mid - 1;
        else                // mid^2 <= n,答案 >= mid
            l = mid;
    }

    // l, r 差距 <= 5,逐个判断
    // 寻找最大的 x*x <= n 的 x
    for(ll x=r; x>=l; x--)
        if(x * x <= n) {
            cout << x << endl;
            return;
        }
}

可能会有人问:“嗯?你这个二分怎么和一般的二分不一样?”

因为这样好写,不容易写炸。如果写“学院派”二分可能不小心就写炸了。

这样写之比“学院派"二分慢一点点,称为”通用二分“。

我们缩小到5个后逐个判断。

可二分性

如果你可以快速判断一个答案是否满足要求,那就满足可二分性,可以尝试二分求解。

区间和(前缀和)

今有 n 个同学,学号为 1 ∼ n。给出每个人的语文成绩。 需要处理 m 次询问,每次询问给定 l, r,查询学号在 [l, r] 范围内的学生的成绩
sum。

即给定一个数组,多次查询区间和。

如果采用分块,每次查询代价是 O ( n ) O(\sqrt n) O(n ),总复杂度 O ( m n ) O(m\sqrt n) O(mn )

那么我们可以弄一个pre数组,来储存1k*的和,求*ik的和即为pre[k]-pre[i] 真是方便呢!

这个pre数组便被成为前缀和。

所以前缀和实际上也不是个很高级的东西。

void init() {

    // pre[k] = a[1] + a[2] + ... + a[k]

  

    for(int i=1; i<=n; i++)

        pre[i] = pre[i-1] + a[i];

}

  

// a[l] + a[l+1] + ... + a[r]

int getSum(int l, int r) {

    return pre[r] - pre[l-1];

}

仅适用于数组固定,如果是动态(插入+查询)的那种就不能使用。

我们需要使用 O(n) 的时间预处理出 pre 数组。 接下来每个询问可以 O(1) 回答。
复杂度在 OI 中简记为 O(n) −O(1),即 O(n) 预处理,每次查询 O(1)。

差分

{%label 差分 red %}可以用来解决{%label 多次区间加,最终求整个数组 red %}的问题。 我们采用 d[x] 记录{%label a 数组从 x 这个位置开始应该增加多少 red %}。 那么,[l, r] 区间加 k 可以表示为:

  • d[l] += k
  • d[r+1] -= k
    最后扫一遍 d 数组即可还原 a[]。
a={0,0,+1,0,0,0,-1,0,0};
pre={0,0,1,1,1,1,0,0};

这样即可实现区间加/减。
不用直接给数组加减,我们只需维护差分数组即可。
前缀和和查询何为逆运算。

想要给1~3加1,只需要给差分数组的[1]+1,[3]-1即可。
在所有的都做完差分后做一次差分即可 O ( n ) O(n) O(n)完成运算。

浇花

题目描述

今有 n n n 盆花摆成一排,编号为 1 ∼ n 1\sim n 1n

机器猫一时兴起,就会去浇花。机器猫每次浇花,是选择一个区间 [ l , r ] [l, r] [l,r],给这个区间内的花浇水。

机器猫一共浇了 m m m 次水。我们想知道,每盆花被浇了多少次。

输入格式

第一行,两个正整数 n , m n, m n,m

接下来 m m m 行,每行两个正整数 l , r l, r l,r,表示一次浇花。

输出格式

仅一行, n n n 个整数,表示每盆花被浇了多少次。

样例 #1

样例输入 #1

5 3
1 4
2 5
3 3

样例输出 #1

1 2 3 2 1

提示

对于 100 % 100\% 100% 的数据, n , m ≤ 100000 n, m\leq 100000 n,m100000

void add(int l, int r) {
    d[l] += 1;
    d[r+1] += -1;
}

void work() {
    while(m--) {
        int l, r;
        cin >> l >> r;
        add(l, r);
    }

    // 通过 d 数组恢复 a 数组
    for(int i=1; i<=n; i++)
        a[i] = a[i-1] + d[i];
    
    for(int i=1; i<=n; i++)
        cout << a[i] << ' ';
    
}

差分是不是非常的有意思和强大呢?对于固定的修改可以很方便的操作。

差分与前缀和

差分可以应对多次输入区间加,最后输出整个序列的问题。

差分的缺陷是不能中途询问,因为中途查询的复杂的是*O(n)*的。

差分与前缀和是两个极端,一个中途不能改,一个中途不能查。

  • 差分可以*O(1)*改,*O(n)*查询。
  • 前缀和可以*O(n)*预处理,*O(1)*查询

(线段树可以中途改和查且复杂的为O(nlogn)

Two-pointers

e g 1 eg_1 eg1

最短区间

题目描述

给定长度为 n n n 的序列 a a a ,以及一个正整数 s s s

要寻找最短的区间,使区间 sum ≥ s \geq s s

输出最短区间的长度。

输入格式

第一行,两个正整数 n , s n, s n,s

接下来一行, n n n 个正整数,表示序列 a a a

输出格式

仅一行,一个正整数,表最短合法区间的长度。

样例 #1

样例输入 #1

7 11
1 3 2 5 3 4 1

样例输出 #1

3

样例 #2

样例输入 #2

7 5
1 3 2 5 3 4 1

样例输出 #2

1

提示

对于 40 % 40\% 40% 的数据, n ≤ 1000 n \leq 1000 n1000

对于 100 % 100\% 100% 的数据, n ≤ 100000 , 1 ≤ a i ≤ 10000 n\leq 100000, 1\leq a_i \leq 10000 n100000,1ai10000

我们可以想到直接 O ( n 2 ) O(n^{2}) O(n2)暴力,但是文明人都不会暴力。所以我们想到了二分,可以 O ( n l o g n ) O(nlogn) O(nlogn)解决。枚举l二分r

#include <bits/stdc++.h>
using namespace std;

int n, s;
int a[100005];

void input() {
    cin >> n >> s;
    for(int i=1; i<=n; i++)
        cin >> a[i];
}

int pre[100005];

void init() {
    for(int i=1; i<=n; i++)
        pre[i] = pre[i-1] + a[i];
}

int getSum(int l, int r) {
    return pre[r] - pre[l-1];
}


// 寻找到一个位置 pos,使得 [begin, pos] 区间和 >=s,且 pos 最小
int findRight(int begin) {
    int l = begin, r = n;

    while(r - l > 5) {
        int mid = (l + r) / 2;

        if(getSum(begin, mid) <= s)
            l = mid;
        else
            r = mid;
    }

    for(int x=l; x<=r; x++)
        if(getSum(begin, x) >= s)   // [begin, x] 是合法区间
            return x;
    
    return 23333333;
}

void work() {
    int ans = 23333333;

    for(int l=1; l<=n; l++) {
        int r = findRight(l);

        ans = min(ans, r - l + 1);
    }

    cout << ans << endl;
}

int main() {
    input();
    init();

    work();

    return 0;
}

但是其实这道题有线性( O ( 1 ) O(1) O(1))的做法.

定义指针lr,一开始都指向1,如果cursum<s就将r向右移,直到cursum>=s为止,统计答案,再将l向右移。

这就是Two-pointers算法。

因为总体只移动了n次,所以时间复杂度为 O ( n ) O(n) O(n)

#include <bits/stdc++.h>
using namespace std;

int n, s;
int a[100005];

void input() {
    cin >> n >> s;
    for(int i=1; i<=n; i++)
        cin >> a[i];
}

void work() {
    int l = 1, r = 1, curSum = a[1];        // 初始
    int ans = 23333333;

    while(l <= n) {
        // 现在要对当前的 l,找到对应的合法最近 r
        while(r <= n && curSum < s) {
            // 当前区间和不够 s,需要右移 r 以增加区间和
            r++;
            curSum += a[r];
        }

        // printf("[%d, %d]\n", l, r);

        if(curSum >= s)
            ans = min(ans, r-l+1);
        
        // 已经统计完当前 l 的答案,l 右移
        curSum -= a[l];
        l++;
    }

    cout << ans << endl;
}

int main() {
    input();
    work();

    return 0;
}

This article was written on 2022/8/5

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值