AcWing 1236. 递增三元组 题解(二分+双指针+前缀和)

原题链接:AcWing 1236. 递增三元组 .

题目描述

给定三个整数数组

A=[A1,A2,…AN],
B=[B1,B2,…BN],
C=[C1,C2,…CN],

请你统计有多少个三元组 (i,j,k) 满足:

1≤i,j,k≤N
Ai<Bj<Ck
输入格式
第一行包含一个整数 N。

第二行包含 N 个整数 A1,A2,…AN

第三行包含 N 个整数 B1,B2,…BN

第四行包含 N 个整数 C1,C2,…CN

输出格式
一个整数表示答案。

数据范围
1≤N≤105,
0≤Ai,Bi,Ci≤105
输入样例:

3
1 1 1
2 2 2
3 3 3

输出样例:

27

题目分析

首先考虑暴力做法,三个数组嵌套枚举,O(n3)的时间复杂度,n≤105一定会超时。

尝试通过枚举的次序进行优化本题,先枚举B数组,在A中寻找小于B[i]的数的个数cnt1,在C中寻找大于B[i]的数的个数cnt2,带有B[i]的合法选择数就是cnt1*cnt2。

用暴力查找时间总的时间复杂度为O(n2),还是会超时。

二分

既然是查找,那么可以考虑进行二分查找,查找前先通过排序预处理三个数组,排序时间复杂度O(nlog2n),枚举B的所有元素+查找A,C中的元素时间复杂度也是O(nlog2n),总的时间复杂度降为O(nlog2n)

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

typedef long long LL;
const int N = 1e5+10;
int num[3][N];

int main() {
    int n;
    scanf("%d", &n);
    for(int i = 0; i < 3; ++i) 
        for(int j = 1; j <= n; ++j) 
            scanf("%d", &num[i][j]);
    for(int i = 0; i < 3; ++i)
        sort(num[i]+1, num[i]+n+1);
        
    LL ans = 0;
    //枚举B,寻找A满足的个数以及C满足的个数相乘
    for(int i = 1; i <= n; ++i) {
        int key = num[1][i];
        //A中二分查找第一个小于key的数的下标
        int pos1 = lower_bound(num[0]+1, num[0]+n+1, key)-num[0]-1;
        //C中二分查找第一个大于key的数的下标
        int pos2 = upper_bound(num[2]+1, num[2]+n+1, key)-num[2];
        if(pos1 >= 1 && pos2 <= n) {
            ans += (LL)pos1*(n-pos2+1);
        }
    }
    cout<<ans<<endl;
    return 0;
}

双指针

进一步对查找进行优化,对于排过序的数组A和B,寻找A中小于B[i]的元素的个数可以考虑双指针算法,因为每个指针最多移动n次,故查找的时间复杂度降到O(n),查找C与查找A同理,只是找第一个大于B的位置。
只需要将上述二分程序中的

//二分
for(int i = 1; i <= n; ++i) {
	int key = num[1][i];
	//A中二分查找第一个小于key的数的下标
	int pos1 = lower_bound(num[0]+1, num[0]+n+1, key)-num[0]-1;
	//C中二分查找第一个大于key的数的下标
	int pos2 = upper_bound(num[2]+1, num[2]+n+1, key)-num[2];
	if(pos1 >= 1 && pos2 <= n) {
	    ans += (LL)pos1*(n-pos2+1);
	}
}

更改为

//双指针
int a = 1, c = 1;
for(int i = 1; i <= n; ++i) {
    int key = num[1][i];
    while(a<=n && num[0][a] < key) a++;
    while(c<=n && num[2][c] <= key) c++;
    
    ans += (LL)(a-1)*(n-c+1);
}

完整的双指针程序为:

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

typedef long long LL;
const int N = 1e5+10;
int num[3][N];

int main() {
    int n;
    scanf("%d", &n);
    for(int i = 0; i < 3; ++i) 
        for(int j = 1; j <= n; ++j) 
            scanf("%d", &num[i][j]);
    for(int i = 0; i < 3; ++i)
        sort(num[i]+1, num[i]+n+1);
        
    LL ans = 0;
    //枚举B,寻找A满足的个数以及C满足的个数相乘
    int a = 1, c = 1;
    for(int i = 1; i <= n; ++i) {
        int key = num[1][i];
        while(a<=n && num[0][a] < key) a++;
        while(c<=n && num[2][c] <= key) c++;
        
        ans += (LL)(a-1)*(n-c+1);
        
    }
    cout<<ans<<endl;
    return 0;
}

前缀和

之前的双指针算法时间复杂度的瓶颈为:排序O(nlog2n)
考虑是否可以不排序在O(n)的时间内解决此问题呢?

既然要排序实现快速的查找A中小于B[i]的数的个数,可以将数组A中所有元素出现的次数存入一个哈希表中,因为数组中元素的范围只有n5, 可以开一个大的数组cnta 作为哈希表。

在枚举B中元素时,我们需要快速查找找小于B[i]的所有元素的总数,只需要在枚举之前先将求出表中各数的前缀和即可。

查找C与查找A同理可得。

C++代码实现

//前缀和
#include <iostream>
#include <cstdio>

using namespace std;

typedef long long LL;
const int N = 1e5+10;
int A[N], B[N], C[N];
int cnta[N], cntc[N], sa[N], sc[N];

int main() {
    int n;
    scanf("%d", &n);
    //获取数i在A中有cntc[i]个,并对cnt求前缀和sa
    for(int i = 1; i <= n; ++i) {
        scanf("%d", &A[i]);
        cnta[A[i]]++;
    }
    sa[0] = cnta[0];
    for(int i = 1; i < N; ++i) sa[i] = sa[i-1]+cnta[i];
    //B只读取即可
    for(int i = 1; i <= n; ++i) scanf("%d", &B[i]);
    
    //获取数i在C中有cntc[i]个,并对cnt求前缀和sc
    for(int i = 1; i <= n; ++i) {
        scanf("%d", &C[i]);
        cntc[C[i]]++;
    }
    sc[0] = cntc[0];
    for(int i = 1; i < N; ++i) sc[i] = sc[i-1]+cntc[i]; 

    //遍历B求解
    LL ans = 0;
    for(int i = 1; i <= n; ++i) {
        int b = B[i];
        //cout<<sa[b]<<" "<<cnta[b]<<" "<<sc[N-1]<<" "<<sc[b]<<endl;
        ans += (LL)sa[b-1]*(sc[N-1]-sc[b]);
    }
    cout<<ans<<endl;
    return 0;
}

运行时间

二分:725 ms
双指针:454 ms
前缀和:179 ms

猜你喜欢

转载自blog.csdn.net/mwl000000/article/details/108209539
今日推荐