LeetCode 1178 猜字谜 代码越短 越难搞 详解 sub = (sub-1)&k
猜个字谜,整个元宵节都不在状态了,果然代码越短越难搞。一个简单的&
操作,困在里面整整一天,一杯水,一包烟,一个与k算一天。不过还是走出来了!这里先行致歉,水平有限,折磨了一天也不能将子集的生成过程讲解出来,不是不懂,而是讲不出。这里抛砖引玉,希望来位大哥指路
二进制数的子集是什么
以 1 0 10 10_{10} 1010为例, 1 0 10 = 101 0 2 10_{10}=1010_2 1010=10102, 二进制表示中,我们关注bit位,它的子集满足
- 为1的bit位可以取0,也可以取1。可以改变 yes
- 为0的bit为不能改变。不能改变 no
也就是 1 y e s 0 n o 1 y e s 0 n o 1_{yes}0_{no}1_{yes}0_{no} 1yes0no1yes0no,共有 2 c o u n t 2^{count} 2count个子集,其中 c o u n t 为 b i t = 1 count为 bit=1 count为bit=1的位数,此处为2。
1 y e s 1_{yes} 1yes | 0 n o 0_{no} 0no | 1 y e s 1_{yes} 1yes | 0 n o 0_{no} 0no |
---|---|---|---|
1 | 0 | 1 | 0 |
1 | 0 | 0 | 0 |
0 | 0 | 1 | 0 |
0 | 0 | 0 | 0 |
热身操作 sub = (sub-1)&sub
注意,这里是 (sub-1)&sub,具体有什么区别马上开讲。
num=(num-1)&num
public static void enumBit(int num){
System.out.println(Integer.toBinaryString(num));
do{
num = (num-1)#
System.out.println(Integer.toBinaryString(num));
}while(num!=0);
}
public static void main(String[] args) {
int num = 12;
Problem1178.enumBit(num);
}
1100
1000
0
很容易看出,num=(num-1)&num 将 num 最右侧的1置0(二进制)。它是怎么实现的呢?我们先从 num-1 讲起。
num-1:
以num=24
为例
[
24
]
10
−
1
=
[
10100
]
2
−
1
=
[
10011
]
2
[24]_{10} - 1 = [10100]_2 - 1 = [10011]_2
[24]10−1=[10100]2−1=[10011]2,右侧为0部分会向高位借1,而右侧第一个1因为借给了低位变成0,而它右侧的0会变成1。
再次强调:(num-1)将num右侧第一个1置0,而其右侧的0变成1。我们再来看看 (num-1)&num是什么操作。
(num-1)&num
一个简单的&
操作,没有太多难处。建议不看配图,自己想想就好了
重要 重要 重要 当前操作位
MarkDown,为了讲解模拟sub=(sub-1)&k
的思路,费劲心思,百般折磨,回过头来添加的这个概念。在sub的二进制表示中,我们从右向左数,第几个bit为1就是第几个操作位
对应的do{ sub=(sub-1)&sub} while(sub!=0);
会执行4次,每次使其对应的操作位上的1置0。但我们代码中没有当前操作位这个变量,只是为了方便讲解
模拟 sub = (sub-1)&k 操作
概述思路
关于思路的讲解,我实在太难了,我不知道怎么讲,才能更简单明了。下面只是为了概述思路,*请大家忽略所有细节,不要疑惑 complete为什么等于1010000(二进制)?当前操作位为什么在这个位置?*怎样将sub_的操作位置0?
以 s u b = 8 5 10 = [ 1010101 ] 2 sub=85_{10}=[1010101]_2 sub=8510=[1010101]2,当前操作位取第三位为例
我们第4步求出来的结果是什么? 我描述不到位,题友们写出程序后,可以参考程序运行,体会结果。这里大概思想已经出来了 枚举+ 递归,我们枚举每一个操作位,递归的求解子集。
枚举过程
从右向左枚举每一个操作位,由于会将操作位上的1置0,所以不会产生重复解。而为什么刚好是解,描述不出来。菜是原罪
模拟 sub=(sub-1)&k的代码
public static List<Integer> enumBit(final int sub){
// 递归出口 0 只有一个子集
if(sub==0){
return Arrays.asList(0);
}
// 递归出口 1 有两个子集 1,0
if(sub==1){
return Arrays.asList(1,0);
}
// 变量初始化
int preComplete = sub;
int complete = sub;
int remain;
List<Integer> res = new ArrayList<>();
List<Integer> subRes=new ArrayList<>();
res.add(sub);
// 枚举每一个操作位
do {
// 进入下一个操作位,将complete的当前操作位从1置0, 从而不会产生重复的结果
complete = (complete-1)&complete;
// 求remain
remain = sub - preComplete;
// 递归 求remain子集合
subRes = enumBit(remain);
// 为子集合每个元素添加偏移,并添加到结果集
for (Integer subRe : subRes) {
res.add(subRe+complete);
}
// 维护变量
preComplete = complete;
} while (complete != 0);
return res;
}
十分抱歉,肝不动了。本想讲解的更详细的,奈何水平不够,已经懂的东西,想要讲解清楚,又上了一个难度啊。我们的模拟操作和&k执行结果是一样的,题友们可自行品味。