คู่มือโอลิมปิกคอมพิวเตอร์

Computer Olympiad Guide for Thai High School Students

Big-O Notation

  • introduction

  • time complexity

  • space complexity

การเขียนโปรแกรมแข่งขันประกอบด้วย

  • การออกแบบอัลกอริทึม

  • การ implement สร้างอัลกอริทึม

การออกแบบอัลกอริทึม

  • ใช้ทักษะ problem solving

  • และ mathematical thinking

  • โดยจำเป็นต้องวิเคราะห์ปัญหาและแก้ปัญหาอย่างสร้างสรรค์

  • อัลกอริทึมที่ใช้แก้ปัญหาจะต้องถูกต้องแล้วมีประสิทธิภาพ

  • หลักสำคัญของโจทย์ปัญหาส่วนใหญ่จะเป็นการคิดสร้างสรรค์อัลกอริทึมที่มีประสิทธิภาพ

  • ความรู้ทางทฤษฎีของอัลกอริทึมเป็นสิ่งสำคัญต่อนักเขียนโปรแกรมเชิงแข่งขัน

  • โดยทั่วไปแล้ว เฉลย (solution) ของปัญหาหนึ่งนั้นจะเป็นการใช้เทคนิคที่แพร่หลายร่วมกับ insight ใหม่ตามโจทย์

  • เทคนิคในการเขียนโปรแกรมเชิงแข่งขันยังเป็นพื้นฐานในการวิจัยสายอัลกอริทึมอีกด้วย

การ implement สร้างอัลกอริทึม

  • ใช้ทักษะการเขียนโปรแกรมที่ดี

  • ในการเขียนโปรแกรมเชิงแข่งขันนั้น เฉลยจะถูกตรวจสอบอัลกอริทึมด้วยชุดทดสอบ

  • ดังนั้น การที่ไอเดียของอัลกอริทึมถูกต้องเท่านั้นยังไม่เพียงพอ แต่การ implement สร้างอัลกอริทึมจะต้องถูกต้องด้วยเช่นกัน

  • style ในการเขียนโค้ดในการแข่งขันจะตรงไปตรงมาและกระทัดรัด

  • โปรแกรมควรถูกเขียนอย่างรวดเร็ว เพราะเวลาจะมีอยู่อย่างจำกัด

การวิเคราะห์อัลกอรึทึม

  • ปัญหาที่กำหนดให้สามารถแก้ปัญหาด้วยอัลกอริทึมหลายแบบในสภาพเงื่อนไขเดียวกัน

  • จำเป็นต้องเรียนรู้วิธีการเปรียบเทียบประสิทธิภาพการทำงานของแต่ละอัลกอริทึม

เพื่อพิจารณาเลือกใช้ได้อย่างถูกวิธี

Motivation problem

  • เราจะรู้ได้อย่างไรว่าโค้ดใดนี้มีประสิทธิภาพที่ดีกว่ากัน ระหว่าง

for (int i = 1; i <= n; i++) {
  for (int j = i + 1; j <= n; j++) {
    // code
  }
}
  • และ

for (int i = 1; i <= n; i++) {
  for (int j = 1; j <= n; j += i) {
    // code
  }
}

ประสิทธิภาพของอัลกอริทึม

  • สามารถแบ่งได้เป็นสองประเภท คือ

    • การใช้เนื้อที่ในหน่วยความจำ หรือความต้องการในการใช้เนื้อที่ของความจำในการเก็บข้อมูล

    • ประสิทธิภาพของเวลาในการทำงาน หรือระยะเวลาที่ใช้ในการประมวลผลข้อมูล

  • โดยทั่วไปแล้วประสิทธิภาพทั้งสองประเภทจะพัฒนาไปพร้อมกันได้ยาก

และส่วนมากจะให้จความสำคัญกับการใช้เวลาอย่างมีประสิทธิภาพมากกว่าเนื้อที่

  • Big O Notation หรืออันดับขนาด (order of magnitude) เป็นเครื่องมือหลักในการวิเคราะห์อัลกอริทึม

Time complexity

  • ประสิทธิภาพของอัลกอริทึมเป็นสิ่งสำคัญในการเขียนโปรแกรมเชิงแข่งขัน

  • โดยทั่วไปนั้นจะง่ายที่จะออกแบบอัลกอริทึมที่แก้ปัญหาได้ช้า แต่ความท้าทายจะอยู่ที่อยากออกแบบอัลกอริทึมที่รวดเร็ว

  • หากอัลกอริทึมนั้นช้าเกินไป จะได้รับคะแนนบางส่วนหรรือไม่ได้เลย

  • การคาดการณ์เวลาที่อัลกอริทึมใช้สำหรับ input ต่าง ๆ

  • หลักการคือการแสดงประสิทธิภาพในรูปแบบของฟังก์ชันโดยมีพารามิเตอร์เป็นขนาดของ input

  • ด้วยการคำนวณ time complexity เราสามารถตรวจสอบความเร็วของอัลกอริทึมได้โดยไม่ต้อง implement ขึ้นมาก่อน

