AcWing 171. 送礼物

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

一、题目描述

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

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

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

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

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

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

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

输入样例

20 5
7
5
4
18
1

输出样例

19

二、【经典错误解法】:\(01\)背包

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

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

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

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

不能\(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\)

\(Code\)

#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,那么可以选择
    // 技巧:为防止两个int相加爆int,同时不想使用long long,可以考虑使用减法
    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>());
    // sort(w, w + n), reverse(w, w + n);

    // 一家一半
    k = n >> 1;

    // 前面开始搜索 0~k-1
    //  dfs1,枚举出了所有可能出现的组合值
    dfs1(0, 0);

    // 前半部重量累加结果由小到大排序
    sort(sum, sum + idx);

    // 去重
    idx = unique(sum, sum + idx) - sum;

    // 后半部分搜索 k~n
    dfs2(k, 0);

    // 输出答案
    printf("%d\n", ans);
    return 0;
}
posted @ 2022-03-10 14:05  糖豆爸爸  阅读(225)  评论(0)    收藏  举报
Live2D