CF913C dp+贪心+数位

本文介绍了一种基于二进制位分析的算法,用于解决在特定价格策略下,如何以最少的成本购买至少L个物品的问题。通过分析不同价格之间的关系,得出购买任意数量商品的最优解。

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

题目链接:

点这里

题目大意:

给定n和L,接下来n个数,第i个数 ai (0<=i<=n-1)代表购买 2i1 个物品的花费,要求购买不少于L个的最小花费是多少

思路:

n,L的数值都比较大,使用多重背包不可行,观察题目可以发现,答案很可能与二进制位有关。可以想到, ai ai+1 之间有三种关系
①2* ai = ai+1
②2* ai < ai+1
③2* ai > ai+1
在第一种关系下,无论是选择 ai 还是 ai+1 对答案没有影响
在第二种关系下,则应购买两个 ai 而不是一个 ai+1
在第三种关系下,应该选用 ai+1 而不是两个 ai
因此,我们可以得出购买 2k 件商品的最优解

思考一个问题:假设恰好需要购买L件,而不是大于等于L件,这个时候,只要在以上的基础,把L对应二进制位上的 ai 全部加起来就是最优解。

当可以购买大于等于L件商品的时候的最优解:假设购买了M件商品为最优解,M>=L。此时从高位到低位,M与L的二进制第一次出现不同的位置i所对应的 ai 小于L比i小的所有位 ai 的和。从这个角度出发可得最优解。

代码:

#include <iostream>
#include <bits/stdc++.h>

