树状数组 / 线段树 / DP :三元上升子序列 -> 多元上升子序列(分析3的方式DP)

本文讲解了如何利用树状数组优化算法解决编程题目中关于三个数a[i] < a[j] < a[k]的计数问题,涉及固定中间值枚举、排序策略、离散化、DP和线段树的应用,提供多种代码实现方式并解析其背后的逻辑。
摘要由CSDN通过智能技术生成

总结:

当你求解某三个值,  i < j < k , 且i,j,k对应的值有某个关系的时候,可以考虑固定中间的值(也就是枚举中间的值),求两边对应的满足条件的值。

比如此题: 求i < j < k  且 a[i] < a[j] < a[k]的总个数,那么我们就可以枚举j然后 求j的两边。左边小于a[j]的情况,右边大于a[j]的情况,这样就大大降低了解题的难度。

题目链接:https://www.luogu.com.cn/problem/P1637

题目:

题目描述

Erwin 最近对一种叫 thair 的东西巨感兴趣。。。

在含有 n个整数的序列a1​,a2​,…,an​ 中,三个数被称作thair当且仅当 i<j<k 且 ai​<aj​<ak​。

求一个序列中 thair 的个数。

输入格式

开始一行一个正整数 n,

以后一行 n 个整数a1​,a2​,…,an​。

输出格式

一行一个整数表示 thair 的个数。

输入输出样例

输入 #1复制

4
2 1 3 4

输出 #1复制

2

输入 #2复制

5
1 2 2 3 4

输出 #2复制

7

说明/提示

样例2 解释

7个 thair 分别是:

  • 1 2 3
  • 1 2 4
  • 1 2 3
  • 1 2 4
  • 1 3 4
  • 2 3 4
  • 2 3 4

数据规模与约定

  • 对于 30\%30% 的数据 保证 n≤100;
  • 对于 60\%60% 的数据 保证n≤2000;
  • 对于 100\%100% 的数据 保证 1≤n≤3×1e4,0≤ai​<263。

 分析1:

此题需要求解三个数之间的关系,所以我们固定一个数(枚举一个数),求解另外两个与此数对应的关系。

题目要求  i < j < k,且 a[i] < a[j] < a[k],

所以我们枚举j。  然后找j 左边 比 a[j] 小的值的个数left[j].

j右边 比 a[j]大的值的个数 right[j]

这样就可以使用乘法原理,left[j] * right[j]得到j的情况下,满足条件的个数。

而找j左边 小于 a[j]的值的个数,就可以使用树状数组的方式。

借鉴 树状数组求逆序对的思路:借鉴此题解:树状数组求逆序对(当然此题解并不是树状数组求逆序对(逆元是 i < j 且 a[i] > a[j]),但是思路基本一样。)

1.求 j 左边小于a【j】的值的个数,首先对a进行排序,按照值从小到大,并且相等时大下标在前的方式。(为什么需要大下标在前,这样当相等的时候,大下标就不会统计到小下标了。防止误加相同值情况。 如: 1 5(1) 5(2) 5(3)  6,如果这样排序的话,树状数组在5(3)前会将5(1)加一次,5(2)加一次, 导致找5(3)前面值的时候,将5(1,2)这两个加了进去。)

2.求j 右边大于a[j]的值的个数,这个就可以按照值从大到小的方式进行排序,并且相等时小下标在前。

这个又是为什么呢?

因为我们树状数组统计的是前缀和。所以我们只会统计1~i - 1的比它大的值,所以需要用 当前的总数 - 统计的前面的次数和。

而小的在前,这样就会将小的下标统计进去,然后当前总数 - 统计的次数就会将小下标的也减去。

额外-1,是因为我们求了i - 1的总和, 所以还要减去本身。(实际上可以使用 求前1~i的总和,这样就不用减1了。但我感觉这个-1属于细节,还是写出来提醒一下为好)

代码实现1:树状数组 + sort()

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

const int N = 3e4 + 10;

pair<long long , int> a[N];  // first存的是值, int 存的是下标

int t[N];

