巧克力板选择问题

131 阅读4分钟

题目链接:www.marscode.cn/practice/w7…

给定n个物品,每个物品的重量为aia_i的平方,以及m个查询,每个查询对应一个背包的最大承重。任务是在每个查询的承重限制下,计算最多可以选择的物品数量。

两种解决方案:

  1. 动态规划:将问题视为0-1背包问题,使用动态规划方法,通过构建状态转移表来计算每个背包容量下的最大物品数;
  2. 贪心:先对物品重量进行排序,构建前缀和数组,然后对每个查询使用二分搜索来快速确定可选择的最大物品数量。

动态规划

代码

时间复杂度O(maxCapacity×n+nlogn)O(maxCapacity\times n + n\log n)

空间复杂度O(maxCapacity×n)O(maxCapacity\times n)

#include <algorithm>
#include <vector>
#include <iostream>
using namespace std;

vector<int> solution(int n, int m, vector<int> a, vector<int> queries) {
    const int maxCapacity = *max_element(queries.begin(), queries.end());
    sort(a.begin(), a.end());
    int maxItems = n;
    while (maxItems > 0 and a[maxItems - 1] * a[maxItems - 1] > maxCapacity) {
        --maxItems;
    }
    vector<int> result(m, 0);
    if (maxItems == 0) {
        return result;
    }
    // dp[c][i]表示当背包容量为c时,仅考虑前i个排序后的巧克力的情况下,最多能够装多少块巧克力。
    vector<vector<int> > dp(maxCapacity + 1, vector<int>(maxItems + 1));
    for (int item = 0; item <= maxItems; ++item) {
        dp[0][item] = 0;
    }
    for (int capacity = 1; capacity <= maxCapacity; ++capacity) {
        dp[capacity][0] = 0;
    }
    for (int capacity = 1; capacity <= maxCapacity; ++capacity) {
        for (int numItems = 1; numItems <= maxItems; ++numItems) {
            const int weight = a[numItems - 1] * a[numItems - 1];
            if (weight > capacity) {
                dp[capacity][numItems] = dp[capacity][numItems - 1];
            } else {
                dp[capacity][numItems] = max(dp[capacity][numItems - 1], dp[capacity - weight][numItems - 1] + 1);
            }
        }
    }
    for (int qi = 0; qi < m; ++qi) {
        result[qi] = dp[queries[qi]][maxItems];
    }
    return result;
}

int main() {
    cout << (solution(5, 5, {1, 2, 2, 4, 5}, {1, 3, 7, 9, 15}) == vector<int>{1, 1, 2, 3, 3}) << endl;
    cout << (solution(4, 3, {3, 1, 2, 5}, {5, 10, 20}) == vector<int>{2, 2, 3}) << endl;
    cout << (solution(6, 4, {1, 3, 2, 2, 4, 6}, {8, 12, 18, 25}) == vector<int>{2, 3, 4, 4}) << endl;

    return 0;
}

思路

这是当成经典的01背包问题来做,每块巧克力重量为边长的平方,价值为1。而相对于经典的01背包问题,本题的动态规划首先对巧克力数组进行了排序,排除了不可能被选中的巧克力(即重量大于最大背包容量的巧克力)。

状态转移方程为:

