如何解决将N个人分成K个组:为什么这个算法ON ^ 2 * K的大O?
问题的描述及其解决方案可以在这里找到
https://www.geeksforgeeks.org/count-the-number-of-ways-to-divide-n-in-k-groups-incrementally/
问题基本上是由N个人组成的,您可以用多少种方法将其分为K个组,以便每个组的人数都大于或等于前一个人数?
解决方案是递归所有可能性,并且可以通过动态编程将其复杂度从O(N K )降低到O(N 2 * K)
我了解旧的递归解决方案的复杂性,但是对为什么动态编程解决方案具有O(N 2 * K)复杂性感到困难。如何得出关于动态编程解决方案时间复杂度的结论?任何帮助将不胜感激!
解决方法
首先,big O notation让我们对两个函数t(n)/i(n)
当n->无穷大时的关系有了一个概念。更具体地说,它是这种关系的上限,表示它是f(n) >= t(n)/i(n)
。 t(n)
代表执行时间的增长速度,i(n)
描述了输入增长的速度。在function space中(我们在那里使用函数而不是数字,并且将函数几乎像数字一样对待:例如,我们可以对它们进行除法或比较)两个元素之间的关系也是一个函数。因此,t(n)/i(n)
是一个函数。
第二,有两种确定该关系范围的方法。
科学的观察方法暗示了下一步。让我们看看用10个输入执行一个算法要花费多少时间。然后让我们最多增加100个输入,然后最多增加1000个,依此类推。输入i(n)
的增长速度是指数级的(10 ^ 1,10 ^ 2,10 ^ 3,...)。假设我们也得到了时间的指数增长速度(分别为10 ^ 1秒,10 ^ 2秒,10 ^ 3秒,...)。
这意味着t(n)/i(n) = exp(n)/exp(n) = 1
,n->无穷大(出于科学纯正的缘故,我们只能在n->无穷大时才对函数进行划分和比较,但这对方法的实用性没有任何意义)。我们至少可以说(请记住,这是一个上限)我们算法的执行时间不会比其输入的增长快。例如,我们可能已经获得了时间增长的二次指数速度。在那种情况下,t(n)/i(n) = exp^2(n)/exp(n) = a^2n/a^n = exp(n),a > 1
,n->无穷大,这意味着我们的时间复杂度为O(exp(n)),大的O表示法仅提醒我们这不是一个严格的界限。另外,值得指出的是,我们选择输入的增长速度并不重要。我们可能想线性增加输入。然后t(n)/i(n) = exp(n)*n/n = exp(n)
表示与t(n)/i(n) = exp^2(n)/exp(n) = a^2n/a^n = exp(n),a > 1
相同。这里重要的是商。
第二种方法是理论上的,主要用于对相对明显的案例进行分析。说,我们从example中获得了一段代码:
// DP Table
static int [][][]dp = new int[500][500][500];
// Function to count the number
// of ways to divide the number N
// in groups such that each group
// has K number of elements
static int calculate(int pos,int prev,int left,int k)
{
// Base Case
if (pos == k)
{
if (left == 0)
return 1;
else
return 0;
}
// if N is divides completely
// into less than k groups
if (left == 0)
return 0;
// If the subproblem has been
// solved,use the value
if (dp[pos][prev][left] != -1)
return dp[pos][prev][left];
int answer = 0;
// put all possible values
// greater equal to prev
for (int i = prev; i <= left; i++)
{
answer += calculate(pos + 1,i,left - i,k);
}
return dp[pos][prev][left] = answer;
}
// Function to count the number of
// ways to divide the number N in groups
static int countWaystoDivide(int n,int k)
{
// Intialize DP Table as -1
for (int i = 0; i < 500; i++)
{
for (int j = 0; j < 500; j++)
{
for (int l = 0; l < 500; l++)
dp[i][j][l] = -1;
}
}
return calculate(0,1,n,k);
}
这里首先要注意的是一个3维数组dp
。它给了我们关于DP算法的时间复杂度的提示,因为通常我们只遍历一次它。然后我们关注数组的大小。它的初始化大小为500*500*500
,因为500
是一个数字,而不是一个函数,所以并不能给我们带来太多好处,严格来说,它不依赖于输入变量。这样做是为了简单起见。实际上,假设dp
,k*n*n
的大小为k <= 500 and n <= 500
。
让我们证明一下。当static int calculate(int pos,int k)
保持不变时,方法pos
具有三个实际变量prev
,left
和k
。 pos
的范围是0 to k
,因为它从0
开始return calculate(0,k);
,并且基本情况是if (pos == k)
,prev
的范围是{ {1}},因为它从1 to left
开始,并在1
处迭代直到left
,最后for (int i = prev; i <= left; i++)
的范围是left
,因为它从{ {1}}在n to 0
,并迭代遍历n
在return calculate(0,k);
。概括地说,0
,for (int i = prev; i <= left; i++)
和pos
的可能组合的数量仅仅是其乘积prev
。
第二件事是证明left
,k*n*n
和pos
的每个范围仅被遍历一次。从代码中可以通过分析以下代码块来确定:
prev
所有3个变量仅在此处更改。 left
通过在每个步骤上添加for (int i = prev; i <= left; i++)
{
answer += calculate(pos + 1,k);
}
而从pos
成长。在0
的每个特定值上,对{{1}的每个特定值组合,将1
从pos
到prev
加1
来更改prev
}和left
,通过从pos
中减去范围为prev
的{{1}}来更改left
。
这种方法背后的思想是,一旦我们按照某种规则迭代输入变量,我们就会得到相应的时间复杂度。例如,我们可以通过在每个步骤上将范围减少两次来遍历元素上的变量。在这种情况下,我们将得到对数复杂度。或者我们可以踩到输入的每个元素,那么我们将得到线性复杂度。
换句话说,毫无疑问,我们根据常识假设每种算法的最小时间复杂度i
。意味着prev to left
和left
增长同样快。这也意味着我们对输入不做任何事情。一旦我们对输入进行处理,t(n)/i(n) = 1
就会比t(n)
大i(n)
倍。根据前几行中显示的逻辑,我们需要估算t(n)
。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。