【二分】洛谷_3902 递增

本文介绍了一种算法,用于找出最少需要修改哪些数字才能使一个整数序列变为严格递增序列。通过使用二分查找法优化插入位置,有效地减少了计算复杂度。

题意

给出n个数,求出修改最少的数字,使得数列严格单调递增。

思路

我们用一个数组s来记录当前存到的数字,每次放进一个数字,我们就判断它是不是比之前的数小,否则我们就二分找到一个最好的位置可以放下它。

代码

#include<cstdio>
int n,s[100001],a,tot,ans;
inline int ef(int x)
{
    int l=1,r=tot,mid;
    while(l<r)
    {
        mid=(l+r)>>1;
        if (s[mid]>=x) r=mid;
        else l=mid+1;
    }
    return l;
}
int main()
{
    scanf("%d",&n);
    scanf("%d",&a);
    s[++tot]=a;
    for (int i=1;i<n;i++)
    {
        scanf("%d",&a);
        if (a<s[tot]) {s[ef(a)]=a; ans++;}//二分位置直接替换,统计次数
        else s[++tot]=a;//否则直接放到后面
    }
    printf("%d",ans);
}
你提到的是 **洛谷 P1314 [NOIP2011 提高组] 聪明的质监员**。 这是一道经典的 **二分答案 + 前缀和优化** 题目。我们来详细分析解法、原理,并提供 AC 代码。 --- ## ✅ 题目大意(P1314) 给定 $ n $ 个矿石,每个矿石有重量 $ w_i $ 和价值 $ v_i $。 现在你要选一个区间 $[L, R]$ 作为“合格区间”,并设定一个标准值 $ W $。 对于每个区间 $[L,R]$,定义它的“检验值” $ Y $ 为: > 在 $[L,R]$ 中所有满足 $ w_i \geq W $ 的矿石中: > - 数量:$ c $ > - 总价值:$ s $ > - 检验值 $ Y = c \times s $ 给你 $ m $ 个区间 $[L_j, R_j]$,总检验值是这些区间的 $ Y_j $ 之和。 目标:选择合适的 $ W $,使得总检验值 $ S = \sum Y_j $ 尽可能接近给定的目标值 $ S_0 $,求最小的 $ |S - S_0| $。 --- ## 🔍 解题思路 ### ❌ 直接枚举 $ W $?不行! - $ W $ 可能从 1 到 $ 10^6 $,甚至更大 - 对每个 $ W $ 都要遍历所有区间计算 $ Y $,时间复杂度太高 ### ✅ 正确做法:**二分答案** 观察性质: - 当 $ W $ 增大时 → 更少的矿石满足 $ w_i \geq W $ → $ c $ 和 $ s $ 减小 → $ Y $ 减小 - 所以 **总检验值 $ S(W) $ 是关于 $ W $ 的非递增函数** 👉 因此可以对 $ W $ 进行 **二分查找**,找到使 $ S $ 最接近 $ S_0 $ 的位置。 --- ## 🧱 算法步骤 1. 输入所有矿石 $ (w_i, v_i) $ 和区间 $ (L_j, R_j) $ 2. 确定二分范围:`l = 1`, `r = max_w + 1` 3. 对每个候选 $ W $: - 预处理前缀数组: - `cnt[i]`: 前 $ i $ 个中满足 $ w_j \geq W $ 的个数 - `sum[i]`: 前 $ i $ 个中满足 $ w_j \geq W $ 的价值和 - 对每个查询区间 $[L_j, R_j]$: - $ c = cnt[R_j] - cnt[L_j - 1] $ - $ s = sum[R_j] - sum[L_j - 1] $ - $ Y_j = c \times s $ - 总 $ S = \sum Y_j $ 4. 二分调整 $ W $,记录最小的 $ |S - S_0| $ --- ## ⚙️ 时间复杂度 - 二分次数:$ O(\log (\max w)) \approx 20 $ - 每次预处理前缀和:$ O(n) $ - 每次处理 $ m $ 个区间:$ O(m) $ - 总体:$ O((n + m) \log \max w) $,完全可行 --- ## ✅ AC C++ 代码 ```cpp #include <iostream> #include <vector> #include <algorithm> #include <climits> using namespace std; typedef long long ll; int main() { int n, m; ll s0; cin >> n >> m >> s0; vector<int> w(n + 1), v(n + 1); for (int i = 1; i <= n; ++i) { cin >> w[i] >> v[i]; } vector<pair<int, int>> queries(m); for (int i = 0; i < m; ++i) { cin >> queries[i].first >> queries[i].second; } // 找最大 w 用于二分上界 int max_w = *max_element(w.begin() + 1, w.end()); ll min_diff = LLONG_MAX; // 二分 W int left = 1, right = max_w + 1; while (left <= right) { int mid = (left + right) / 2; // 构造前缀数组:满足 w[i] >= mid 的才计入 vector<int> cnt(n + 1, 0); vector<ll> sum(n + 1, 0); for (int i = 1; i <= n; ++i) { cnt[i] = cnt[i - 1]; sum[i] = sum[i - 1]; if (w[i] >= mid) { cnt[i]++; sum[i] += v[i]; } } // 计算总检验值 S ll total_S = 0; for (const auto& q : queries) { int L = q.first, R = q.second; int c = cnt[R] - cnt[L - 1]; ll s = sum[R] - sum[L - 1]; total_S += (ll)c * s; // 注意强转防止溢出 } // 更新最小差值 min_diff = min(min_diff, abs(total_S - s0)); // 二分调整 if (total_S >= s0) { left = mid + 1; // S 太大,说明 W 太小,需增大 W } else { right = mid - 1; // S 太小,说明 W 太大,需减小 W } } cout << min_diff << endl; return 0; } ``` --- ## ✅ 关键点解释 | 技巧 | 说明 | |------|------| | `cnt` 和 `sum` 前缀数组 | 快速计算任意区间内符合条件的个数和总价值 | | 二分方向判断 | 若 `total_S >= s0`,说明当前 $ W $ 还不够大 → 要提高 $ W $ 来降低 $ S $ | | `abs(total_S - s0)` | 必须在每次迭代中更新,不能只取最终值 | | 使用 `long long` | `total_S` 可能达到 $ 10^{10} $ 或更高 | --- ## 🧪 样例测试建议 输入样例: ``` 5 3 10 1 1 2 2 3 3 4 4 5 5 1 2 2 3 3 4 ``` 输出应为某个较小的整数(具体看数据),程序会输出最接近 $ S_0=10 $ 的绝对差。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值