题解:洛谷 B4177 尽量接近

【题目来源】

洛谷:B4177 [BCSP-X 2024 6 月初中组] 尽量接近

【题目描述】

给出 \(N\) 个整数,要求从中选出若干个数,使得它们的和尽量接近整数 \(K\)

【输入】

  • 第一行两个正整数 \(N, K\)
  • 第二行 \(N\) 个数,表示给出的数字。

【输出】

共一行一个整数,表示最接近 \(K\) 的和。如果不唯一,输出较小的那个。

【输入样例】

4 12
5 6 9 4

【输出样例】

11

【核心思想】

  1. 问题分析:给定 \(N\) 个整数和一个目标值 \(K\),从中选出若干个数使它们的和尽量接近 \(K\)(差值最小,差值相同时取较小和)。这是一个01 背包的变形问题,核心在于用布尔 DP 记录所有可达的子集和,再从中筛选最接近 \(K\) 的值。

  2. 算法选择

    • 01 背包(布尔型)\(f[i][j]\) 表示考虑前 \(i\) 个数能否凑出和 \(j\)
    • 答案提取:在所有可达的和中找到与 \(K\) 差值最小者,差值相同时取较小和
  3. 关键步骤

    • 初始化:读取 \(N\)\(K\)\(a[1..N]\),计算总和 \(sum = \sum_{i=1}^{N} a_i\)
    • DP 递推\(i\)\(1\)\(N\)\(j\)\(0\)\(sum\)):
      • 不选 \(a_i\)\(f[i][j] = f[i-1][j]\)
      • \(a_i\)(若 \(j \geq a_i\)):\(f[i][j] \mathrel{|}= f[i-1][j-a_i]\)
    • 边界\(f[0][0] = true\)(考虑 \(0\) 个数时和为 \(0\) 可达)
    • 答案提取:遍历 \(j\)\(0\)\(sum\),若 \(f[N][j] = true\)
      • 计算 \(diff = |j - K|\)
      • \(diff < minn\):更新 \(minn = diff\)\(ans = j\)
      • \(diff = minn\)\(j < ans\):更新 \(ans = j\)(取较小和)
    • 输出答案 \(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \times sum)\)\(sum\) 为所有数的总和
    • 空间复杂度:\(O(N \times sum)\),布尔型 DP 数组(可滚动优化至 \(O(sum)\)
  5. 01 背包的核心思想

    • 子集和判定:布尔型 DP 将"能否凑出和 \(j\)"作为状态,通过或运算累积所有可能的选择组合
    • 状态转移的完备性:每个数只有选或不选两种决策,\(f[i][j]\) 覆盖前 \(i\) 个数的所有子集情况
    • 答案域遍历:DP 完成后不是直接输出 \(f[N][K]\),而是遍历所有可达和找最优,因为 \(K\) 本身可能不可达
    • 平局处理:差值相同时取较小和,通过 \(j < ans\) 的条件确保
    • 适用于子集和判定、最接近目标值、资源分配可行性类问题

【算法标签】

普及 #01背包

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 55, K = 1000005; // N:最大整数个数, K:DP数组第二维上限
int n, k; // n:整数个数, k:目标和
int a[N]; // a[i]:第i个整数的值
bool f[N][K]; // f[i][j]:考虑前i个数,能否凑出和为j的子集

int main()
{
    cin >> n >> k; // 读入整数个数和目标和
    int sum = 0; // 所有数的总和
    for (int i=1; i<=n; i++)
    {
        cin >> a[i]; // 读入每个整数
        sum += a[i]; // 累加总和
    }

    f[0][0] = true; // 边界条件:考虑0个数时,和为0是可达的
    for (int i=1; i<=n; i++) // 外层循环:枚举每个整数
        for (int j=0; j<=sum; j++) // 内层循环:枚举可能的子集和
        {
            f[i][j] = f[i-1][j]; // 不选第i个数:继承前i-1个数的状态
            if (j>=a[i]) // 如果当前和j大于等于a[i]
                f[i][j] |= f[i-1][j-a[i]]; // 选第i个数:检查j-a[i]是否可达
        }

    int ans = 0; // 最接近k的子集和
    int minn = 1e9; // 当前找到的最小差值
    for (int j=0; j<=sum; j++) // 遍历所有可能的子集和
        if (f[n][j]) // 如果和j是可达的
        {
            int diff = abs(j-k); // 计算与目标k的差值
            if (diff<minn) // 如果差值更小,更新答案
            {
                minn = diff;
                ans = j;
            }
            else if (diff==minn && j<ans) // 差值相同但和更小,取较小值
            {
                ans = j;
            }
        }
    cout << ans << endl; // 输出最接近k的子集和
    return 0;
}

【运行结果】

4 12
5 6 9 4
11
posted @ 2026-08-11 11:26  团爸讲算法  阅读(25)  评论(0)    收藏  举报