ทำไมความซับซ้อนในการคำนวณ 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++;
            }
        }
    }
}

ฉันไม่เข้าใจว่า j = i, 2i, 3i ... สุดท้ายได้อย่างไร forวนซ้ำทำงาน n ครั้ง ฉันเดาว่าฉันไม่เข้าใจว่าเราสรุปifได้อย่างไร

แก้ไข: ฉันรู้วิธีคำนวณความซับซ้อนของลูปทั้งหมดยกเว้นว่าทำไมลูปสุดท้ายจึงเรียกใช้ i ตามตัวดำเนินการ mod ... ฉันแค่ไม่เห็นว่ามันเป็นอย่างไร โดยพื้นฐานแล้วทำไมถึงไม่ j% ฉันขึ้นไปที่ i * i มากกว่า i?


5
คุณสามารถลดความซับซ้อนของรหัสนี้โดยหลายขนาดใหญ่ปัจจัย คำแนะนำ : ผลรวมของตัวเลข 1 ถึง n คือ ((n + 1) * n) / 2 คำแนะนำ 2 : for (j = i; j < i *i; j += i)จากนั้นคุณไม่จำเป็นต้องทดสอบโมดูลัส (เพราะjรับประกันได้ว่าจะหารด้วยi)
Elliott Frisch

1
O () ฟังก์ชั่นเป็นฟังก์ชั่น ball-park ดังนั้นการวนซ้ำในตัวอย่างนี้จะเพิ่มความซับซ้อน ลูปที่สองวิ่งขึ้นไป n ^ 2 คำสั่ง if จะถูกละเว้น
Christoph Bauer

11
@ChristophBauer ifงบจะไม่ละเว้นอย่าง ifคำสั่งนี้หมายถึงความซับซ้อนคือ O (n ^ 4) แทนที่จะเป็น O (n ^ 5) เพราะมันทำให้เกิดการวนรอบด้านในสุดที่จะดำเนินการเพียงiครั้งเดียวแทนi*iเวลาสำหรับการทำซ้ำแต่ละครั้งของวงที่สอง
kaya3

1
@ kaya3 พลาดส่วนทั้งหมดk < n^2ดังนั้นจึงเป็น O (n ^ 5) แต่ความรู้ (จากการทำความเข้าใจif) แนะนำ O (n ^ 4)
Christoph Bauer

1
หากนี่ไม่ใช่แค่แบบฝึกหัดในชั้นเรียนให้เปลี่ยนลูปที่สองเป็น (int j = i; j <i * i; j + = i)
Cristobol Polychronopolis

คำตอบ:


49

มาติดฉลากลูป A, B และ 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 ( n ) ครั้ง
  • ห่วง B iterates O ( ฉัน2 ) ครั้งต่อซ้ำของ สำหรับแต่ละการทำซ้ำเหล่านี้:
    • j % i == 0 ถูกประเมินซึ่งใช้เวลา O (1)
    • เมื่อวันที่ 1 / iของการวนซ้ำวนซ้ำ C วนซ้ำjทำ O (1) ทำงานต่อการวนซ้ำ เนื่องจากjคือ O ( i 2 ) โดยเฉลี่ยและนี่เป็นเพียงการทำซ้ำ1 / iของการวนรอบ B ค่าใช้จ่ายเฉลี่ยคือ O ( i 2)  /  i ) = O ( i )

การคูณทั้งหมดเข้าด้วยกันเราจะได้ O ( n  ×  i 2  × (1 +  i )) = O ( n  ×  i 3 ) เนื่องจากฉันโดยเฉลี่ย O ( n ) นี่คือ O ( n 4 )


ส่วนที่ยุ่งยากของสิ่งนี้คือการบอกว่าifสภาพเป็นจริงเพียง 1 / iของเวลา:

โดยพื้นฐานแล้วทำไมถึงไม่ j% ฉันขึ้นไปที่ i * i มากกว่า i?