dp[c][i]={dp[c][i1],if ai2>cmax(dp[c][i1], dp[cai2][i1]+1),otherwisedp[c][i] = \begin{cases} dp[c][i-1], & \text{if } a_i^2 > c \\ \max(dp[c][i-1],\ dp[c - a_i^2][i-1] + 1), & \text{otherwise} \end{cases}

其中:

  • dp[c][i]dp[c][i] 表示在背包容量为 cc 且仅考虑前 ii 个(索引为0,1,...,i10,1,...,i-1)排序后的巧克力时,最多可以装入的巧克力块数。
  • ai2a_i^2 是第 ii 块巧克力的重量。

这个方程的含义是:

  • 如果当前考虑的第 ii 块巧克力的重量超过了当前背包的容量 cc,则不装入这块巧克力,状态值与不考虑这块巧克力时的状态值相同,即 dp[c][i1]dp[c][i-1]
  • 否则,可以选择不装入这块巧克力,状态值为 dp[c][i1]dp[c][i-1],或者选择装入这块巧克力,此时状态值为装入这块巧克力后的剩余容量 cai2c - a_i^2 对应的状态值加上1,即 dp[cai2][i1]+1dp[c - a_i^2][i-1] + 1。两者取最大值作为当前状态值。

前缀和+二分搜索(贪心)

代码

时间复杂度O((n+m)logn)O((n+m)\log n)。排序的时间复杂度为 O(nlogn)O(n \log n)。对每个查询做二分搜索复杂度为 O(logn)O(\log n),总查询量为 mm,因此查询的时间复杂度为O(mlogn)O(m \log n)

空间复杂度O(n+m)O(n+m)。巧克力重量数组和前缀和数组均为O(n)O(n),结果数组为O(m)O(m)

#include <vector>
#include <iostream>
#include <algorithm>
#include <numeric>
using namespace std;

vector<int> solution(int n, int m, vector<int> a, vector<int> queries) {
    // 计算每块巧克力的重量(边长平方)
    vector<int> chocolateWeights(n);
    transform(a.begin(), a.end(), chocolateWeights.begin(),
              [](const int &sideLength)-> int { return sideLength * sideLength; });

    // 将巧克力重量排序
    sort(chocolateWeights.begin(), chocolateWeights.end());

    // 构建前缀和: prefixWeightSums[i] 为最轻的 i 块巧克力重量总和
    vector<int> prefixWeightSums(n + 1);
    exclusive_scan(chocolateWeights.begin(), chocolateWeights.end(), prefixWeightSums.begin(), 0);
    prefixWeightSums[n] = prefixWeightSums[n - 1] + chocolateWeights[n - 1];

    // 针对每个背包承重 queries[i],二分搜索可携带的巧克力数量
    vector<int> maxChocolateCounts(m);
    for (int i = 0; i < m; i++) {
        const int &capacity = queries[i];
        // 找到 prefixWeightSums 中第一个大于 capacity 的位置
        const auto it = upper_bound(prefixWeightSums.begin(), prefixWeightSums.end(), capacity);
        int count = static_cast<int>(distance(prefixWeightSums.begin(), it)) - 1;
        if (count < 0) {
            count = 0; // 容量过小,可能导致计算结果为 -1
        }
        maxChocolateCounts[i] = count;
    }

    return maxChocolateCounts;
}

int main() {
    cout << (solution(5, 5, {1, 2, 2, 4, 5}, {1, 3, 7, 9, 15}) == vector<int>{1, 1, 2, 3, 3}) << endl;
    cout << (solution(4, 3, {3, 1, 2, 5}, {5, 10, 20}) == vector<int>{2, 2, 3}) << endl;
    cout << (solution(6, 4, {1, 3, 2, 2, 4, 6}, {8, 12, 18, 25}) == vector<int>{2, 3, 4, 4}) << endl;
    return 0;
}

思路

这道题的特殊之处在于每个物品的价值为1。因此可以采用贪心的做法。思路如下:

  1. 将边长转换为重量。 先对原数组 a 做一次映射,得到每块巧克力的重量数组 chocolateWeights
  2. 排序。 将得到的巧克力重量数组从小到大排序。
  3. 构建前缀和。 构建一个长度为 n + 1 的前缀和数组 prefixWeightSums,其中 prefixWeightSums[0] = 0prefixWeightSums[i] = prefixWeightSums[i - 1] + chocolateWeights[i - 1]。这样 prefixWeightSums[i] 就表示最轻的 i 块巧克力的总重量。
  4. 处理查询。 对于每个查询(最大承重 w),采用二分搜索在前缀和数组中找出最大的 i,使得 prefixWeightSums[i] <= w