子集划分问题

本文介绍了如何使用背包算法和优化搜索策略解决LeetCode中的等和子集问题,以及如何计算连续整数集合的子集和划分。通过实例展示了如何用C++实现背包和爆搜方法来解决数组划分问题,包括LeetCode 416、1365和不同场景的子集和计算。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、将一个数组划分为两个子集

  • 数组里面数取值范围无限制:爆搜+优化
  • 数组里面数取值范围有限制:背包

1、Leetcode416 分割等和子集

\quad 给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。每个数组中的元素不会超过 100;数组的大小不会超过 200。

输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5][11].
int f[10010];
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for(int num: nums) sum += num;
        if(sum & 1) return false;
        int m = sum / 2;
        for(int i = 0; i <= m; i ++ ) f[i] = 0;
        f[0] = 1;
        for(int num: nums)
            for(int j = m; j >= num; j -- )
                f[j] |= f[j - num];
        return f[m] == 1;
    }
};

2、Acwing1365 子集的和

\quad 对于很多由 1∼N 构成的连续整数集合,我们都可以将其划分为两个子集,并使得两个子集的和相等。例如,当 N=3 时,我们可以将集合 {1,2,3} 划分为子集 {1,2} 和 {3},这也是唯一的一种满足条件的划分方式。当 N=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}

\quad 现在,给定 N,请你计算将 1∼N 构成的连续整数集合划分为和相等的两个子集,共有多少种划分方式。将一种划分方式的某个子集内部的元素之间进行顺序调整仍看作是同一种划分方式。

输入格式
共一行包含整数 N。
输出格式
输出一个整数,表示划分方案数。
如果无法划分,则输出 0。

数据范围
1≤N≤39
输入样例:
7
输出样例:
4

\quad 两种解法,一种是背包;一种是优化后的爆搜。

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

int solve(vector<int> &a){
    int n = a.size();
    int sum = 0;
    for(auto num: a) sum += num;
    if(n <= 1 || (sum & 1)) return 0;
    
    vector<int> b, c;
    int mid = n / 2;
    for(int i = 0; i < mid; i ++ ) b.push_back(a[i]);
    for(int i = mid; i < n; i ++ ) c.push_back(a[i]);
    unordered_map<int, int> cntb, cntc;

    for(int s = 0; s < (1 << b.size()); s ++ ){
        int cur = 0;
        for(int i = 0; i < b.size(); i ++ ){
            if(s >> i & 1) cur += b[i];
        }
        cntb[cur] ++ ;
    }

    for(int s = 0; s < (1 << c.size()); s ++ ){
        int cur = 0;
        for(int i = 0; i < c.size(); i ++ ){
            if(s >> i & 1) cur += c[i];
        }
        cntc[cur] ++ ;
    }
    int res = 0; 
    for(pair<int, int> p : cntb) res += p.second * cntc[sum / 2 - p.first];
    return res / 2;
}

int solve2(vector<int> &a){
    int n = a.size();
    int sum = 0;
    for(auto num: a) sum += num;
    if(n <= 1 || (sum & 1)) return 0;
    
    sum /= 2;
    vector<vector<long long>> dp(n + 1, vector<long long>(sum + 1, 0));
    dp[0][0] = 1;
    for(int i = 1; i <= n; i ++ ){
        for(int j = 0; j <= sum; j ++ ){
            if(j < a[i - 1]) dp[i][j] = dp[i - 1][j];
            else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - a[i - 1]];
        }
    }
    return dp[n][sum] / 2;
}

int main(int argc, char const *argv[])
{
    int n; cin >> n;
    vector<int> a;
    for(int i = 1; i <= n; i ++ ) a.push_back(i);
    cout << solve2(a) << endl;
    return 0;
}

二、划分为若干个子集

1、Leetcode698 划分为k个相等的子集

\quad 给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。

输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4
输出: True
说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。
1 <= k <= len(nums) <= 16
0 < nums[i] < 10000
class Solution {
public:
    vector<int> a;
    vector<int> s;
    bool dfs(int u, int cnt, int sum){ // 遍历到第u个数,当前有cnt个集合
        if(u == a.size()){
            return true;
        }
        for(int i = 0; i < cnt; i ++ ){
            if(s[i] + a[u] > sum) continue;
            s[i] += a[u];
            if(dfs(u + 1, cnt, sum)) return true;
            s[i] -= a[u];
        }
        // 将第u个数放入cnt+1个集合
        if(cnt < s.size() && a[u] <= sum){
            s[cnt] = a[u];
            if(dfs(u + 1, cnt + 1, sum)) return true;
            s[cnt] = 0;
        }
        return false;
    }
    bool canPartitionKSubsets(vector<int>& nums, int k) {
        s.resize(k);
        a = nums;
        int sum = 0;
        for(int num: nums) sum += num;
        if(sum % k != 0) return false;
        sum /= k;
        return dfs(0, 0, sum);
    }
};

