我在网上发现了许多具有O(2 ^ n)复杂度的解决方案。有人可以帮我弄清楚下面给出的代码的时间复杂性。它也涉及很多位操作,我在那个区域真的很弱,所以我没有完全掌握代码。如果有人能够解释代码,那就太好了。
private static void findSubsets(int array[])
{
int numOfSubsets = 1 << array.length;
for(int i = 0; i < numOfSubsets; i++)
{
int pos = array.length - 1;
int bitmask = i;
System.out.print("{");
while(bitmask > 0)
{
if((bitmask & 1) == 1)
System.out.print(array[pos]+",");
{
bitmask >>= 1;
pos--;
}
System.out.print("}");
}
}
这是最佳解决方案吗?
答案 0 :(得分:7)
此代码的工作原理是使用二进制数与正好n位和一组n个元素的子集之间的连接。如果将集合中的每个元素分配给单个位并将“1”表示“包含子集中的元素”而将“0”表示“从子集中排除元素”,则可以轻松地在两者之间进行转换。例如,如果集合包含a,b和c,那么100可能对应于子集a,011对应于子集bc等。尝试使用此洞察再次阅读代码。
就效率而言,上述代码在实际和理论上都非常快。任何列出子集的代码都必须花费大量时间来列出这些子集。所需时间与必须列出的元素总数成比例。此代码每列出一个项目花费O(1)工作,因此渐近最优(当然,假设没有那么多元素溢出使用的int)。
此代码的总复杂性可以通过计算打印的总元素数来确定。我们可以通过一些数学来解决这个问题。注意,有n个选择0个大小为0的子集,n选择1个大小为1的子集,n选择2个大小为2的子集,等等。因此,打印的元素总数由
给出C = 0×(n选择0)+ 1×(n选择1)+ 2×(n选择2)+ ... + n×(n选择n)
注意(n选择k)=(n选择n - k)。因此:
C = 0×(n选择n)+ 1×(n选择n - 1)+ 2×(n选择n - 2)+ ... + n×(n选择0)
如果我们将这两者加在一起,我们就得到了
2C = n×(n选择0)+ n×(n选1)+ ... + n×(n选n)
= n×(n选择0 + n选择1 + ... + n选择n)
二项式定理表明带括号的表达式是2 n ,所以
2C = n2 n
所以
C = n2 n-1
因此,确实打印了n2 n-1 元素,因此该方法的时间复杂度为Θ(n 2 n )。
希望这有帮助!
答案 1 :(得分:5)
这是另一种导出时间复杂度的方法(与@templatetypedef相比)。
让 M 成为代码中的步骤总数。你的外部for循环运行 2 N 次,内部运行 log(i)次,所以我们有:
将 2 提升到上述等式的每一边并简化:
取上述等式两边的 log ,并使用Sterling Approximation( Log(x!)~xLog(x) - x )
要解决位操作中的弱点,实际上并不需要它。它在您的代码中以三种方式使用,所有这些都可以用较少混淆的函数替换:
1 << array.length
)→(Math.pow(2, array.length)
)x >>= 1
)→(x /= 2
)(x & 1)
→(x % 2)
另外,为了解决代码正在做的事情,它实际上是使用显示here的方法将每个数字(0到2 N )转换为二进制形式,并作为@templatetypedef states,1表示相应的元素在集合中。以下是使用此方法将156转换为二进制的示例:
以您的代码为例:
pos = array.length - 1;
bitmask = 156; // as an example, take bitmask = 156
while(bitmask > 0){
if(bitmask%2 == 1) // odd == remainder 1, it is in the set
System.out.print(array[pos]+",");
bitmask /= 2; // divide by 2
pos--;
}
通过对所有位掩码执行此操作(0到2 N ),您将找到所有唯一可能的集合。
<强> 编辑: 强>
这里看一下英镑近似中的比率(n2 n / log 2 (2 n !)你可以看到它当n变大时,快速接近统一:
答案 2 :(得分:1)
我们说array.length
是n。此代码的工作原理是根据从0到2 ^ n的所有数字的二进制表示来选择或排除集合中的元素,这些数字正是集合的所有组合。
外部for循环的复杂度为O(2 ^ n),因为numOfSubsets = 1 << array.length
为2 ^ n。对于内部循环,您正在向右移动,最坏的情况是111 ... 1(n位设置为1),因此在最坏的情况下您将获得O(n)复杂度。因此总复杂度将为O(n *(2 ^ n))。
答案 3 :(得分:1)
https://github.com/yaojingguo/subsets提供了两种算法来解决子集问题。 void Notepad::on_pushButton_10_clicked(){
ui->lineEdit->setText("test");
}
算法与问题中给出的代码相同。 Iter
算法使用递归来访问每个可能的子集。两种算法的时间复杂度为Recur
。在Θ(n*2^n)
算法中,Iter
语句执行1
次。 n*2^n
语句执行2
(基于@ templatetypedef&#39; s分析)。使用n*2^(n-1)
表示a
的费用。并使用1
表示b
的费用。总费用为2
。
n*2^n*a + n*2^(n-1)*b
以下是if ((i & (1 << j)) > 0) // 1
list.add(A[j]); // 2
算法的主要逻辑:
Recur
对帐单result.add(new ArrayList<Integer>(list)); // 3
for (int i = pos; i < num.length; i++) { // 4
list.add(num[i]);
dfs(result, list, num, i + 1);
list.remove(list.size() - 1);
}
的费用3
与n*2^(n-1)*b
相同。另一个成本是1
循环。每个循环迭代包括三个函数调用。 4
总共执行4
次。使用2^n
表示d
的费用。总费用为4
。下图是此算法的递归树,其中包含2^n*d + n*2^(n-1)*b
。更精确的分析需要以不同方式处理{1, 2, 3, 4}
叶节点。
2^(n-1)
比较这两种算法的复杂性是将
Ø --- 1 --- 2 --- 3 --- 4
| | |- 4
| |- 3 --- 4
| |- 4
|- 2 --- 3 --- 4
| |- 4
|- 3 --- 4
|- 4
(1)与n*2^n*a
(2)进行比较。将(1)除以(2),我们得到2^n*d
。如果n * a / d
小于n*a
,则d
比Iter
快。我使用Recur
来衡量这两种算法的效率。以下是一次运行的结果:
Driver
它表明,对于小n: 16
Iter mills: 40
Recur mills: 19
n: 17
Iter mills: 78
Recur mills: 32
n: 18
Iter mills: 112
Recur mills: 10
n: 19
Iter mills: 156
Recur mills: 149
n: 20
Iter mills: 563
Recur mills: 164
n: 21
Iter mills: 2423
Recur mills: 1149
n: 22
Iter mills: 7402
Recur mills: 2211
,n
比Recur
快。