题解:AcWing 241 楼兰图腾

【题目来源】

AcWing:241 楼兰图腾 - AcWing题库

【题目描述】

在完成了分配任务之后,西部 \(314\) 来到了楼兰古城的西部。

相传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(V),一个部落崇拜铁锹(),他们分别用 V 的形状来代表各自部落的图腾。

西部 \(314\) 在楼兰古城的下面发现了一幅巨大的壁画,壁画上被标记出了 \(n\) 个点,经测量发现这 \(n\) 个点的水平位置和竖直位置是两两不同的。

西部 \(314\) 认为这幅壁画所包含的信息与这 \(n\) 个点的相对位置有关,因此不妨设坐标分别为 \((1,y_1),(2,y_2),\dots,(n,y_n)\),其中 \(y_1\sim y_n\)\(1\)\(n\) 的一个排列。

西部 \(314\) 打算研究这幅壁画中包含着多少个图腾。

如果三个点 \((i,y_i),(j,y_j),(k,y_k)\) 满足 \(1\le i\lt j\lt k\le n\)\(y_i>y_j,y_j<y_k\),则称这三个点构成 V 图腾;

如果三个点 \((i,y_i),(j,y_j),(k,y_k)\) 满足 \(1\le i\lt j\lt k\le n\)\(y_i<y_j,y_j>y_k\),则称这三个点构成 图腾;

西部 \(314\) 想知道,这 \(n\) 个点中两个部落图腾的数目。

因此,你需要编写一个程序来求出 V 的个数和 的个数。

【输入】

第一行一个数 \(n\)

第二行是 \(n\) 个数,分别代表 \(y_1,y_2,\dots,y_n\)

【输出】

两个数,中间用空格隔开,依次为 V 的个数和 的个数。

【输入样例】

5
1 5 3 2 4

【输出样例】

3 4

【核心思想】

  1. 问题分析:给定 \(n\) 个点 \((i, y_i)\),其中 \(y\)\(1 \sim n\) 的排列。需要统计 V 图腾(\(y_i > y_j < y_k\)\(i < j < k\))和 图腾(\(y_i < y_j > y_k\)\(i < j < k\))的数量。关键观察是:以中间点 \(j\) 为基准,V 要求左边比 \(y_j\) 大的点数 \(\times\) 右边比 \(y_j\) 大的点数, 要求左边比 \(y_j\) 小的点数 \(\times\) 右边比 \(y_j\) 小的点数。因此问题转化为对每个位置 \(j\),统计其左右两侧比它大/小的元素个数。

  2. 算法选择

    • 树状数组(Fenwick Tree)\(O(\log n)\) 维护前缀和,支持动态插入和查询"已出现且 \(\leq x\)"的元素数量
    • 两次扫描:第一次从左到右统计每个位置左侧的信息,第二次从右到左统计右侧信息并计算答案
  3. 关键步骤

    • 第一次扫描(从左到右)
      • 遍历 \(i\)\(1\)\(n\)
        • Greater[i] = query(n) - query(y):已出现且 \(> y_i\) 的元素个数(左侧比 \(y_i\) 大的数量)
        • Lower[i] = query(y - 1):已出现且 \(< y_i\) 的元素个数(左侧比 \(y_i\) 小的数量)
        • add(y, 1):将 \(y_i\) 加入树状数组
    • 重置树状数组
    • 第二次扫描(从右到左)
      • 遍历 \(i\)\(n\)\(1\)
        • res1 += Greater[i] * (query(n) - query(y))V 图腾数 = 左侧比 \(y_i\)\(\times\) 右侧比 \(y_i\)
        • res2 += Lower[i] * query(y - 1) 图腾数 = 左侧比 \(y_i\)\(\times\) 右侧比 \(y_i\)
        • add(y, 1):将 \(y_i\) 加入树状数组
    • 输出\(res1\)V 数量)和 \(res2\) 数量)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \log n)\),两次扫描各 \(n\) 次树状数组操作
    • 空间复杂度:\(O(n)\),树状数组和辅助数组
  5. 树状数组统计逆序对的核心思想

    • 前缀和即计数:树状数组维护的是值域 \([1, n]\) 上的频次前缀和,query(y) 表示已出现且 \(\leq y\) 的元素数量,从而 query(n) - query(y) 表示已出现且 \(> y\) 的数量
    • 方向分离统计:第一次从左到右只统计左侧信息,第二次从右到左只统计右侧信息,两者相乘得到以当前点为中间点的三元组数量
    • 乘积原理:对于固定的中间点 \(j\)V 图腾要求左右两侧均比 \(y_j\) 大,两侧选择独立,因此总数为左侧大数个数 \(\times\) 右侧大数个数
    • 值域映射:由于 \(y_i\)\(1 \sim n\) 的排列,值域与定义域一致,可以直接用 \(y_i\) 作为树状数组下标,无需离散化
    • 适用于三元组统计、逆序对扩展、左右两侧信息统计等场景

【解题思路】

【算法标签】

树状数组

【代码详解】

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

#define int long long  // 使用long long类型
const int N = 200005;  // 定义数组最大长度

int n;                // 数组长度
int a[N];             // 存储原始数组
int tr[N];            // 树状数组
int Greater[N];       // 存储每个元素右边比它大的元素个数
int Lower[N];         // 存储每个元素右边比它小的元素个数

// 计算x的最低有效位(用于树状数组索引计算)
int lowbit(int x)
{
    return x & -x;
}

// 向树状数组添加值:在位置x增加c
void add(int x, int c)
{
    // 从x开始向上更新所有相关节点
    for (int i = x; i <= n; i += lowbit(i))
    {
        tr[i] += c;
    }
}

// 查询前缀和:计算前x个元素的和
int query(int x)
{
    int res = 0;
    // 从x开始向下累加所有相关节点
    for (int i = x; i; i -= lowbit(i))
    {
        res += tr[i];
    }
    return res;
}

signed main()
{
    // 输入数组长度n
    cin >> n;

    // 输入原始数组
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }

    // 第一遍扫描:从左到右计算每个元素左边的情况
    for (int i = 1; i <= n; i++)
    {
        int y = a[i];
        // 计算当前元素右边比它大的元素个数
        Greater[i] = query(n) - query(y);
        // 计算当前元素右边比它小的元素个数
        Lower[i] = query(y - 1);
        // 将当前元素加入树状数组
        add(y, 1);
    }

    // 重置树状数组
    memset(tr, 0, sizeof(tr));

    int res1 = 0, res2 = 0;  // res1-逆序对总数,res2-顺序对总数

    // 第二遍扫描:从右到左计算结果
    for (int i = n; i; i--)
    {
        int y = a[i];
        // 计算逆序对总数
        res1 += Greater[i] * (query(n) - query(y));
        // 计算顺序对总数
        res2 += Lower[i] * (query(y - 1));
        // 将当前元素加入树状数组
        add(y, 1);
    }

    // 输出结果:逆序对总数和顺序对总数
    cout << res1 << " " << res2 << endl;

    return 0;
}

【运行结果】

5
1 5 3 2 4
3 4
posted @ 2026-07-02 22:46  团爸讲算法  阅读(10)  评论(0)    收藏  举报