双向搜索(美其名曰"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场比赛分成了1mid场和mid+1N场,时间复杂度是(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
",ans);
return 0;
}
回过头谈谈本题的时间复杂度,前面在这里设置了一个小悬念.本题中mid分得均匀的话,就是6=3+3,则时间复杂度就是(150^3);而没分好mid,6=2+4,则时间复杂度(150^4),妥妥超时.在这里卡了半个小时
TC-572-D1L2 (没找到题目链接)
有一个神秘的常数K,它是s位数,现还有n个s位数,且已知每个数有多少位与K是相同的.请判断K的无解,多解或唯一解,无解输出Liar,多解输出Ambiguity,唯一解则输出唯一解.
注意:所有出现的数都允许有前导零.
(s<=9,n<=50)
以下讨论假设s取最大值9.
分析:如果我们暴力枚举K的每一位,根据K最多有9位数,每一位上是0~9这10个数字中任意一个,则时间复杂度是(10^9),超时!!!
考虑如何优化?
不妨先枚举K的前4位,时间复杂度(10^4),可以接受.然后将枚举出来的所有结果 与给出的n个数中的每一个数的前4位比较,得到这些枚举出来的结果和每一个数在前4位中分别有几位是相同的,可以先存入一个数组中,最后再将这个数组hash存到map中去.
像上面那样,又继续枚举k的后5位,时间复杂度(10^5).将这一次枚举出来的所有结果 与给出的n个数中的每一个数的后5位比较,得到这些枚举出来的结果和每一个数在后5位中分别有几位是相同的,把这一次的结果与标准值作差,存入一个数组中,又将数组hash,看map中是否有一样的哈希值存在.
如果存在,则表示有解(有可能是唯一解,也有可能有多个解)
此题的难点主要是在于一些细节,比如解的情况的判断,前导零的输出....直接结合代码分析了.
const int mod=1e9+7;
int n,s,mid,ans;
int same[51],a[51][10],b[51],c[10];
char ch[10];
map<int,int> hash;
int haxi(){
unsigned int cnt=17;
for(int i=1;i<=n;i++)
cnt=(cnt+b[i])*19+23;
return cnt%mod;
}
//将b数组哈希起来
//unsigned是为了防止溢出,就只int也可以(吧)!
//这些17,19,23,mod都是为了减少哈希冲突的概率
//如果只错了一两个点,就可能是rp太低,出现哈希冲突了.
int turn(int i,int j){
int cnt=0;
for(;i<=j;i++)
cnt=cnt*10+c[i];
return cnt;
}
//将枚举出来的K的第i~j位,从数组c中提出来,转变为一个十进制的整数
void dfs1(int num){//枚举k的前一部分
if(num==mid+1){
for(int i=1;i<=n;i++){
b[i]=0;//初始化,因为我们是在重复利用b数组
for(int j=1;j<=mid;j++)
b[i]+=(a[i][j]==c[j]?1:0);
//这是对比的过程,对比的结果存入b数组中.
//b[i]记录第i个数的前一部分与枚举结果相同的位的个数
if(b[i]>same[i])return;
//如果前一部分相同的位的个数比标准值还大,肯定不合法
}
//将b数组哈希,得到一个整数,用x表示
//将枚举出来的k的前一部分转变为十进制整数,用y表示
//明确一下,x的数值没有意义,它只表示枚举出来的某一个状态
int x=haxi(),y=turn(1,mid);
//hash.find(x)!=hash.end():我们在map中找x,
//如果找不到,它会自动在map的最后开辟一个新空间给x,
//所以如果x不在最后,就说明之前一定已经有x存在了.
//如果之前x这个状态已经搜到过,这次又搜到,标记为-1;
//否则如果是第一次搜到这个状态就赋值为y.
if(hash.find(x)!=hash.end())
hash[x]=-1;
else hash[x]=y;
return;//亲测return不加会RE
}
for(int i=0;i<=9;i++)
c[num]=i,dfs1(num+1);
//num表示第几位,结果先存入c数组中
//c[j]表示K的第j位是什么数字
//一定是0~9这10个数字都要枚举,因为K可以有前导零
}
void dfs2(int num){
if(num==s+1){
for(int i=1;i<=n;i++){
b[i]=0;
for(int j=mid+1;j<=s;j++)
b[i]+=(a[i][j]==c[j]?1:0);
if(b[i]>same[i])return;
b[i]=same[i]-b[i];//与标准值作差
}
int x=haxi(),y=turn(mid+1,s);
if(hash.find(x)!=hash.end()){
int cnt=hash[x];
if(cnt==-1||ans){
puts("Ambiguity");
exit(0);
}
//x状态的map值是-1,说明之前在dfs1时(即前半部分),
//它至少两次被搜到,进而说明一个后半部分会对应至少
//两个前半部分,显然就是存在多解了.
//我们看到ans只有可能在下面那行代码中被修改,
//如果ans值不为0,说明之前,后半部分已经有一个状态
//与前半部分对应上了,进而说明一个前半部分会对应至少
//两个后半部分,显然也是存在多解了.
else{
for(int i=mid+1;i<=s;i++)
cnt*=10;
ans=cnt+y;
//cnt是当前情况下对应的前半部分的k的值,
//y是当前情况下后半部分的k的值,
//所以这一步是在将两个部分合并为一个十进制整数,
//即得到了最终要输出的完整的k.
}
}
return;
}
for(int i=0;i<=9;i++)
c[num]=i,dfs2(num+1);
}
int main(){
n=read();s=read();//n个s位的数
mid=s/2;
for(int i=1;i<=n;i++){
cin>>ch;same[i]=read();
for(int j=1;j<=s;j++)
a[i][j]=ch[j-1]-'0';
}
//为了处理方便,把s位数当做一个字符串读入
//然后存入一个二维的整型数组中
//same[i]:第i个数与K相同的位的个数
//a[i][j]:第i个数的第j位是什么数
dfs1(1);dfs2(mid+1);
//分别对前后两部分爆搜
//不管是分成9=4+5,还是9=5+4,时间复杂度都一样.
if(ans){
int cnt=ans;
for(int i=1;i<=s;i++){
if(!cnt)printf("0");
cnt/=10;
}
printf("%d",ans);
//处理前导零的情况
//解释一下,假设常数K带有前导零,k=000156
//但根据上面的程序运行下来,cnt=ans=156
//所以我们要写个循环补上输出前面的零
}
else puts("Liar");//判断无解的情况
return 0;
}