Por que a complexidade computacional O (n ^ 4)?

50
int sum = 0;
for(int i = 1; i < n; i++) {
    for(int j = 1; j < i * i; j++) {
        if(j % i == 0) {
            for(int k = 0; k < j; k++) {
                sum++;
            }
        }
    }
}

Eu não entendo como quando j = i, 2i, 3i ... o último forloop é executado n vezes. Acho que simplesmente não entendo como chegamos a essa conclusão com base na ifdeclaração.

Edit: Eu sei como calcular a complexidade de todos os loops, exceto por que o último loop é executado i vezes com base no operador mod ... Eu simplesmente não vejo como é eu. Basicamente, por que não posso subir até i * i em vez de eu?

user11452926
fonte
5
Você pode reduzir a complexidade desse código por vários fatores grandes . Dica : A soma dos números de 1 a n é ((n + 1) * n) / 2 Dica 2 : for (j = i; j < i *i; j += i)então você não precisa do teste de módulo (porque jé garantido que é divisível por i).
Elliott Frisch
11
A função O () é uma função ball-park, portanto, qualquer loop neste exemplo está aumentando a complexidade. O segundo loop está executando até n ^ 2. As instruções if são ignoradas.
Christoph Bauer
11
As ifdeclarações @ChristophBauer não são absolutamente ignoradas. Essa ifdeclaração significa que a complexidade é O (n ^ 4) em vez de O (n ^ 5), porque faz com que o loop mais interno execute apenas os itempos em vez de os i*itempos para cada iteração do segundo loop.
kaya3
11
@ kaya3 perdeu totalmente a parte. k < n^2Então é O (n ^ 5), mas o conhecimento (ao entender o if) sugere O (n ^ 4).
Christoph Bauer
11
Se este não é apenas um exercício de classe, altere o segundo loop para for (int j = i; j <i * i; j + = i)
Cristobol Polychronopolis

Respostas:

49

Vamos rotular os loops A, B e C:

int sum = 0;
// loop A
for(int i = 1; i < n; i++) {
    // loop B
    for(int j = 1; j < i * i; j++) {
        if(j % i == 0) {
            // loop C
            for(int k = 0; k < j; k++) {
                sum++;
            }
        }
    }
}
  • O loop A itera O ( n ) vezes.
  • Laço B itera O ( i 2 ) vezes por iteração de um . Para cada uma dessas iterações:
    • j % i == 0 é avaliado, o que leva tempo O (1).
    • Em 1 / i dessas iterações, o loop C itera j vezes, realizando O (1) trabalho por iteração. Uma vez que j é O ( i 2 ) em média, e este só é feito por 1 / i iterações do ciclo B, o custo médio é O ( i 2  /  i ) = O ( i ).

Multiplicando tudo isso juntos, obtemos O ( n  ×  i 2  × (1 +  i )) = O ( n  ×  i 3 ). Como i é, em média, O ( n ), este é O ( n 4 ).


A parte complicada disso é dizer que a ifcondição é verdadeira apenas 1 / i do tempo:

Basicamente, por que não posso subir até i * i em vez de eu?

De fato, jsobe j < i * i, não apenas j < i. Mas a condição j % i == 0é verdadeira se e somente se jfor um múltiplo de i.

Os múltiplos de identro do intervalo são i, 2*i, 3*i, ..., (i-1) * i. Existem i - 1esses, portanto, o loop C é atingido i - 1vezes apesar dos i * i - 1tempos de iteração do loop B.

kaya3
fonte
2
Em O (n × i ^ 2 × (1 + i)) por que 1 + i?
Soleil
3
Como a ifcondição leva tempo O (1) em todas as iterações do loop B. Ela é dominada pelo loop C aqui, mas eu contei isso acima, então está apenas "mostrando meu trabalho".
kaya3 12/02
16
  • O primeiro loop consome niterações.
  • O segundo loop consome n*niterações. Imagine o caso quando i=n, então j=n*n.
  • O terceiro loop consome niterações porque é executado apenas algumas ivezes, onde ié limitado nno pior dos casos.

Assim, a complexidade do código é O (n × n × n × n).

Espero que isso ajude você a entender.

Mohammed Deifallah
fonte
6

Todas as outras respostas estão corretas, só quero alterar o seguinte. Eu queria ver se a redução de execuções do k-loop interno era suficiente para reduzir a complexidade real abaixo. O(n⁴).Então, escrevi o seguinte:

