折半搜索 meet in the middle
折半搜索
引入:
一般来说,很多元素少的题都可以用暴力搜索直接写。
比如说枚举每个点的使用情况...每个物品的使用....这类问题,时间复杂度为 \(O(2^n)\)
但是,当给定的 \(n\) 变大时,我们应该怎么办,这就引出了折半搜索( \(meet in the middle\) ).
定义:
主要思想是把整个搜索过程分为两半,分别搜索,最后将两半的结果合并。
这样可以把时间复杂度降到 \(O(n2^{n/2})\) 水平。
使用:
一般来说,就是将前一半搜索后的条件和情况,分组存到 \(map,vector,set\) 等存储器中
进行后一半搜索时,判断其中有没有条件和情况与之相匹配,进行记录。
可以通过队列,递归解决搜索时的问题,一定要灵活运用
例题:
这道题看数据范围,典型的卡暴力搜索时间,因此使用折半搜索。
通过二进制存储点的状态,开 \(long long\) ,然后记录操作情况避免重复即可。
// P2962 [USACO09NOV]Lights G
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pll pair<ll,ll>
const int N=605;
vector<int> e[N];
map<ll,int> maps[2];
int val[N],mul[N];
int n,m;
inline ll change(ll ch,int x){
if((ch>>x)&1) return ch-((ll)1<<x);
return ch+((ll)1<<x);
}
queue<pll >q;
int solve(){
ll now=((ll)1<<n)-1;//完美情况,所有都变成1
q.push({0,0});
while(!q.empty()){
pll p=q.front(); q.pop();
int k=maps[0][p.first];
for(int i=0;i<n/2;i++){
if((p.second>>i)&1) continue;//当前已经亮过,避免重复开关
pll pp=p;
pp.second+=((ll)1<<i); pp.first=change(pp.first,i);//将此处开灯后的变化
for(auto y:e[i]) pp.first=change(pp.first,y);//连接的边改变
if(maps[0].count(pp.first)==0){//没有这种亮灯情况
maps[0][pp.first]=k+1;//存储操作次数
q.push(pp);//将这种亮灯情况继续扩展
}
}
}
q.push({0,0});
while(!q.empty()){
pll p=q.front(); q.pop();
int k=maps[1][p.first];
for(int i=n/2;i<n;i++){
if((p.second>>i)&1) continue;
pll pp=p;
pp.second+=((ll)1<<i); pp.first=change(pp.first,i);
for(auto y:e[i]) pp.first=change(pp.first,y);
if(maps[0].count(pp.first^now)) return maps[0][pp.first^now]+k+1;//0中有匹配的情况
if(maps[1].count(pp.first)==0) maps[1][pp.first]=k+1,q.push(pp);//不行,也存储
}
}
return -1;
}
int main(){
cin>>n>>m;
mul[0]=1; for(int i=1;i<n;i++) mul[i]=mul[i-1]*2;
for(int i=1,x,y;i<=m;i++){
scanf("%d%d",&x,&y); x--,y--;
e[x].push_back(y);e[y].push_back(x);
}
printf("%d\n",solve());
system("pause");
return 0;
}
依旧是熟悉的数据范围。
我们将前一半的情况存到 \(vector[1]\) 中,进行从小到大排序.
将后一半 \(vector[2]\) 中的元素与之匹配,两者之和小于 \(m\) 即可,这里可以用 \(upperbound\) 轻松解决。
// P4799 [CEOI2015 Day2]世界冰球锦标赛
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=50;
int n,m;
int val[N],ans,len;
vector<int> a,b;
void dfs(int st,int en,int sum,vector<int> &now){
if(sum>m) return;
if(st>en){
now.push_back(sum); return;
}
dfs(st+1,en,sum+val[st],now); dfs(st+1,en,sum,now);
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++) scanf("%lld",&val[i]);
dfs(1,n/2,0,a); dfs(n/2+1,n,0,b);
sort(a.begin(),a.end());
len=b.size();
for(int i=0;i<len;i++)
ans+=upper_bound(a.begin(),a.end(),m-b[i])-a.begin();
//搜索在 A 中的元素有多少可以和 B 的情况匹配,这里是返回小于等于的下标,因此可以看成元素的个数
cout<<ans<<endl;
system("pause");
return 0;
}
[USACO12OPEN]Balanced Cow Subsets G
虽然看着不是折半搜索,但是确实是折半搜索。
我们搜索前一半,记录加和为正数/负数情况(因为后一半可以进行补充)
这样就避免了只从前一半/后一半选取数字选不到的情况.
//P3067 [USACO12OPEN]Balanced Cow Subsets G
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e6+7;
int n,val[25],vis[N],ans,mid,cnt;
vector<int> d[N];
map<int,int> M;
void dfs1(int i,int sum,int now){
if(i>mid){
if(!M.count(sum)) M[sum]=++cnt;//map离散化编号
d[M[sum]].push_back(now);//当前状态可以拼出来sum的和
return;
}
dfs1(i+1,sum,now);
dfs1(i+1,sum+val[i],now|(1<<(i-1)));
dfs1(i+1,sum-val[i],now|(1<<(i-1)));//这里可以看成插了负数
}
void dfs2(int i,int sum,int now){
if(i>n){
if(M.count(sum)){
int x=M[sum];
for(auto y:d[x]) vis[y|now]=1;
}
return;
}
dfs2(i+1,sum,now);
dfs2(i+1,sum+val[i],now|(1<<(i-1)));
dfs2(i+1,sum-val[i],now|(1<<(i-1)));
}
signed main(){
cin>>n; mid=n/2;
for(int i=1;i<=n;i++) cin>>val[i];
dfs1(1,0,0);
dfs2(mid+1,0,0);
for(int i=1;i<=(1<<n);i++) ans+=vis[i];
cout<<ans<<endl;
system("pause");
return 0;
}
不关注的有难了😠😠😠https://b23.tv/hoXKV9