TOJ 1611 Moo University - Financial Aid -- 线段树 + DP

探讨了在ACM竞赛中解决一个特定的算法问题,该问题要求在满足一定条件下找到一组二元组中X的最大中位数值。通过动态规划与线段树实现了高效的解决方案。

题目链接:http://acm.tzc.edu.cn/acmhome/problemdetail.do?&method=showdetail&id=1611

题目大意:给定c个二元组(x, y),要求从中选出n(n为奇数)个,满足这n个二元组的y之和不超过给定的f,而且x的中位数最大。输出最大的中位数。

分析:先将二元组(记为cow)按X排序,然后枚举每个X作为中位数。这时需要在X之前和X之后各选n / 2个数,因为已经按X排序,不管怎么选都可以满足X是中位数的条件,所以只需要各自选取Y最小的n / 2个数,判断它们的和是否满足不超过f这个条件。如果满足,X可行,否则不行。如果按X从大到小枚举X,那么遇到的第一个可行X即为答案。剩下的问题在于如何高效地求出最小的n / 2个数的和。

可以用动态规划解决这个问题。用一个长为n / 2的数组a记录以cow[i].X为中位数时前面最小的n / 2个Y,同时用dp[i]表示这些数中的最大值的下标,那么计算dp[i + 1]时,如果cow[i + 1].Y >= a[dp[i]],则dp[i + 1] = dp[i]。否则用cow[i + 1].Y替换a[dp[i]],同时计算出新的最大值下标也就是dp[i + 1]。这涉及单点更新及全区间的最大值查询,用线段树可以实现。至于求和,在计算dp[i]的时候顺便就可以求出来了。对于cow[i].X之后的数,再进行一次同样的操作。下面的代码中的dp保存的就是n / 2个数的和,因为最大值下标可以直接从线段树中读出,不需要另外存储。求出dp之后再进行一次线性扫描就可以求出答案了。

