一、引入
在之前的学习中,我们已经学习了很多种搜索方法。让我们先看一道题:
题目描述
今年的世界冰球锦标赛在捷克举行。Bobek 已经抵达布拉格,他不是任何团队的粉丝,也没有时间观念。他只是单纯的想去看几场比赛。如果他有足够的钱,他会去看所有的比赛。不幸的是,他的财产十分有限,他决定把所有财产都用来买门票。
给出 Bobek 的预算和每场比赛的票价,试求:如果总票价不超过预算,他有多少种观赛方案。如果存在以其中一种方案观看某场比赛而另一种方案不观看,则认为这两种方案不同。
输入格式
第一行,两个正整数
N
N
N 和
M
(
1
≤
N
≤
40
,
1
≤
M
≤
1
0
18
)
M(1 \leq N \leq 40,1 \leq M \leq 10^{18})
M(1≤N≤40,1≤M≤1018),表示比赛的个数和 Bobek 那家徒四壁的财产。
第二行,
N
N
N 个以空格分隔的正整数,均不超过
1
0
16
10^{16}
1016,代表每场比赛门票的价格。
输出格式
输出一行,表示方案的个数。由于 N N N 十分大,注意:答案 ≤ 2 40 \le 2^{40} ≤240。
首先,尝试简单的深搜,其代码如下:
#include<bits/stdc++.h>
#define ll long long
#define bug printf("---OK---")
#define pa printf("A: ")
#define pr printf("\n")
#define pi acos(-1.0)
#define INF 0x3f3f3f3f3f3f3f3f
using namespace std;
ll n,m,a[42];
ll cnt;
void dfs(ll k,ll sum){
if(sum>m){
return;
}
if(k==n+1){
cnt++;
return;
}
dfs(k+1,sum+a[k]);
dfs(k+1,sum);
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
dfs(1,0);
cout<<cnt;
return 0;
}
但是……
这时,就需要另外一种强大的方法——折半搜索。
二、介绍
1 概念
折半搜索主要思想是将整个搜索过程分成两半,分别搜索,最后将两半的结果合并。这样,原本需要进行的指数级别的搜索,其复杂度的指数可以减半,即让复杂度从 O ( 2 n ) O(2^n) O(2n) 降到 O ( 2 n / 2 ) O(2^{n/2}) O(2n/2)。
2 回到题目
2.1 思路
我们可以将所有的比赛分为两部分,然后分别对这两部分进行搜索,得到两个答案序列,再将这两个答案序列进行合并,即可得到最终的答案。
2.2 举例
假设我们有 4 场比赛,票价分别为 1, 2, 3, 4 元,Bobek 有 5 元钱。
我们可以将比赛分为两部分,第一部分包含前两场比赛,第二部分包含后两场比赛。然后我们分别对这两部分进行搜索,得到两个答案序列:
- 第一部分的答案序列(数组a)为:0, 1, 2, 3(表示不买票,只买第一场的票,只买第二场的票,两场都买)
- 第二部分的答案序列(数组b)为:0, 3, 4, 7
然后我们将这两个答案序列进行合并,得到所有可能的观赛方案的总票价:0, 1, 2, 3, 3, 4, 5, 6, 4, 5, 6, 7, 7, 8, 9, 10。
最后,我们只需要统计总票价不超过 5 元的观赛方案的数量,即可得到最终的答案。
在实际操作中,我们可以在第二遍dfs时,使用二分直接累加结果。
2.3 代码
#include<bits/stdc++.h>
#define ll long long
#define bug printf("---OK---")
#define pa printf("A: ")
#define pr printf("\n")
#define pi acos(-1.0)
#define INF 0x3f3f3f3f3f3f3f3f
using namespace std;
ll n,m,a[42],b[1500002];
ll ans,cnt;
void dfs(ll k,ll lim,ll sum,ll flag){
if(sum>m){
return;
}
if(k>lim){
if(flag==0){
b[++cnt]=sum;
}
else{
ll k=upper_bound(b+1,b+1+cnt,m-sum)-b;
ans+=k-1;
}
return;
}
dfs(k+1,lim,sum,flag);
dfs(k+1,lim,sum+a[k],flag);
}
int main(){
cin>>n>>m;
int i,j;
for(i=1;i<=n;i++){
cin>>a[i];
}
dfs(1,n/2,0,0);
sort(b+1,b+1+cnt);
dfs(n/2+1,n,0,1);
cout<<ans;
return 0;
}
三、实践
1 XOR
1.1 题目描述
给出一个 n × m 的网格,每个格子上有权值 a[i][j],现在zhqwq要从 (1,1) 走到 (n,m)去抱走wyz,每次只能向右或向下走,
沿路计算异或和,求异或和等于 k 的路径数。
1.2 思路
开一个
m
a
p
map
map ,其中,
m
p
[
i
]
[
j
]
mp[i][j]
mp[i][j] 表示坐标,其映射值为走到此处的步数。
采用折半搜索。第一遍dfs记录方案,第二遍累计答案
m
p
[
x
]
[
y
]
[
s
u
m
⊕
k
⊕
a
[
x
]
[
y
]
]
mp[x][y][sum\oplus k\oplus a[x][y]]
mp[x][y][sum⊕k⊕a[x][y]] 。
1.3 代码
#include<bits/stdc++.h>
#define ll long long
#define bug printf("---OK---")
#define pa printf("A: ")
#define pr printf("\n")
#define pi acos(-1.0)
#define INF 0x3f3f3f3f3f3f3f3f
using namespace std;
ll n,m,k,a[22][22];
map<ll,ll> mp[22][22];
ll ans,cnt;
void dfs1(ll x,ll y,ll sum){
if(x>n||y>m){
return;
}
if(x+y==(n+m)/2+1){
++mp[x][y][sum];
return;
}
dfs1(x+1,y,a[x+1][y]^sum);
dfs1(x,y+1,a[x][y+1]^sum);
}
void dfs2(ll x,ll y,ll sum){
if(x<1||y<1){
return;
}
if(x+y==(n+m)/2+1){
ans+=mp[x][y][sum^k^a[x][y]];
return;
}
dfs2(x-1,y,a[x-1][y]^sum);
dfs2(x,y-1,a[x][y-1]^sum);
}
int main(){
cin>>n>>m>>k;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
}
}
dfs1(1,1,a[1][1]);
dfs2(n,m,a[n][m]);
cout<<ans;
return 0;
}