洛谷P1637 三元上升子序列 解题报告

作为一个三元的上升序列,我们很容易想到子序列枚举中间的元素


我们记

  • Lef[i]为 A[i]左边小于A[i]的元素个数
  • Rit[i]为 A[i]右边大于A[i]的元素个数

根据乘法原理,有:

  • 以A[i]为中间元素的合法序列个数为*Lef[i]Rit[i]**

如何得到Lef[] 和Rit[] 数组呢?(以Lef为例)

如果我们在枚举中间元素A[i]的过程中再次遍历A[0],A[1]...A[i-1],那么时间复杂度将达到O(n^2),显然超时。

换一种思路: “在i前(不含)小于A[i]的元素数量和” 等价于 “在i前(不含)小于等于A[i]的元素数量和减去A[i](这个数值)出现的次数”。

虽然这种说法令人感到莫名其妙,但它或许可以启示我们的同学联想到“前缀和”。 前缀和的思路、“逆序对”的目标进一步让我们联想到树状数组求逆序对。按刘汝佳大神的说法,“动态更新并求解逆序对,正是树状数组的标准用法”。

我们可以用树状数组c[val]记录已经被考虑的数中小于等于val的个数,初始化为0,每次更新时,调用add(A[i],1)。


在本题中,A[i]<=INT_MAX,直接定义树状数组c[INT_MAX]显然失智,所以我们不妨离散化处理。我个人认为,离散化绝对比其他“不用离散”的方法容易想到,如果是在考场上,我们可以更加轻车熟路地用离散来处理这道题目。


POJ3928和本题是类似的,不过那一题更加友善——保证任意A[i]不同,并给出了不必离散处理的A[i]范围,有兴趣的同学可以在那一题上加强练习。

代码如下,时间复杂度O(nlogn),空间复杂度O(n)

#include<bits/stdc++.h>
using namespace std;
const int maxn=3e4+10;
int n,m;
int c1[maxn],c2[maxn];//double_tree_arr
int A[maxn],_A[maxn];//discrete_arr
int Lef[maxn],Rit[maxn];//Counter

inline int _Q(int val){//查询A[i]对应的映射值 
	return lower_bound(_A+1,_A+m+1,val)-_A;
}

inline int lowbit(int i){
	return i&(-i);
}

void add(int *C,int pos,int val){
	while(pos<=maxn){
		C[pos]+=val;
		pos+=lowbit(pos);
	}
}

int sum(int *C,int pos){
	int res=0;
	while(pos>0){
		res+=C[pos];
		pos-=lowbit(pos);
	}
	return res;
}
//以上是树状数组模板,在函数里以数组指针作参数 
int main(){
	cin>>n;
	for(int i=1;i<=n;++i){
		scanf("%d",&A[i]);
		_A[i]=A[i];
	}
	sort(_A+1,_A+n+1);
	m=unique(_A+1,_A+n+1)-(_A+1);
	//小细节,我们希望映射值在i...3e4之间,所以需要减去_A+1 
	//discrete
	for(int i=1;i<=n;++i){
		add(c1,_Q(A[i]),1);
		Lef[i]=sum(c1,_Q(A[i])-1);
		//“减去A[i]出现个数”的隐式体现,就是我们只计算“A[i]-1(映射意义上)”的出现个数
	}
	for(int i=n;i>=1;--i){
		add(c2,_Q(A[i]),1);
		Rit[i]=n-i-(sum(c2,_Q(A[i]))-1);
		//小细节,计算Rit时需要注意表达式与Lef不同 
	}
	long long ans=0;
	for(int i=2;i<n;++i) ans+=Lef[i]*Rit[i];
	//“乘法原理”的显式体现 
	cout<<ans;
	return 0;
}


最后总结一下程序实现中需要注意的一些细节

  • A[i]最有多maxn=3e4个不同的数值,离散后最大的映射值也不超过3e4;我们的树状数组应以max{A[i]}作为下标上界,所以我们直接使用maxn作为下标上界。
  • 本人在程序里定义了两个树状数组c1,c2,并在树状数组的操作函数里用参数将它们区分开来,如果只想用一个树状数组的话,不要忘记在顺、逆两次遍历之间清空数组。
  • 因为我们的树状数组只能求解“小于(等于)”的数量,所以Lef和Rit的计算必须分开。
  • 同样是由于上一条原因,函数sum在逆序时计算出来的其实是“i+1...n中小于等于A[i]的元素个数”,我们把它记为K。那么在 i+1...n这n-i个数字中,“小于等于A[i]”的数字出现的个数其实是K-1个(因为在位置i的A[i]不应被算进去),而这些数并不是我们需要 的,于是,(n-i)-(K-1)就是我们需要的Rit[i]的值了。

 

