欢迎光临
我们一直在努力

CCF-GESP计算机学会等级考试2026年9月五级C++T2 饮品调制

P17456 [GESP202609 五级] 饮品调制

题目描述

你想调制一份甜度恰到好处的饮品给你的朋友们品尝。

nnn 种原料可供用于调制饮品。第 iii 种原料存量有 viv_ivi 升,每升含有 sis_isi 克糖分。你可以自由选择原料加入饮品,但每种原料的使用量不得超过其剩余存量。也就是说,假设第 iii 种原料选用 kik_iki 升,应当有 0≤ki≤vi0\\le k_i\\le v_i0kivikik_iki 可以取 000viv_ivi 之间的任何数字(包括小数)。

一份甜度恰到好处的饮品需要保证甜度恰好为 ttt。最终你调制得到的饮品甜度将为 ∑i=1nki⋅si∑i=1nki\\frac{\\sum_{i=1}^{n}k_i\\cdot s_i}{\\sum_{i=1}^{n}k_i}i=1nkii=1nkisi。为了让更多的朋友喝到饮品,请问最多能调制出多少升甜度恰到好处的饮品?如果无法调制出甜度恰到好处的饮品,则认为答案是 000

输入格式

第一行,两个整数 n,tn,tn,t,分别表示原料种类数量,恰到好处的甜度。

接下来 nnn 行,每行两个整数 vi,siv_i,s_ivi,si,分别表示第 iii 种原料的存量体积,每升含有的糖分质量。

输出格式

一行,一个小数,表示能调制出的甜度恰到好处的饮品最大体积,保留三位小数。

输入输出样例 #1

输入 #1

4 2
6 1
5 2
8 5
1 0

输出 #1

14.667

输入输出样例 #2

输入 #2

2 5
3 4
5 3

输出 #2

0.000

说明/提示

对于 40%40\\%40% 的测试点,保证 n=2n=2n=2

对于所有测试点,保证 1≤n≤20001\\le n\\le 20001n20000≤t≤2000\\le t\\le 2000t2001≤vi≤1001\\le v_i\\le 1001vi1000≤si≤2000\\le s_i\\le 2000si200

解法一

我的题解(第一人称)

题目要求选出若干原料,每种原料选取量不超过存量,使得混合饮品甜度恰好等于 ttt,并且最大化饮品总体积。
甜度公式:
∑kisi∑ki=t
\\frac{\\sum k_i s_i}{\\sum k_i}=t
kikisi=t

变形得到:∑ki(si−t)=0\\sum k_i(s_i-t)=0ki(sit)=0

