送礼物

送礼物

题目描述

在这里插入图片描述


核心思路

这题不能用01背包来解,因为01背包的时间复杂度是 O ( N ∗ V ) O(N*V) O(NV),在这里就是 O ( N ∗ G [ i ] ) = O ( 46 ∗ ( 2 31 − 1 ) ) O(N*G[i])=O(46*(2^{31}-1)) O(NG[i])=O(46(2311)),那么一定会超时,因此可以考虑用爆搜。

这类问题的直接解法就是进行"指数型"的枚举——搜索每个礼物选还是不选,时间复杂度是 O ( 2 N ) O(2^N) O(2N),如果在搜索过程中已选的礼物重量之和已经超过了 W W W则可以停止搜索,及时剪枝。

但是在此题中, N ≤ 46 N\leq 46 N46,时间复杂度 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 Wt的数值最大的一个,用二者的和更新答案。

对于第一次搜索的时间复杂度是 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/2log2N/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/2log2N/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/2log2N/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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值