subset-sum DP solution, plus extra improvement

main
Luca Lombardo 6 months ago
parent 90766e45dc
commit 872c275e14

@ -0,0 +1,8 @@
[package]
name = "subset-sum"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

@ -0,0 +1,120 @@
```rust
impl Solution {
pub fn can_partition(nums: Vec<i32>) -> bool {
let sum: i32 = nums.iter().sum();
if sum % 2 != 0 {
return false;
}
let target = sum / 2;
let mut dp = vec![false; (target + 1) as usize];
dp[0] = true;
for num in nums {
for i in (num..=target).rev() {
dp[i as usize] = dp[i as usize] || dp[(i - num) as usize];
}
}
dp[target as usize]
}
}
```
The task is to determine whether a given set can be partitioned into two subsets such that the sum of elements in both subsets is equal. The dynamic programming solution works as follows:
1. First, it calculates the sum of all elements in the set. If the sum is odd, it immediately returns false because it's impossible to split an odd number evenly.
2. If the sum is even, it sets a target as half of the sum. The goal is to find a subset of the original set that sums up to this target.
3. It then initializes a DP array of size `target + 1` with all elements set to false, except for the first one which is set to true. This DP array is used to keep track of the possible sums that can be formed using the elements of the set. The index `i` in the DP array represents a possible sum, and the value at `dp[i]` is true if it's possible to form this sum using the elements of the set, and false otherwise.
4. The algorithm then iterates over each number in the set. For each number, it iterates over the DP array in reverse order from `target` to `num`. This is done to avoid using the same element multiple times.
5. For each `i` in the range from `num` to `target`, it updates `dp[i]` to be true if `dp[i]` is already true (meaning we can form the sum `i` without the current number), or if `dp[i - num]` is true (meaning we can form the sum `i` by adding the current number to a previously possible sum `i - num`).
6. Finally, it returns the value of `dp[target]`. If this is true, it means we can partition the set into two subsets with equal sum.
### Complexity analysis
* **Time Complexity:** $O(n*sum/2)$ where $n$ is the number of elements in the input array and sum is the sum of all elements. This is because, for each element in the array, the code iterates up to $sum/2$ times.
* **Space Complexity:** $O(sum/2)$ because we use a DP array of size $sum/2$.
## Improvement
The given implementation is already optimal in terms of time and space complexity. However, some minor improvements can be made
1. You could use an early return if you find a number in the array that is greater than the target sum. Such a number could not be part of any subset that adds up to the target sum.
2. You could sort the array in descending order before starting the main loop. This way, you would start with the largest numbers, and if you can't reach the target sum with them, you can return false earlier.
3. If the input array is very large, you could use a bitset instead of a boolean array for the dp array to save space.
```rust
impl Solution {
pub fn can_partition(mut nums: Vec<i32>) -> bool {
let sum: i32 = nums.iter().sum();
if sum % 2 != 0 {
return false;
}
let target = sum / 2;
nums.sort_unstable_by(|a, b| b.cmp(a));
if nums[0] > target {
return false;
}
let mut dp = vec![false; (target + 1) as usize];
dp[0] = true;
for num in nums {
for i in (num..=target).rev() {
dp[i as usize] = dp[i as usize] || dp[(i - num) as usize];
}
if dp[target as usize] {
return true;
}
}
dp[target as usize]
}
}
```
![](https://i.imgur.com/mWRD3Hi.png)
Note that we are using `sort_unstable_by` instead of `sort_by` because it can be faster for certain inputs, and we don't care about preserving the relative order of equal elements (i.e we don't need a stable sort). The `|a, b| b.cmp(a)` part is a closure that compares two elements, and it's written in such a way that it sorts the array in descending order. The `cmp` method compares two values and returns an ordering, and by comparing `b` with `a` instead of `a` with `b`, we get the elements in descending order.
# Extra
We can use a DP approach to find the actual subsets that add up to the target sum. The idea is as follows:
1. Instead of a boolean DP array, we use a DP array of vectors. Each `dp[i]` will store all the subsets that sum up to `i`.
2. Initialize `dp[0]` with an empty subset (since there's one way to sum up to 0, which is by taking no elements).
3. For each number in the array, we iterate over the DP array in reverse order from `target` to `num`. For each `i` in this range, if `dp[i - num]` is not empty, add `num` to all subsets in `dp[i - num]` and append these new subsets to `dp[i]`.
4. Finally, `dp[target]` will contain all the subsets that sum up to the target.
Here's a Python code snippet that implements this idea:
```rust
fn can_partition(nums: Vec<i32>) -> Vec<Vec<i32>> {
let total: i32 = nums.iter().sum();
if total % 2 != 0 {
return vec![];
}
let target = total / 2;
let mut dp = vec![vec![]; (target + 1) as usize];
dp[0] = vec![vec![]];
for &num in nums.iter() {
for i in (num as usize..=target as usize).rev() {
let subsets = dp[(i - num as usize)].clone();
for mut subset in subsets {
subset.push(num);
dp[i].push(subset);
}
}
}
dp[target as usize].clone()
}
```
This function returns all the subsets that sum up to the target. If there are no such subsets, it returns an empty list.
Note that this solution has a higher time and space complexity than the original solution, as it needs to store all possible subsets instead of just a boolean value for each sum.

@ -0,0 +1,50 @@
// impl Solution {
// pub fn can_partition(nums: Vec<i32>) -> bool {
// let sum: i32 = nums.iter().sum(); // sum of all elements
// if sum % 2 != 0 { // if sum is odd, we can't partition into two equal subsets
// return false;
// }
// let target = sum / 2; // target sum of each subset
// let mut dp = vec![false; (target + 1) as usize]; // dp[i] = true if we can get sum i, false otherwise. First we initialize all to false. The size of dp is target + 1 because we need to include 0.
// dp[0] = true; // we can always get sum 0
// for num in nums {
// for i in (num..=target).rev() { // we iterate from target to num because if we iterate from num to target, we will be using the same element multiple times
// dp[i as usize] = dp[i as usize] || dp[(i - num) as usize]; // if we can get sum i - num, we can get sum i by adding num to it
// }
// }
// dp[target as usize] // return the last element of dp
// }
// }
impl Solution {
pub fn can_partition(mut nums: Vec<i32>) -> bool {
let sum: i32 = nums.iter().sum();
if sum % 2 != 0 {
return false;
}
let target = sum / 2;
nums.sort_unstable_by(|a, b| b.cmp(a));
if nums[0] > target {
return false;
}
let mut dp = vec![false; (target + 1) as usize];
dp[0] = true;
for num in nums {
for i in (num..=target).rev() {
dp[i as usize] = dp[i as usize] || dp[(i - num) as usize];
}
if dp[target as usize] {
return true;
}
}
dp[target as usize]
}
}
struct Solution;
fn main() {
assert_eq!(Solution::can_partition(vec![1, 5, 11, 5]), true);
assert_eq!(Solution::can_partition(vec![1, 2, 3, 5]), false);
}
Loading…
Cancel
Save