P4799透彻解析
这篇文章仅适用于想要切一道水紫的同学。(神犇退散)
那么听了课的都知道,这个题是一个仅有蓝题难度的双向搜水紫。
究竟是怎么搜的呢?一起来和小编看看吧
基本思路
思路0
直接枚举每一个子集,再判断是否合法。
浅显易懂,但复杂度$O(2^n)$,显然不行。
思路1
把东西分两半,分别枚举子集,在出来的两堆子集里分别挑一个。
这个看上去是优化了,但复杂度仍是$O((2^{n/2})^2)=O(2^n)$,没有变化。
思路2
还是把东西分两半,分别枚举子集,但这次只在一堆子集里挑。
每挑出一个,就去另一堆里,找有多少子集能和这个子集配对。
乍一看,$O(2^{n/2}×2^{n/2})$(第一个$2^{n/2}$是枚举子集,第二个$2^{n/2}$是找配对),没有优化。
但是配对的过程是可以优化的。
下面把找配对的那一堆称为$a$,挑子集的一堆称为$b$。
现在我们需要完成这个任务:
给定数列 $a$ 和两个数 $m,x$,求存在多少个 $i$,使得 $a_i+x≤m$
这里只需要枚举 $b$,让 $b_i=x$ 即可,关键是如何解决这个问题。
没思路?没事,我们把问题转化成 $a_i≤m-x$。
给定数列 $a$ 和一个数 $p$,求 $a$ 中有多少个数 $≤p$。
(这里 $p=m-x$)。看到这里相信大家都有答案了:
先把 $a$ 排一遍序,在 $a$ 中二分查找 $p$,下标位置就是答案!
最后,复杂度:
- 排序:$O(2^{n/2}log(2^{n/2}))=O(2^{n/2}n)$
- 二分:$O(log(2^{n/2}))=O(n)$
- 二分套枚举 $b$:$O(2^{n/2}n)$
- 所以总复杂度:$O(2^{n/2}n)$
代码实现
其实代码能力好的看完上面完全可以写出来了,这里只说一些细节:
- 子集枚举:可以用 $DFS$,也可以用深基的。
但 $DFS$ 太$\tiny lan$难$\tiny de$调$\tiny xie$,所以我用的是深基的版本。
- 二分查找:这里找的是小于等于,很明显两个$STL$ 都无法直接解决。
但这里稍微思考一下,就可以想到,$upper\_bound$结果-1就是小于等于了。
如果$upper\_bound$查到第 $pos$ 位,那么 $0……pos-1$ 都是合法的。
那么 $0……pos-1$ 有几位呢? $pos$ 位。
所以,$upper\_bound$查到第几位,就是有几个数符合要求。
那么代码就出来了:
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
int n, m, f[101], mid, ans, a[3000001], b[3000001], ca, cb;
void subset(int l, int r, int v[], int &cnt)
{
int len = r - l, u = 1 << len, t;
for(int s = 0;s < u;++s)
{
t = 0;
for(int i = 0;i < len;++i)
if(s & (1 << i)) t += f[i + l];
v[cnt++] = t;
}
}
signed main()
{
cin >> n >> m;mid = n >> 1;
for(int i = 0;i < n;++i)
cin >> f[i];
subset(0, mid, a, ca);
subset(mid, n, b, cb);
sort(a, a + ca);
for(int i = 0;i < cb;++i)
ans += upper_bound(a, a + ca, m - b[i]) - a;
cout << ans;
return 0;
}