浅谈折半搜索
折半搜索(又称meet in the middle),顾名思义,就是将原有的数据分成两部分分别进行搜索,最后在中间合并的算法。
设对 的大小进行搜索所需要的时间复杂度为 ,合并时间复杂度为 ,那么折半搜索所需要的时间复杂度就为 。
一般来说搜索的时间复杂度是指数级别的,而合并的时间复杂度通常不会太高,因此进行折半搜索基本上能让我们通过比暴力算法将近大一倍的数据范围。
下面通过两道经典的题目来对折半搜索做一个简单的讲解。
1. luoguP4799 [CEOI2015 Day2]世界冰球锦标赛#
简明题意:
一个人有 元钱。有 场比赛,每场比赛的门票价格为 。
问这个人有多少种看比赛的方案(一场不看也算做一种,两种方案不同当且仅当两种方案所看的比赛中有至少一场不同)
数据范围:
的范围过大,考虑搜索。但是枚举每场比赛看或者不看的方案数高达 ,显然不能通过本题。
于是我们就要使用折半搜索的思想。将比赛分为 和 两部分,分别算出两部分的所有可能的花费的钱数。
这时情况总数只不过有 种,存储起来简直是绰绰有余。
然后将 部分的比赛进行排序,然后每次取出 部分的一个比赛,进行二分查找统计可行的方案即可。(也可以双指针)
code:
#define int long long//不开long long见祖宗
int n,m,ans,price[100];//ans是总的方案数
int ansl,ansr,l[1<<21],r[1<<21];//ansl是l部分的方案总数,ansr是r部分的方案总数,l和r分别存储两部分的所有方案
void ldfs(int ll,int rr,int now)
{
if(now>m)return;
if(ll>rr)//注意判定方法
{
l[++ansl]=now;//增添一种新方案
return;
}
ldfs(ll+1,rr,now+price[ll]);//看ll这场比赛
ldfs(ll+1,rr,now);//不看
}
void rdfs(int ll,int rr,int now)//同上
{
if(now>m)return;
if(ll>rr)
{
r[++ansr]=now;
return;
}
rdfs(ll+1,rr,now+price[ll]);
rdfs(ll+1,rr,now);
}
signed main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)scanf("%lld",&price[i]);
ldfs(1,n/2,0);rdfs(n/2+1,n,0);//对两部分分别进行搜索
sort(l+1,l+ansl+1);//对l数组进行排序,方便后续的合并操作
for(int i=1;i<=ansr;i++)ans+=upper_bound(l+1,l+ansl+1,m-r[i])-l-1;//显然,如果两部分价钱的和不超过m,那就有了一种总的方案
printf("%lld\n",ans);
return 0;
}
2. luoguP3067 [USACO12OPEN]Balanced Cow Subsets G#
简明题意:求从 个数 () 任意选出一些数,使这些数可以分为和相等的两部分的方案数。
数据范围:。
这道题比刚刚的题目更加复杂了一些,每个数有三种状态,可以不选,可以放到左边集合,也可以放到右边集合。
,依然爆炸。于是同样考虑折半搜索。
可以做一个转化, 对于每个数 , 它对当前集合的贡献可为 , 分别代表不选,选到当前集合,选到对面集合. 这样我们就把一组合法的情况转化为了两边的和为 .
但是,这道题毒瘤的地方在于,可能会有重复的情况。
出现这种重复情况的原因在于这道题选出的数是没有顺序的,比如下面的数据:
4
1 1 1 1
我们用 来表示将元素放到左集合,用 来表示将元素放到右集合.
以四个数全选的情况为例. 会有下面的情况:
(中间的.
表示我们是分别对左右两部分进行搜索的)
实际上这就是 , 这显然不是除以一个 就能解决的了的。
但是我们可以直接保存每一种情况的具体选法。
因为 很小, 我们使用状态压缩即可.
code:
#define int long long
using namespace std;
int n,ans,lastpos,m[30],lcnt,rcnt,lp,rp;
bool book[1<<21];
struct node{int now,sum;}l[1<<21],r[1<<21];
bool lcmp(node xx,node yy){return xx.sum<yy.sum;}
bool rcmp(node xx,node yy){return xx.sum>yy.sum;}
void ldfs(int ll,int rr,int now,int sum)
{
if(ll>rr)
{
lcnt++;
l[lcnt].now=now;
l[lcnt].sum=sum;
return;
}
ldfs(ll+1,rr,now,sum);
ldfs(ll+1,rr,now+(1<<(ll-1)),sum+m[ll]);
ldfs(ll+1,rr,now+(1<<(ll-1)),sum-m[ll]);
}
void rdfs(int ll,int rr,int now,int sum)
{
if(ll>rr)
{
rcnt++;
r[rcnt].now=now;
r[rcnt].sum=sum;
return;
}
rdfs(ll+1,rr,now,sum);
rdfs(ll+1,rr,now+(1<<(ll-1)),sum+m[ll]);
rdfs(ll+1,rr,now+(1<<(ll-1)),sum-m[ll]);
}
signed main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++)scanf("%lld",&m[i]);
ldfs(1,n/2,0,0);rdfs(n/2+1,n,0,0);
//容易发现我们可以通过排序+双指针来进行较快速的枚举
sort(l+1,l+lcnt+1,lcmp);sort(r+1,r+rcnt+1,rcmp);
lp=rp=1;
while(lp<=lcnt&&rp<=rcnt)
{
while(l[lp].sum+r[rp].sum>0&&rp<=rcnt)rp++;
lastpos=rp;
while(l[lp].sum+r[rp].sum==0&&rp<=rcnt)
{
if(book[l[lp].now|r[rp].now]==0)
{
book[l[lp].now|r[rp].now]=1;
ans++;
}
rp++;
}
if(l[lp].sum==l[lp+1].sum)rp=lastpos;//注意, 如果有sum一样的还要再重新扫一遍
//这样如果遇到一车相同的好像会比较慢, 不过我对这玩意的去重也没什么好想法==
lp++;
}
printf("%lld\n",ans-1);//记得减掉啥都不选的
return 0;
}
练习题:
CF888E Maximum Subsequence
CF1006F Xor-Paths
CF525E Anya and Cubes
作者:pjykk
出处:https://www.cnblogs.com/pjykk/p/15369832.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现