送礼物
题目描述
核心思路
这题不能用01背包来解,因为01背包的时间复杂度是 O ( N ∗ V ) O(N*V) O(N∗V),在这里就是 O ( N ∗ G [ i ] ) = O ( 46 ∗ ( 2 31 − 1 ) ) O(N*G[i])=O(46*(2^{31}-1)) O(N∗G[i])=O(46∗(231−1)),那么一定会超时,因此可以考虑用爆搜。
这类问题的直接解法就是进行"指数型"的枚举——搜索每个礼物选还是不选,时间复杂度是 O ( 2 N ) O(2^N) O(2N),如果在搜索过程中已选的礼物重量之和已经超过了 W W W则可以停止搜索,及时剪枝。
但是在此题中, N ≤ 46 N\leq 46 N≤46,时间复杂度 O ( 2 46 ) O(2^{46}) O(246)还是太高了,肯定会超时。这时我们可以采用双向搜索DFS的思想,把礼物分成两半。
- 首先,第一次搜索时,我们搜索出从前一半礼物中选出若干个,可能达到的 0 0 0~ W W W之间的所有重量值,存放在一个数组A中,并对数组A进行排序、去重。
- 然后,我们进行第二次搜索,尝试从后一半礼物中选出一些。对于每个可能达到的重量值 t t t,在第一部分得到的数组A中二分查找 ≤ W − t \leq W-t ≤W−t的数值最大的一个,用二者的和更新答案。
对于第一次搜索的时间复杂度是 O ( 2 N / 2 ) O(2^{N/2}) O(2N/2),因为第一次搜索时,只搜一半的数量是 N / 2 N/2 N/2,然后爆搜时对于每一件物品,都有选和不选两种状态,因此是 O ( 2 N / 2 ) O(2^{N/2}) O(2N/2)。
对于第二次搜索的时间复杂度是 O ( 2 N / 2 ∗ l o g 2 N / 2 ) O(2^{N/2}*log2^{N/2}) O(2N/2∗log2N/2),其中 2 N / 2 2^{N/2} 2N/2解释同上,然后 l o g 2 N / 2 log2^{N/2} log2N/2是二分查找的时间复杂度。
因此,总的时间复杂度就是 O ( 2 N / 2 + 2 N / 2 ∗ l o g 2 N / 2 ) O(2^{N/2}+2^{N/2}*log2^{N/2}) O(2N/2+2N/2∗log2N/2)。
优化剪枝:
- 优化搜索顺序:把礼物按照重量从大到小降序排列后再分半、搜索
- 选取适当的"折半分半点":因为第二次搜索需要在第一次搜索得到的数组中进行二分查找,效率相对较低,所以我们应该稍微增加第一次搜索的礼物数量,减少第二次搜索的礼物数量。也就是说我们要平衡两次搜索的时间复杂度,即尽量让 2 N / 2 2^{N/2} 2N/2和 2 N / 2 ∗ l o g 2 N / 2 2^{N/2}*log2^{N/2} 2N/2∗log2N/2是相等的。经检验发现,当取第1到 N / 2 + 2 N/2+2 N/2+2个礼物作为"前一半",取第 N / 2 + 3 N/2+3 N/2+3到第N个礼物作为"后一半"时,搜索的速度最快。
代码
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 46;
int n, m, k; //m是达达最多可以搬动的物品的重量之和 n是物品的个数 k是把这些物品划分成两部分的分割线
int w[N]; //每个物品的重量
// 1<<25表示2的25次方
int weights[1 << 25];//weights数组用来存储一个集合中所有物品的可能取得到的重量
int cnt = 1;//因为当什么都不选的时候,weights集合里面就是0,即weights[0]=0,所以这里cnt=1,从有1个物品开始继续算
int ans; //达达在他的力气范围内一次性能搬动的最大重量
//u表示当前枚举到前半个区间中的哪个物品了 s表示weights集合中可能取到的物品的重量之和
void dfs1(int u, int s)
{
//如果u等于k,那么u从0枚举一直到k-1,总共枚举了k个物品,所以当u==k时,说明已经枚举完了k个物品
//不能写成u==k+1,因为写成u==k+1那么u从0到k总共枚举了k+1个物品,超过了
//不能写成u==k-1,因为写成u==k-1那么u从0到k-2总共枚举了k-1个物品,不足
if (u == k)
{
weights[cnt++] = s;//计算一下weights集合里面的可能取到的物品的重量
return;//回溯到上一个状态
}
//如果不选择当前这个u物品,那么就递归去看它的下一个物品即u+1物品
dfs1(u + 1, s);
//如果选择了当前这个u物品,并且s加上当前u这个物品的重量后还没有超过最大的承受能力,那么就接着递归下一个物品即u+1物品
if ((LL)s + w[u] <= m) //这里题目给定的物品的重量的数据过大,可以爆int,所以用long long
dfs1(u + 1, s + w[u]);
}
//u表示当前枚举到前半个区间中的哪个物品了 s表示weights集合中可能取到的物品的重量之和
void dfs2(int u, int s)
{
//这里不写u==n而写成u>=k是因为如果写成u==n,假设n=1,那么k=n/2+2=2,将k传参给u,此时u=2,递归再次进入dfs,就会一直陷入死循环
//但是如果写成u>=n,那么u=2>n=1,就可以进入if语句,就可以回溯,那么就不会陷入死循环
if (u >= n)
{
//进行二分查找操作
//当结束dfs1函数时,weights数组它存储的数据的下标是从0~cnt-1 此时因为cnt++ 所以cnt是出界的,没有数据
int l = 0, r = cnt - 1;//所以这里r=cnt-1而不是r=cnt
while (l < r)
{
int mid = l + r + 1 >> 1;
//m是总体积,s是右半部分的体积,那么m-s就是左半部分的体积,我们要让m-s尽量的大,这样左边+右边才可能尽量的大(但不超过m)
//在左半部分的区间中二分出尽可能最大的那个物品的重量
if (m - s >= weights[mid])//可以这么写
l = mid;
//if ((LL)s + weights[mid] <= m) //也可以这么写
// l = mid;
else
r = mid - 1;
}
//说明此时的l就是我们要找的那个左边最大的那个物品
//比较一下取最大的ans就答案
ans = max(ans, s + weights[l]);
return;//回溯
}
//如果不选择当前这个u物品,那么就递归去看它的下一个物品即u+1物品
dfs2(u + 1, s);
//如果选择了当前这个u物品,并且s加上当前u这个物品的重量后还没有超过最大的承受能力,那么就接着递归下一个物品即u+1物品
if ((LL)s + w[u] <= m)
dfs2(u + 1, s + w[u]);
}
int main()
{
cin >> m >> n;
for (int i = 0; i < n; i++)
cin >> w[i];
sort(w, w + n);//先让这些物品的重量从小到大排序
reverse(w, w + n);//优化性剪枝,让这些物品的重量从大到小排序
k = n / 2 + 2;//划分的分割线
//前半部分深搜从第0个物品,weights集合中可能取到的物品的重量为0开始
dfs1(0, 0);
sort(weights, weights + cnt);//给weights数组从小到大排序
//unique(weights, weights + cnt)是去掉weights数组中的重复元素,如果有重复的元素,则只保留一个,返回的是迭代器
//unique(weights, weights + cnt) - weights;返回的是删除重复元素后weights数组中的元素个数
cnt = unique(weights, weights + cnt) - weights;
//后半部分从第k个物品到第n个物品,weights集合中可能取到的物品的重量为0开始进行深搜
dfs2(k, 0);
cout << ans << endl;
return 0;
}