二分+贪心(总结)

这篇博客总结了利用二分和贪心算法解决的五道编程题,包括poj2456、poj1064、nyoj914、hdu4004/nyoj619及nyoj680。通过二分法缩小搜索范围,结合贪心策略求解最优解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近做了几道二分+贪心的题目,做下总结

基本思路:通过二分,将范围逐步缩小,直到最优解

1、poj2456

/*
题意:
    有n个牛栏,选m个放进牛,相当于一条线段上有 n 个点,选取 m 个点,
使得相邻点之间的最小距离值最大

思路:贪心+二分
    二分枚举相邻两牛的间距,判断大于等于此间距下能否放进所有的牛。
*/
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e6+10;
int a[N],n,m;
bool judge(int k)//枚举间距k,看能否使任意两相邻牛
{
    int cnt = a[0], num = 1;//num为1表示已经第一头牛放在a[0]牛栏中
    for(int i = 1; i < n; i ++)//枚举剩下的牛栏
    {
        if(a[i] - cnt >= k)//a[i]这个牛栏和上一个牛栏间距大于等于k,表示可以再放进牛
        {
            cnt = a[i];
            num ++;//又放进了一头牛
        }
        if(num >= m) return true;//所有牛都放完了
    }
    return false;
}
void solve()
{
    int l = 1, r = a[n-1] - a[0];//最小距离
<think>好的,我现在需要帮助用户理解C++中如何使用动态规划、贪心算法和二分查找来以O(n log n)的时间复杂度解决最长递增子序列(LIS)问题。首先,我应该回顾用户之前的提问历史。用户之前已经得到了一个示例代码,现在想更深入地了解这三种方法如何结合使用。 首先,我需要确认用户的基础知识。他们可能已经了解动态规划的基本概念,但可能不清楚如何结合贪心二分查找。我的解释需要从基本概念入手,逐步深入,确保结构清晰。 接下来,用户的问题涉及三个技术点:动态规划、贪心算法和二分查找。我需要分别解释这三者的作用,以及它们是如何协同工作的。动态规划通常用于解决子问题重叠的情况,但传统的动态规划解决LIS的时间复杂度是O(),这里通过贪心策略和二分查找优化到O(n log n)。需要详细说明这种优化是如何实现的。 然后,我需要分解示例代码中的关键部分。比如,tails数组的作用,为什么使用lower_bound函数,以及每次迭代中的替换操作如何保证正确性。用户可能对tails数组的维护逻辑有疑问,需要解释清楚贪心策略在这里的应用,即总是尽可能保持较小的末尾元素,以便后续有更多可能形成更长的子序列。 此外,用户之前的相关问题涉及其他方法、内存优化和处理负数的情况。虽然当前问题没有直接提到这些,但在解释过程中可能需要简要提及,以增强理解的全面性。例如,可以提到即使在处理负数时,算法依然有效,因为比较是基于数值大小而非符号。 还需要注意代码中的关键点,比如lower_bound的使用,这实际上就是二分查找的应用,用来快速定位插入或替换的位置。这里需要说明二分查找如何将时间复杂度从O()降低到O(n log n)。 可能存在的疑问点包括:为什么替换tails中的元素不会影响最终结果?这时候需要解释,替换操作保持了相同长度的子序列的最小末尾,从而允许未来更长的子序列扩展。这种贪心选择确保了后续步骤的最优性。 最后,总结整个算法的工作流程,强调各部分的协同作用,并再次说明时间复杂度的优势。确保用户能够理解每个步骤的目的和整体算法的效率提升。</think>以下是采用 **动态规划 + 贪心 + 二分查找** 实现 \(O(n \log n)\) 时间复杂度的 **最长递增子序列 (LIS)** 的详细解析: --- ### **算法核心思想** 1. **动态规划**:通过维护一个数组 `tails`,记录不同长度递增子序列的最小末尾值。 2. **贪心策略**:总是选择尽可能小的末尾值,为后续元素提供更大的增长空间。 3. **二分查找**:快速定位元素在 `tails` 中的插入位置,优化时间复杂度。 --- ### **C++ 代码实现** ```cpp #include <iostream> #include <vector> #include <algorithm> using namespace std; int lis(vector<int>& nums) { if (nums.empty()) return 0; vector<int> tails; // 记录不同长度递增子序列的最小末尾值 for (int num : nums) { // 二分查找:找到第一个 >= num 的位置 auto it = lower_bound(tails.begin(), tails.end(), num); if (it == tails.end()) { tails.push_back(num); // 如果找不到,说明可以延长当前最长子序列 } else { *it = num; // 替换为更小的值,保证未来可能形成更长序列 } } return tails.size(); // tails的长度即为LIS的长度 } int main() { vector<int> nums = {10, 9, 2, 5, 3, 7, 101, 18}; cout << "LIS长度: " << lis(nums); // 输出4 (子序列2,3,7,101) return 0; } ``` --- ### **关键步骤解析** #### 1. **`tails`数组的作用** - `tails[i]` 表示 **长度为 \(i+1\) 的递增子序列的最小末尾值**。 - 例如:若 `tails = [2,5,7]`,表示: - 长度为1的LIS末尾最小为2, - 长度为2的LIS末尾最小为5, - 长度为3的LIS末尾最小为7。 #### 2. **贪心策略的体现** - **目标**:让 `tails` 中的值尽可能小,从而为后续元素提供更多增长可能。 - **操作**:对于新元素 `num`: - 如果 `num > tails.back()`,直接添加到末尾(延长最长子序列)。 - 否则,**用 `num` 替换 `tails` 中第一个 >= `num` 的值**,从而降低该位置的末尾值。 #### 3. **二分查找的优化** - `lower_bound` 函数在 \(O(\log n)\) 时间内找到插入位置,将整体时间复杂度优化到 \(O(n \log n)\)。 - **为什么有效**:`tails` 数组始终保持严格递增,二分查找总能找到正确位置。 --- ### **复杂度分析** | 操作 | 时间复杂度 | 空间复杂度 | |--------------|------------|------------| | 遍历数组 | \(O(n)\) | | | 二分查找 | \(O(\log n)\) | | | **总体** | \(O(n \log n)\) | \(O(n)\) | --- ### **举例说明** 以输入 `nums = [2,5,3,7]` 为例: 1. **初始**:`tails = []` 2. **处理2**:`tails = [2]` 3. **处理5**:`5 > 2` → `tails = [2,5]` 4. **处理3**:替换 `5` → `tails = [2,3]` 5. **处理7**:`7 > 3` → `tails = [2,3,7]` 最终 LIS 长度为3(如 `[2,3,7]`),尽管中间替换了 `5`,但正确保留了最长子序列的可能性。 --- ### **为什么这个算法正确?** 1. **替换不影响结果**:替换操作仅优化后续可能,不破坏已记录的最长子序列长度。 2. **严格递增性**:`tails` 数组始终保持严格递增,确保二分查找的有效性。 --- ### **适用场景** - **数值范围**:无论正负数均适用(依赖大小比较)。 - **大数据量**:相比 \(O(n^2)\) 的DP方法,此算法能高效处理 \(n \leq 10^5\) 的数据量。 如果需要进一步解释具体步骤或数学证明,请随时提出!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值