楼兰图腾:树状数组维护区间和,支持单点修改

原题链接:https://www.acwing.com/problem/content/243/
在这里插入图片描述

最傻逼的做法:


// 最垃圾的做法  当然是tle了
#include<iostream>

using namespace std;

const int N = 200010;

typedef long long LL;

LL  a[N];

int main()
{
    int n;
    cin >> n;
    
    for(int i=0; i<n; i++) scanf("%d",&a[i]);
    
    LL res_1 = 0, res_2 = 0;
    
    for(int i=0; i<n-2; i++)
        for(int j=i+1; j<n-1; j++)
            for(int k=j+1; k<n; k++)
            {
                if(a[i] < a[j] && a[k] < a[j]) res_2++;
                if(a[i] > a[j] && a[k] > a[j]) res_1++;
            }
            
    printf("%lld %lld\n",res_1, res_2);
    
    return 0;
}


一般的做法

//有技巧的暴力:继续超时。
// (暴力枚举) O(n2)
// 一种朴素做法就是遍历所有点i, 分别统计i位置左边比a[i]小的数的个数m、右边比a[i]小的数的个数n,运用乘法原理:
// 1. 第一步从左边m个数中任选一个,有m种选法
// 2. 第二步从右边n个数中任选一个,有n种选法
// 那么在i位置组成图腾∧的方案数一共是 m * n。
// 累加每个点的方案数,即为所有组成图腾∧的方案总数。

// 时间复杂度O(n2)。
#include <iostream>
#include <cstdio>

using namespace std;

const int N = 2000010;

typedef long long LL;

int a[N];
//ll[i]表示i的左边比第i个数小的数的个数
//rl[i]表示i的右边比第i个数小的数的个数
//lg[i]表示i的左边比第i个数大的数的个数
//rg[i]表示i的右边比第i个数大的数的个数
int ll[N], rl[N], lg[N], rg[N];

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

    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j < i; j++)
        {
            //a[]保存的是1 ~ n的一个排列,不可能相等
            if(a[j] < a[i]) ll[i] ++;
            else lg[i] ++;
        }
    }

    for(int i = 1; i <= n; i++)
    {
        for(int j = n; j > i; j--)
        {
            if(a[j] < a[i]) rl[i] ++;
            else rg[i] ++;
        }
    }

    LL resV = 0, resA = 0;
    for(int i = 1; i <= n; i++)
    {
        resV += (LL)lg[i] * rg[i];
        resA += (LL)ll[i] * rl[i];
    }

    printf("%lld %lld\n", resV, resA);

    return 0;
}

使用树状数组对查询和插入操作进行优化。之前插入是O(1), 查询是O(n). 现在查询和插入都是O(logn)

对于每个数a[i], 我们只需要知道i的左边和右边分别有多少数小于a[i], 然后这俩数相乘就是对于a[i] 来说’∩’的个数。 依次遍历每个a[i], 求和就是总个数。

朴素的做法总的时间复杂度n方。使用树状数组后变成nlogn。下面详细解释一下。

因为输入数是一个全排列,不会有重复的数。而对于每个a[i],我们想知道他的左边有多少大于他的数,有多少小于他的数,对于右边亦是如此。

我们采用哈希的思想,如果一个数y出现了,那么c[y] 就为1。如果我们不使用树状数组,每次判断有多少个数小于y,就需要遍历一遍c数组,这样的 时间复杂度O(n)

但是对于数组c,我们可以使用树状数组t[]来维护c的前缀和。sum(x)返回c[1~x]的前缀和。
这样每次求前边有多少个数小于y,就直接就sum(y-1)就行了。表示范围再1~y-1的数,总共
出现过多少次,时间复杂度是O(logn).

全程都在直接维护c的树状数组t,c数组都没有出现过!!!

求左边:
求左边有多少个数<y: sum(y-1)表示数组左边有多少小于等于y-1的数。
求左边有多少个数>y: sum(n) - sum(y) 即小于等于n的数的总和 - 小于等于y的数的总和.

初始化树状数组t[]: 每次添加一个a[i], 使用add(a[i], 1) 函数,表示在c数组中a[i]的位置
也就是c[a[i]] += 1,同时结果体现在树状数组t中。维护数组数组。c自始至终就没有出现过。

求右边:
a[i] 加进来之后再c[a[i]]进行标记,同时维护tr[],
因为求得是右边有多少个,因此需要从右往左遍历a[i]
这样右边的a[i]才能先于左边的a[i]出现。
小于继续求sum(y-1)。大于继续求sum(n) - sum(y)

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 200010;

int n;
int a[N];
int tr[N];
int Greater[N], lower[N];

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

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

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

int main()
{
    scanf("%d", &n);

    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);

    for (int i = 1; i <= n; i ++ )
    {
        int y = a[i];
        Greater[i] = sum(n) - sum(y);
        lower[i] = sum(y - 1);
        add(y, 1);
    }

    memset(tr, 0, sizeof tr);
    LL res1 = 0, res2 = 0;
    for (int i = n; i; i -- )
    {
        int y = a[i];
        res1 += Greater[i] * (LL)(sum(n) - sum(y));
        res2 += lower[i] * (LL)(sum(y - 1));
        add(y, 1);
    }

    printf("%lld %lld\n", res1, res2);

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值