ในความเป็นจริงjจะขึ้นไปj < i * iไม่ใช่แค่ขึ้นอยู่กับj < iแต่ขึ้นอยู่กับ แต่เงื่อนไขj % i == 0เป็นจริงถ้าหากว่าjมีหลายตัวiเท่านั้น

คูณของiอยู่ในช่วงที่มีi, 2*i, 3*i, ... (i-1) * i, มีi - 1ของเหล่านี้จึงห่วง C ถึงi - 1ครั้งแม้จะห่วง B iterating i * i - 1ครั้ง


2
ใน O (n × i ^ 2 × (1 + i)) ทำไม 1 + i?
Soleil

3
เนื่องจากifเงื่อนไขใช้เวลา O (1) ทุกครั้งของการวนซ้ำของลูป B มันถูกครอบงำโดยลูป C ที่นี่ แต่ฉันนับมันด้านบนดังนั้นมันจึงเป็นแค่ "แสดงการทำงานของฉัน"
kaya3

16
  • การวนnซ้ำครั้งแรกใช้การวนซ้ำ
  • การวนn*nซ้ำครั้งที่สองจะใช้การวนซ้ำ ลองจินตนาการถึงกรณีที่แล้วi=nj=n*n
  • การวนnซ้ำครั้งที่สามใช้การวนซ้ำเนื่องจากมันดำเนินการเพียงiครั้งเดียวเท่านั้นซึ่งiจะถูกล้อมรอบnในกรณีที่เลวร้ายที่สุด

ดังนั้นความซับซ้อนของรหัสคือ O (n × n × n × n)

ฉันหวังว่านี่จะช่วยให้คุณเข้าใจ


6

คำตอบอื่น ๆ ทั้งหมดถูกต้องฉันแค่ต้องการแก้ไขต่อไปนี้ ฉันอยากจะดูว่าถ้าการลดการเอ็กซีคิวต์ของ k-loop นั้นเพียงพอแล้วที่จะลดความซับซ้อนที่แท้จริงด้านล่างO(n⁴).ดังนั้นฉันจึงเขียนสิ่งต่อไปนี้:

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);
}

หลังจากดำเนินการตามนี้จะเห็นได้ชัดว่าความซับซ้อนนั้นจริงn⁴แล้ว บรรทัดสุดท้ายของการส่งออกมีลักษณะเช่นนี้:

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

สิ่งนี้แสดงให้เห็นว่าความแตกต่างสัมพัทธ์จริงระหว่างจริงn⁴และความซับซ้อนของส่วนรหัสนี้เป็นปัจจัยเชิงเส้นกำกับที่มีค่า0.124... (จริง ๆ 0.125) แม้ว่ามันจะไม่ให้คุณค่าที่แน่นอนแก่เรา แต่เราสามารถอนุมานได้ดังนี้:

ความซับซ้อนเวลาเป็นสิ่งn⁴/8 ~ f(n)ที่fเป็นหน้าที่ของคุณ / วิธีการ

  • หน้าวิกิพีเดียในสัญลักษณ์ Big O ระบุไว้ในตารางของ 'Family of Bachmann – Landau notation' ที่~กำหนดขีด จำกัด ของตัวถูกดำเนินการทั้งสองด้านเท่ากัน หรือ:

    f เท่ากับ g asymptotically

(ฉันเลือก 363 เป็นขอบเขตบนที่ไม่รวมเนื่องจากn = 362เป็นค่าสุดท้ายที่เราได้รับผลลัพธ์ที่สมเหตุสมผลหลังจากนั้นเราเกินพื้นที่ยาวและค่าสัมพัทธ์กลายเป็นลบ)

ผู้ใช้ kaya3 คิดออกดังต่อไปนี้:

ค่าคงที่เชิงเส้นกำกับเท่ากับ 1/8 = 0.125 โดยวิธี; นี่คือสูตรที่แน่นอนผ่าน Wolfram Alpha


