[NOIP 2007 普及组] 纪念品分组

GitHub跳转原题关系图返回列表

排序后用双指针贪心,每次尝试将最便宜和最贵的纪念品配成一组。

OJ: luogu

题目 ID: P1094

难度:普及-

标签:贪心排序双指针python

日期: 2026-07-07 00:00

题意

n 件纪念品,每组最多两件,且每组价格和不能超过 w。求最少需要分成多少组。

思路

先看一个小数据暴力:

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * date: 2026-07-07 00:00:00
 */
// brute.cpp:小数据暴力解,递归枚举所有分组方式,求最少组数。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 20;

int w, n;
int p[MAXN];        // 纪念品价格
int assigned[MAXN]; // assigned[i] = 1 表示第 i 个纪念品已分组
int ans;            // 最少组数

// 递归:每次找到第一个未分组的纪念品,决定它的分组方式
void dfs(int cur_groups) {
    // 找到第一个未分组的纪念品
    int first = -1;
    for (int i = 1; i <= n; i++) {
        if (!assigned[i]) {
            first = i;
            break;
        }
    }

    // 全部已分组,更新答案
    if (first == -1) {
        if (cur_groups < ans) ans = cur_groups;
        return;
    }

    // 剪枝:当前组数已经 >= 当前最优,不继续
    if (cur_groups >= ans) return;

    // 方案1:first 单独成组
    assigned[first] = 1;
    dfs(cur_groups + 1);
    assigned[first] = 0;

    // 方案2:first 和另一个未分组的纪念品配成一组(需满足 p ≤ w)
    for (int j = first + 1; j <= n; j++) {
        if (!assigned[j] && p[first] + p[j] <= w) {
            assigned[first] = 1;
            assigned[j] = 1;
            dfs(cur_groups + 1);
            assigned[first] = 0;
            assigned[j] = 0;
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> w >> n;
    for (int i = 1; i <= n; i++) {
        cin >> p[i];
    }

    ans = n;  // 最多 n 组(每个单独一组)
    dfs(0);

    cout << ans << '\n';
    return 0;
}

暴力每次选择第一个未分组物品,枚举它单独一组或和另一个未分组物品配对,复杂度指数级。

正解先排序,然后用双指针:

  • left 指向当前最便宜的纪念品;
  • right 指向当前最贵的纪念品;
  • 如果 prices[left] + prices[right] <= w,把它们放一组;
  • 否则,最贵的连最便宜的都配不了,只能单独一组。

每轮一定会处理掉当前最贵的纪念品,因此组数加 1

为什么这样最优?当前最贵物品如果不能和最便宜物品配对,那它不可能和任何其他物品配对,只能单独一组。如果能配对,把最便宜的和它放一起不会浪费更好的机会,因为最便宜物品最容易和别人配,拿来配当前最难处理的最贵物品是安全的。

Python 知识

  • prices.sort() 原地升序排序。
  • 双指针用 leftright 表示当前还没分组的最小/最大物品。
  • while left <= right 覆盖剩一件物品的情况。

代码

python
import sys


def main():
    data = list(map(int, sys.stdin.buffer.read().split()))
    limit = data[0]
    n = data[1]
    prices = data[2:2 + n]

    prices.sort()
    left = 0
    right = n - 1
    answer = 0

    while left <= right:
        if prices[left] + prices[right] <= limit:
            left += 1
        right -= 1
        answer += 1

    print(answer)


if __name__ == "__main__":
    main()

复杂度

排序复杂度是 O(nlogn)O(n \log n),双指针扫描是 O(n)O(n)

空间复杂度是 O(n)O(n)

总结

“每组最多两个、容量有限、组数最少”常用排序双指针。每次先处理最难安置的最大物品。