题解:洛谷 P1466 集合

【题目来源】

洛谷:P1466 [USACO2.2] 集合 Subset Sums - 洛谷

【题目描述】

对于从 1∼n 的连续整数集合,能划分成两个子集合,且保证每个集合的数字和是相等的。举个例子,如果 n=3,对于 {1,2,3} 能划分成两个子集合,每个子集合的所有数字和是相等的:

{3} 和 {1,2} 是唯一一种分法(交换集合位置被认为是同一种划分方案,因此不会增加划分方案总数)如果 n=7,有四种方法能划分集合 {1,2,3,4,5,6,7},每一种分法的子集合各数字和是相等的:

{1,6,7} 和 {2,3,4,5}

{2,5,7} 和 {1,3,4,6}

{3,4,7} 和 {1,2,5,6}

{1,2,4,7} 和 {3,5,6}

给出 n,你的程序应该输出划分方案总数。

【输入】

输入文件只有一行,且只有一个整数 n

【输出】

输出划分方案总数。

【输入样例】

7

【输出样例】

4

【核心思想】

  1. 问题分析:给定 \(n\),将集合 \(\{1, 2, \ldots, n\}\) 划分为两个子集,使两子集元素和相等。求划分方案总数(交换两子集位置视为同一种方案)。总和 \(S = \frac{n(n+1)}{2}\),若 \(S\) 为奇数则无解;否则每个子集目标和为 \(m = \frac{S}{2}\)。问题转化为:从 \(\{1, \ldots, n\}\) 中选取若干个数,使其和恰好为 \(m\) 的方案数。

  2. 算法选择

    • 01 背包变形(计数型 DP)\(dp[i][j]\) 表示从前 \(i\) 个数中选取若干个数,使其和为 \(j\) 的方案数
    • 状态转移:对于第 \(i\) 个数,可选可不选
      • 不选:方案数为 \(dp[i-1][j]\)
      • 选:方案数为 \(dp[i-1][j-i]\)(前提是 \(j \ge i\)
    • 最终答案\(dp[n][m]\),因交换两子集视为同一种方案,无需除以 2
  3. 关键步骤

    • 读入\(n\)
    • 计算总和\(tot = \frac{n(n+1)}{2}\)
    • 奇数特判:若 \(tot \bmod 2 = 1\),输出 \(0\) 并退出
    • 初始化\(m = \frac{tot}{2}\)\(dp[1][1] = 1\)(选数字 1 和为 1 的方案有 1 种)
    • DP 递推\(i\)\(2\)\(n\)\(j\)\(0\)\(m\)):
      • \(j < i\)\(dp[i][j] = dp[i-1][j]\)(当前数太大,无法选取)
      • \(j \ge i\)\(dp[i][j] = dp[i-1][j] + dp[i-1][j-i]\)(不选或选第 \(i\) 个数)
    • 输出\(dp[n][m]\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \cdot m) = O(n^3)\),其中 \(m = \frac{n(n+1)}{4}\)
    • 空间复杂度:\(O(n \cdot m) = O(n^3)\),二维 DP 数组
  5. 计数型 DP 的核心思想

    • 子集和问题转化:集合划分等价于找一个子集使其和为总和的一半,另一半自动确定
    • 方案数累加:与 01 背包求最大值不同,计数型 DP 将"取最大值"改为"方案数相加"
    • 避免重复计数:因只统计一个子集的方案数,另一个子集被唯一确定,自然避免了交换位置的重复
    • 奇数和无解:总和为奇数时无法均分,直接返回 0
    • 适用于子集划分、整数拆分、计数型背包等问题

【解题思路】

【算法标签】

普及 #递推

【代码详解】

#include <bits/stdc++.h>
using namespace std;
int n, m, dp[45][4000];
int main()
{
    cin >> n;  // 输入n
    m = n*(n+1)/4;  // 每个组合的数的总和要是1-n总和的1/2
    int tot = n*(n+1)/2;  // 计算n个数的总和
    if (tot%2==1) {  // 这里要特判(否则最后一个测试点无法通过)
        cout << 0 << endl;  // 如果和为奇数,就找不到方案
        return 0;
    }
    dp[1][1] = 1;  // dp[i][j],i为第i个数,j为背包大小(有点类似01背包,但递推公式不完全是)
    for (int i=2; i<=n; i++) {  // 从第二个数开始遍历
        for (int j=0; j<=m; j++) {  // 遍历背包大小
            if (j<i) {  // 01背包这里是j<w[i],题目中w[i]=i,所以写成j<i
                dp[i][j] = dp[i-1][j];  // 如果背包放不下,方案数等于上一个i的方案数
            } else {  // 如果装的下
                dp[i][j] = dp[i-1][j] + dp[i-1][j-i];  // 方案数等于上一个i的方案数,加上上一个i的j-i的方案数(这里不用像01背包加c[i])
            }
        }
    }
    cout << dp[n][m] << endl;  // 输出结果
    return 0;
}

【运行结果】

7
4
posted @ 2026-08-03 08:47  团爸讲算法  阅读(21)  评论(0)    收藏  举报