[题解]P9234 [蓝桥杯 2023 省 A] 买瓜

题目:

题目描述

小蓝正在一个瓜摊上买瓜。瓜摊上共有 $n$ 个瓜,每个瓜的重量为 $A_i$。小蓝刀功了得,他可以把任何瓜劈成完全等重的两份,不过每个瓜只能劈一刀。

小蓝希望买到的瓜的重量的和恰好为 $m$。

请问小蓝至少要劈多少个瓜才能买到重量恰好为 $m$ 的瓜。如果无论怎样小蓝都无法得到总重恰好为 $m$ 的瓜,请输出 $-1$。

输入格式

输入的第一行包含两个整数 $n,m$,用一个空格分隔,分别表示瓜的个数和小蓝想买到的瓜的总重量。

第二行包含 $n$ 个整数 $A_i$,相邻整数之间使用一个空格分隔,分别表示每个瓜的重量。

输出格式

输出一行包含一个整数表示答案。

输入输出样例 #1

输入 #1

3 10
1 3 13

输出 #1

2

说明/提示

【评测用例规模与约定】

对于 $20 %$ 的评测用例,$n \leq 10$;

对于 $60 %$ 的评测用例,$n \leq 20$;

对于所有评测用例,$1 \leq n \leq 30$,$1 \leq A_i \leq 10^9$,$1 \leq m \leq 10^9$。

思路:

非常好的折半搜索+剪枝题目,很考验实现和码力

由于直接/2会导致浮点数丢失精度,所以等效将目标重量m*2

劈瓜/不劈瓜也就变成了选v[i]/v[i]*2

由于这道题重量$w$和目标$k$值非常大,瓜总数$n$非常小

所以选择DFS全量搜索求解:

auto dfs=[&](auto& self,int step,ll weight,int div)->void{
    if(weight>2*m||step>n){
        return;
    }
    if(weight==2*m){
        cost=min(cost,div);
    }
    self(self,step+1,weight,div);
    self(self,step+1,weight+v[step]*2,div);
    self(self,step+1,weight+v[step],div+1);
};

然后你就可以收获半屏的TLE/MLE……

1.优化1:折半搜索的应用

全量枚举的总搜索量在$3^{30}$左右,1s限制必定超时

所以可以使用折半搜索进行优化:

将整个搜索看作左、右两半,分别搜索,其中记录左侧的重量weight与劈瓜数div

记录$weight<m*2$的所有情况并置于数组sum中,排序

右侧搜索同左侧,但在对于结果的处理上略有不同:

对于任意$weight<2m$,使用lower_bound查找重量为($2m-weight$)且劈瓜数最少的的左侧sum选项,记录并刷新答案ans

这里是标准的折半搜索应用:将一整个任务拆分为两半,再通过其他方法合并答案、查找最优解

2.代码:

vector<pair<ll,int>> sum;	//记录重量+刀数
auto dfs=[&](auto& self,int step,ll weight,int div)->void{
    if(weight>2*m){
        return;
    }
    if(step==mid){
        sum.emplace_back(weight,div);
        return;
    }
    self(self,step+1,weight,div);
    self(self,step+1,weight+v[step],div+1);
    self(self,step+1,weight+v[step]*2,div);
};
dfs(dfs,0,0,0);

sort(sum.begin(),sum.end(),cmp);
int ans=n+1;

auto right=[&](auto& self,int step,ll weight,int div)->void{
    if(weight>2*m) return ;
    if(step==n){
        //二分查找匹配的左侧值,排序默认小的劈数在前所以不用搞特判
        int left=lower_bound(sum.begin(),sum.end(),2*m-weight,cmp2)-sum.begin();
        if(left!=sum.size()&&sum[left].first+weight==2ll*m){
            ans=min(ans,div+sum[left].second);
        }
        return;
    }
    self(self,step+1,weight,div);
    self(self,step+1,weight+v[step],div+1);
    self(self,step+1,weight+v[step]*2,div);
};
right(right,mid,0,0);
cout<<(ans==n+1? -1:ans)<<'\n';

时间复杂度减了快一半,然而还是会TLE好几个点……

3.优化2:剪枝全家桶

这是本题的核心:如何大刀阔斧地剪枝,才能将时间压缩至1s内?