using namespace std;
long long INF=9223372036854775807;
long long s[31];
long long dp[31];
long long a[31];
int main()
{
    int n,L;
    memset(a,0,sizeof a);
    scanf("%d%d",&n,&L);
    for(int i=0;i<n;i++)
    {
        scanf("%I64d",&dp[i]);
    }
    for(int i=0;i<n-1;i++)
    {
        dp[i+1]=min(dp[i+1],2*dp[i]);
    }
    long long ans=INF;
    long long sum=0;
    for(int i=n-1;i>=0;i--)
    {
        long long need=L/(1<<i);
        sum+=need*dp[i];
        L-= need<<i;
        ans=min(ans,sum+(L>0)*dp[i]);/**(L>0)代表有余数,当前位的低位还有1**/
    }
    printf("%I64d\n",ans);
    return 0;
}
<think>我们正在讨论C++中的upper_bound函数的使用。根据用户的问题,他想要理解并优化代码片段:`upper_bound(dp+1, dp+res+1, f[i]) - dp` 的作用和用法。 首先,我们来分析这个表达式: - `upper_bound` 是C++标准库中的一个函数,用于在已排序的序列中查找第一个大于给定值的元素的位置。 - 它的基本用法是:`upper_bound(first, last, value)`,其中`first`和`last`是序列的迭代器(或指针),`value`是要查找的值。 - 它返回一个迭代器(或指针),指向序列中第一个大于`value`的元素。如果找不到这样的元素,则返回`last`。 在表达式`upper_bound(dp+1, dp+res+1, f[i]) - dp`中: - `dp` 是一个数组(或指针),`dp+1`指向数组的第二个元素(索引1),`dp+res+1`指向数组索引为`res+1`的位置(即范围是`[1, res]`,因为序列从`dp+1`开始,到`dp+res+1`结束,实际上是左闭右开区间`[dp+1, dp+res+1)`)。 - 因此,`upper_bound(dp+1, dp+res+1, f[i])` 会在数组`dp`的子数组(从索引1到索引`res`)中查找第一个大于`f[i]`的元素的位置。 - 然后,减去`dp`(数组的起始地址)得到的是该元素在数组中的索引(下标)。因为`dp`是数组的起始地址,所以`指针 - dp`得到的就是从0开始的下标。但是注意,这里搜索的范围是从`dp+1`开始,所以返回的下标可能是从1到`res+1`(如果找不到,则返回`dp+res+1`,减去`dp`后得到`res+1`)。 例如,假设`dp`数组从索引0开始,但这里我们使用索引1到`res`(即`dp[1]`到`dp[res]`)。那么,`upper_bound(dp+1, dp+res+1, f[i])`返回的指针可能是`dp+k`(其中`k`在1到`res+1`之间),然后减去`dp`得到`k`(即下标k)。注意,如果返回的是`dp+res+1`,那么减去`dp`就是`res+1`。 这个表达式的作用是:在`dp`数组的[1, res]区间内,找到第一个大于`f[i]`的元素的位置(下标)。如果所有元素都不大于`f[i]`,则返回`res+1`。 这种用法常见于动态规划中维护一个有序的序列,特别是用于优化最长上升子序列(LIS)等问题的O(nlogn)解法。在LIS问题中,我们经常维护一个数组`dp`,其中`dp[i]`表示长度为i的上升子序列的最后一个元素的最小值。然后,对于每个新元素`f[i]`,我们使用`upper_bound`(或`lower_bound`)来查找插入位置,从而更新`dp`数组。 在最长上升子序列问题中,通常我们使用`lower_bound`来查找第一个大于等于当前元素的位置(用于非严格递增)或者严格递增时使用`lower_bound`(因为要求严格递增时,如果存在相等的元素,则不能放在同一个长度上,但实际做法中,我们通常用`lower_bound`来找到替换位置)。而`upper_bound`则用于严格递增序列中,查找第一个大于当前元素的位置(即替换位置)。 然而,在用户给出的代码片段中,使用的是`upper_bound`,所以它查找的是第一个大于`f[i]`的位置。这个位置可以用于替换操作,从而维护一个严格递增的序列。 例如,在最长递增子序列的O(nlogn)算法中: - 我们维护一个数组`dp`,`dp[1...res]`是当前长度为1,2,...,res的递增子序列的末尾元素的最小值,并且这个数组是递增的。 - 对于每个元素`a[i]`: - 如果`a[i]`大于`dp[res]`,则扩展:`dp[++res] = a[i]`。 - 否则,在`dp[1..res]`中找到第一个大于`a[i]`的位置`pos`(即`upper_bound`返回的位置),然后令`dp[pos] = a[i]`。 这里,`pos = upper_bound(dp+1, dp+res+1, a[i]) - dp`,然后更新`dp[pos] = a[i]`。这样,我们保证了`dp`数组的单调性,并且用更小的值替换,使得后续扩展的可能性更大。 因此,用户代码中的表达式就是用来获取这个位置的索引。 关于优化:使用`upper_bound`本身已经是二分查找,时间复杂度为O(logn),所以效率已经很高。但我们可以考虑以下几点优化: 1. 确保数组`dp`是严格递增的,这样`upper_bound`才能正确工作。 2. 如果数组`dp`的大小(即`res`)很大,可以考虑使用更高效的二分查找实现,但标准库的`upper_bound`已经足够高效。 3. 在循环中,如果`f[i]`比当前`dp`数组的最大值(即`dp[res]`)还要大,那么可以直接添加到末尾,避免二分查找。即: ```c++ if (f[i] > dp[res]) { dp[++res] = f[i]; } else { int pos = upper_bound(dp+1, dp+res+1, f[i]) - dp; dp[pos] = f[i]; } ``` 这样可以减少一些不必要的查找(虽然最坏情况下还是O(logn))。 4. 注意数组边界:确保`res`在合理范围内,避免数组越界。 另外,用户提供的引用中,引用[1]和[4]提到了`lower_bound`和`upper_bound`的用法,引用[2]和[5]则展示了一些动态规划的代码,其中引用[2]的代码中使用了`lower_bound`。我们可以参考这些代码来理解。 在引用[2]的代码中,有: ```c++ int now = e[i].x - e[i].len; int num = lower_bound(cf+1, cf+1+n, now) - cf; ``` 这里,`lower_bound`在数组`cf`(从1到n)中查找第一个大于等于`now`的位置,返回该位置的指针,减去`cf`得到下标`num`。如果找不到,则返回`n+1`。 而用户询问的是`upper_bound`,它与`lower_bound`的区别在于: - `lower_bound`:返回第一个大于等于value的元素位置。 - `upper_bound`:返回第一个大于value的元素位置。 因此,在需要严格大于的情况下,使用`upper_bound`。 总结: - 表达式`upper_bound(dp+1, dp+res+1, f[i]) - dp`的作用:在数组`dp`的[1, res]范围内(即从`dp[1]`到`dp[res]`)查找第一个大于`f[i]`的元素的下标(如果找不到,返回`res+1`)。 - 用法:通常用于动态规划中维护单调序列,比如优化最长上升子序列(LIS)问题。 - 优化:可以添加一个判断,如果当前值大于序列最大值则直接扩展,避免二分查找。 最后,根据系统指令,我们需要生成相关问题: §§相关问题§§ 1. 在什么情况下应该使用`upper_bound`而不是`lower_bound`? 2. 如何用`upper_bound`实现最长上升子序列(LIS)的O(nlogn)算法? 3. 在使用`upper_bound`时,如何处理数组下标从0开始的情况? 4. 除了动态规划,`upper_bound`还有哪些常见的应用场景?</think>### `upper_bound(dp+1, dp+res+1, f[i]) - dp` 的作用和用法详解 #### 1. **核心作用** 该表达式在已排序的数组 `dp` 的子区间 `[1, res]` 中,**查找第一个严格大于 `f[i]` 的元素的位置(下标)**。返回值是一个整数下标,范围在 `1` 到 `res+1` 之间: - 若找到满足条件的元素,返回其下标(如 `k`)。 - 若所有元素均不大于 `f[i]`,则返回 `res+1`(表示越界位置)。 #### 2. **关键语法解析** - **`upper_bound(first, last, value)`** - `first`:搜索区间的起始指针(`dp+1` 指向 `dp[1]`)。 - `last`:搜索区间的结束指针(`dp+res+1` 指向 `dp[res]` 的下一个位置,即左闭右开区间 `[dp+1, dp+res+1)`)。 - `value`:待比较的值(`f[i]`)。 - **`- dp` 的意义** 将返回的指针转换为下标(指针差值)。若返回指针为 `dp+k`,则 `(dp+k) - dp = k`(即下标 `k`)。 #### 3. **典型应用场景** 常见于动态规划(DP)中维护**单调递增序列**,例如优化最长上升子序列(LIS)问题: ```cpp int res = 0; // 当前序列长度 for (int i = 1; i <= n; i++) { // 在 dp[1..res] 中找第一个大于 f[i] 的位置 int pos = upper_bound(dp+1, dp+res+1, f[i]) - dp; if (pos > res) { dp[++res] = f[i]; // 扩展序列 } else { dp[pos] = f[i]; // 替换为更小的值,保持递增性 } } ``` - **优化效果**:将 LIS 的复杂度从 $O(n^2)$ 降至 $O(n \log n)$[^1][^5]。 #### 4. **与 `lower_bound` 的区别** | 函数 | 返回值条件 | 示例(数组 `[1,3,5]`, `value=3`) | |---------------|--------------------------|-----------------------------------| | `lower_bound` | 第一个 **≥** `value` 的元素 | 返回下标 `2`(元素 `3`) | | `upper_bound` | 第一个 **>** `value` 的元素 | 返回下标 `3`(元素 `5`) | #### 5. **优化建议** 1. **提前判断边界**:若 `f[i]` 大于当前最大值 `dp[res]`,直接扩展序列,避免二分查找: ```cpp if (f[i] > dp[res]) { dp[++res] = f[i]; } else { int pos = upper_bound(dp+1, dp+res+1, f[i]) - dp; dp[pos] = f[i]; } ``` 2. **确保数组有序**:`dp[1..res]` 必须严格递增,否则结果错误。 3. **下标从 0 开始的处理**:若数组下标从 `0` 开始,需调整指针范围: ```cpp // 在 dp[0..res-1] 中查找 int pos = upper_bound(dp, dp+res, f[i]) - dp; ``` #### 6. **示例分析** 假设 `dp = [0, 2, 5, 7]`(有效区间 `[1,3]`),`res=3`: - 若 `f[i] = 4`:`upper_bound` 返回 `dp+3`(元素 `5`),下标 `pos = 3`。 - 若 `f[i] = 9`:返回 `dp+4`(越界),`pos = 4`(需扩展序列)。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值