5
แน่นอน O (n⁴) * 0.125 = O (n⁴) การคูณรันไทม์ด้วยปัจจัยคงที่ในเชิงบวกจะไม่เปลี่ยนความซับซ้อนเชิงซีมโทติค
Ilmari Karonen

นี่เป็นเรื่องจริง อย่างไรก็ตามฉันพยายามที่จะสะท้อนความซับซ้อนที่เกิดขึ้นจริงไม่ใช่การประมาณการระดับบน เมื่อฉันพบว่าไม่มีไวยากรณ์อื่นสำหรับแสดงความซับซ้อนของเวลานอกเหนือจากสัญกรณ์ O ฉันก็กลับไปที่ อย่างไรก็ตามมันไม่สมเหตุสมผลเลยที่จะเขียนแบบนี้
TreffnonX

คุณสามารถใช้สัญกรณ์น้อยเพื่อบอกว่าเวลาซับซ้อนแค่ไหนn⁴/8 + o(n⁴)แต่ก็เป็นไปได้ที่จะแสดงออกอย่างเข้มงวดn⁴/8 + O(n³)กับโอตัวใหญ่
kaya3

@TreffnonX big OH เป็นแนวคิดทางคณิตศาสตร์ที่แข็งแกร่ง ดังนั้นสิ่งที่คุณทำคือผิด / ไม่มีความหมาย fundementally แน่นอนคุณมีอิสระที่จะกำหนดแนวคิดทางคณิตศาสตร์ใหม่ แต่นั่นเป็นหนอนขนาดใหญ่ที่คุณกำลังเปิดอยู่ วิธีการกำหนดในบริบทที่เข้มงวดขึ้นคือสิ่งที่อธิบายถึง kaya3 คุณจะสั่ง "ต่ำ" และกำหนดด้วยวิธีนั้น (แม้ว่าในวิชาคณิตศาสตร์คุณมักจะใช้การแลกเปลี่ยน)
paul23

คุณถูก. ฉันแก้ไขตัวเองอีกครั้ง เวลานี้ผมใช้การเจริญเติบโต asymtotic ขีด จำกัด เช่นเดียวกับที่กำหนดไว้ในครอบครัวของ Bachmann-รถม้าสัญลักษณ์บนen.wikipedia.org/wiki/Big_O_notation#Little-o_notation ฉันหวังว่าตอนนี้จะถูกต้องทางคณิตศาสตร์มากพอที่จะไม่ปลุกระดมการประท้วง;)
TreffnonX

2

ลบifและโมดูโลโดยไม่ต้องเปลี่ยนความซับซ้อน

นี่คือวิธีการดั้งเดิม:

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;
}

หากคุณสับสนโดยifand modulo คุณสามารถ refactor พวกเขาออกไปโดยjกระโดดจากโดยตรงiไป2*iยัง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;
}

เพื่อให้ง่ายยิ่งขึ้นในการคำนวณความซับซ้อนคุณสามารถแนะนำj2ตัวแปรตัวกลาง เพื่อให้ทุกตัวแปรลูปเพิ่มขึ้น 1 ในแต่ละการทำซ้ำ:

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;
}

คุณสามารถใช้การดีบักหรือโรงเรียนเก่าSystem.out.printlnเพื่อตรวจสอบว่าi, j, ktriplet เหมือนกันเสมอในแต่ละวิธี

การแสดงออกของรูปแบบปิด

ตามที่ผู้อื่นกล่าวถึงคุณสามารถใช้ความจริงที่ว่าผลรวมของn จำนวนเต็มแรกเท่ากับn * (n+1) / 2(ดูตัวเลขสามเหลี่ยม ) หากคุณใช้การทำให้เข้าใจง่ายนี้สำหรับทุกลูปคุณจะได้รับ:

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

เห็นได้ชัดว่ามันไม่ซับซ้อนเหมือนกับรหัสต้นฉบับ แต่มันกลับค่าเดียวกัน

