กล้วยไม้ (Orchid)

toi13_orchid

ในข้อนี้ โจทย์ต้องการหาว่าการเปลี่ยนกล้วยไม้ให้น้อยที่สุดกี่ต้น จึงจะทำให้ความสูงของต้นกล้วยไม้ เรียงจากน้อยไปมาก

สังเกตว่า การเปลี่ยนความสูงของต้นกล้วยไม้ เทียบเท่ากับการเอากล้วยไม้ออกไปเลย ดังนั้น เราต้องการ หยิบกล้วยไม้ออกไป ให้น้อยที่สุด ถามว่าหยิบกี่ต้นเพื่อให้ได้ความสูงยังคงเรียงจากน้อยไปมากอยู่

สังเกตต่อมาว่า การ หยิบออกให้น้อยที่สุด คือการ รักษาไว้ให้มากที่สุด โดยเราจะเปลี่ยนแนวคิดจากการหยิบออกเป็น เลือกกล้วยไม้ให้ได้จำนวนมากที่สุดที่เรียงจากน้อยไปมาก ซึ่งปัญหาดังกล่าวมีชื่อเรียกว่า Longest Nondecreasing Subsequence ซึ่งเป็น variant หนึ่งของปัญหาโด่งดังที่เรียกว่า Longest Increasing Subsequence (LIS)

ปัญหาดังกล่าวสามารถแก้ได้ในเวลา O(N2)\mathcal{O}(N^2) ด้วยวิธีการ Dynamic Programming โดยให้ dpidp_i แทนจำนวนต้นกล้วยไม้ที่มากที่สุดที่สามารถรักษาไว้ได้ หากพิจารณาเฉพาะกล้วยไม้ในช่วง 11 ถึง ii และหยิบต้นที่ ii แน่ๆแล้วด้วย

(หลังจากนี้จะใช้สัญลักษณ์ AiA_i แทนความสูงของต้นที่ ii ตั้งแต่ต้นที่ 11 ถึง NN)

สังเกตความสัมพันธ์ของสถานะต่างๆดังนี้ dpi=maxj<iAjAidpj+1dp_i = \max_{\substack{j<i \\ A_j \leq A_i}}{dp_j+1} กล่าวคือ หากมั่นใจว่าจะหยิบกล้วยไม้ต้นที่ ii แน่ๆ เราจะต้องไปหาว่า หากหยิบต้นที่ jj ก่อนหน้า ii ที่เตี้ยกว่าหรือเท่ากับต้นที่ ii จะหยิบต้นไหนดีที่สุด (คือการหา max นั่นเอง) เมื่อมั่นใจว่าค่าดีสุดแล้ว เรานำามาบวก 1 เพื่อคิดค่าต้นปัจจุบันไปด้วย ก็จะได้ dpidp_i สำเร็จ

ตัวอย่างโค้ด

int dp[1000005];
int A[1000005]; // Height of i-th orchid
for (int i = 1; i <= N; i++) {
  int opt = 0; // In case there is no predecessor
  for (int j = 1; j < i; j++) {
    if (A[j] <= A[i]) {
      opt = max(opt, dp[j]); // Update optimal value
    }
  }
  dp[i] = opt + 1;
}

ต่อมาเราสังเกตว่าเวลา O(N2)\mathcal{O}(N^2) นั้นยังไม่เร็วพอ และมีวิธีการ optimize ให้ดียิ่งขึ้น โดยการทำ Longest Nondecreasing Subsequence ใน O(NlogN)\mathcal{O}(N \log N) โดยเปลี่ยนวิธีการเก็บ dpdp ดังนี้

เราจะค่อยๆพิจารณาเริ่มจากอาเรย์ว่างเปล่า แล้วค่อยๆใส่เพิ่มตัวท้ายไปเรื่อยๆจนครบทั้ง NN ตัว

