14

I have a Relational Schema with attributes (A B C D). I have a set of Functional Dependencies with me too.

Now I need to determine the closure for all the possible subsets of R's attributes. That's where I am stuck. I need to learn how to find subsets (non-repeating) in PHP.

My Array is stored like this.

$ATTRIBUTES = ('A', 'B', 'C', 'D').

so my subsets should be

$SUBSET = ('A', 'B', 'C', 'D', 'AB', 'AC', AD', 'BC', 'BD', 'CD', 'ABC', 'ABD', 'BCD', 'ABCD')

The code shouldn't be something big but for some reason I can't get my head around it.

Pang
  • 8,605
  • 144
  • 77
  • 113
dtx
  • 536
  • 5
  • 12
  • Does order matter? Also I think you might have to use recursion. – Pwnna May 23 '11 at 04:23
  • no the order wont matter. i can order it by the size of the string later, i just need a way to get the subsets – dtx May 23 '11 at 04:31
  • Basically you're just looking for the handshake algorithm? – Pwnna May 23 '11 at 04:37
  • that's more a power set question. You're asking to generate the power'set of `$attributes` – fbstj May 23 '11 at 04:41
  • 1
    http://www.php.net/manual/en/function.shuffle.php#88408 is a power-set generator example in php – fbstj May 23 '11 at 04:43
  • precisely, the set of all subsets or the powerset. all this time I had been looking with the wrong keyword. thanks. edit: if u want u can make that an answer,i can vote u up. peace – dtx May 23 '11 at 04:56

5 Answers5

23

You wish for the power set of $attributes? That is what your question implies.

An example can be found here (quoted for completeness)

<?php 
/** 
* Returns the power set of a one dimensional array, a 2-D array. 
* [a,b,c] -> [ [a], [b], [c], [a, b], [a, c], [b, c], [a, b, c] ]
*/ 
function powerSet($in,$minLength = 1) { 
   $count = count($in); 
   $members = pow(2,$count); 
   $return = array(); 
   for ($i = 0; $i < $members; $i++) { 
      $b = sprintf("%0".$count."b",$i); 
      $out = array(); 
      for ($j = 0; $j < $count; $j++) { 
         if ($b{$j} == '1') $out[] = $in[$j]; 
      } 
      if (count($out) >= $minLength) { 
         $return[] = $out; 
      } 
   } 
   return $return; 
} 
fbstj
  • 1,622
  • 1
  • 15
  • 21
  • cool was trying to convert this from the java one, wasnt working this is :) – Danuofr Jul 23 '13 at 22:23
  • @FallingBullets is there any way to modify this so it WILL return repeats? ie from (1,2,3) return (2 3)(1 2)(1 3)(1 2 3)(1 1 2)(1 1 3)(2 2 1) etc? – Jen Born Feb 18 '14 at 03:12
  • 1
    Technically this outputs all subsets bar the empty set. Just a technical point - it's not exactly the power set of an array. – James Stott Dec 29 '14 at 12:27
  • That's easy enough to fix, change `$return = array();` to `$return = array(array());` – fbstj Dec 30 '14 at 19:06
  • 1
    Remove `$b=sprintf(...)`, use `$i>>$j&1` instead of `$b{$j} == '1'` - shorter and faster. – Titus May 27 '17 at 08:13
  • 1
    To render `$members`and `$count` obsolete: `for($i=1<$u)if($i‌​>>$j&1)$out[]=$u​];...}`. This way (with `$i` not hitting `0`), it´s also guaranteed that `$out` is never empty, so if `$minLength` is `1`, you don´t need `count($out)` either. – Titus May 27 '17 at 09:50
19

Using php array_merge we can have a nice short powerSet function

function powerSet($array) {
    // add the empty set
    $results = [[]];

    foreach ($array as $element) {
        foreach ($results as $combination) {
            $results[] = array_merge(array($element), $combination);
        }
    }

    return $results;
}
Yada
  • 27,686
  • 20
  • 97
  • 138
1

Here a backtracking solution.

given a function that returns all the L-lenght subsets of the input set, find all the L-lenght subsets from L = 2 to dataset input length

<?php

function subsets($S,$L) {
    $a = $b = 0;
    $subset = [];
    $result = [];
    while ($a < count($S)) {
        $current = $S[$a++];
        $subset[] = $current;
        if (count($subset) == $L) {
            $result[] = json_encode($subset);
            array_pop($subset);
        }
        if ($a == count($S)) {
            $a = ++$b;
            $subset = [];
        }
    }
    return $result;
}



