获取集合的所有子集

前言

今天看到一个有意思的问题,要获取一个集合的所有子集,使用PHP语言表示的话,就是要找到一个数组的所有子集数组,如[1,2,3],结果应该是

[1]
[2]
[3]
[1,2]
[1,3]
[2,3]
[1,2,3]

PHP代码

function powerSet($in,$minLength = 1) {
    $count = count($in);
    $members = pow(2,$count);
    $return = [];
    for ($i = 1; $i < $members; $i++) {
        $b = sprintf("%0".$count."b",$i);
        $out = [];
        for ($j = 0; $j < $count; $j++) {
            if ($b{$j} == '1') $out[] = $in[$j];
        }
        if (count($out) >= $minLength) {
            $return[] = $out;
        }
    }
    return $return;
}

上面代码的巧妙之处在于通过二进制的方式来解决问题。拿[1,2,3]来举例,一共有3个数字,构造子集就是从数组中拿出若干数字组成数组。3个数字可以看成二进制的3位,一共有如下情况

001  ---  1
010  ---  2
011   --- 3
100  ---  4
101  ---  5
110  ---  6
111   --- 7

可以看出我们只要把二进制表示方式中,找出所有「1」所在位置在数组中对应的数字找出来就是所有子集了。比如[1,2,3]对应关系如下

001  ---  [3]
010  ---  [2]
011   --- [2,3]
100  ---  [1]
101  ---  [1,3]
110  ---  [1,2]
111   --- [1,2,3]

这样就找到了所有子集了。sprintf("%0".$count."b",$i);就是把1 - \(2^n\)用二进制方式表示出来,$b{$j} == '1'就是找出1所在的数组索引下标。

代码优化

上面的代码有可以优化的地方,如下

function powerSetUpgrade($in,$minLength = 1) {
    $count = count($in);
    $members = pow(2,$count);
    $return = [];
    for ($i = 1; $i < $members; $i++) {
        $out = [];
        for ($j = 0; $j < $count; $j++) {
            if ($i>>$j&1) $out[] = $in[$j];
        }
        if (count($out) >= $minLength) {
            $return[] = $out;
        }
    }
    return $return;
}

与上面代码的区别有2点

  • 去掉了$b = sprintf("%0".$count."b",$i);
  • 把取1的判断条件由$b{$j} == '1'变成$i>>$j&1
    通过位运算符>>&来找出「1」所在位置下标,这种方法比上面的性能要好,但是不是很好理解。还拿[1,2,3]举例,对应关系如下
001  ---  [1]
010  ---  [2]
011   --- [1,2]
100  ---  [3]
101  ---  [1,3]
110  ---  [2,3]
111   --- [1,2,3]

这种方式从对应的关系来看不是很直观,和正常的顺序是相反的,比如上面的方法001找到的是第3个数字「3」,很直观,但是通过位运算符001找到的却是第1个数字「1」,不过最终结果是一样的。

参考

posted @ 2022-04-15 16:04  whyly  阅读(187)  评论(0编辑  收藏  举报