สมมติว่าแต่ละ ii ในกาลเวลาใดๆ เมื่อเราสมมติว่า Longest Nondecreasing Subsequence จะยาว ii แน่ๆ ลองพิจารณาทุก subsequence ที่เป็นไปได้ตามเงื่อนไขดังกล่าว สังเกตว่าเราต้องการให้ตัวสุดท้ายมีค่า น้อยที่สุดเท่าที่จะเป็นไปได้ เพราะหากมีค่าน้อยเท่าไหร่ ก็ยิ่งสามารถมีโอกาสนำไปใช้ต่อได้มากเท่านั้น

ตัวอย่างเช่น 1 5 2 3 หากพิจารณา Longest Nondecreasing Subsequence ความยาว 22 แล้ว จะได้ว่ามีได้ทั้งหมด ดังนี้ 1 5, 1 2, 1 3, 2 3 สังเกตว่าข้อมูลที่มีผลมีเพียงตัวสุดท้ายเท่านั้น (นั่นคือ 5, 2, 3, 3 ตามลำดับ) หากต้องการเติม 6 ต่อท้าย เราสามารถเติมใน subsequence ไหนก็ได้ แต่หากต้องการเติม 2 เราจะไม่สามารถเติมหลัง subsequence ที่จบด้วย 3 หรือ 5 ได้เลย เพราะจะทำให้มันไม่เรียงจากน้อยไปมาก

เพื่อให้มองเห็นง่าย เราจะเก็บ LiL_i แทนว่า หาก Longest Nondecreasing Subsequence ยาว ii แล้ว ตัวสุดท้ายของเหล่า subsequence ทุกแบบที่เป็นไปได้ที่ยาว ii นั้น จะมีค่า น้อยที่สุดเท่าที่จะเป็นไปได้ เท่าใด

พิจารณาตัวอย่าง 1 2 6 9 7 4 5 3

ในขณะแรก LL จะยังคงว่างเปล่า

ต่อมาเมื่อพิจารณา 1 จะได้

LL = [1]

ต่อมาเมื่อพิจารณา 2 จะได้

LL = [1, 2]

ต่อมาเมื่อพิจารณา 6 จะได้

LL = [1, 2, 6]

ต่อมาเมื่อพิจารณา 9 จะได้

LL = [1, 2, 6, 9]

ต่อมาเมื่อพิจารณา 7 จะได้

LL = [1, 2, 6, 7]

ต่อมาเมื่อพิจารณา 4 จะได้

LL = [1, 2, 4, 7]

ต่อมาเมื่อพิจารณา 5 จะได้

LL = [1, 2, 4, 5]

ต่อมาเมื่อพิจารณา 3 จะได้

LL = [1, 2, 3, 5]

ผลสุดท้ายแล้ว คำตอบ จำนวนต้นกล้วยไม้ที่มากที่สุดที่สามารถรักษาไว้ได้ ก็คือขนาดของ LL นั่นเอง

คำถามต่อมาคือ หากทำวิธีนี้แล้ว จะต้องรักษาสภาพ LL ในแต่ละช่วงเวลาอย่างไร ซึ่งวิธีการอย่างง่ายคือ สังเกตว่า หากมี LL เดิมคงอยู่แล้ว แล้วมีการเติม xx มาพิจารณา แล้ว จะมีผลอย่างไรต่อ LL สังเกตว่า หากเป็นไปได้เราก็อยากให้ LL ยาวขึ้นอยู่แล้ว นั่นคือ หาก xx มากกว่าหรือเท่ากับตัวสุดท้ายของ LL เราจะเติม xx ต่อท้าย LL ไป แต่หากไม่เป็นเช่นนั้น เราจะดูว่า xx จะไปช่วยลดค่า LiL_i บางอันได้หรือเปล่า เพื่อให้สามารถใช้ค่าปัจจุบันเพิ่มโอกาสไปต่อกับค่าในอนาคตได้ สังเกตว่า xx จะสามารถนำไปต่อท้าย LiL_i เฉพาะที่มีค่าน้อยกว่าหรือเท่ากับ xx เท่านั้น และสังเกตว่า LL จะเรียงจากน้อยไปมากเสมอ แสดงว่าหาก xx น้อยกว่าค่าสูงสุดใน LL จะต้องมี ii ที่ Li>xL_i > x และ (i=1i = 1 หรือ Li1xL_{i-1} \leq x) ที่เราสามารถนำ xx ไปต่อหลังจาก Li1L_{i-1} ได้ (หรือใช้ xx เป็นตัวแรกหาก i=1i = 1) เราสามารถทำการแทนที่ LiL_{i} ด้วย xx ได้เลย เพื่อทำให้ LL นั้น optimal (ลองสังเกตตัวอย่างในแต่ละขั้น จะเห็นภาพได้ชัดเจนยิ่งขึ้น)

