[题解]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;
}
浙公网安备 33010602011771号