int right2[N]; // i的右边比它大的元素个数
int left2[N]; //i的左边比它小的元素个数

int n;

int lowbit(int x)
{
    return x & (-x);
}

bool cmp1(pair<long long,int >a , pair<long long, int >b)
{
    if(a.first == b.first)
    {
        return a.second > b.second; // 大序号在前,树状数组通过序号进行统计
    }
    return a.first < b.first;
}

bool cmp2(pair<long long,int >a , pair<long long, int >b)
{
    if(a.first == b.first)
    {
        return a.second < b.second; // 小序号在前
    }
    return a.first > b.first;
}

void add(int x)
{
    for(int i = x ; i <= n ; i += lowbit(i))
    {
        t[i] += 1;
    }
}

int find2(int x)
{
    int res = 0;
    for(int i = x ; i ; i -= lowbit(i))
    {
        res += t[i];
    }
    return res;
}

int main()
{
    scanf("%d",&n);
    for(int i = 1 ; i <= n ; i++)
    {
        scanf("%lld",&a[i].first);
        a[i].second = i;
    }

    sort(a + 1 , a + 1 + n,cmp1); // 此cmp1从小到大排序,并且序号大的在前,这样统计小的序号的时候,不会额外加大的序号中的值,统计大的序号的时候不会额外加小的序号的值。


    // 树状数组找左边比它小的,存入到left中
    for(int i = 1 ; i <= n ; i++)
    {
        add(a[i].second);
        left2[a[i].second ] = find2(a[i].second - 1);
    }

    memset(t,0,sizeof t);
    
    // 这个cmp2主要是为了找右边比当前i大的值,这个需要注意,由于我们的树状数组统计的是前缀和,所以是1~某个值出现的总次数,那么要统计某个值右边比它大的值的次数。就需要从大到小排序,并且小序号排前面,这样当出现相同值情况的时候,大序号会将1~大序号前一个的下标出现的比它大的值的次数统计进来,然后用当前的总数 - 统计的值 - 1(大序号本身)就是大序号右边比它大的值了
    sort(a + 1 , a + 1 + n , cmp2);

    for(int i = 1 ; i <= n ; i++)
    {
        add(a[i].second);
        right2[a[i].second ] = i - find2(a[i].second - 1) - 1; // 减去它本身
    }


    long long ans = 0;
    for(int i = 1 ; i <= n ; i++)
    {
        ans += (long long)left2[i] * right2[i];
    }
    printf("%lld\n",ans);
    return 0;
}

分析2:看有人用到了离散化,突然一想,我前面的sort()好像确实也有离散化的影子,如果使用离散化的话我们就不用在sort()的时候考虑那么多的cmp()排序的细节情况了(值相等的时候,下标按照什么样得方式排序为好)。 

思路基本与上面一样,但是使用了离散化之后,就不在需要考虑sort()中排序方面的细节上的问题了,方便了许多。

但是求右边的时候还是要注意,需要使用当前遍历的个数 - 树状数组找到的个数

代码实现2:离散化 + 树状数组

# include <iostream>
# include <cstring>
# include <algorithm>
# include <vector>
using namespace std;

const int N = 3e4 + 10;

long long a[N];
vector<long long> b;//离散化

int t[N];

int right2[N]; // i的右边比它大的元素个数
int left2[N]; //i的左边比它小的元素个数

int n;

int lowbit(int x)
{
    return x & (-x);
}

void add(int x)
{
    for(int i = x ; i <= n ; i += lowbit(i))
    {
        t[i] += 1;
    }
}

int find2(int x)  //树状数组求1~x的前缀和
{
    int res = 0;
    for(int i = x ; i ; i -= lowbit(i))
    {
        res += t[i];
    }
    return res;
}

int find3(int x)  // 二分找离散化后的值的对应的下标
{
    int l = 1 , r = b.size();
    while(l < r)
    {
        int mid = ( l + r ) / 2;
        if(b[mid] >= x)
        {
            r = mid;
        }
        else
        {
            l = mid + 1;
        }
    }
    return l;
}

