子集和就是01背包问题

正整数数组分成"等和"子集

选一些数,使它们和等于sum/2。01背包问题,和值相当于容量。

递推式先从二维数组写法入手,设dp[i][j]表示[0..i]的子集和等于j是否可行,dp[i][j] = dp[i-1][j]/*不取数i*/ || dp[i-1][j-nums[i]]/*取数i*/,只依赖dp[i-1][],可用一维数组写法。处理每个物品,逆序遍历各容量,d[j]=dp[j]||dp[j-num]

vector<bool> dp(sum + 1, false);
dp[0] = true;
for (int num : nums) {
	for (int j = sum; j >= num; j--) {
		dp[j] = dp[j] || dp[j - num];
	}
}
return dp[sum];

数组分成“等和”的K个子集

分成k个子集,就只能回溯法。背包问题只能选一个子集,也就是分成两个子集。

正整数数组各项加上正负号,使它们和为S的方式数

问题即:分成两个子集,使它们差为S。设加正号的成子集和为A,加负号的成子集和为B,A-B=S, A+B=sum,所以A=(sum+S)/2,B=(sum-S)/2,要满足sum>=Ssum-S是偶数。

问题转变成:选一些数,使它们和等于(sum-S)/2(比(sum+S)/2遍历范围更小)。01背包问题,返回方式数。

递推式先从二维数组写法入手,设dp[i][j]表示[0..i]的子集和等于j的方式数,dp[i][j] = dp[i-1][j]/*不取数i*/ + dp[i-1][j-nums[i]]/*取数i*/。处理每个物品,逆序遍历各容量,d[j]+=dp[j-num]

主要思想与第k步走+k或-k这题类似

分成"和最接近"的两个子集

int lastStoneWeightII(vector<int>& stones) {
    // 分成和最接近的两个子集,求 和<=sum/2的最大子集,01背包
    // 设dp[i][v]表示前i个数的子集和等于v是否可行。
    // dp[i][v] = dp[i-1][v]/*不取nums[i]*/ || dp[i-1][v-num]/*取nums[i]*/。
    // 递推式i这维依赖于i-1项,可省掉i这一维。dp[v] = dp[v] || dp[v-num]。v逆序遍历。
    int sum = 0;
    for (int w : stones) 
        sum += w;
    
    vector<bool> dp(sum / 2, false);
    dp[0] = true;
    for (int w : stones) {
        for (int v = sum / 2; v >= w; v--) {
            dp[v] = dp[v] || dp[v-w];
        }
    }
    for (int v = sum / 2; v >= 0; v--) {
        if (dp[v]) return sum - v - v;
    } 
    return -1; // invalid
}

分成“和最接近”的两个等长子集

从2n个数中选n个数,使它们的和最接近sum/2,不妨取<=sum/2的最大数。

设dp[i][k][v]表示从前i个数中取k个数、子集和等于v是否可行。考虑第i个数,dp[i][k][v]=dp[i-1][k][v]/*不取第i个数*/||dp[i-1][k-1][v-num]/*取第i个数*/。递推式i这维依赖于i-1项,可省掉i这一维。二维费用01背包问题,逆序遍历k和v:dp[k][v]=dp[k][v]||dp[k-1][v-num]

dp[0][0] = true;
for (int num : nums) {
    // 01背包,逆序遍历k和v
    for (int k = n; k >= 1; k--) {
        for (int v = sum/2; v >= num; v--) {
            dp[k][v] = dp[k][v] || dp[k-1][v-num];
        }
    }
}

最终取dp[n][sum/2..0]中第一个为true的。

类似ksum问题,二维费用背包问题

能被3整除的最大子集和

int maxSumDivThree(vector<int>& nums) {
    // 设dp[i][j]表示nums[0..i]的被3除余j的最大子集和
    // dp[i][j]=max(dp[i-1][j], dp[i-1][(j-nums[i])%3]+nums[i])
    // 第i项只依赖于第i-1项,可省掉i这维,i仍从左往右遍历
    // dp[j]=max(dp[j], dp[(j-num)%3]+num)=max(dp[j], dp[(j+2*num)%3]+num)
    vector<int> dp({0, INT_MIN, INT_MIN});
    for (int num : nums) {
        vector<int> ndp(3);
        for (int j = 0; j < 3; j++) {
            ndp[j] = max(dp[j], dp[(j+2*num)%3] + num);
        }
        swap(dp, ndp);
    }
    return dp[0];                            
}

子集排列数

int combinationSum4(vector<int>& nums, int target) {
    // 和为i的子集排列数 dp[i]=sum(dp[i-num])
    // 子集排列数的写法刚好 将背包问题的外层物品循环改为内层
    vector<int> dp(target + 1, 0);
    dp[0] = 1;
    for (int i = 0; i <= target; i++) {
        for (int num : nums) {
            if (i - num >= 0) dp[i] += dp[i-num];
        }
    }
    return dp[target];        
}