树状数组优化DP

树状数组优化DP  

P1020导弹拦截  
本题要求出最长不升子序列和最长上升子序列的长度,
转移方程为 
$$f_i=\max_{j<i,a_j\ge a_i}f_j+1,g_i=\max_{j<i,a_j<a_i}f_j+1$$
 

 $$$ans1$$$ 就是 $$$\max\{f\}-1$$$, $$$ans2$$$ 就是 $$$\max\{g\}$$$ 。

好的,由于朴素DP时间为 $O(n^2)$,无法通过 $n=10^5$ 的测试点,这里就有了优化方法,
当然你也可以用二分,不过那样就没法知道具体内容了(二分代码文末给出 qwq

这里介绍 BIT 优化这个过程。每次我们要找出前面已经加入的比该数字小的数中函数值最大的,
由于 $a_i$ 值域很小,这里省去了离散化的过程 。可以用权值BIT解决此问题。
对于第一问,倒序枚举该数组,每次加入时 询问小于等于该数字的 最大值 ,再 $+1$,
更新 $ans1$,之后插入该数字到 $t_{a_i}$ 中(也可以称得上更新了) 

const int M=5e4;
struct BIT{
  int v[M+1];
  void add(int p,int x){
    for(;p<=M;p+=p&-p) v[p]=max(v[p],x);
  }
  int ask(int p){
    int ans=0;
    for(;p;p-=p&-p) ans=max(ans,v[p]);
    return ans;
  }
}t1,t2;
...
for(int i=n-1,f;~i;--i){
  t1.add(a[i],f=t1.ask(a[i])+1),ans1=max(ans1,f);
}


第二问则正序枚举即可,每次问小于该数字的最大值 
 

for(int i=0,f;i<n;++i){
  t2.add(a[i],f=t2.ask(a[i]-1)+1),ans2=max(ans2,f);
}

对于 最长不升子序列的划分数=`LIS` 的证明
假设 `LNIS` 的划分数 为 $a$ ,`LIS` 的长度为 $b$  
1. 由于 `LIS` 中的两个数满足 $x_1<x_2$,所以这两个数字不能划分在一个 `LNIS` 中,于是就有 $a>=b$
2. 设 `S[n]=` 以 $a$ 开头的 `LNIS` 长度为 $n$ 的 $a$ 的下标 构成的集合 ,共有 $S_1 \cdots S_n$ 这些。
比如,对序列 $\{1,4,2,5,6,3,2,5,7,3\}$, `LIS` = $\{1,2,3,5,7\}$ = $5$, `S[1]` = $\{1,7,10\}$, `S[2]` = $\{3,6,8,9\}$, `S[3]` = $\{2,4,5\},$ 
则可以发现,集合中的下标对应的元素是单调递增的,因为如果存在 `S[x[i]] >S[x[i+1]]` ,
则 $x_i$ 这个元素可以接在 $x_{i+1}$ 前,使得 以 $x_i$ 开头的 LNIS 长度变为 $x+1$,矛盾。 
于是就有 `S` 等价于 `LIS` 的结论。 (其实还是有疑问 qwq
***
参考代码:树状数组

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+1,M=5e4;
struct{
  int v[M+1];
  void add(int p,int x){
    for(;p<=M;p+=p&-p) v[p]=max(x,v[p]);
  } 
  int ask(int p){
    int ans=0;
    for(;p;p-=p&-p) ans=max(ans,v[p]);
    return ans;
  }
}t1,t2;
int a[N],n,ans1,ans2;
main(){
  //cin.tie(0)->sync_with_stdio(0);
  while(~scanf("%d",a+n))++n;
  for(int i=n-1,f;~i;--i)
    t1.add(a[i],f=t1.ask(a[i])+1),ans1=max(ans1,f);
  for(int i=0,f;i<n;++i)
    t2.add(a[i],f=t2.ask(a[i]-1)+1),ans2=max(ans2,f);
  cout<<ans1<<'\n'<<ans2;
}


参考代码:二分
 

//longest not increasing subsequence
//longest increasing subsequence
#include<bits/stdc++.h>
using namespace std;
int n,a[100001];
vector<int> lnis,lis;
#define pb push_back
main(){
  while(~scanf("%d",a+n))++n;
  lnis.pb(*a),lis.pb(*a);
  for(int i=1;i<n;++i){
    if(a[i]<=*lnis.rbegin()) lnis.pb(a[i]);
    else*upper_bound(lnis.begin(),lnis.end(),a[i],greater<int>())=a[i];
    if(a[i]>*lis.rbegin()) lis.pb(a[i]);
    else*lower_bound(lis.begin(),lis.end(),a[i])=a[i];
  }
  cout<<lnis.size()<<'\n'<<lis.size();
}

<think>嗯,用户问的是用O(n log n)的树状数组优化DP,要给出伪代码。首先,我需要回忆一下这种优化通常应用在什么场景。常见的应该是动态规划中的状态转移需要优化的情况,比如最长递增子序列(LIS)的问题,或者类似的序列问题,其中状态转移涉及到查询某个范围内的最大值或最小值,这时候用树状数组或者线段树可以将时间复杂度从O(n²)降到O(n log n)。 接下来,我需要确认用户可能已经了解的基础知识。比如,他们应该知道动态规划的基本概念,以及树状数组(Fenwick Tree)的结构和基本操作,比如单点更新和区间查询。不过,用户可能对如何将两者结合起来还不太清楚,所以需要一步步解释。 首先,我应该举一个具体的例子,比如最长递增子序列问题,来说明如何应用树状数组优化。然后,分步骤解释整个过程:状态定义、转移方程,以及如何用树状数组来高效处理这些转移。 然后,伪代码的结构需要清晰。可能需要先初始化树状数组,然后遍历每个元素,对于每个元素,查询当前最大DP值,并更新树状数组。需要注意的是,如果元素的范围很大,可能需要进行离散化处理,将原始数据映射到较小的区间,以减少树状数组的空间消耗。 在写伪代码时,要确保正确使用树状数组的更新和查询操作。例如,在LIS问题中,每个元素的位置可能需要离散化后的索引,查询比当前元素小的所有位置中的最大值,然后加一,再更新当前位置的值。 同时,还需要考虑边界情况,比如元素全部相同的情况,或者逆序的情况。此外,要确保伪代码的步骤正确,并且时间复杂度确实是O(n log n),这涉及到树状数组每次操作的时间是O(log n),而遍历所有元素是O(n),所以总时间是O(n log n)。 最后,可能需要添加注释或说明,解释伪代码中的关键步骤,以及为什么这样做可以优化时间复杂度。还要提醒用户注意离散化的步骤,因为实际应用中数据范围可能很大,直接使用树状数组可能不可行,必须进行离散化处理。 总结一下,回答的结构应该是:先介绍应用场景,然后分步骤解释DP状态和优化方法,给出伪代码,并解释关键步骤,最后讨论离散化和注意事项。这样用户能够逐步理解整个过程,并了解如何实现。</think>好的,我将以最长递增子序列(LIS)问题为例,演示如何用树状数组优化DP到O(n log n)。以下是分步说明和伪代码: --- ### **应用场景** 适用于满足以下条件的DP问题: 1. 状态转移方程形如 $dp[i] = \max\{dp[j]\} + 1$ (其中 $j < i$ 且 $a[j] < a[i]$) 2. 需要快速查询**值域区间**内的极值 --- ### **伪代码实现** ```python # 输入:数组 a[1...n] # 输出:最长递增子序列长度 # 离散化处理(若数值范围较大) sorted_unique = sorted(set(a)) rank = {v:i+1 for i, v in enumerate(sorted_unique)} # 值映射到1~m # 初始化树状数组(维护值域上的最大值) size = len(sorted_unique) BIT = [0] * (size + 1) max_len = 0 for num in a: # 查询比当前值小的区间中的最大值 r = rank[num] - 1 curr_len = query(BIT, r) + 1 # 更新结果 max_len = max(max_len, curr_len) # 更新树状数组 update(BIT, rank[num], curr_len) return max_len # 树状数组操作(最大值版本) def query(BIT, index): res = 0 while index > 0: res = max(res, BIT[index]) index -= index & -index return res def update(BIT, index, value): while index < len(BIT): BIT[index] = max(BIT[index], value) index += index & -index ``` --- ### **关键步骤解析** 1. **离散化**(第3-4行) 将原始数据映射到紧凑的整数范围,保证树状数组空间复杂度为$O(n)$ 2. **树状数组设计**(第7行) 每个位置维护**值域区间**内的最大DP值 (例如:BIT[5] 存储所有值 ≤5 的元素对应的最大DP值) 3. **查询逻辑**(第12行) 查询当前元素值对应**左侧区间**的最大值,即 $\max\{dp[j]\}$ 其中 $a[j] < a[i]$ 4. **更新逻辑**(第18行) 将当前元素的DP值更新到对应的值域位置 --- ### **时间复杂度分析** | 步骤 | 时间复杂度 | |-----|-----------| | 离散化 | $O(n \log n)$ | | 遍历元素 | $O(n)$ | | 每次查询/更新 | $O(\log n)$ | | **总计** | $O(n \log n)$ | --- ### **注意事项** 1. 树状数组通常维护**前缀最大值**,若需要区间最大值需改用线段树 2. 离散化时需要根据具体问题选择排序方式(升序/降序) 3. 对于非严格递增(允许相等)的情况,需调整查询的区间范围 这种优化技巧还可应用于: - 带权LIS问题 - 二维偏序问题 - 特定背包问题的优化 需要根据具体问题调整树状数组维护的内容(最大值/最小值/求和等)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值