int main()
{
    scanf("%d",&n);
    for(int i = 1 ; i <= n ; i++)
    {
        scanf("%lld",&a[i]);
        b.push_back(a[i]);
    }
    b.push_back(-1);//由于树状数组的原因,下标从1开始

    sort(b.begin(),b.end());
    b.erase(unique(b.begin(),b.end()),b.end());

    for(int i = 1 ; i <= n ; i++)
    {
        int x = find3(a[i]);
        add(x);
        left2[i] = find2(x - 1);
    }

    memset(t,0,sizeof t);
    for(int i = n ; i >= 1 ; i--)
    {
        int x = find3(a[i]);
        add(x);
        right2[i] = (n - i + 1) - find2(x);
    }

    long long ans = 0;
    for(int i = 1 ; i <= n ; i++)
    {
        ans += (long long)left2[i] * right2[i];
    }
    printf("%lld\n",ans);


    return 0;
}

分析3:DP + 树状数组优化 -》解决多元上升子序列问题:

此思路可以相当于上面两种思路的一种拓展:此种思路可以解决的问题不在仅限于三个数,而可以解决多个数满足此种条件下的情况。

而我们现在主要以3个数来描述此题。

f[i][j]的状态表示:选择i个数,以a[j]作为结尾的上升子序列的满足条件的总数。

状态计算:

暴力来看:

k为所有满足 k < j 且 a[k] < a[j]情况下的所有取值情况

则f[i][j] = f[i - 1][k]的总和。

如:

前j个数中,t1小于 j , a[t1 ] < a[j] .  t2 小于 j , a[t2 ] < a[j],则

f[i][j] = f[i - 1][t1] + f[i - 1][t2];

i的循环共3个,1,2,3的取值

j的取值有n个。

而这之中k的取值也有j种,并且需要满足 k < j  , a[k] < a[j]的条件

而这个最后一层循环,则可以使用树状数组进行优化。

在前面的两种方式的树状数组操作中:

如果k < j 并且 a[k] < a[j]的话,对应的k的位置 + 1,

因为我们需要统计的是小于 或者 大于 时的个数。所以满足条件的话个数 + 1.

但是现在我们需要的满足条件的时候不再是个数 + 1了, 而是需要 + f[i - 1][k].

所以在树状数组的add操作中,我们将 + 1改为 + f[i - 1][k]的值即可。

而每次遍历i的时候,都要将c清空,因为每一个i对应的都是一次新的树状数组的更新情况。只与 f[i - 1][]有关,与其他之前的值无关。

代码实现3:DP + 树状数组优化 -》解决多元上升子序列问题

同时注意,使用此种思路不在局限于只能求3个数的情况,而是可以求任意多个数(如n)的情况:f[n][1~n].

# include <iostream>
# include <vector>
# include <algorithm>
# include <cstring>
using namespace std;

const int N = 3e4 + 10;

long long f[4][N];

long long a[N];

vector<long long> b; // 离散化

long long c[N];

int lowbit(int x)
{
    return x & -x;
}

int n;

int find3(int x)  // 二分查找离散化的下标
{
    int l = 1 , r = b.size();
    while(l < r)
    {
        int mid = ( l + r ) / 2;
        if(b[mid] >= x)
        {
            r = mid;
        }
        else
        {
            l = mid + 1;
        }
    }
    return l;
}

void add(int x,int v)
{
    for(int i = x ; i <= n ; i += lowbit(i))
    {
        c[i] += v;
    }
}

long long find2(int x)
{
    long long res = 0;
    for(int i = x ; i ; i -= lowbit(i))
    {
        res += c[i];
    }
    return res;
}


