(力扣)
前提:
这个数组的子集进行或运算能得到最大的数肯定是全部数一起或,秉承或运算不要白不要的原则。
第一种方法:二进制枚举
这种方法其实我觉得是为了引出下面两种更方便的方法提出的
用二进制位来存储各个数是否加入或运算
假如nums数组是n个数字,那么我们只需要遍历1<<16种情况
每一种情况中,我们根据位数上的数是否是1来确定是否要加入或运算
public int countMaxOrSubsets1(int[] nums) { int kind=1<<nums.length; int max=0; for (int num : nums) { max|=num; //最大数肯定是全部进行或 } int ans=0; for (int i = 1; i < kind; i++) { int temp=0; for (int j = 0; j < nums.length; j++) { if((i>>j&1)==1) temp|=nums[j]; //j位为1代表j要参与异或 } if(temp==max) ans++; //如果或之后的值为最大值 ans++ } return ans; }
第二种 状态压缩DP
分析第一种方法,可以发现我们重复计算了 相同的数进行或运算
[例子]: 10101 10100 这两种情况,10100知道了,自然就可以知道10101
得到最右边一位的1的位置
0000100 i
0101100 当前的选择代表的数 cur
0101000 pre
result[cur] 代表当前选择所得到的或操作结果
有公式:
result[cur]=result[pre]|nums[i]
所以我们只要保证i的选择是从小到大就行了
static HashMap<Integer,Integer> map; static { map=new HashMap<>(); for (int i = 0; i < 16; i++) { map.put(1<<i,i); } } public int countMaxOrSubsets2(int[] nums){ int max=0;int ans=0; for (int num : nums) { max|=num; //最大数肯定是全部进行或 } int kind=1<<nums.length; int[] result=new int[kind+1]; Arrays.fill(result,0); //用来存放前面遍历过的情况的结果,例如下标3代表00011即只选第一个和第二个的情况 for (int i = 1; i < kind; i++) { int num010=i&-i;//得到最右边的1(为什么是最右边,看note.md) int index=map.get(num010); int pre=i-num010; result[i]=result[pre]|nums[index]; if(result[i]==max) ans++; } return ans; }
第二种做法,有一个细节是找到最右边的位,这个点我思考了一会,觉得左边会更合理,
比如 得到result[00001]
那么自然 00011就是选择2与result[00001]进行或操作
但是比起求最左边的1,求最右边的1更加方便,并且我们只要知道小的就可以求大的
因为pre肯定比cur要小
result[cur]=result[pre]|nums[i]
第三种 dfs
依靠递归来记录每次的结果
int max=0;int ans=0; public int countMaxOrSubsets(int[] nums){ for (int num : nums) { max|=num; //最大数肯定是全部进行或 } dfs(0,0,nums); return ans; } public void dfs(int i,int preResult,int[] nums){ if(i== nums.length&&preResult==max) ans++; //一定要等到所有情况都遍历完了再判断,不然前面判断,后面又判断,会多算那些后面是0的情况 else if(i<nums.length){ //if-else if这两个分支不是非此即彼的情况:i==nums.length but preResult!=max dfs(i + 1, preResult, nums); dfs(i + 1, preResult | nums[i], nums); } }