ตัวอย่างโค้ด

int L[1000005];
int A[1000005]; // Height of i-th orchid
int size = 0;
for (int t = 1; t <= N; t++) {
  int x = A[t];
  if (x >= L[size]) { // In case we can add x to the end of L.
    L[size + 1] = x;
    size++;
  } else {
    for (int i = 1; i <= size; i++) {
      if (L[i] > x && (i == 1 || L[i - 1] <= x)) {
        L[i] = x;
        break;
      }
    }
  }
}

ซึ่งเราจะเห็นได้ว่า โค้ดข้างต้นก็ยังใช้เวลา O(N2)\mathcal{O}(N^2) อยู่

ต่อมาเราสามารถทำให้เร็วขึ้นได้โดยอาศัยหลักการที่ว่า LL นั้นเรียงจากน้อยไปมาก โดยเราสามารถทำการ Binary Search ใน O(logN)\mathcal{O}(\log N) เพื่อหาตำแหน่ง ii ในเวลาใดๆ ดังนี้

int L[1000005];
int A[1000005]; // Height of i-th orchid
int size = 0;
for (int t = 1; t <= N; t++) {
  int x = A[t];
  if (x >= L[size]) { // In case we can add x to the end of L.
    L[size + 1] = x;
    size++;
  } else {
    int lo = 1;
    int hi = size;
    int mid;
    while (lo < hi) {
      mid = (lo + hi) / 2;
      if (x > L[mid])
        hi = mid;
      else
        lo = mid + 1;
    }
    L[mid] = x;
  }
}

เท่านี้ อัลกอริทึมดังกล่าวก็จะสามารถทำงานในเวลา O(NlogN)\mathcal{O}(N \log N) ได้แล้ว (หรือเพื่อความแม่นยำ สามารถเรียกได้ว่าทำงานในเวลา O(NlogL)\mathcal{O}(N \log |L|)) เมื่อ L|L| แทนขนาดของ Longest Nondecreasing Subsequence

นอกจากนี้ เพื่อความง่าย จึงมีแนวทางการเขียนยอดนิยมสำหรับการแก้โจทย์ข้อนี้ ดังนี้

vector<int> L;
int A[1000005];
for (int t = 1; t <= N; t++) {
  int x = A[t];
  auto it = upper_bound(L.begin(), L.end(), x);
  if (it == L.end())
    L.push_back(x);
  else
    *it = x;
}

โดยฟังก์ชัน upper_bound(L.begin(), L.end(), x) จะทำการหาว่า ii ที่น้อยที่สุดที่ Li>xL_i > x เป็นเท่าใด และคืนค่าเป็น random access iterator ชี้ตำแหน่งนั้น โดย assume ว่า LL นั้นเรียงจากน้อยไปมากแล้ว ต่อมาเราตรวจสอบว่า ii ดังกล่าวนั้น พ้นขอบเขตของ LL หรือไม่ หากพ้นไปแล้วถือว่าสามารถนำ xx ต่อท้าย LL ได้ แต่หากอยู่ภายใน LL จะสามารถปรับลดค่าดังกล่าวลงมาได้เลย