หากคุณ google ข้อกำหนดแรกคุณสามารถสังเกตเห็นว่า0 0 0 2 11 35 85 175 322 546 870 1320 1925 2717 3731ปรากฏใน"หมายเลขสเตอร์ลิงชนิดแรก: s (n + 2, n)" มีการ0เพิ่มสองรายการในตอนเริ่มต้น ก็หมายความว่าsumเป็นจำนวน Stirling s(n, n-2)ของชนิดแรก


0

เรามาดูสองห่วงแรกกันก่อน

อันแรกง่าย ๆ มันวนลูปจาก 1 ถึง n อันที่สองน่าสนใจกว่า มันไปจาก 1 ถึง i กำลังสอง ลองดูตัวอย่าง:

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  

โดยรวมแล้วรวมมีi and j loops1^2 + 2^2 + 3^2
มีสูตรสำหรับผลรวมของสี่เหลี่ยม n แรกคือซึ่งเป็นประมาณ n * (n+1) * (2n + 1) / 6O(n^3)

คุณมีคนสุดท้ายk loopที่ลูปจาก 0 ถึงถ้าหากj j % i == 0ตั้งแต่jไปจาก 1 ถึงi^2, j % i == 0เป็นจริงสำหรับiครั้ง ตั้งแต่รอบi loopซ้ำnO(n)คุณมีหนึ่งเป็นพิเศษ

ดังนั้นคุณต้องO(n^3)จากi and j loopsและอื่น ๆO(n)จากk loopรวมที่ยิ่งใหญ่ของO(n^4)


ฉันรู้วิธีคำนวณความซับซ้อนของลูปทั้งหมดยกเว้นว่าทำไมลูปสุดท้ายจึงเรียกใช้งานฉันตามตัวดำเนินการ mod ... ฉันแค่ไม่เห็นว่ามันเป็นอย่างไร โดยพื้นฐานแล้วทำไมถึงไม่ j% ฉันขึ้นไปที่ i * i มากกว่า i?
11452926

1
@ user11452926 สมมติว่าฉันคือ 5 j จะไปจาก 1 ถึง 25 ในวงที่ 2 อย่างไรก็ตามj % i == 0เฉพาะเมื่อ j คือ 5, 10, 15, 20 และ 25 5 เท่าเช่นค่าของ i หากคุณจะเขียนตัวเลขจาก 1 ถึง 25 ใน 5 x 5 สี่เหลี่ยมเฉพาะคอลัมน์ที่ 5 จะมีตัวเลขหารด้วย 5 ซึ่งใช้ได้กับจำนวนใด ๆ ของ i วาดกำลังสองของ n ด้วย n โดยใช้ตัวเลข 1 ถึง n ^ 2 คอลัมน์ที่ n จะประกอบด้วยตัวเลขที่หารด้วย n คุณมี n แถวดังนั้นตัวเลข n จาก 1 ถึง n ^ 2 หารด้วย n
Silviu Burcea

ขอบคุณ! มีเหตุผล! เกิดอะไรขึ้นถ้ามันเป็นจำนวนโดยพลการเช่น 24 แทนที่จะเป็น 25, เคล็ดลับสแควร์จะยังคงทำงาน?
user11452926

25 มาถึงเมื่อiเข้าชม 5 ดังนั้นjลูปจาก 1 ถึง 25 คุณจะไม่สามารถเลือกหมายเลขที่ต้องการได้ หากลูปที่สองของคุณไปที่จำนวนคงที่เช่น 24 แทนi * iนั่นจะเป็นจำนวนคงที่และจะไม่ถูกผูกไว้nดังนั้นจะเป็นO(1)เช่นนั้น หากคุณกำลังคิดเกี่ยวj < i * iกับการเทียบกับj <= i * iที่จะไม่สำคัญเท่าที่จะมีnและn-1การดำเนินงาน แต่ในสัญกรณ์บิ๊กโอทั้งสองวิธีO(n)
Silviu Burcea
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.