<think> 我们正在讨论三元上升序列。首先,我需要明确三元上升序列的定义。 三元上升序列通常指的是在一个序列中,找到三个下标 i < j < k,使得对应的元素 a_i < a_j < a_k。 这个问题通常要求我们统计给定序列中满足条件的三元组的个数。 现在,用户要求了解定义及算法实现。因此,我将分两步: 1. 给出三元上升序列的准确定义。 2. 提供算法实现(通常有暴力法和高效的方法,高效方法一般使用树状数组或线段树)。 另外,根据系统指令,需要生成相关问题。 注意:在算法实现中,我们可能会使用树状数组(Fenwick Tree)或线段树(Segment Tree)来高效地计算前缀和,从而将时间复杂度从O(n^3)降低到O(n log n)。 步骤: 1. 定义:在一个序列a中,若存在三个位置i, j, k(i<j<k)满足a_i < a_j < a_k,则称(i,j,k)构成一个三元上升序列。 2. 算法思路: 我们可以考虑中间元素j,那么对于每个j,我们希望统计左边比a_j小的元素个数(记为left[j]),以及右边比a_j大的元素个数(记为right[j])。 那么,以j为中间元素的三元组个数为left[j] * right[j]。 最终答案就是对所有j的left[j]*right[j]求和。 具体做法: a. 预处理left数组:从左到右遍历,对于每个位置j,我们需要知道在j的左边有多少个数小于a_j。 我们可以用树状数组来维护:从左到右扫描,每扫描一个数,就将该数加入树状数组。然后查询当前小于a_j的数的个数。 b. 预处理right数组:从右到左遍历,对于每个位置j,我们需要知道在j的右边有多少个数大于a_j。 同样,用树状数组维护:从右到左扫描,每扫描一个数,将其加入树状数组,然后查询当前大于a_j的数的个数(即总数减去小于等于a_j的数的个数,注意树状数组维护的是从右向左扫描过的数)。 c. 注意:由于序列中的数可能很大,我们需要离散化(将原数组映射到1~n的范围内)以便使用树状数组。 3. 代码实现(以树状数组为例,使用离散化)。 由于用户要求用中文回答,我们将用中文解释。 另外,根据系统指令,行内数学表达式用$...$,独立公式用$$...$$。 最后,生成相关问题。 注意:在回答中引用的段落末尾自然地添加引用标识。由于我们没有外部引用,这里可以省略,但如果有必要可以添加(例如,如果提到某个经典问题或算法出处)。 实现步骤详细说明: 离散化:将原数组中的每个元素替换为它在所有元素排序后的排名(注意去重,相同元素按顺序排名,但为了满足严格上升,我们通常将相同元素视为相等,但排名不同?实际上,由于要求严格上升,所以相等的情况不满足,因此我们可以将相等元素视为不同的元素,但离散化时保留原顺序关系?) 然而,在树状数组处理中,我们要求严格小于,所以离散化时要注意:如果遇到相同的元素,我们需要保证在统计左边小于当前数的时候,相同的数不会被认为是小于。因此,我们可以在离散化时对相同的数赋不同的值(但是这样会破坏相等关系?)所以,为了保证严格大小关系,我们通常这样处理: 离散化:将数组排序,然后去重(但实际上不去重,而是用排序后的下标作为离散值,但是要注意:对于相同的元素,我们希望在树状数组中先出现的数先加入,后加入的数在查询时不会把相同的数算作小于(因为它们是相等的)。所以,我们在离散化时,如果两个元素相等,则按照下标顺序(小的下标对应更小的离散值?或者更大的?)需要保持一致。 实际上,我们可以这样离散化:将原数组映射为一个新的数组,使得映射后的数组保持原数组的大小关系,对于相等的元素,按照索引递增的顺序分配离散值(递增或递减?)。如果我们从左向右扫描,那么我们希望先出现的相同元素在离散化后更小,这样后出现的相同元素在查询时不会把先出现的相同元素算作小于(因为我们要的是严格小于)。所以,对于相等的元素,我们让先出现的数离散值更小?或者更大? 但是注意:在树状数组统计“左边小于当前数”时,我们希望相等的数不影响。因此,我们可以让相等的数离散化后相同?但这样在加入树状数组时,我们加入的是相同的值,那么在统计小于当前数的个数时,相等的数不会被包含(因为小于不包括等于)。所以我们可以直接对原数组去重排序后,每个数用其排名(即第几小)作为离散值,相等的数排名相同。 但是,这样会有一个问题:当我们在树状数组中加入一个数x(离散值为rank),那么当我们查询小于x(即小于rank)的个数时,不会包含等于x的数,所以没有问题。 然而,当我们处理数组中有重复元素的情况时,我们希望: 对于当前元素a_j,我们只统计左边严格小于a_j的个数,那么重复的数(等于a_j)不会被计入。 因此,离散化时可以直接使用排序去重,然后每个数用其排名(即第几小)作为离散值,重复的数具有相同的排名。 但是,在树状数组更新时,我们更新的是该排名对应的位置,如果有重复元素,每次更新同一个位置,那么当我们查询小于某个排名r时,实际上包含了所有小于r的数(包括其他的重复数?),但是不包括排名为r的数。所以这样是可以的。 步骤: 离散化函数:将数组a映射为离散化后的数组b,其中b[i] = a[i]在a中的排名(从1开始)。 例如:a = [1,3,2,3] -> 排序后:[1,2,3,3] -> 去重:[1,2,3] -> 那么1->1, 2->2, 3->3, 3->3? 但是不去重,直接排序后,第一个3是第3小,第二个3也是第3小。所以b=[1,3,2,3] 然后: left数组:从左到右扫描,每扫描一个位置i,在树状数组中查询[1, b[i]-1]的和(即小于b[i]的数的个数),然后将b[i]位置的计数加1(更新树状数组)。 right数组:从右到左扫描,每扫描一个位置i,在树状数组中查询[b[i]+1, n]的和(即大于b[i]的数的个数),然后将b[i]位置的计数加1。 注意:树状数组的大小为离散化后的最大值(即n,原数组长度,因为离散化后最大排名为n)。 最后,对于每个位置j,三元组个数为left[j]*right[j],然后累加。 代码实现(使用树状数组): 树状数组类(支持单点更新和前缀和查询): 初始化:长度为n+1(索引1到n) 更新:在位置idx加1 查询:query(i)返回[1,i]的和 对于left数组: tree1 = Fenw(n) # 树状数组 left = [0] * len(a) 遍历i从0到n-1: x = b[i] # 离散化后的值 left[i] = tree1.query(x-1) # 查询小于x的个数 tree1.update(x, 1) # 在x位置加1 对于right数组: tree2 = Fenw(n) right = [0] * len(a) 遍历i从n-1到0: x = b[i] # 查询大于x的个数:即当前树状数组中已经加入的数(位置i右边的数)中大于x的个数 # 树状数组中总数为 (n-1-i) ?不对,我们是从后往前加入的,所以当前树状数组中已经加入的数个数为(n-1-i),那么大于x的个数等于总数 - 小于等于x的个数 // 但我们树状数组支持查询前缀和,所以小于等于x的个数为tree2.query(x),那么大于x的个数为 (当前总数) - tree2.query(x) 当前总数 = 从i+1到n-1的个数,即(n-1-i)?但是我们在树状数组中加入的是从后往前扫描的,所以当前树状数组中一共有(n-1-i)个数。因此,大于x的个数 = (n-1-i) - tree2.query(x) // 注意:这样不对,因为tree2.query(x)查询的是小于等于x的个数,但是我们的树状数组只存了扫描过的数,所以当前总数就是当前树状数组中的元素个数(即n-1-i),那么大于x的个数为(当前树状数组中的元素个数 - 小于等于x的个数)即:(n-1-i) - tree2.query(x) 但是这样会有问题:因为树状数组的query(x)返回的是前缀和,即[1,x]的和,而我们的离散化是从1到n的,所以这样计算是对的。 另一种方法是:我们查询[x+1, n]的和?树状数组可以支持区间和查询,但通常我们通过query(n)-query(x)来计算[x+1,n]的和。所以: right[i] = tree2.query(n) - tree2.query(x) // 注意:tree2.query(n)就是树状数组中所有数的和,即当前加入的元素个数=n-1-i。所以两种方法都可以。 但是,我们更推荐使用区间和查询:right[i] = tree2.range_query(x+1, n) 或者用 query(n)-query(x) 然而,我们的树状数组一般只提供前缀和查询,所以: right[i] = tree2.query(n) - tree2.query(x) # 查询[x+1, n]的和 然后更新:tree2.update(x, 1) 注意:离散化后的值范围是1~n,所以树状数组初始化为长度为n+1(索引0不用)。 但是,从后往前扫描时,总数是逐渐增加的,所以我们应该用tree2.query(n)表示当前所有加入的数的个数(因为每个位置加1,所以tree2.query(n)就等于当前加入的数的个数)。所以: right[i] = tree2.query(n) - tree2.query(x) # 即大于x的数的个数 但是,这里tree2.query(n)就是当前树状数组中元素的总个数(也就是已经加入的元素个数,即从最后一个位置到当前位置之前的个数),所以也可以写成: count = n-1-i # 因为从后往前扫描,在扫描i位置时,已经扫描了i+1到n-1,一共n-1-i个元素。 right[i] = count - tree2.query(x) # 注意tree2.query(x)返回的是当前树状数组中<=x的个数,那么大于x的个数就是count - (<=x的个数) 但是,这两种方法在数值上相等吗?是的,因为tree2.query(n)=count。所以两种写法都可以。 但是,在代码中我们可以选择第二种写法,因为这样不用调用tree2.query(n),效率可能稍微高一点(常数级别)。不过为了清晰,我们也可以使用第一种。 然而,需要注意的是,我们的树状数组在更新时,每个位置加1,所以tree2.query(n)就等于当前加入的元素个数(即count)。 因此,我们选择第一种方法:right[i] = tree2.query(n) - tree2.query(x) 最后,注意:树状数组的更新和查询都是O(log n)的,所以总时间复杂度为O(n log n)。 下面给出代码实现(Python): </think> ### 三元上升序列的定义及算法实现 #### 定义 三元上升序列是指在给定序列 $a_1, a_2, \dots, a_n$ 中,存在三个下标 $i, j, k$ $(i < j < k)$ 满足严格递增关系: $$ a_i < a_j < a_k $$ 统计满足该条件的三元组 $(i,j,k)$ 的数量即为**三元上升序列问题**。例如序列 $[1,3,2,4]$ 中,三元组 $(1,3,4)$(对应值 $1<2<4$)和 $(1,2,4)$(对应值 $1<3<4$)均满足条件[^1]。 #### 算法实现(树状数组优化) 高效解法基于**树状数组(Fenwick Tree)**,时间复杂度为 $O(n \log n)$,步骤如下: 1. **离散化** 将原序列映射到 $[1, n]$ 的整数范围,消除数值大小影响: ```python def discretize(arr): sorted_arr = sorted(set(arr)) return {val: idx + 1 for idx, val in enumerate(sorted_arr)} ``` 2. **计算左侧较小元素** 从左向右扫描,用树状数组动态统计每个位置左侧小于 $a_j$ 的元素数量: ```python class FenwickTree: def __init__(self, size): self.n = size self.tree = [0] * (size + 1) def update(self, idx, delta): while idx <= self.n: self.tree[idx] += delta idx += idx & -idx def query(self, idx): res = 0 while idx: res += self.tree[idx] idx -= idx & -idx return res n = len(arr) disc = discretize(arr) left_smaller = [0] * n fenw_left = FenwickTree(n) for j in range(n): val = disc[arr[j]] left_smaller[j] = fenw_left.query(val - 1) # 查询小于val的数量 fenw_left.update(val, 1) # 插入当前值 ``` 3. **计算右侧较大元素** 从右向左扫描,统计每个位置右侧大于 $a_j$ 的元素数量: ```python right_larger = [0] * n fenw_right = FenwickTree(n) for j in range(n - 1, -1, -1): val = disc[arr[j]] # 查询大于val的数量 = 总数 - 小于等于val的数量 right_larger[j] = (n - 1 - j) - fenw_right.query(val) fenw_right.update(val, 1) ``` 4. **统计结果** 对每个位置 $j$,组合左侧较小和右侧较大数量: ```python total = 0 for j in range(n): total += left_smaller[j] * right_larger[j] return total ``` #### 完整代码示例 ```python def count_triplets(arr): # 离散化 sorted_arr = sorted(set(arr)) disc = {val: idx + 1 for idx, val in enumerate(sorted_arr)} n = len(arr) # 树状数组类 class FenwickTree: def __init__(self, size): self.n = size self.tree = [0] * (size + 1) def update(self, idx, delta): while idx <= self.n: self.tree[idx] += delta idx += idx & -idx def query(self, idx): res = 0 while idx: res += self.tree[idx] idx -= idx & -idx return res # 计算左侧较小元素 left_smaller = [0] * n fenw_left = FenwickTree(n) for j in range(n): val = disc[arr[j]] left_smaller[j] = fenw_left.query(val - 1) fenw_left.update(val, 1) # 计算右侧较大元素 right_larger = [0] * n fenw_right = FenwickTree(n) for j in range(n - 1, -1, -1): val = disc[arr[j]] cnt_right = fenw_right.query(val) # 右侧小于等于val的数量 right_larger[j] = (n - 1 - j) - cnt_right fenw_right.update(val, 1) # 合并结果 return sum(left_smaller[j] * right_larger[j] for j in range(n)) # 示例 arr = [2, 1, 3, 5, 4] print(count_triplets(arr)) # 输出:3(对应(1,3,5)、(1,3,4)、(2,3,5)) ``` #### 算法分析 - **时间复杂度**:离散化 $O(n \log n)$,两次扫描 $O(n \log n)$,总复杂度 $O(n \log n)$ - **空间复杂度**:$O(n)$ 相比暴力解法 $O(n^3)$,树状数组通过动态维护前缀和显著提升效率[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值