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)$

    代码实现

    其实代码能力好的看完上面完全可以写出来了,这里只说一些细节:

  1. 子集枚举:可以用 $DFS$,也可以用深基的。

但 $DFS$ 太$\tiny lan$难$\tiny de$调$\tiny xie$,所以我用的是深基的版本。

  1. 二分查找:这里找的是小于等于,很明显两个$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;
}
posted @ 2021-08-14 15:59  5k_sync_closer  阅读(16)  评论(0编辑  收藏  举报  来源