3.1: 抛弃pair,改为单数据压缩

vector<pair<ll,int>>单个数可以吃掉16位(long long8位,int要对齐所以也扩充至8位)

开销太贵了,直接把两个数压缩到一起:

$sum=weight*32+div$

为什么一定乘32? n的最大值为30,要压至一个数中就要保证没有重叠的数位,n最大只有五个二进制位所以*32直接可以保证两数无交叉

取weight时将$sum/32$,此时后5位截断,不会被div的值影响

取div时将$sum%32$($ & 31$更快),此时只取后五位,不会被weight影响

3.2: 后缀数组:不够加直接整枝剪除

预处理一个后缀数组suf:

由于DFS时选瓜是按照从左至右的顺序,所以对于每个瓜v[step]:

$weight+suf[step]<tar -> return$

右侧同理:$weight+suf[step]+maxleft<tar -> return$

其中maxleft是左侧的最大重量,排序后可以直接取sum[sum.size()-1]

3.3: 提前排序:改变遍历顺序

本题最玄学(?)的优化

理论上排序函数sort时间复杂度为平均$nlogn$,使用后时间只会增不会减

但它直接把数组遍历变得可控:左侧数组全为较小值,右侧数组全为最大值

原本左右侧值大小不定,两侧搜索次数趋于平均,最终耗时刚好卡在1点0几秒上

排序后搜索权重倒向左侧,左侧值小很难达tar被减去,几乎为全量搜索,而右侧的值都非常大,一般取不了几个数就会判值返回

这还变相减少了lowewr_bound的调用次数,性能瓶颈再-1

最终收益远超最初排序的消耗

4.AC代码:

#include<bits/stdc++.h>
using namespace std;
using ll=long long;

void solve(){
    ll n,m;
    cin>>n>>m;
    vector<ll> v(n),suf(n+1,0); //suf[i]:v[i]~v[n-1]的和
    for(auto& a:v) cin>>a;
    sort(v.begin(),v.end());
    for(int i=n-1;i>=0;i--){
        suf[i]=suf[i+1]+(v[i]<<1);
    }
    ll mid=n/2,tar=(m<<1);
    vector<ll> sum;    //将pair压缩为weight*32+div (因为n<=30,也就是最多切30刀,换算为2进制最近的装载为32)
    auto dfs=[&](auto& self,int step,ll weight,int div)->void{
        if(weight>tar||weight+suf[step]<tar){   //剪枝:重量已超目标/重量加上后续所有都不够
            return;
        }
        if(step==mid){
            sum.push_back((weight<<5)+div);
            return;
        }
        self(self,step+1,weight,div);
        self(self,step+1,weight+(v[step]<<1),div);
        self(self,step+1,weight+v[step],div+1);
    };
    dfs(dfs,0,0,0);
    if(sum.empty()){
        cout<<-1<<'\n';
        return;
    }
    sort(sum.begin(),sum.end());
    int ans=n+1;
    ll maxleft=(sum[sum.size()-1]>>5);
    auto right=[&](auto& self,int step,ll weight,int div)->void{
        if(weight>tar||ans<=div||weight+suf[step]+maxleft<tar) return;     //剪枝:现有重量>tar/现有的 div 已经大于等于最优答案/现有重量+后续所有+左侧最大重仍不够
        if(weight==tar){
            ans=min(ans,div);
            return;
        }
        if(step==n){
            ll ned=(tar-weight)<<5;
            auto left=lower_bound(sum.begin(),sum.end(),ned);       //lower_bound也要用压缩后的键去查找
            if(left!=sum.end()&&(*left>>5)+weight==tar){     //用/32来取出weight,%32取出div
                ans=min(ans,div+(int)(*left&31));
            }
            return;
        }
        self(self,step+1,weight,div);
        self(self,step+1,weight+(v[step]<<1),div);
        self(self,step+1,weight+v[step],div+1);
    };
    right(right,mid,0,0);
    cout<<(ans==n+1? -1:ans)<<'\n';
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int t=1;
    //cin>>t;
    while(t--){
        solve();
    }
    return 0;
}
posted @ 2026-11-11 16:04  yiliyu  阅读(3)  评论(0)    收藏  举报