#include 
  
   
#include 
   
    
#include 
    
     
#include 
     
      
#include 
      
       
#include 
        #include 
        
          #include 
         
           #include 
          
            #include 
           
             #include 
            
              #include 
             
               #include 
              
                #include 
               
                 #include 
                
                  #include 
                 
                   #define mp make_pair #define X first #define Y second #define MEMSET(a, b) memset(a, b, sizeof(a)) using namespace std; typedef unsigned int ui; typedef long long ll; typedef unsigned long long ull; typedef pair
                  
                    pii; typedef vector
                   
                     vi; typedef vi::iterator vi_it; typedef map
                    
                      mii; typedef priority_queue
                     
                       pqi; typedef priority_queue
                      
                       , greater
                       
                         > rpqi; typedef priority_queue
                        
                          pqp; typedef priority_queue
                         
                          , greater
                          
                            > rpqp; const int MAX_C = 100000 + 2; const int MAX_N = 10000 + 2; pii cow[MAX_C]; int a[MAX_N]; int dp[2][MAX_C]; int cnt; struct { int left; int right; int max_index; inline int mid() { return (left + right) >> 1; } } st[MAX_N * 4]; void build(int l, int r, int idx) { st[idx].left = l; st[idx].right = r; if (l == r) st[idx].max_index = cnt++; else { int mid = st[idx].mid(), lc = idx << 1, rc = lc | 1; build(l, mid, lc); build(mid + 1, r, rc); int lm = st[lc].max_index, rm = st[rc].max_index; st[idx].max_index = a[lm] > a[rm] ? lm : rm; } } void update(int pos, int idx) { if (st[idx].left != st[idx].right) { int mid = st[idx].mid(), lc = idx << 1, rc = lc | 1; if (pos <= mid) update(pos, lc); else update(pos, rc); int lm = st[lc].max_index, rm = st[rc].max_index; st[idx].max_index = a[lm] > a[rm] ? lm : rm; } } int main(int argc, char *argv[]) { // freopen("D:\\in.txt", "r", stdin); int n, c, f, i; cin >> n >> c >> f; for (i = 0; i < c; ++i) scanf("%d%d", &cow[i].X, &cow[i].Y); //先处理n = 1时的特殊情况 if (n == 1) { int ans = -1; for (int j = 0; j < c; ++j) if (cow[j].Y <= f) ans = max(ans, cow[j].X); cout << ans << endl; return 0; } sort(cow, cow + c); //计算dp[0] for (i = 0; i < (n >> 1); ++i) dp[0][n >> 1] += (a[i] = cow[i].Y); cnt = 0; build(0, (n >> 1) - 1, 1); for (; i < c - (n >> 1); ++i) { int mi = st[1].max_index; if (a[mi] > cow[i].Y) { dp[0][i + 1] = dp[0][i] - a[mi] + cow[i].Y; a[mi] = cow[i].Y; update(mi, 1); } else dp[0][i + 1] = dp[0][i]; } //计算dp[1] for (i = 0; i < (n >> 1); ++i) dp[1][c - (n >> 1) - 1] += (a[i] = cow[c - 1 - i].Y); cnt = 0; build(0, (n >> 1) - 1, 1); for (i = c - (n >> 1) - 1; i > (n >> 1); --i) { int mi = st[1].max_index; if (a[mi] > cow[i].Y) { dp[1][i - 1] = dp[1][i] - a[mi] + cow[i].Y; a[mi] = cow[i].Y; update(mi, 1); } else dp[1][i - 1] = dp[1][i]; } for (i = c - (n >> 1) - 1; i >= (n >> 1); --i) if (dp[0][i] + dp[1][i] + cow[i].Y <= f) break; if (i >= (n >> 1)) cout << cow[i].X << endl; else cout << -1 << endl; return 0; } 
                          
                         
                        
                       
                      
                     
                    
                   
                  
                 
                
               
              
             
            
           
          
         
        
      
     
    
   
  

<think>我们要解最长上升子序列(Longest Increasing Subsequence, LIS)问题。传统的动态规划解法时间复杂度为$O(n^2)$,而使用线段树(或树状数组)可以优化到$O(n \log n)$。 思路: 1. 离散化:由于序列中的元素可能很大,我们首先将序列中的元素映射到其排名(即离散化),这样线段树的下标范围就可以控制在[1, n]。 2. 动态规划定义:定义dp[i]为以第i个元素结尾的最长上升子序列的长度。 3. 状态转移:对于每个位置i,我们需要在区间[1, rank[i]-1](即所有小于当前元素的值)中查询最大的dp值,然后dp[i] = max_value + 1。 4. 线段树的作用:线段树用来维护区间最大值。当我们处理到第i个元素时,我们已经处理了它之前的所有元素,并且将dp值更新到了线段树中对应离散化值的位置。这样我们就可以在O(log n)的时间内完成区间最大值的查询和单点更新。 步骤: 1. 离散化原数组。 2. 初始化线段树(支持区间最大值查询和单点更新)。 3. 遍历数组中的每个元素: a. 在线段树中查询[1, 当前元素离散化后的值-1]区间内的最大值max_val。 b. 当前元素的dp值为max_val+1。 c. 更新线段树中当前元素离散化值位置的值(即更新为当前的dp值)。 4. 整个数组的LIS即为所有dp[i]的最大值。 注意:线段树通常用于区间查询和单点更新,这里我们维护的是区间最大值。 下面我们用C++实现这个算法,并加上详细注释。</think>### C++实现线段树结合动态规划解最长上升子序列(LIS) #### 算法思路 1. **离散化**:将序列元素映射到排名,缩小值域范围 2. **动态规划**:`dp[i]`表示以`nums[i]`结尾的LIS长度 3. **线段树优化**:维护区间最大值,实现$O(n\log n)$复杂度 - 查询区间$[1, rank-1]$的最大`dp`值 - 更新位置$rank$的值为`dp[i]` #### C++代码实现 ```cpp #include <iostream> #include <vector> #include <algorithm> #include <climits> using namespace std; class SegmentTree { private: vector<int> tree; // 线段树数组 int n; // 原始数组长度 // 构建线段树 void build(int node, int start, int end) { if (start == end) { tree[node] = 0; // 初始化为0 return; } int mid = (start + end) >> 1; build(node << 1, start, mid); build(node << 1 | 1, mid + 1, end); tree[node] = max(tree[node << 1], tree[node << 1 | 1]); } public: SegmentTree(int size) { n = size; tree.resize(4 * n); // 4倍空间 build(1, 1, n); // 从根节点开始构建 } // 更新操作:将位置idx的值设为val void update(int idx, int val, int node = 1, int start = 1, int end = -1) { if (end == -1) end = n; if (start == end) { tree[node] = max(tree[node], val); // 取最大值避免覆盖 return; } int mid = (start + end) >> 1; if (idx <= mid) update(idx, val, node << 1, start, mid); else update(idx, val, node << 1 | 1, mid + 1, end); tree[node] = max(tree[node << 1], tree[node << 1 | 1]); // 更新父节点 } // 区间查询:[l, r]的最大值 int query(int l, int r, int node = 1, int start = 1, int end = -1) { if (end == -1) end = n; if (r < start || end < l) return 0; // 区间无交集 if (l <= start && end <= r) return tree[node]; // 完全包含 int mid = (start + end) >> 1; int left_val = query(l, r, node << 1, start, mid); int right_val = query(l, r, node << 1 | 1, mid + 1, end); return max(left_val, right_val); } }; // 离散化函数 vector<int> discretize(vector<int>& nums) { vector<int> sorted = nums; sort(sorted.begin(), sorted.end()); sorted.erase(unique(sorted.begin(), sorted.end()), sorted.end()); vector<int> ranks(nums.size()); for (int i = 0; i < nums.size(); ++i) { // 获取元素在排序后数组中的位置(从1开始) ranks[i] = lower_bound(sorted.begin(), sorted.end(), nums[i]) - sorted.begin() + 1; } return ranks; } // 使用线段树优化的LIS算法 int lengthOfLIS(vector<int>& nums) { if (nums.empty()) return 0; // 离散化处理 vector<int> ranks = discretize(nums); int max_rank = *max_element(ranks.begin(), ranks.end()); SegmentTree st(max_rank); // 创建线段树 vector<int> dp(nums.size(), 1); // dp数组初始化 int ans = 1; for (int i = 0; i < nums.size(); ++i) { if (ranks[i] > 1) { // 查询[1, rank-1]区间最大值 dp[i] = st.query(1, ranks[i] - 1) + 1; } // 更新线段树当前位置 st.update(ranks[i], dp[i]); ans = max(ans, dp[i]); // 更新全局最大值 } return ans; } int main() { vector<int> nums = {10, 9, 2, 5, 3, 7, 101, 18}; cout << "LIS length: " << lengthOfLIS(nums) << endl; // 输出:4 return 0; } ``` #### 关键点解析 1. **离散化处理**(第47-58行) - 将原始数组排序去重后映射到排名 - 例:`[10,9,2,5]` → 排名`[4,3,1,2]` - 作用:将值域压缩到$[1,n]$,减少线段树空间 2. **线段树操作**(第11-44行) - **查询**:在$O(\log n)$时间内获取区间$[1, rank-1]$的最大`dp`值 - **更新**:在$O(\log n)$时间内更新位置$rank$的值为当前`dp[i]` 3. **动态规划转移**(第66-74行) - 状态转移方程:$dp[i] = \max_{j<i \text{且} nums[j]<nums[i]} \{dp[j]\} + 1$ - 线段树优化:将$O(n)$的遍历优化为$O(\log n)$的区间查询 4. **复杂度分析** - 时间复杂度:$O(n \log n)$(离散化$O(n \log n)$,线段树操作$O(n \log n)$) - 空间复杂度:$O(n)$(存储离散化数组和线段树) #### 算法对比 | 方法 | 时间复杂度 | 空间复杂度 | 适用场景 | |--------------------|------------|------------|------------------------| | 朴素DP | $O(n^2)$ | $O(n)$ | 小规模数据 | | 贪心+二分 | $O(n \log n)$ | $O(n)$ | 仅需长度,无需序列 | | **线段树+DP** | $O(n \log n)$ | $O(n)$ | 需完整DP数组的场景 | 此解法结合了动态规划的状态定义和线段树的区间查询优势,特别适用于需要获取完整DP数组的场景(如需要输出所有LIS)[^1][^2]。 --- ### 相关问题 1. **如何修改代码以输出具体的最长上升子序列?** (需回溯DP数组重建LIS路径) 2. **线段树在动态规划中还能优化哪些经典问题?** (如:最大子段和、带权区间调度等) 3. **如何处理存在重复元素的最长非严格上升子序列?** (需调整离散化和查询区间) 4. **树状数组能否替代线段树实现相同优化?** (对比两种数据结构的实现复杂度和常数因子) [^1]: 动态规划的精髓在于将目标值分解成0~n逐个解决,每次选取数组元素时调整状态 [^2]: 线段树等高级数据结构适用于优化区间查询类问题
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值