题目描述

集合 s 中有整数 1 到 n,牛牛想从中挑几个整数组成一个新的集合。
现在牛妹给牛牛加了 m 个限制 ,每个限制包含两个整数 u 和 v ,且 u 和 v 不能同时出现在新集合中 。请问牛牛能组成的新集合多少种。可以选 0 个数。返回一个整数,即新集合的种类数。
示例1
输入:3,2,[(1,2),(2,3)]
返回值:5
说明:当 n = 3 时,共有 8 个子集,当加上限制 (1, 2), (2, 3) 后,合法的自己有 [], [1], [2], [3], [1, 3] 共 5 个

题目分析

对于n个不同的整数,它们能够组成的集合总数为2^n个,题目在此基础上增加了一些限制,基本的思路是将所有可能的情况记录下来,然后每种情况都做一次限制的判断,若满足则记录个数。如下图,对例题的集合分析。
图片说明

解题思路

方法1:使用数组记录使用过的数字,dfs遍历所有可能的情况,每个数字可以有选择和不选择两种情况,记录满足条件的情况数
1.因为n不超过20,所以可以定义一个长度为21的数组:boolean[] used = new boolean[21]; // boolean[i]表示i数字是否被加入集合中;
2.dfs的过程如下:

dfs(n, x, limit){
  if(n>x) return;
  // 选择x加入集合,used[x] = true;
  dfs(n, x+1, limit);
  // 不选择x加入集合,used[x] = false;
  dfs(n, x+1, limit);
}

方法2:使用一个数直接代替数字的集合,0~2^n刚好是数字的所有集合情况,对每种情况直接判断记录满足限制条件的集合
此方法与方法1思想类似,都是遍历所有的集合情况,不过这里直接用到了一个int变量 mask来表示集合情况(mask的二进制位置上是1的表明在集合中),mask的取值范围是0~2^n;

代码实现

方法1:穷举递归所有可能的情况

    // 记录数字是否被使用过,数字最大为20
    boolean[] used = new boolean[21];
    int res = 0;
    public int solve (int n, int m, Point[] limit) {
        // write code here
        // 从1开始
        dfs(n, 1, limit);
        return res;
    }

    public void dfs(int n, int x, Point[] limit){
        if(x>n){
            // 说明集合选择完成,判断集合中的数字是否满足限制
            for(int i=0;i<limit.length;i++){
                if(used[limit[i].x] && used[limit[i].y]){
                    // 不满足条件直接返回
                    return;
                }
            }
            res++;
            return;
        }
        // 选择x
        used[x] = true;
        dfs(n, x+1, limit);
        // 不选择x
        used[x] = false;
        dfs(n, x+1, limit);
    }

时间复杂度:,所有的数字组合情况为2^n种,每种都要进行最多m的次限制条件判断,总共花费的时间为2^n*m;
空间复杂度:,集合最多有n个数字,递归深度为n;

方法2:使用数字代替穷举情况,直接对每种情况进行判断

    public int solve (int n, int m, Point[] limit) {
        // write code here
        int res = 0;
        for(int mask=0;mask<(1<<n);mask++){
            // mask 从0 到 2^n,可以表示成数字的所有集合
            boolean valid = true;
            for(Point point:limit){
                // 对于每个数字集合,需要判断是否满足条件
                if(isvalid(mask, point.x,point.y)){
                    valid = false;
                    break;
                }
            }
            // 当情况满足条件,则结果数加1
            if(valid){
                res++;
            }
        }
        return res;
    }

    public boolean isvalid(int mask, int u, int v){
        // 检验u和v是否同时出现在mask中(看mask中的u对应的位置是否是1)
        if(((mask>>(u-1))&1) == 1 && ((mask>>(v-1))&1) == 1){
            return true;
        }
        return false;
    }

时间复杂度:,所有的数字组合情况为2^n种,每种都要进行最多m的次限制条件判断,总共花费的时间为2^n*m;
空间复杂度:,只需要常数级别的变量空间;