2、Leetcode1681 最小不兼容性

\quad 给你一个整数数组 nums​​​ 和一个整数 k 。你需要将这个数组划分到 k 个相同大小的子集中,使得同一个子集里面没有两个相同的元素。一个子集的 不兼容性 是该子集里面最大值和最小值的差。请你返回将数组分成 k 个子集后,各子集 不兼容性 的 和 的 最小值 ,如果无法分成分成 k 个子集,返回 -1 。子集的定义是数组中一些数字的集合,对数字顺序没有要求。

输入:nums = [6,3,8,1,3,1,2,2], k = 4
输出:6
解释:最优的子集分配为 [1,2][2,3][6,8][1,3] 。
不兼容性和为 (2-1) + (3-2) + (8-6) + (3-1) = 6 。

输入:nums = [5,3,3,6,3,3], k = 3
输出:-1
解释:没办法将这些数字分配到 3 个子集且满足每个子集里没有相同数字。

1 <= k <= nums.length <= 16
nums.length 能被 k 整除。
1 <= nums[i] <= nums.length
#include <iostream>
class Solution {
public:
    int ans;
    vector<int> small;
    vector<int> big;
    vector<int> sz;
    vector<unordered_set<int>> vis;
    void dfs(vector<int> &a, int num, int u, int cnt){ // 当前枚举到第u份工作,一共开了cnt个集合
        int t = 0;  // 存放当前方案的极差之和
        for(int i = 0; i < small.size(); i ++ ) t += big[i] - small[i];
        if(t >= ans) return;
        if(u == a.size()){
            int t = 0;  // 存放当前方案的极差之和
            for(int i = 0; i < small.size(); i ++ ) t += big[i] - small[i];
            ans = min(ans, t);
            return;
        }
        for(int i = 0; i < cnt; i ++ ){
            if(sz[i] >= num) continue;  // 当前集合元素个数为num个,不能再加入新元素
            if(vis[i].count(a[u])) continue;
            int t1 = big[i], t2 = small[i];
            big[i] = max(big[i], a[u]);
            small[i] = min(small[i], a[u]);
            sz[i] ++ ;
            vis[i].insert(a[u]);
            dfs(a, num, u + 1, cnt);
            big[i] = t1, small[i] = t2;
            sz[i] -- ;
            vis[i].erase(a[u]);
        }
        // 新开一个集合
        if(cnt < big.size()){
            big[cnt] = a[u];
            small[cnt] = a[u];
            sz[cnt] ++ ;
            vis[cnt].insert(a[u]);
            dfs(a, num, u + 1, cnt + 1);
            big[cnt] = 0, small[cnt] = 0;
            sz[cnt] -- ;
            vis[cnt].erase(a[u]);
        }
    }
    int minimumIncompatibility(vector<int>& nums, int k) {
        ans = 1e9;
        big.resize(k);
        small.resize(k);
        sz.resize(k);
        vis.resize(k);
        dfs(nums, nums.size() / k, 0, 0);
        if(ans == 1e9) return -1;
        return ans;
    }
};

3、Leetcode1723 完成所有工作的最短时间

\quad 给你一个整数数组 jobs ,其中 jobs[i] 是完成第 i 项工作要花费的时间。
请你将这些工作分配给 k 位工人。所有工作都应该分配给工人,且每项工作只能分配给一位工人。工人的 工作时间 是完成分配给他们的所有工作花费时间的总和。请你设计一套最佳的工作分配方案,使工人的 最大工作时间 得以 最小化 。返回分配方案中尽可能 最小 的 最大工作时间 。

输入:jobs = [3,2,3], k = 3
输出:3
解释:给每位工人分配一项工作,最大工作时间是 3 。

输入:jobs = [1,2,4,7,8], k = 2
输出:11
解释:按下述方式分配工作:
1 号工人:128(工作时间 = 1 + 2 + 8 = 112 号工人:47(工作时间 = 4 + 7 = 11)
最大工作时间是 111 <= k <= jobs.length <= 12
1 <= jobs[i] <= 107
class Solution {
public:
    vector<int> s;
    vector<int> a;
    int ans = 1e9;
    void dfs(int u, int cnt, int curMax){
        if(curMax >= ans) return;
        if(u == a.size()){
            int t = 0;
            ans = min(ans, curMax);
            return;
        }
        for(int i = 0; i < cnt; i ++ ){
            s[i] += a[u];
            dfs(u + 1, cnt, max(curMax, s[i]));
            s[i] -= a[u];
        }
        if(cnt < s.size()){
            s[cnt] = a[u];
            dfs(u + 1, cnt + 1, max(curMax, s[cnt]));
            s[cnt] = 0;
        }
    }
    int minimumTimeRequired(vector<int>& jobs, int k) {
        s.resize(k);
        a = jobs;
        ans = 1e9;
        dfs(0, 0, 0);
        return ans;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值