折半枚举
有时候,问题的规模比较大,外面无法枚举所有元素的组合,但能枚举一半或者一部分的组合。此时,将问题拆分成两半或几部分后分别枚举,再合并他们,这一方法往往非常有效。
举个例子
例题: 给定各有n个整数的四个数列A,B,C,D。要从每个数列中个取出1个数,使四个数的和为0,求出这样组合的个数。当一个组合中有多个相同的数字时,把他们当不同的数字看待。(poj 2785)
思路: 从四个数列中选择的话共有n4种情况,所以全部枚举一遍肯定不行,不过将他们对半分成AB和CD再考虑,就可以快速的解决了。从两个数列中选择的话就只有n2种组合,可以进行枚举。从A、B中选出a,b后,为使总和为0,所以应使c+d = -a-b。因此枚举C、D中取数字,然后计算所有情况的和,将其排个序,这样就可以二分搜索了,时间复杂度是O(n2logn)。
代码模板:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 4010;
long long n,a[N],b[N],c[N],d[N],sum[N*N],ans; //sum是c和d数列所有和的数组
void solve()
{
for(int i = 0;i < n;i++)
for(int j = 0;j < n;j++)
sum[i*n+j] = c[i] + d[j]; //计算c和d数列的和
sort(sum,sum+n*n); //排序
for(int i = 0;i < n;i++)
{
for(int j = 0;j < n;j++)
{
long long cd = -(a[i]+b[j]); //先求出此时c和d的和应该是多少,记为cd
ans += upper_bound(sum,sum+n*n,cd) - lower_bound(sum,sum+n*n,cd); //然后找c和d的和中有几个cd,二分找到大于的地址减去大于等于的地址就是等于cd的个数
}
}
cout << ans << endl;
}
int main()
{
cin >> n;
for(int i = 0;i < n;i++)
cin >> a[i] >> b[i] >> c[i] >> d[i];
solve();
return 0;
}
lower_bound和upper_bound函数具体可以参考此文章
超大背包问题
我们现在有体积和价值分别为v和w的n个物品。现在从这些物品中选出体积不超过m的物品放进背包中,求所有挑选方案中价值总和的最大值。
(1 <= n <= 40,1 <= wi,vi <= 1015,1 <= m <= 1015)
思路: 这就是一个很基础的01背包问题,但我们知道,01背包的时间复杂度是O(nm),n是物品数,m是背包体积,因为我们用了两层循环一层循环物品,一层循环体积。但此处的背包体积非常大,达到了1015,若再用nm的复杂度肯定会超时,所以针对这个体积超大的背包,我们应该利用n比较小这一特点,去考虑问题。
最简单的想法是枚举所有选物品的情况,但挑选物品的方法共有2n种,所以不能直接枚举,但我们可以拆成两部分,分别枚举,220的复杂度还是可以接受的。
当我们把前半部分所有情况枚举出来后,如何判断后面一部分呢?记前半部分一种选取方法对应的体积和价值总和分别为v1和w1。这样在后半部分寻找总体积v2 <= m-v1时使v2达到最大就好了。我们要思考从所有枚举的(v2,w2)的集合中高效的找到max{w2|v2 <= m-v1},因此我们可以排除所有的v2[i] < v2[j]且w1[i] > w1[j]的 j的情况,这一点可以按照v2,w1排序筛出来。得到的所有元素都满足v2[i] < v2[j]且w2[i] < w2[j](如下图),要计算max{w2|v2 <= m-v1}只需要找到满足v2 <= m-v1的最大i就可得到,这里可以用二分完成。所以总的时间复杂度是O(2(n/2)n)。
如下图,若某种选法的总体积大于前一个但总价值还小于前一个(红圈内的情况),那我们肯定不选择这种选法,所以我们直接删去这种选法来简化计算。
代码详解:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long ll;
const int N = 50;
const ll INF = 0x3f3f3f3f;
ll n,m,v[N],w[N];
vector<pair<ll,ll> > p; //p存第一次枚举出的所有的v和w
void solve()
{
int nn = n / 2; //分成两半
for(int i = 0;i < 1 << nn;i++) //用二进制的每一位的0和1表示该件物品选与不选,如第五位若是1表示选第五件物品,若为0表示不选第五件物品,1左移nn位可以表示nn件物品的所有选取情况
{
ll v1 = 0,w1 = 0;
for(int j = 0;j < nn;j++) //循环nn件物品
{
if(i >> j & 1) //判断第i位是否为1
{
v1 += v[j]; //若为1表示选了第j件物品,加上体积和价值
w1 += w[j];
}
}
p.push_back({v1,w1}); //存入p数组
}
sort(p.begin(),p.end()); //对p排序
//筛除必不可能选的元素
int num = 1; //num来存下所有可以选的物品
for(int i = 1;i < 1 << nn;i++) //循环所有物品(此时已经按体积从小到大排好序了)
if(p[num-1].second < p[i].second) //如果当前的价值大于刚才存的最后一个的价值,就把这个存进去
p[num++] = p[i]; //存进去
//枚举后半部分
ll ans = 0;
for(int i = 0;i < 1 << (n-nn);i++) //循环后半部分的所有情况
{
ll v2 = 0,w2 = 0;
for(int j = 0;j < n-nn;j++) //循环后半部分所有物品
{
if(i >> j & 1) //若选取第nn+j件物品
{
v2 += v[nn+j]; //加上第nn+j件物品的体积和价值
w2 += w[nn+j];
}
}
if(v2 <= m) //如果只选取后半部分的体积已经超过了m就不用再判断了
{
ll tw = (lower_bound(p.begin(),p.begin()+num,make_pair(m-v2,INF))-1)->second; //在第一部分的情况中查找体积,找到第一个大于m-v2的情况,再往回找一个,就是最大的v1了
ans = max(ans,w2 + tw); //答案就是此时的w1+w2,并找到最大值
}
}
cout << ans << endl; //输出答案即可
}
int main()
{
cin >> n >> m;
for(int i = 0;i < n;i++)
cin >> v[i] >> w[i];
solve();
return 0;
}