双向搜索(美其名曰"meet in the middle").
意思其实还是挺好理解的:算法同时运行两个搜索,一个从初始状态正向搜索,另一个从目标状态反向搜索,当两者在中间汇合时搜索停止.
可能直接结合题目会比较好解释.
[CEOI2015 Day2] 世界冰球锦标赛
题意:N场比赛,每场比赛有一个票价,现有M元钱,求有多少种不同的观赛方案?\((N<=40\),\(M<=10^{18})\)
分析:如果直接暴搜,枚举每场比赛看或者不看,显然,时间复杂度是\(2^{40}\),肯定会超时.
考虑如何优化?我们可以先只搜索枚举前\(N/2\)场比赛看或者不看,时间复杂度是\(2^{N/2}\),最多也就\(2^{20}\),可以接受,然后把这些枚举出来的方案的花费存入一个数组中.
像上面那样,再搜索枚举后\(N/2\)场比赛看或者不看,将花费存入另一个数组中.
对第二个数组sort排序,然后遍历第一个数组,对于每一个第一个数组中的元素(方案)在第二个数组中二分(因为此时第二个数组是具有单调性的)找到使总花费合法的最大下标,下标值就是对于第一个数组中该方案下在第二个数组中能匹配到的所有方案,所以直接把下标值累加到ans中.
(不知道这里我有没有解释得很明白)
ll n,m,ans,mid,num1,num2;
ll price[41],l[10000005],r[10000005];
//l,r数组尽量开大点,你也不知道它有多少方案
inline void dfs1(ll num,ll tot){
//num表示当前枚举到了第几场比赛
//tot表示当前的花费
if(num==mid+1){
l[++num1]=tot;
return;
}
//num1是方案数,每个方案的花费存入l数组中
dfs1(num+1,tot);
//对于第num场比赛,我可以选择不看
if(tot+price[num]<=m)
dfs1(num+1,tot+price[num]);
//花费可以接受的话,我可以选择看第num场比赛
}
inline void dfs2(ll num,ll tot){
if(num==n+1){
r[++num2]=tot;
return;
}
dfs2(num+1,tot);
if(tot+price[num]<=m)
dfs2(num+1,tot+price[num]);
}
inline ll solve(ll money){
int x=1,y=num2,mid;
while(x<=y){
mid=(x+y)>>1;
if(money<r[mid])y=mid-1;
else x=mid+1;
}
return y;
}//在第二个数组r中二分,找到最大的能匹配到的下标
int main(){
n=read();m=read();
mid=n/2;
for(ll i=1;i<=n;i++)
price[i]=read();
dfs1(1,0);dfs2(mid+1,0);
//分别对前N/2和后N/2场比赛爆搜,枚举所有方案
sort(r+1,r+num2+1);
for(ll i=1;i<=num1;i++){
ans+=solve(m-l[i]);
}
cout<<ans<<endl;
return 0;
}
通过双向搜索,我们使得原本超时的\(2^{40}\)优化为可以接受的\(2^{20}\).另外,我想补充说明一下这个mid.
本题中我们是将N场比赛分成了1~mid场和mid+1~N场,时间复杂度是\(2^{max(mid,N-mid)}\),所以说分得最均匀时,效率最高.
假设N=40,mid分得均匀时,时间复杂度是\(2^{20}\);而你分得时候一下子没有想好,可能分成了19和21的局面,此时时间复杂度是\(2^{21}\),在本题中仍然是可以接受的(亲测:前者用时3144ms,后者用时4871ms),但这种情况如果出现在下一题,你就没有那么幸运了.
NOI 2001 方程的解数(没找到题目链接)
\(k_1x_1^{p_1}+k_2x_2^{p_2}+...+k_nx_n^{p_n}=0\)
方程中的所有数均为整数.
设未知数\(1≤x_i≤M\),\(i=1...n\),求方程的整数解的个数.
\((1<=n<=6,1<=M<=150)\)
以下假设n取最大值6来讨论:
如果按照题意,直接枚举每个未知数的取值,时间复杂度\(150^6\),肯定超时.
考虑如何优化?先将前3个未知数的解搜索枚举一遍,得到方程前半部分可能的取值.
(这里不是未知数x的取值,而是\(k_1x_1^{p_1}+k_2x_2^{p_2}+k_3x_3^{p_3}\)的和的取值,下面同理).此时时间复杂度\(150^3\),是可以接受的.把这些值取负存入一个数组中.
再像上面那样,对后三个未知数搜索枚举一遍,将取值(不用取负)存入另一个数组中.
将两个数组都sort排序,然后遍历第一个数组,对于第一个数组中每一个元素(方程前半部分的取值),在第二个数组中找到一个与之相等的值,这两个就构成了一个合法方案.
(本来方程前半部分的值加上后半部分的值应该满足相加等于零,但由于我们对于前半部分的取值是取负存入数组中的,所以就变成了找相等.)
就按照上面这种方法,结合听上去很高大上的尺取法和乘法原理,就能得到所有的方案数了.
int n,m,mid,num1,num2,ans;
int k[7],p[7],l[5000005],r[5000005];
void dfs1(int num,int tot){
if(num==mid+1){
l[++num1]=-tot;
return;
}
for(int i=1;i<=m;i++)
dfs1(num+1,tot+k[num]*pow(i,p[num]));
}
//搜索不用解释吧,跟上题差不多
//只是上题对于一场比赛可以不看,本题对于一个x必须取值
//所以就少了一种搜索情况.
void dfs2(int num,int tot){
if(num==n+1){
r[++num2]=tot;
return;
}
for(int i=1;i<=m;i++)
dfs2(num+1,tot+k[num]*pow(i,p[num]));
}
int main(){
n=read();m=read();
mid=n/2;
for(int i=1;i<=n;i++){
k[i]=read();
p[i]=read();
}
dfs1(1,0);dfs2(mid+1,0);
sort(l+1,l+num1+1);
sort(r+1,r+num2+1);
for(int i=1,j=1;i<=num1;i++){
if(l[i]<r[j])continue;
//因为两个数组都是单调递增的
//此时l[i]比r[j]小,所以无论j指针怎么跳
//也不会找到使得l[i]与r[j]相等的j
//(注意这里的i,j都只能++)
while(r[j]<l[i]&&j<=num2)j++;
if(j>num2)break;
//如果j直接跳出去了,说明当前的l[i]比r数组中
//所有的元素都要大,那么根据单调性,后面的l[i]会更大
//说明已经没有合法的l[i]=r[j]的方案了,直接退出去.
if(l[i]==r[j]){
int cnt=l[i],x=0,y=0;
while(l[i]==cnt&&i<=num1)
i++,x++;
while(r[j]==cnt&&j<=num2)
j++,y++;
//上面这样搞几个指针跳来跳去就是所谓尺取法
ans+=x*y;
//这里就是所谓乘法原理求方案数
i--;
//这里要i--是因为上面尺取法的时候,i已经跳到了一个
//使l[i]不同的值,等下for循环又有i++,就会导致
//跳过一个l[i]没有扫到,所以这里一定要i--;
}
}
printf("%d\n",ans);
return 0;
}
回过头谈谈本题的时间复杂度,前面在这里设置了一个小悬念.本题中mid分得均匀的话,就是6=3+3,则时间复杂度就是\(150^3\);而没分好mid,6=2+4,则时间复杂度\(150^4\),妥妥超时.在这里卡了半个小时