กฏของการคำนวณ

  • time complexity ของอัลกอริทึมหนึ่งจะถูกแทนด้วยสัญลักษณ์ O(⋯) โดยจุดสามจุดแสดงถึงฟังก์ชันใด ๆ

  • โดยทั่วไปแล้ว ตัวแปร n แสดงถึงขนาดของ input

  • ตัวอย่างเช่น หาก input เป็นอาเรย์ของตัวเลข n จะเป็นขนาดของอาเรย์นั้น และหาก input เป็น string n จะเป็นความยาวของ string นั้น

การวนซ้ำ

  • เหตุผลที่พบบ่อยจากการที่อัลกอริทึมช้าเกินไปเกิดจากการใช้การวนซ้ำจำนวนมากสำหรับ input

  • ยิ่งวนซ้ำหลายชั้น อัลกอริทีมจะยิ่งช้า

  • หากมีการวนซ้ำ k ชั้น time complexity จะเป็น O(nk)

ตัวอย่างเช่น time complexity ของโค้ดด้านล่างจะเป็น O(n)

for (int i = 1; i <= n; i++) {
  // code
}

และ time complexity ของโค้ดด้านล่างนี้ O(n2)

for (int i = 1; i <= n; i++) {    // ชั้นที่ 1
  for (int j = 1; j <= n; j++) {  // ชั้นที่ 2
    // code
  }
}

Order of magnitude

  • time complexity จะไม่สนใจจำนวนรอบเป๊ะ ๆ ภายใน loop แต่จะสนใจเฉพาะ order of magnitude

  • ในตัวอย่างต่อไปนี้ จำนวนรอบในการวนเป็น 3n, n + 5 และ $l\frac{n}{2}r$ ครั้ง แต่ time complexity นั้นเท่ากันคือ O(n)

for (int i = 1; i <= 3*n; i++) {
  // code
}
for (int i = 1; i <= n+5; i++) {
  // code
}
for (int i = 1; i <= n; i += 2) {
  // code
}

ในอีกตัวอย่างหนึ่ง time complexity ของโค้ดต่อไปนี้เป็น O(n2)

for (int i = 1; i <= n; i++) {
  for (int j = i+1; j <= n; j++) {
   // code
  }
}

Phase

  • ถ้าอัลกอริทึมประกอบไปด้วยหลาย phase ติดต่อกันแล้ว complexity จะเป็น time complexity ที่สูงสุดของ phase หนึ่ง

  • เนื่องจาก bottleneck จะอยู่ที่ phase ที่ช้าที่สุด

  • ตัวอย่างเช่น โค้ดต่อไปนี้ประกอบด้วย 3 phases ที่มี time complexity O(n), O(n2) และ O(n)

  • ดังนั้นคอขวดจะเป็น O(n2)

for (int i = 1; i <= n; i++) {
  // code O(n)
}
for (int i = 1; i <= n; i++) {
  for (int j = 1; j <= n; j++) {
    // code O(n^2)
  }
}
for (int i = 1; i <= n; i++) {
  // code O(n)
}

กรณีหลายตัวแปร

  • บางครั้ง time complexity จะขึ้นอยู่กับหลายปัจจัย

  • ซึ่งเราสามารถใช้หลายตัวแปรแทนได้

  • ตัวอย่างเช่น time complexity ของโค้ดด้านล่างเป็น O(nm)

for (int i = 1; i <= n; i++) {
  for (int j = 1; j <= m; j++) {
    // code
  }
}

Recursion

  • time complexity ของฟังก์ชัน recursive จะขึ้นอยู่กับจำนวนครั้งที่ฟังก์ชันนั้นถูกเรื่องและ time complexity ของการเรียกฟังก์ชันแต่ละครั้ง

  • ซึ่งจะนำสองค่านี้มาคูณกัน

  • ยกตัวอย่างเช่น

void f(int n) {
  if (n == 1) return;
  f(n-1);
}
  • การเรียกฟังก์ชัน f(n) จะเรียกฟังก์ชันทั้งหมด n ครั้ง และแต่ละครั้งจะใช้ time complexity O(1)

  • ดังนั้น time complexity รวมคือ O(n) ⋅ O(1) = O(n)

  • อีกตัวอย่าง เช่น

void g(int n) {
  if (n == 1) return;
  g(n-1);
  g(n-1);
}
  • ในกรณีนี้ การเรียกฟังก์ชั้นแต่ละครั้งจะเรียกเพิ่มอีกสองครั้ง ยกเว้นกรณี n = 1

  • พิจารณาสิ่งที่เกิดขึ้นจากการเรียก g ด้วยพารามิเตอร์ n

  • ตารางด้านล่างแสดงจำนวนการเรียก function ที่เกิดขึ้น

function call

number of calls

g(n)

1

g(n − 1)

2

g(n − 2)

4

g(1)

2n − 1

  • จากตารางนี้ time complexity คือ 1 + 2 + 4 + ⋯ + 2n − 1 = 2n − 1 = O(2n)

