【基础算法】三元组

115 阅读3分钟

🌹作者:云小逸
📝个人主页:云小逸的主页
📝Github:云小逸的Github
🤟motto:要敢于一个人默默的面对自己,==强大自己才是核心==。不要等到什么都没有了,才下定决心去做。种一颗树,最好的时间是十年前,其次就是现在!学会自己和解,与过去和解,努力爱自己。==希望春天来之前,我们一起面朝大海,春暖花开!==🤟 👏专栏:C++👏 👏专栏:Java语言👏👏专栏:Linux学习👏
👏专栏:C语言初阶👏👏专栏:数据结构👏👏专栏:备战蓝桥杯👏

@TOC


前言

今天我们继续学习算法,加油。这篇文章写的是三元组问题。希望这篇可以有幸帮助到你,码字不易,请多多支持。 在这里插入图片描述


题目描述

给定一个长度为 nn 的序列,求其中有多少个三元组 (i,j,k)(i,j,k) 满足 1i<j<kn1\le i<j<k\le nai<ak<aja_i<a_k<a_j

输入格式

第一行包含整数 nn

第二行包含 nn 个整数,表示完整序列。

输出格式

输出一个整数,表示满足条件的三元组个数。

数据范围

1n1051\le n \le 10^5

1ai1091\le a_i\le 10^9

输入样例:

5
3 1 2 4 5

输出样例:

2

题解

算法1

(暴力枚举) O(n3)O(n^3)

题目很容易想到使用三重循环暴力枚举所有的三元组 (i,j,k)(i, j, k) , 但这样的时间复杂度是 O(n3)O(n^3) 的,无法通过本题。

C++ 代码

#include<bits/stdc++.h>
using namespace std;
const int maxn = 100003;

int a[maxn], n, ans;

signed main()
{
    cin >> n;
    for(int i=1; i<=n; i++) cin >> a[i];

    for(int i=1; i<=n; i++)
        for(int j=i+1; j<=n; j++)
            for(int k=j+1; k<=n; k++)
                if(a[i]<a[k] && a[k]<a[j]) ans++;

    cout << ans << endl;
    return 0;
}

算法2

(求出两边增长序列) O(nlogn)O(nlogn)

下面为正解。

我们可以把一个合法的三元组拆分成两个增长的区间,并且中间点是固定的。如下图所示:

即先枚举中间数jj,再分别枚举左边最小数ii和右边最大数kk。 如果已经枚举到了jj,那么在[1,j1][1,j−1]这些位置中必定存在一个最小的ai=amina_i=a_{min},在[j+1,n][j+1,n]这些位置中也必定存在一个最大的ak=amaxa_k=a_{max},这样就可以将所有符合条件的三元组划分为O(n)O(n)个区间:对于一个特定的jj,其对答案的贡献就是amina_{min}前面的数和amaxa_{max}后面的数的数量乘积。

而求出这些前缀最小值和后缀最大值是可以通过单调栈实现的。具体方法是:

  • 对于每个位置ii,维护一个单调递增的栈stackstack,栈中一开始存放的是从左到右第一个大于等于aia_i的数
  • 栈里面的数是递增的,每次加入一个新数aja_j时,将栈中小于aja_j的数全部弹出
  • 此时栈顶就是aja_j前面最小的数

同理,我们也可以维护右边的后缀最大值,这样就能快速求出在jj左边比aja_j小的数和在jj右边比aja_j大的数。时间复杂度降为 O(nlogn)O(nlogn)

C++ 代码

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn = 100003;

int a[maxn], n, ans;
int mx[maxn], st[maxn], top;

signed main()
{
    cin >> n;
    for(int i=1; i<=n; i++) cin >> a[i];

    top = 0;
    for(int i=1; i<=n; i++)
    {
        while(top && a[st[top]]>=a[i]) top--;
        if(top) mx[i] = st[top];
        st[++top] = i;
    }

    top = 0;
    for(int i=n; i>=1; i--)
    {
        while(top && a[st[top]]<=a[i]) top--;
        if(top) ans += mx[i]? (top-st[mx[i]]) : top;
        st[++top] = i;
    }

    cout << ans << endl;
    return 0;
}

算法3

(离散化+二分查找) O(nlognlogn)O(nlognlogn)

另外一种解决方法是先对序列进行离散化,然后按照升序排列。对于i<j<ki<j<k这样的三元组,我们可以枚举jjkk,然后在[1,j)[1,j)这一段中寻找最小的aia_i满足条件,即ai<aja_i<a_j,如果找到这样一个数,则该数前面的所有数都是符合条件的。

考虑如何寻找aia_i,显然朴素的方法是从j1j-1开始往前扫描,找到第一个小于aja_j的数axa_x,再判断是否有ai<axa_i<a_x。这样的时间复杂度是 O(n2)O(n^2) 的,无法通过本题。

不过我们可以使用二分查找来优化这个过程: 对于每个jjkk,我们维护两个大小为 nn的数组sortedsortedreversedreversed,分别表示将序列按照升序排列得到的新序列和降序排列得到的新序列。对于一个固定的位置jj,我们只需要找到sortedsorted中,在前面且比aja_j小的元素中的max(reversed)max(reversed),以及在后面且比aja_j小的元素中的min(reversed)min(reversed)即可更新答案。

具体实现详见代码。

C++ 代码

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn = 100003;

int n, ans;
int a[maxn], sorted[maxn], reversed[maxn];
int s[maxn], r[maxn];

signed main()
{
    cin >> n;
    for(int i=1; i<=n; i++)
    {
        cin >> a[i];
        sorted[i] = reversed[i] = a[i];
    }

    sort(sorted+1, sorted+n+1);
    sort(reversed+1, reversed+n+1, greater<int>());

    for(int i=1; i<=n; i++)
    {
        int j = lower_bound(sorted+1, sorted+n+1, a[i]) - sorted;
        ans += (j-1) - (lower_bound(reversed+1, reversed+n+1, a[i], greater<int>()) - reversed);

        int k = lower_bound(s+1,s+i+1,a[i])-s-1;
        if(k) ans -= (lower_bound(r+1,r+k+1,a[i],greater<int>())-r);
        s[k+1] = a[i]; r[k+1] = min(r[k],a[i]);
    }

    cout << ans << endl;
    return 0;
}

最后

十分感谢你可以耐着性子把它读完和我可以坚持写到这里,送几句话,对你,也对我:

1.把时间尺度拉长,拉长十年看当下

2.不说负面情绪,只描述事实;

3.越专注于过好自己,能量和幸运越会照顾你; 只解决问题,不做没有意义的担心,输了就认;

4.学会原谅自己,要允许自己做错事,允许自己出现情绪波动,我知道你已经很努力很努力在做好了

5.所有你害怕的、想逃避的事情,最终都要面对,既然这样不如选择坦然面对。即使结果不如人愿,没关系,至少这个过程是享受的,而不是一路带着恐惧和害怕。

最后如果觉得我写的还不错,请不要忘记==点赞==✌,==收藏==✌,加==关注==✌哦(。・ω・。)

愿我们一起加油,奔向更美好的未来,愿我们从懵懵懂懂的一枚==菜鸟==逐渐成为==大佬==。加油,为自己点赞!