• AcWing 171. 送礼物


    \(AcWing\) \(171\). 送礼物

    题目传送门

    一、题目描述

    达达帮翰翰给女生送礼物,翰翰一共准备了 \(N\) 个礼物,其中第 \(i\) 个礼物的重量是 \(G[i]\)

    达达的力气很大,他一次可以搬动 重量之和不超过 \(W\)任意多个 物品。

    达达希望一次搬掉 尽量重 的一些物品,请你告诉达达在他的力气范围内 一次性能搬动的最大重量 是多少

    输入格式
    第一行两个整数,分别代表 \(W\)\(N\)

    以后 \(N\) 行,每行一个正整数表示 \(G[i]\)

    输出格式
    仅一个整数,表示达达在他的力气范围内一次性能搬动的最大重量。

    数据范围
    \(1≤N≤46\)
    \(1≤W,G[i]≤2^{31}−1\)

    二、\(01\)背包解法

    本题是不能使用\(01\)背包的,原因:

    • 原因\(1\)
      \(01\)背包的时间复杂度:\(NV\)
      本题:\(N=46 V=2^{31}−1 =2147483647\) ,也就是\(int\)的上限,所以\(N*V\),上面的至少 \(2e9 * 46\)

      \(C++\)每秒能算\(1\)亿次,\(100000000=1e8\),所以\(01\)背包肯定会\(TLE\)

    • 原因\(2\)
      因为我们需要给结果数组\(f[N]\)初始化,这代表的是每一个可能的体积能装的最大重量是多少,而本题的上限\(2^{31}−1\)实在太大了,\(C++\)开不了这么大的数组,会 Segmentation Fault,也就是数组越界。

    总结:无论从时间上,还是空间上来讲,都无法\(AC\)掉本题,需要想其它的办法。

    贴上不能\(AC\)\(01\)背包解法:

    #include <bits/stdc++.h>
    
    using namespace std;
    const int N = 110;
    
    int n, m;
    int w[N];
    int f[N];
    
    int main() {
        cin >> m >> n;
        for (int i = 1; i <= n; i++) cin >> w[i];
    
        for (int i = 1; i <= n; i++)
            for (int j = m; j >= w[i]; j--)
                f[j] = max(f[j], f[j - w[i]] + w[i]);
    
        cout << f[m] << endl;
        return 0;
    }
    

    三、题目解析

    每种物品有选与不选两种情况,共\(8\times 1e6\)种情况
    \(N=46,N/2=23,2^{23} \approx 8 \times 1e6\)

    四、准备知识

    需要用到的知识点:在一个从小到大的有序数组中,找到 小于等于某个数 的 最大值

    总结\(1\)

    \(STL\)的二分常数较大,在竞赛中需要自己 手写二分 代码。

    lower_bound函数返回第一个 大于等于 目标值的位置
    upper_bound函数返回第一个 大于 目标值的位置
    若容器中的元素都比目标值小则返回最后一个元素的下一个位置!

    总结\(2\):

    手动版本 \(upper\_bound\)

      int l = 0, r = idx; //[左闭右开)
      while (l < r) {
          int mid = (l + r) >> 1;
          if (sum[mid] > m - s) //upper_bound不就是说找到第一个大于吗,这里就是大于
              r = mid;
          else
              l = mid + 1;
      }
      l--;   
    

    \(l\)是第一个大于目标值的位置,可能找到,也可能找不到:

    • 找到
      停在第一个大于目标值的位置,那么目标值在\(l-1\)的位置上
    • 找不到
      停在数组最后一个元素的后面,就是越界了,表示数组中不存在大于指定目标值的数字,那么数组中最后一个元素,就是目前数组中的最大值,即\(l-1\)

    手动版本 \(lower\_bound\)

      int l = 0, r = idx; // [左闭右开)
      while (l < r) {
          int mid = (l + r) >> 1;
          if (sum[mid] >= m - s)
              r = mid;
          else
              l = mid + 1;
      }
      if (sum[l] != m - s) l--;  
    

    \(l\)是第一个大于等于目标值的位置,也可能等于,也可能大于,也可能出界了没找到。

    • 需要进行判断\(sum[l]== m-s\),如果相等,\(l\)就是答案,如果不相等,则说明可能冒了,需要\(l--\),就是小于目标值的第一个位置啦
    // (3)、STL版本 upper_bound,通过了 12/13个数据, 查找大于m-s的值
      int l = upper_bound(sum, sum + idx, m - s) - sum;
      l--;
    
    // (4)、STL版本 lower_bound 通过了 12/13个数据, 查找大于等于m-s的值
      int l = lower_bound(sum, sum + idx, m - s) - sum;
      if (sum[l] != m - s) l--;
    

    总结\(3\)

    • lower_boundupper_boundcheck办法其实挺简单的,就是>=>,直接按这个不等号方向来写逻辑就行了,然后再根据逻辑思考,是选择upper_bound还是lower_bound ,虽然lower_boundupper_bound 都是可以找到答案的,但我们用upper_bound可以直接找到越过的上界,直接--就是答案,省去了判断

    • 可以这样记忆:>,<=upper_bound(); <,>=lower_bound()
      不是说只能这样做,是这样做最方便、最简单

    总结\(4\):

    达达的力气很大,他一次可以搬动重量之和不超过 \(W\) 的任意多个物品。
    \(1≤W≤2^{31}−1\)

    背包容量确实是没有超过\(INT\)的上限,但是,由于在代码执行过程中,会尝试在现有累加重量\(s\)的基础上,再加上当前礼物的重量\(W[u]\),是存在\(INT+INT>INT\_MAX\)的情况的,有两种办法处理:

    • 强制转为\(long ~ long\)
    typedef long long ll;
    if((ll)s+ w[u] <=m) //将s由int强制转为ll,再进行加法,C++就理解为两个数字都是ll,不会越界
        ...
    
    • 不用加法,用减法
      if (sum[mid] > m - s)
          ...
    

    完美避开\(ll\)转换!

    总结\(5\)

    双向\(DFS\)

    对重量进行降序排序, 优先枚举大重量

    先枚举前面 \(N / 2\)个重量, 打表, 去重, 排序

    对剩余的\(N - N/2\)个重量,枚举,二分查找两数相加小于等于\(W\)的最大值

    五、实现代码

    #include <bits/stdc++.h>
    
    using namespace std;
    const int N = 48; // 1≤N≤46
    
    //双向DFS
    int n;               // n个礼物
    int m;               // 重量之和不超过m,上限
    int k;               // 前k个,即索引下标0~k-1
    int w[N];            // 每个礼物的重量
    
    //前半部分收集到的所有和,下标因为一直在保持++状态,所以最后一次执行完,也可以理解为前半部分数组的个数
    //在排序去重后,此变更也可以视为前半段数组的元素个数,在二分中,因为需要使用的是索引号:0~idx-1
    int sum[1 << (N / 2)], idx;
    int ans; //最大重量
    
    // u:第几号礼物,下标从0开始
    // s:本路线上累加的礼物物理和
    void dfs1(int u, int s) {
        if (u == k) {       //如果能够到达第k个下标位置,表示前面0~k-1共k个选择完毕
            sum[idx++] = s; //记录礼物重量和
            return;
        }
        //如果加上u号物品重量,不会超过上限m,那么可以选择
        if (s <= m - w[u]) dfs1(u + 1, s + w[u]);
        //放弃u号物品,走到下一个u+1号面前
        dfs1(u + 1, s);
    }
    
    //后半部分
    void dfs2(int u, int s) {
        if (u == n) {
            //目标:在一个从小到大的有序数组中,找到 小于等于某个数 的最大值
            int l = 0, r = idx; //[左闭右开)
            while (l < r) {
                int mid = (l + r) >> 1;
                if (sum[mid] > m - s)
                    r = mid;
                else
                    l = mid + 1;
            }
            l--; // l是第一个大于目标值的位置,我们要找的是小于等于目标值的位置,l-1就是答案
    
            //更新更大的重量
            ans = max(ans, sum[l] + s);
            return;
        }
    
        //如果加上u号物品重量,不会超过上限m,那么可以选择
        if (s <= m - w[u]) dfs2(u + 1, s + w[u]);
    
        //放弃当前礼物
        dfs2(u + 1, s);
    }
    
    int main() {
        scanf("%d %d", &m, &n);                         //先读入m再读入n,别整反了
        for (int i = 0; i < n; i++) scanf("%d", &w[i]); //每个礼物重量
    
        //由大到小排序,搜索范围会小
        sort(w, w + n, greater<int>());
    
        //一家一半
        k = n >> 1;
    
        //前面开始搜索 0~k-1
        // dfs1,枚举出了所有可能出现的组合值
        dfs1(0, 0);
    
        //结果排序
        sort(sum, sum + idx);
        //去重
        idx = unique(sum, sum + idx) - sum; //最后这个-1是换算出sum数据最后一个的下标
    
        //后半部分搜索 k~n
        dfs2(k, 0);
    
        //输出答案
        printf("%d\n", ans);
        return 0;
    }
    
  • 相关阅读:
    DataGrid行单元格合并显示 (增加交错行颜色设置)
    IListHelper 实现IList到DataSet和DataTable的数据转换
    DataGrid 分页自定义控件
    反射原理的简单例子
    DataTable转换为Excel文件
    经典面试题集锦
    优秀技术网站展播!
    DataGrid行单元格合并显示
    Windows Service Application Overview
    GridView.SortExpression Property
  • 原文地址:https://www.cnblogs.com/littlehb/p/15989239.html
Copyright © 2020-2023  润新知