Java 把N个人分成K组:为什么这个算法的大O是O(N^2*K)?

Java 把N个人分成K组:为什么这个算法的大O是O(N^2*K)?,java,algorithm,recursion,dynamic-programming,complexity-theory,Java,Algorithm,Recursion,Dynamic Programming,Complexity Theory,有关问题及其解决方案的说明,请参见此处 基本上,问题是给定N个人,你有多少种方法可以将他们分成K组,这样每个组的人数都大于或等于之前的一组 解决方案是通过各种可能性进行递归,通过动态规划可以将其复杂性从O(NK)降低到O(N2*K) 我理解旧递归解决方案的复杂性,但很难理解为什么动态规划解决方案具有O(N2*K)复杂性。关于动态规划解的时间复杂性,人们是如何得出这个结论的?任何帮助都将不胜感激 首先,给出了当n->无穷大时两个函数t(n)/i(n)之间的关系。更具体地说,它是这种关系的上界,也

有关问题及其解决方案的说明,请参见此处

基本上,问题是给定N个人,你有多少种方法可以将他们分成K组,这样每个组的人数都大于或等于之前的一组

解决方案是通过各种可能性进行递归,通过动态规划可以将其复杂性从O(NK)降低到O(N2*K)

我理解旧递归解决方案的复杂性,但很难理解为什么动态规划解决方案具有O(N2*K)复杂性。关于动态规划解的时间复杂性,人们是如何得出这个结论的?任何帮助都将不胜感激

首先,给出了当n->无穷大时两个函数
t(n)/i(n)
之间的关系。更具体地说,它是这种关系的上界,也就是说它是
f(n)>=t(n)/i(n)
t(n)
代表执行时间的增长速度,
i(n)
描述输入的增长速度。在中(我们处理函数而不是数字,并且将函数视为数字:例如,我们可以划分或比较它们),两个元素之间的关系也是一个函数。因此,
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相同的表达式。这里重要的是商

第二种方法是理论方法,主要用于分析相对明显的案例。比如说,我们有一段代码来自:

所有3个变量仅在此处更改
pos
通过在每个步骤上添加
1
0
增长。在
pos
的每个特定值上,通过将
1
prev
添加到
left
来更改
prev
,在
pos
prev
的每个特定值组合上,通过减去
i
来更改
left
,其范围为
prev
,从
左侧开始

这种方法背后的思想是,一旦我们按照某种规则迭代输入变量,我们就会得到相应的时间复杂度。例如,我们可以通过在每个步骤中将范围减小两次来迭代元素上的变量。在这种情况下,我们将得到对数复杂度。或者我们可以对输入的每个元素进行处理,然后得到线性复杂度


换句话说,我们毫无疑问地假设每个算法的最小时间复杂度
t(n)/i(n)=1
。这意味着
t(n)
i(n)
增长同样快。这也意味着我们对输入不做任何处理。一旦我们对输入做了一些事情,
t(n)
就会变成
f(n)
i(n)
大几倍。根据前几行中显示的逻辑,我们需要估计
f(n)

我认为这可能会简化为硬币兑换问题,使用动态规划方法需要O(nk)复杂性。
// 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); 
} 
for (int i = prev; i <= left; i++)  
{ 
    answer += calculate(pos + 1, i,  
                       left - i, k); 
}