Complexity classes

  • รายการต่อไปนี้รวม time complexity ของอัลกอริทึมที่เจอบ่อย

  • O(1) constant-time algorithm

    • เวลาที่ใช้ในการรันจะเป็น constant ซึ่งไม่สนใจขนาดของ input

    • ยกตัวอย่างเช่นอัลกอริทึมที่มีสูตรคำนวณคำตอบโดยไม่ต้องทำซ้ำใด ๆ

  • O(log n) logarithmic algorithm

    • อัลกอริทึมแบบ logarithmic ส่วนใหญ่จะหารครึ่งขนาดของ input ที่จำเป็นต้องพิจารณาในแต่ละขั้นตอน

    • เวลาที่ใช้ในการรันจะเป็น logarithmic เนื่องจาก log2n เท่ากับจำนวนครั้งที่หาร n ด้วย 2 จนกว่าจะเป็น 1

  • $O\left( \sqrt{n} \right)$ square root algorithm

    • ช้ากว่า O(log n) แต่เร็วกว่า O(n)

    • เป็นกรณีที่หารขนาด input n ด้วย $\sqrt{n} = \frac{n}{\sqrt{n}}$

  • O(n) linear algorithm

    • อัลกอริทึมแบบ linear จะวนใน input เป็นจำนวนครั้งที่แน่นอน

    • ส่วนใหญ่แล้วจะเป็น time complexity ที่ค่อนข้างดี เนื่องจากปกติแล้วจะจำเป็นต้องอ่านค่าทุกค่าใน input

  • O(nlog n)

    • ส่วนใหญ่จะเกิดจากการเรียงข้อมูล

    • หรือ data structure ที่ใช้ O(log n) ในแต่ละ operation

  • O(n2) quadratic algorithm

    • ส่วนใหญ่ใช้ลูปสองชั้น

    • เช่นการพิจารณาทุกคู่ของ input

  • O(n3) cubic algorithm

    • ลูปสามชั้น

  • O(2n)

    • การพิจารณาทุก subsets ของ input

    • ตัวอย่างเช่น subsets ของ {1, 2, 3} คือ

    , {1}, {2}, {3}, {1, 2}, {1, 3}, {2, 3} และ {1, 2, 3}.

  • O(n!)

    • การพิจารณาทุกการจัดเรียงของ input

    • ตัวอย่างเช่นการจัดเรียงของ {1, 2, 3} คือ

    (1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2) and (3, 2, 1).

การประเมินประสิทธิภาพ

  • จากการคำนวณ time complexity ของอัลกอริทึมนั้นทำให้สามารถตรวจสอบประสิทธิภาพของอัลกอริทึมก่อนการ implement ได้

  • จุดอ้างอิงคร่าวๆ ให้คาดว่า C++ สามารถรันได้ 1, 000, 000, 000 (พันล้าน) คำสั่งต่อหนึ่งวินาที

  • ตัวอย่างเช่นถ้าข้อจำนวนของโจทย์นั้นให้เวลาในการคำนวณหนึ่งวินาทีและ n = 105

  • ถ้า time complexity เป็น O(n2) อัลกอริทึมนี้จะรันทั้งหมด (105)2 = 10{10} คำสั่ง

  • ซึ่งโดยคร่าวใช้เวลาประมาณสิบวินาที ซึ่งจะช้าเกินไปสำหรับโจทย์ข้อนี้

  • ในอีกทางหนึ่ง หากว่าเรารู้ขนาด input แล้ว เราสามารถเดา time complexity ของอัลกอริทึมที่ใช้แก้ปัญหาได้เช่นกัน

  • ตารางต่อไปนี้บอกการประเมินที่พบบ่อยในกรณีที่ให้เวลาในการคำนวณหนึ่งวินาที

  • ยกตัวอย่างเช่น หากขนาดของ input เป็น n = 105 time complexity ที่ต้องการจากอัลกอริทึมก็คือ O(n) หรือ O(nlog n)

  • ด้วยความรู้นี้จะทำให้คิดอัลกอริทึมได้ง่ายขี้นจากการตัดอัลกอริทึมที่ใช้เวลานานออก

input size

required time complexity

N ≤ 10

O(N!)

N ≤ 20

O(2N)

N ≤ 500

O(N3)

N ≤ 5000

O(N2)

N ≤ 106

O(Nlog N) or O(N)

N is large

O(1) or O(log N)

ตัวแปร constant

  • อย่างไรก็ตาม ควรจำว่า time complexity นั้นแค่เป็นการประมาณประสิทธิภาพเท่านั้นเพราะไม่คิด constant factors

  • ตัวอย่างเช่น อัลกอริทึมที่ใช้เวลา O(n) อาจมีการคำนวณทั้งหมด $\frac{n}{2}$ หรือ 5n คำสั่ง

  • ซึ่งเป็นสิ่งสำคัญต่อเวลาที่ใช้จริงในการรันอัลกอริทึม

space complexity

  • ใช้ Big O Notation เป็นเครื่องมือเช่นกัน

  • เปลี่ยนจากการนับขนาดเวลา เป็นนับขนาด memory เช่น

    • ขนาดของ array ที่เราประกาศ