$S = [ 'A', 'B', 'C', 'D'];
$L = 2;


// L = 1 -> no need to do anything
print_r($S);

for ($i = 2; $i <= count($S); $i++)
    print_r(subsets($S,$i));
darioguarascio
  • 997
  • 9
  • 14
1

Based on @Yada's answer, this will generate the power set of an array, but preserve the original array's keys in each subset (the return value is still numerically & sequentially indexed). This very useful if you need subsets of an associative array.

The subsets also retain the element order of the original array. I added a stable sort to $results because I needed it, but you can omit it.

function power_set($array) {
    $results = [[]];
    foreach ($array as $key => $value) {
        foreach ($results as $combination) {
            $results[] = $combination + [$key => $value];
        }
    }

    # array_shift($results); # uncomment if you don't want the empty set in your results
    $order = array_map('count', $results);
    uksort($results, function($key_a, $key_b) use ($order) {
        $comp = $order[$key_a] - $order[$key_b]; # change only this to $order[$key_b] - $order[$key_a] for descending size
        if ($comp == 0) {
            $comp = $key_a - $key_b;
        }
        return $comp;
    });
    return array_values($results);
}

Given OP's input, var_dump(power_set(['A', 'B', 'C', 'D'])); provides:

array(16) {
  [0] =>
  array(0) {
  }
  [1] =>
  array(1) {
    [0] =>
    string(1) "A"
  }
  [2] =>
  array(1) {
    [1] =>
    string(1) "B"
  }
  [3] =>
  array(1) {
    [2] =>
    string(1) "C"
  }
  [4] =>
  array(1) {
    [3] =>
    string(1) "D"
  }
  [5] =>
  array(2) {
    [0] =>
    string(1) "A"
    [1] =>
    string(1) "B"
  }
  [6] =>
  array(2) {
    [0] =>
    string(1) "A"
    [2] =>
    string(1) "C"
  }
  [7] =>
  array(2) {
    [1] =>
    string(1) "B"
    [2] =>
    string(1) "C"
  }
  [8] =>
  array(2) {
    [0] =>
    string(1) "A"
    [3] =>
    string(1) "D"
  }
  [9] =>
  array(2) {
    [1] =>
    string(1) "B"
    [3] =>
    string(1) "D"
  }
  [10] =>
  array(2) {
    [2] =>
    string(1) "C"
    [3] =>
    string(1) "D"
  }
  [11] =>
  array(3) {
    [0] =>
    string(1) "A"
    [1] =>
    string(1) "B"
    [2] =>
    string(1) "C"
  }
  [12] =>
  array(3) {
    [0] =>
    string(1) "A"
    [1] =>
    string(1) "B"
    [3] =>
    string(1) "D"
  }
  [13] =>
  array(3) {
    [0] =>
    string(1) "A"
    [2] =>
    string(1) "C"
    [3] =>
    string(1) "D"
  }
  [14] =>
  array(3) {
    [1] =>
    string(1) "B"
    [2] =>
    string(1) "C"
    [3] =>
    string(1) "D"
  }
  [15] =>
  array(4) {
    [0] =>
    string(1) "A"
    [1] =>
    string(1) "B"
    [2] =>
    string(1) "C"
    [3] =>
    string(1) "D"
  }
}
Walf
  • 6,713
  • 2
  • 36
  • 52
0

Following @fbstj answer, I update the function:

function powerSet(array $in, int $minLength = 0): array
{
    $return = [];
    
    if ($minLength === 0) {
        $return[] = [];
    }

    for ($i = 1 << count($in); --$i;) {
        $out = [];

        foreach ($in as $j => $u) {
            if ($i >> $j & 1) {
                $out[] = $u;
            }
        }

        if (count($out) >= $minLength) {
            $return[] = $out;
        }
    }
    
    return $return;
}

Since power set functions can increase by a lot the memory load (2count($in) iterations), consider using Generator:

function powerSet(array $in, int $minLength = 0): \Generator
{
    if ($minLength === 0) {
        yield [];
    }

    for ($i = 1 << count($in); --$i;) {
        $out = [];

        foreach ($in as $j => $u) {
            if ($i >> $j & 1) {
                $out[] = $u;
            }
        }

        if (count($out) >= $minLength) {
            yield $out;
        }
    }
}

Usage:

foreach (powerSet(range(1, 10)) as $value) {
    echo implode(', ', $value) . "\n";
}
Benjamin
  • 158
  • 1
  • 12