Miracle of Algorithm


Divide and Conquer เป็นหลักการที่ทำงานได้อย่างมหัศจรรย์มาก หลักการนี้อาจจะขัดแย้งกับความรู้สึกของมนุษย์ทั่วไป แต่กลับใช้งานได้อย่างน่าทึ่ง ในการสัมภาษณ์งานระดับโลก ถ้ามีการถามปัญหา Divide and Conquer แล้วเรามองไม่ออก หรือไม่รู้จัก คนสัมภาษณ์จะรู้ได้ทันทีว่า คนนี้เป็นแค่ Novice หรือ คนที่พอทำงานได้ ไม่ได้เก่งหรือเชี่ยวชาญอะไร
Divide and Conquer มักจะแบ่งข้อมูลออกเป็นสองส่วน แล้วหาคำตอบของทั้งสองส่วนมารวมเข้าด้วยกัน บทความนี้จะใช้ Divide and Conquer เพื่อแก้ปัญหาที่มีชื่อเสียง นั่นคือการนับจำนวน Inversion ใน Array ลองติดตามกันว่าปัญหานี้เป็นยังไง
Inversion คือ ข้อมูลที่อยู่ผิดตำแหน่ง แทนที่จะเรียงจากน้อยไปมาก ก็มีตัวน้อยกว่ามันตามมา เช่น
3 5 1 9 6
มี Inversion นับออกมาเป็นคู่ คือ
3 - 1
5 - 1
9 - 6
ปัญหานี้ให้เขียน Code เพื่อนับจำนวน Inversion จาก Array ที่กำหนดให้ ตัวอย่างการใช้วิธี Brute Force คือจับมาเทียบกันทีละคู่
int count(int[] data) {
	int count = 0;
	for (int i = 0; i < data.length; i++) {
		for (int j = i + 1; j < data.length; j++) {
			if (data[i] > data[j]) {
				count++;
			}
		}
	}
	return count;
}
วิธีข้างบนใช้เวลาทำงานแบบ O(n^2) เมื่อมีข้อมูล 1 ล้านตัว ต้องใช้เวลาทำงาน 1 ล้านล้านครั้ง ซึ่งยังดีไม่พอ Computer ทั่วไปทุกวันนี้อาจจะต้องใช้เวลาเป็นเดือนในการทำงาน
ส่วนวิธีที่นิยมใช้คือ Divide and Conquer จะใช้ได้เวลาแค่ O(n log n) ถ้ามีข้อมูล 1 ล้านตัว จะเสียเวลาทำงานเพียง 20 ล้านครั้งเท่านั้น
เริ่มต้นจากแบ่งข้อมูลเป็นสองส่วน ฝั่งซ้ายกับฝั่งขวา แบ่งจนเหลือฝั่งละตัว ก็จะสามารถนับ Inversion ของข้อมูลซ้ายขวานี้ได้ 1 หรือ 0
3 5 1 9 6
L R            มี 0 Inversion

อีกด้านมี 9 กับ 6 มี 1 Inversion อย่าลืม Merge ข้อมูลให้เรียงลำดับเป็น 6 กับ 9
3 5 1 9 6
      L R      มี 1 Inversion
      6 9

สามตัวแรก มี 2 Inversions และ Merge ข้อมูลกลายเป็น 1, 3, 5
3 5 1 6 9
L L R          มี 2 Inversions
1 3 5
สุดท้ายข้อมูลเรียงลำดับเรียบร้อย ไม่มี Inversion เพิ่มเติม รวมมี 3 Inversions
1 3 5 6 9
L L L R R      มี 0 Inversion

ตัวอย่าง Code
class Solution {
	int[] buffer;
	public int solution(int[] data) {
		buffer = new int[data.length];
		return count(data, 0, data.length-1);
	}
	int count(int[] a, int left, int right) {
		if (left >= right) return 0;
		int mid = (left + right) / 2;
		int countLeft = count(a, left, mid);
		int countRight = count(a, mid + 1, right);

		int count = 0;
		for (int k = left, i = left, j = mid + 1; 
			k <= right; k++) {
			boolean useLeft = true;
			if (i<=mid && j<=right && a[i] > a[j]) {
				count += mid - i + 1;
				useLeft = false;
			}
			if (i>mid && j<=right) {
				useLeft = false;
			}
			if (i<=mid && j<=right && a[i] <= a[j]) {
				useLeft = true;
			}
			if (i<=mid && j>right) {
				useLeft = true;
			}
			if (useLeft) {
				buffer[k] = a[i];
				i++;
			} else {
				buffer[k] = a[j];
				j++;
			}
		}
		for (int p = left; p <= right; p++) {
			a[p] = buffer[p];
		}
		return count + countLeft + countRight;
	}
}