PrevNext
Not Frequent
 0/12

Sum over Subsets DP

Authors: Rameez Parwez, Siyong Huang, Aakash Gokhale

Taking bitmask DP to the next level.

Edit This Page

Prerequisites

Resources
CF

Good explanation + problem list

GFG

Goes over brute force solutions

CF

Characterizing SOS DP as multidimensional prefix sums

Sum Over Subsets (SOS) is technique used to efficiently calculate the sum of values for all subsets of a given set or bitmask. Instead of manually generating every subset, which can be slow, SOS uses bitwise operations to quickly go through only the necessary subsets.

The main trick is that for any bitmask xx, we can directly loop through all its subsets ii using:

i=(i1)&xi = (i - 1)\&x

This ensures we efficiently handle only the subsets of xx, skipping unnecessary combinations.

Implementation

Consider an array AA of 2n2^n elements. For a bitmask xx, we define F(x)F(x) as the sum of A[i]A[i] over all subset mask ixi \subseteq x. That is:

F(x)=ixA[i]F(x) = \sum_{i \subseteq x} A[i]

The goal is to compute F(x)F(x) for all x=0,1,2,....,2n1x = 0, 1, 2, ...., 2^n - 1.

Solution

Brute Force

The naive solution is to iterate over all pair of masks, summing A[i]A[i] only when one of them is a subset of the other (i.e.,i&x=ii \& x = i).

C++

vector<int> sos(1 << n);
for (int x = 0; x < (1 << n); x++) {
// iterate over all other sets and checks whether they're a subset of x
for (int i = 0; i < (1 << n); i++) {
if ((i & x) == i) {
sos[x] += a[i];
}
}
}

Python

sos = [0] * (1 << n)
for x in range(1 << n):
# iterate over all other sets and checks whether they're a subset of x
for i in range(1 << n):
if (x &i) == i:
sos[x] += a[i]

This solution has a time complexity of O(2n2n)=O(4n)\mathcal{O}(2^n \cdot 2^n) = O(4^n), which is impractical for large nn.

Optimized Solution

Instead of iterating over all 2n2^n bitmasks for ii, we can optimize by iterating only over the subset mask of xx.

C++

vector<int> sos(1 << n);
for (int x = 0; x < (1 << n); x++) {
sos[x] = a[0];
// iterate over all subsets of x directly
for (int i = x; i > 0; i = (i - 1) & x) {
sos[x] += a[i];
}
}

Python

sos = [0] * (1 << n)
for x in range(1 << n):
sos[x] = a[0]
i = x
# iterate over all subsets of x directly
while i > 0:
sos[x] += a[i]
i = (i - 1) & x

How does this works?

Time Complexity: O(3n) \mathcal{O}(3^n)

Proof

Faster Solution using Dynamic Programming

The above solution still has redundancy. For example, if a bitmask ii has kk unset bits, then A[i]A[i] is summed 2k2^k times. If we precompute and reuse these sums, we can eliminate repeated additions.

Subset Mask Partitioning with S(x,i)S(x, i)

We define the subset S(x,i)S(x, i) as follows:

S(x,i)={yxyx<2i+1}S(x, i) = \{y \subseteq x \mid y \oplus x < 2^{i + 1}\}

In simpler terms, S(x,i)S(x, i) contains all subset masks of xx whose bits to the left of the ii-th bit match those of xx.

For example:

S(11010110,3)={11010000,11010010,11010100,11010110}S(11010110, 3) = \{11010000,11010010,11010100,11010110\}

The subsets S(x,i)S(x, i) can be recursively partitioned as follows:

  1. If the ii-th bit of xx is 00, then S(x,i)=S(x,i1)S(x, i) = S(x, i - 1).
  2. If the ii-th bit of xx is 11:
  • Subsets with the ii-th bit 1:S(x,i1)1: S(x, i - 1).
  • Subsets with the ii-th bit 0:S(x2i,i1)0 : S(x \oplus 2^i, i - 1).

Thus:

S(x,i)={S(x,i1)if the i-th bit is 0S(x,i1)S(x2i,i1)if the i-th bit is 1S(x, i) = \begin{cases} S(x, i - 1) & \text{if the i-th bit is 0} \\ S(x, i - 1) \cup S(x \oplus 2^i, i - 1) & \text{if the i-th bit is 1} \end{cases}

Using the above partitioning, we define a DP table dp[x][i]dp[x][i] where:

dp[x][i]=yS(x,i)A[y]dp[x][i] = \sum_{y \in S(x, i)} A[y]

Time Complexity: O(N2N)\mathcal{O}(N \cdot 2^N)

C++

vector<int> sos(1 << n);
vector<vector<int>> dp(1 << n, vector<int> (n));
for (int x = 0; x < (1 << n); x++) {
dp[x][0] = a[x];
if (x & 1) {
dp[x][0] += a[x ^ 1];
}
for (int i = 1; i < n; i++) {
dp[x][i] = dp[x][i - 1];

Python

sos = [0] * (1 << n)
dp = [[0] * n for _ in range(1 << n)]
for x in range(1 << n):
dp[x][0] = a[x]
if x & 1:
dp[x][0] += a[x ^ 1]
for i in range(1, n):
dp[x][i] = dp[x][i - 1]
if x & (1 << i):
dp[x][i] += dp[x ^ (1 << i)][i - 1]
sos[x] = dp[x][n - 1]

Optimized Memory Usage

Since dp[x][i]dp[x][i] only depends on dp[x][i1]dp[x][i - 1], we can reuse the DP array.

C++

for (int i = 0; i < (1 << n); i++) {
sos[i] = a[i];
}
for (int i = 0; i < n; i++) {
for (int x = 0; x < (1 << n); x++) {
if (x & (1 << i)) {
sos[x] += sos[x ^ (1 << i)];
}
}
}

Python

sos = [0] * (1 << n)
for i in range(1 << n):
sos[i] = a[i]
for i in range(n):
for x in range(1 << n):
if x & (1 << i):
sos[x] += sos[x ^ (1 << i)]

General Problems

StatusSourceProblem NameDifficultyTags
CSESEasy
Show TagsSOS DP
PlatinumEasy
Show TagsCombo, DP
CFNormal
Show TagsBitmasks, SOS DP
PlatinumNormal
Show TagsNT, Prefix Sums
kilonovaHard
Show TagsBitmasks, NT, SOS DP
CFHard
Show TagsBitmasks, DP
CFHard
Show TagsBitmasks, Combinatorics, DP
CFHard
Show TagsBitmasks, DP
CFHard
Show TagsBitmasks, DP
ACHard
Show TagsBitmasks, DP
JOIHard
Show TagsSOS DP
CFInsane
Show TagsBitmasks, DP, SOS DP

Module Progress:

Join the USACO Forum!

Stuck on a problem, or don't understand a module? Join the USACO Forum and get help from other competitive programmers!

PrevNext