for (int n = 1; n < 363; ++n) {
    int sum = 0;
    for(int i = 1; i < n; ++i) {
        for(int j = 1; j < i * i; ++j) {
            if(j % i == 0) {
                for(int k = 0; k < j; ++k) {
                    sum++;
                }
            }
        }
    }

    long cubic = (long) Math.pow(n, 3);
    long hypCubic = (long) Math.pow(n, 4);
    double relative = (double) (sum / (double) hypCubic);
    System.out.println("n = " + n + ": iterations = " + sum +
            ", n³ = " + cubic + ", n⁴ = " + hypCubic + ", rel = " + relative);
}

Depois de executar isso, torna-se óbvio que a complexidade é de fato n⁴. As últimas linhas de saída são assim:

n = 356: iterations = 1989000035, n³ = 45118016, n = 16062013696, rel = 0.12383254507467704
n = 357: iterations = 2011495675, n³ = 45499293, n = 16243247601, rel = 0.12383580700180696
n = 358: iterations = 2034181597, n³ = 45882712, n = 16426010896, rel = 0.12383905075183874
n = 359: iterations = 2057058871, n³ = 46268279, n = 16610312161, rel = 0.12384227647628734
n = 360: iterations = 2080128570, n³ = 46656000, n = 16796160000, rel = 0.12384548432498857
n = 361: iterations = 2103391770, n³ = 47045881, n = 16983563041, rel = 0.12384867444612208
n = 362: iterations = 2126849550, n³ = 47437928, n = 17172529936, rel = 0.1238518469862343

O que isso mostra é que a diferença relativa real entre o valor real n⁴ e a complexidade desse segmento de código é um fator assintótico em relação a um valor próximo 0.124...(na verdade, 0,125). Embora não nos dê o valor exato, podemos deduzir o seguinte:

A complexidade do tempo é n⁴/8 ~ f(n)ondef está sua função / método.

  • A página da wikipedia na notação Big O indica nas tabelas das 'notações Família de Bachmann – Landau' que a ~ definição do limite dos dois lados do operando é igual. Ou:

    f é igual a assintoticamente

(Escolhi 363 como limite superior excluído, porque n = 362 é o último valor para o qual obtemos um resultado razoável. Depois disso, excedemos o espaço longo e o valor relativo se torna negativo.)

O usuário kaya3 descobriu o seguinte:

A constante assintótica é exatamente 1/8 = 0,125, a propósito; aqui está a fórmula exata via Wolfram Alpha .

TreffnonX
fonte
5
Obviamente, O (n⁴) * 0,125 = O (n⁴). Multiplicar o tempo de execução por um fator constante positivo não altera a complexidade assintótica.
Ilmari Karonen
Isso é verdade. No entanto, eu estava tentando refletir a complexidade real, não a estimativa superior. Como não encontrei outra sintaxe para expressar a complexidade do tempo além da notação O, voltei a esse ponto. No entanto, não é 100% sensato escrevê-lo assim.
TreffnonX 12/02
Você pode usar pouca notação para dizer que a complexidade do tempo é n⁴/8 + o(n⁴), mas é possível dar uma expressão mais rígida n⁴/8 + O(n³)com O grande de qualquer maneira.
kaya3 12/02
@TreffnonX big OH é um conceito sólido matemático. Então, o que você está fazendo é fundamentalmente errado / sem sentido. É claro que você pode redefinir conceitos matemáticos, mas essa é uma grande lata de minhocas que você está abrindo. A maneira de defini-la em um contexto mais rigoroso é o que o kaya3 descreveu, você segue uma ordem "inferior" e a define dessa maneira. (Embora em matemática você normalmente use o recíproco).
paul23 12/02
Você está certo. Eu me corrigi novamente. Desta vez, uso o crescimento assintótico no mesmo limite, conforme definido nas notações da Família de Bachmann-Landau em en.wikipedia.org/wiki/Big_O_notation#Little-o_notation . Espero que agora esteja matematicamente correto o suficiente para não provocar revolta;)
TreffnonX
2

Retirar if e module sem alterar a complexidade

Aqui está o método original:

public static long f(int n) {
    int sum = 0;
    for (int i = 1; i < n; i++) {
        for (int j = 1; j < i * i; j++) {
            if (j % i == 0) {
                for (int k = 0; k < j; k++) {
                    sum++;
                }
            }
        }
    }
    return sum;
}