我的思路:

  • 先把所有原料按照每升糖分s从小到大排序。
  • 特判:如果所有原料甜度全部大于t,或者全部小于t,一定无法调配,直接输出0。
  • 一开始我直接选取全部原料,计算此时混合甜度。
  • 如果当前甜度小于t:代表低甜度原料太多。我不断尝试完整剔除当前区间最左侧甜度最小的原料。如果删掉它之后甜度依旧不超过t,就直接删掉;否则只剔除这一份原料的一部分,刚好把甜度调到t,输出答案结束程序。
  • 如果当前甜度大于t:代表高甜度原料太多。我不断尝试完整剔除当前区间最右侧甜度最大的原料。如果删掉它之后甜度依旧不低于t,就直接删掉;否则只剔除这一份原料的一部分,刚好把甜度调到t,输出答案结束程序。
  • 当循环结束,代表当前保留区间混合甜度正好等于t,直接输出此时总体积。
  • 原理:最优方案一定是排序后一段连续的原料,最多只对区间左端点或者右端点的原料取一部分体积。所以不断从两端剔除原料的贪心策略成立。

    带完整注释代码

    #include <bits/stdc++.h>
    using namespace std;
    int n,t;
    // 原料结构体:v存量体积,s每升糖分
    struct node{
    int v,s;
    };
    node a[2005];
    double anss=0,ansv=0; // anss:总糖分,ansv:总体积
    // 比较函数:按糖分s从小到大排序
    bool cmp(node x,node y){
    return x.s<y.s;
    }
    int main() {
    cin>>n>>t;
    // 读入每种原料
    for(int i=1;i<=n;i++){
    cin>>a[i].v>>a[i].s;
    }
    sort(a+1,a+n+1,cmp);
    // 边界判断:所有原料甜度都大于t,或者全部小于t,不可能调制成功,输出0
    if (a[1].s>t||a[n].s<t){
    cout<<"0.000";
    return 0;
    }
    // 初始把所有原料全部选上,计算总糖分和总体积
    for(int i=1;i<=n;i++){
    anss+=a[i].s*a[i].v;
    ansv+=a[i].v;
    }
    int p=1; // p指向当前区间最左端(甜度最小)
    // 如果当前混合甜度小于目标t:说明低甜原料太多,要删掉最左边低甜原料
    while (anss/ansv<t){
    // 如果把左端这个原料完整删掉之后,甜度仍然<=t,可以直接完整删除
    if ((anss-a[p].s*a[p].v)/(ansv-a[p].v)<=t){
    anss-=a[p].s*a[p].v;
    ansv-=a[p].v;
    p++;
    }else{
    // 不能完整删掉,只删除左端原料的一部分,刚好让甜度等于t,算出答案直接输出
    ansv-=(ansv*t-anss)/(t-a[p].s);
    cout<<fixed<<setprecision(3)<<ansv;
    return 0;
    }
    }
    p=n; // p指向当前区间最右端(甜度最大)
    // 如果当前混合甜度大于目标t:说明高甜原料太多,要删掉最右边高甜原料
    while (ansv>0&&anss/ansv>t){
    // 如果把右端这个原料完整删掉之后,甜度仍然>=t,可以直接完整删除
    if ((anss-a[p].s*a[p].v)/(ansv-a[p].v)>=t){
    anss-=a[p].s*a[p].v;
    ansv-=a[p].v;
    p–;
    } else {
    // 不能完整删掉,只删除右端原料的一部分,刚好让甜度等于t,算出答案直接输出
    ansv-=(ansv*t-anss)/(t-a[p].s);
    cout<<fixed<<setprecision (3)<<ansv;
    return 0;
    }
    }
    // 循环结束代表当前区间甜度恰好等于t,直接输出总体积
    cout<<fixed<<setprecision (3)<<ansv;
    return 0;
    }


    解法二(前缀+后缀扫描版本)

    题解

    同样是这道饮品调制题,原料依旧按糖分从小到大排序。
    核心性质:最优解一定是排序后的一段连续原料。
    我换了一种扫描思路:

  • 排序+特判,和解法一完全一致。
  • 前缀扫描(从左往右,不断加入甜度越来越高的原料)
    逐个把原料从左向右加入,累加总糖分、总体积。

    • 如果加入当前原料之后,混合甜度超过t:这份原料不能全部拿,只取一部分,恰好让甜度等于t,输出答案结束。
    • 如果全部前缀加完甜度刚好等于t,直接输出总体积。
  • 如果前缀全部加完甜度仍然小于t,说明我们需要从右端开始取连续区间,做后缀扫描(从右往左,不断加入甜度越来越低的原料)
    逐个把原料从右向左加入,累加总糖分、总体积。

    • 如果加入当前原料之后,混合甜度小于t:这份原料不能全部拿,只取一部分,恰好让甜度等于t,输出答案结束。
  • 原理:

    • 从左往右取连续一段:左边甜度低,右边甜度高。不断向右扩展,甜度单调上升。一旦超过目标t,答案就在这个位置。
    • 如果全部前缀甜度还不够,说明我们要选取靠右侧的连续区间。从最右端向左扩展,甜度单调下降。一旦低于目标t,答案就在这个位置。

    这个写法利用了排序后连续区间的甜度单调性,不需要双端循环剔除,只用两次单向扫描就可以求出最大体积。

    带完整注释代码

    #include <bits/stdc++.h>
    using namespace std;
    int n,t;
    // 原料结构体:v存量体积,s每升糖分
    struct node{
    int v,s;
    };
    node a[2005];
    double anss=0,ansv=0; // anss总糖分,ansv总体积
    // 比较函数:按糖分s从小到大排序
    bool cmp(node x,node y){
    return x.s<y.s;
    }
    int main() {
    cin>>n>>t;
    // 读入所有原料
    for(int i=1;i<=n;i++){
    cin>>a[i].v>>a[i].s;
    }
    sort(a+1,a+n+1,cmp);
    // 边界判断:全部原料甜度大于t,或者全部小于t,无法调制
    if (a[1].s>t||a[n].s<t){
    cout<<"0.000";
    return 0;
    }
    // 从前向后依次累加原料(前缀),不断加入甜度越来越大的原料
    for(int i=1;i<=n;i++){
    anss+=a[i].s*a[i].v;
    ansv+=a[i].v;
    // 如果加入当前原料之后,混合甜度超过t
    if (anss/ansv>t){
    // 当前原料不能全部取,只取一部分,让甜度等于t,算出答案输出
    ansv-=(ansv*t-anss)/(t-a[i].s);
    cout<<fixed<<setprecision(3)<<ansv;
    return 0;
    }
    }
    // 前缀全部加完甜度刚好等于t,直接输出答案
    if (anss/ansv==t){
    cout<<fixed<<setprecision(3)<<ansv;
    return 0;
    }
    // 前缀全部加完甜度仍然小于t。清空变量,从后往前累加(后缀),加入甜度越来越小的原料
    anss=0,ansv=0;
    for(int i=n;i>=1;i–){
    anss+=a[i].s*a[i].v;
    ansv+=a[i].v;
    // 如果加入当前原料之后,混合甜度小于t
    if (anss/ansv<t){
    // 当前原料不能全部取,只取一部分,让甜度等于t,算出答案输出
    ansv-=(ansv*t-anss)/(t-a[i].s);
    cout<<fixed<<setprecision(3)<<ansv;
    return 0;
    }
    }
    return 0;
    }


    两个解法对比小结

  • 解法一:先全选,两端剔除。适合直观理解,先拿全部原料,再一点点删掉不合适的两端原料。
  • 解法二:前缀+后缀扫描。利用区间甜度单调性,从小到大扩展区间,一旦跨过目标甜度就截断,代码逻辑也很简洁。
    两个算法本质都利用同一个结论:最优方案一定是排序后的一段连续原料,最多对区间端点取部分体积,都可以AC本题。
  • 赞(0)
    未经允许不得转载:171主机测评 » CCF-GESP计算机学会等级考试2026年9月五级C++T2 饮品调制
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址