int main()
{
    scanf("%d",&n);
    for(int i = 1 ; i <= n ; i++)
    {
        scanf("%lld",&a[i]);
        b.push_back(a[i]);
    }
    b.push_back(-0x3f3f3f3f);

    sort(b.begin(),b.end());
    b.erase(unique(b.begin(),b.end()),b.end());

    for(int i = 1 ; i <= n ; i++)
    {
        f[1][i] = 1;
    }
    for(int i = 2;  i <= 3 ; i++)
    {
        memset(c,0,sizeof c);

        for(int j = 1 ; j <= n ; j++) // 以j作为结尾
        {
            int idx = find3(a[j]);
            add(idx,f[i - 1][j]);
            f[i][j] = find2(idx - 1);
        }
    }
    long long ans = 0;
    for(int i = 1 ; i <= n ; i++)
    {
        ans += f[3][i];
    }
    printf("%lld\n",ans);
    return 0;
}

 分析4:线段树的操作:(单点修改 + 区间查询)

要用线段树来解决的话,通过离散化的方式,然后找到当前的点的对应下标后,进行单点修改,这个对应下标的值 + 1.  然后区间查询,1~这个点 - 1的总和。就可以找到对应的左边小于当前节点值的总数。

同样的道理,从后往前循环,找当前点 + 1 到 最后端点的,就可以找到对应的从右往前大于当前值的总个数

所以线段树的结构体为:

struct Node

{

        int l,r;

        int sum; //每个区间的点出现次数的总和

}

代码实现:线段树:

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

const int N = 3e4 + 10;

int n;

long long a[N];
vector<long long> b; // 用于离散化

int left2[N];
int right2[N];

struct Node
{
    int l,r;
    long long sum;
}edgs[4 * N];

int find2(int x)
{
    int l = 1 , r = b.size();
    while(l < r)
    {
        int mid = (l + r) / 2;
        if(b[mid] >= x)
        {
            r = mid;
        }
        else
        {
            l = mid + 1;
        }
    }
    return l;
}

void pushup(int u)
{
    edgs[u].sum = edgs[2 * u].sum + edgs[2 * u + 1].sum;
}

void build(int u , int l , int r)
{
    edgs[u].l = l , edgs[u].r = r;
    if(l == r)
    {
        edgs[u].sum = 0; // 第二次build()时,需要将对应的值清空掉
        return;
    }
    int mid = ( edgs[u].l + edgs[u].r ) / 2;
    build(2 * u , l , mid);
    build(2 * u + 1 , mid + 1 , r);
    pushup(u);
}

void modify(int u , int x , int v) // 将x增加v
{
    if(edgs[u].l == x && edgs[u].r == x)
    {
        edgs[u].sum += v;
        return;
    }
    int mid = (edgs[u].l + edgs[u].r) / 2;
    if(x <= mid)
    {
        modify(2 * u , x , v);
    }
    else
    {
        modify(2 * u + 1 , x , v);
    }
    pushup(u);
}

long long query(int u , int l , int r) // l~r的总值
{
    if(edgs[u].l >= l && edgs[u].r <= r)
    {
        return edgs[u].sum;
    }
    int mid = (edgs[u].l + edgs[u].r) / 2;
    long long ans = 0;
    if(l <= mid)
    {
        ans += query(2 * u , l , r);
    }
    if(r > mid)
    {
        ans += query(2 * u + 1 , l , r);
    }
    return ans;
}

int main()
{
    scanf("%d",&n);
    for(int i = 1;  i <= n ; i++)
    {
        scanf("%d",&a[i]);
        b.push_back(a[i]);
    }
    b.push_back(-0x3f3f3f3f);

    sort(b.begin(),b.end());
    b.erase(unique(b.begin(),b.end()),b.end());

    build(1,1,b.size());

    //找左边比它小的
    for(int i = 1 ; i <= n ; i++)
    {
        int x = find2(a[i]);
        modify(1,x,1);
        left2[i] = query(1,1,x - 1); // 左边 1 ~ x - 1出现的次数
    }


    build(1,1,b.size());
    // 找右边比它大的
    for(int i = n ; i >= 1 ; i--)
    {
        int x = find2(a[i]);
        modify(1,x,1);
        right2[i] = query(1,x + 1 ,n);
    }

    long long ans = 0;
    for(int i = 1 ; i <= n ; i++)
    {
        ans += (long long)left2[i] * right2[i];
    }
    printf("%lld\n",ans);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值