Se você está confuso com o ifmódulo e, basta refatorá-lo, jsaltando diretamente de ipara 2*ipara 3*i...:

public static long f2(int n) {
    int sum = 0;
    for (int i = 1; i < n; i++) {
        for (int j = i; j < i * i; j = j + i) {
            for (int k = 0; k < j; k++) {
                sum++;
            }
        }
    }
    return sum;
}

Para facilitar ainda mais o cálculo da complexidade, é possível introduzir uma j2variável intermediária , para que cada variável de loop seja incrementada em 1 a cada iteração:

public static long f3(int n) {
    int sum = 0;
    for (int i = 1; i < n; i++) {
        for (int j2 = 1; j2 < i; j2++) {
            int j = j2 * i;
            for (int k = 0; k < j; k++) {
                sum++;
            }
        }
    }
    return sum;
}

Você pode usar a depuração ou a velha escola System.out.printlnpara verificar se o i, j, ktrigêmeo é sempre o mesmo em cada método.

Expressão de formulário fechado

Conforme mencionado por outros, você pode usar o fato de que a soma dos primeiros n números inteiros é igual a n * (n+1) / 2(consulte números triangulares ). Se você usar essa simplificação para cada loop, obterá:

public static long f4(int n) {
    return (n - 1) * n * (n - 2) * (3 * n - 1) / 24;
}

Obviamente, não é a mesma complexidade que o código original, mas retorna os mesmos valores.

Se você pesquisar os primeiros termos no Google, poderá notar que eles 0 0 0 2 11 35 85 175 322 546 870 1320 1925 2717 3731aparecem em "Números Stirling do primeiro tipo: s (n + 2, n)". , com dois 0s adicionados no início. Isso significa que sumé o número de Stirling do primeiro tipo s(n, n-2) .

Eric Duminil
fonte
0

Vamos dar uma olhada nos dois primeiros loops.

O primeiro é simples, está repetindo de 1 a n. O segundo é mais interessante. Vai de 1 a i ao quadrado. Vamos ver alguns exemplos:

e.g. n = 4    
i = 1  
j loops from 1 to 1^2  
i = 2  
j loops from 1 to 2^2  
i = 3  
j loops from 1 to 3^2  

No total, o i and j loopscombinado tem 1^2 + 2^2 + 3^2.
Existe uma fórmula para a soma dos primeiros n quadrados n * (n+1) * (2n + 1) / 6, que é aproximadamenteO(n^3) .

Você tem um último k loopque executa um loop de 0 a jse e somente se j % i == 0. Desde jvai de 1 a i^2, j % i == 0é verdade por ivezes. Como a i loopiteração termina n, você tem um extra O(n).

Então você tem O(n^3)a partir i and j loopse outra O(n)a partir k loopde um total deO(n^4)

Silviu Burcea
fonte
Eu sei como calcular a complexidade de todos os loops, exceto por que o último loop é executado i vezes com base no operador mod ... Eu simplesmente não vejo como é eu. Basicamente, por que não posso subir até i * i em vez de eu?
user11452926 11/02
11
@ user11452926 digamos que eu tinha 5. j passaria de 1 a 25 no 2º loop. No entanto, j % i == 0somente quando j for 5, 10, 15, 20 e 25. 5 vezes, como o valor de i. Se você escrever os números de 1 a 25 em 5 x 5 quadrados, apenas a quinta coluna conterá os números divisíveis por 5. Isso funciona para qualquer número de i. Desenhe um quadrado de n por n usando os números 1 a n ^ 2. A enésima coluna conterá os números divisíveis por n. Você tem n linhas, portanto n números de 1 a n ^ 2 são divisíveis por n.
Silviu Burcea
Obrigado! faz sentido! E se fosse um número arbitrário como 24 e não 25, o truque quadrado ainda funcionará?
user11452926 11/02
25 chega quando iatinge 5; portanto, os jloops de 1 a 25, você não pode escolher um número arbitrário. Se o seu 2º loop for para um número fixo, por exemplo, 24, em vez de i * i, esse seria um número constante e não estaria vinculado n, como seria O(1). Se você está pensando em j < i * ivs. j <= i * i, isso não importará muito, pois haverá operações ne n-1, mas na notação Big-oh, os dois meiosO(n)
Silviu Burcea