Skip to content

Lời giải bài Area Query - ICPC Việt Nam Regional 2023

25 Dec 2023

Bài Area Query có thể coi là bài khó nhất trong kì thi ICPC Việt Nam regional 2023 vừa rồi. Đây là bài không có đội nào giải được trong kì thi chính thức (bảng rank xem tại đây). Đây là một bài khó một phần là vì chủ đề của bài: hình học và cấu trúc dữ liệu. Các bạn có thể đọc đề bài và thử sức trên VNOJ.

Tác giả của bài này là anh chemthan, và lời giải của tác giả có sử dụng cấu trúc dữ liệu link-cut tree. May mắn là thành viên trong ban ra đề, mình đã được thử sức với bài này trước khi kì thi diễn ra, với thời gian suy nghĩ thoải mái hơn tất cả các thí sinh trong kì thi 😇. Nhưng mình chưa bao giờ sử dụng link-cut tree, nên mình đã cố gắng nghĩ ra cách khác không sử dụng đến cấu trúc dữ liệu đó. Và đúng là sau tầm 3 buổi tối, cuối cùng mình cũng đã đưa ra được nhận xét hết sức thú vị để giải quyết bài toán này. Mình muốn chia sẻ quan sát này qua trang blog này.

Quan sát cơ bản

Ù nhưng tại sao bài hình lại sử dụng link-cut tree? Bởi vì có cấu trúc cây xuất hiện khi cắt đa giác thành các đa giác con: nếu như ta coi một đa giác con được cắt ra là một đỉnh, và hai đỉnh có cạnh nối với nhau khi chúng có chung đường chéo (chính là đường cắt cho bởi các truy vấn), ta đồ thị thu được sẽ là một cây.

Đây là quan dễ. Nhưng nếu điều này chưa hiển nhiên đối với bạn thì sau đây là chứng minh của mình:

Chứng minh cấu trúc cây

Ta có thể chứng minh sủ dụng phương pháp quy nạp.

Nhận thấy đa giác không có đường cắt nào đã mãn cấu trúc cây, vì đây là đồ thị gồm một đỉnh.

Bây giờ ta có thể giả thuyết rằng điều này đúng khi ta cắt một đa giác bất kì có số đỉnh nhỏ hơn thành một số phần, và các đa giác được chia ra thỏa mãn cấu trúc cây. Bây giờ ta sẽ chứng minh điều này đúng với đa giác có đỉnh.

Ta xét một đường cắt bất kì. Đường cắt này sẽ chia đa giác thành hai đa giác con lồi . Ta cũng có thể chia các đường cắt còn lại thành tập: tập đường cắt chỉ cắt và tập đường cắt chỉ cắt . Không có đường cắt nào sẽ cắt cả , vì nếu vậy đường cắt này phải cắt cả , mâu thuẫn với đề bài.

Sau khi chia ra như vậy, ta có thể thấy tập sẽ cắt đa giác thành các vùng tạo thành cấu trúc cây (theo giả thuyết), và tương tự tập cũng sẽ cắt đa giác thành các vùng tạo thành cấu trúc cây.

Bây giờ đường cắt chính là cạnh duy nhất được thêm vào để nối hai cây lại thành một cây. ĐPCM.

Dưới đây là ví dụ cho cấu trúc cây khi cắt đa giác với các đường chéo không giao nhau:

TIP

Ê, bạn thấy đống chữ bên trái của cái hình giống gì không? Đúng rùi, nó có format y như input format của bài Area Query! Và bạn có thể tương tác với hình bằng cách SỬA đống chữ đó. Hãy thử copy sample test của bài Area Query (mà không có số 0 đánh dấu kết file) vào thử xem.

Sample input của bài Area Query
7
1 1
1 7
2 8
4 7
7 4
8 2
7 1
15
A 1 3
A 1 4
A 1 5
A 1 6
? 1 3
R 1 3
A 2 4
R 1 6
A 5 7
? 2 5
R 1 5
R 1 4
A 4 7
A 2 7
? 7 4
7
1 1
1 7
2 8
4 7
7 4
8 2
7 1
15
A 1 3
A 1 4
A 1 5
A 1 6
? 1 3
R 1 3
A 2 4
R 1 6
A 5 7
? 2 5
R 1 5
R 1 4
A 4 7
A 2 7
? 7 4

Quay trở lại bài toán. Ý tưởng sử dụng link-cut tree thật ra rất tự nhiên cho bài toán, bởi vì khi cắt một phần đa giác thành hai đa giác nhỏ hơn, ta sử dụng thao tác cut của cấu trúc giữ liệu, và khi gộp hai phần đa giác lại, ta sử dụng thao tác link. Việc sử dụng link-cut tree ở đây giúp ta duy trì được cấu trúc của cây một cách rõ ràng.

Tuy nhiên ta không nhất thiết phải duy trì cây rõ ràng như vậy, mà thật ra các đường cắt đã cho ta cấu trúc của cây một cách ngầm định. Cấu trúc thay thế ở đây là cấu trúc Euler tour trên cây.

Về Euler tour trên cây

Euler tour trên cây, hay còn gọi là trải phẳng trên cây, là cách biến đổi cây thành mảng một chiều, và mảng này được sử dụng để giải quyết bài toán thay vì giải quyết bài toán trực tiếp trên cây.

Chủ đề này đã được trình bày rất chi tiết ở trên VNOI wiki. Nhưng mình muốn đề cập đến bài viết trên Codeforces. Bài viết trên Codeforces tuy đơn giản nhưng có đề cập đến 3 cách biểu diễn. VNOI wiki cũng có đề cập đến các cách biểu diễn, nhưng chỉ nói trong phần ứng dụng chứ không đặt tên.

Trong bài blog này mình sẽ đề cập dến biểu diễn loại 2loại 3 như bài viết trên Codeforces. Với biểu diễn loại 2, mỗi đỉnh trên cây sẽ nằm trên tour ở hai điểm là . Còn với loại 3, ngoài hai vị trí đầu và cuối, mỗi lần khi đi từ đỉnh con quay lại đỉnh cha, đỉnh cha lại được thêm vào Euler tour.

Cấu trúc Euler tour của bài Area query

Câu trúc Euler tour thật ra đã được cho bởi các đường cắt!

Giả sử ta đang đứng tại đỉnh của đa giác đã cho, và ta lần lượt thăm các đỉnh . Với thứ tự đi như vậy, ta có thể thăm các đường cắt (một đường cắt được thăm khi ta thăm một mút của nó).

Thứ tự thăm các đường cắt như trên cho ta Euler tour của cây!

Dưới đây là một ví dụ đơn giản:

TIP

Nhắc lại bạn, ở trên không phải là hình tĩnh, mà có thể tương tác được bằng cách sửa lại input ở bên phải ٩(◕‿◕。)۶.

Trên hình có ghi lại Euler tour loại 2, và các mút của các đường cắt đã thăm theo thứ tự đã nêu trên. Chính xác hơn thì thứ tự thăm đường cắt gần cho ra Euler tour, vì nó không xét đỉnh gốc. Điều này có thể dễ dàng sửa được bằng cách thêm đỉnh gốc vào đầu và đuôi của danh sách. Nhưng điểm đáng chú ý nhất vẫn là thứ tự thăm một đường cắt sẽ cho ta hai thông số của một đỉnh nào đó.

Ù nhưng có gì đó thiểu thiểu không nhỉ? Nếu có nhiều đường cắt có chung mút thì sao? Thật ra ta vẫn có thể sắp xếp được thứ tự thăm của những đường cắt như vậy. Tỉ dụ ta có các đường cắt có mút chung. Để sắp xếp thứ tự thăm cho các đường cắt này, ta sẽ xét vị trí tương đối của mút còn lại so với . Với đường cắt thì vị trí tương đối của so với sẽ là . Ta sẽ thăm đường cắt có vị trí tương đối của theo thứ tự GIẢM DẦN.

Mô tả ở trên là mô tả chuẩn xác nhưng hơi phức tạp và khó tưởng tượng. Một mô tả ít chuẩn xác nhưng dễ tưởng tượng hơn như sau: xét đỉnh lần lượt là đỉnh liền trước và liền sau với trên đa giác. Ta thực hiện quét xoay vòng từ đến với tâm quét là . Thứ tự quét các đường cắt sẽ là thứ tự thăm chúng.

Mô tả thứ tự quét xoay vòng

Man nghe vẫn có vẻ phức tạp quá. Để mình lấy luôn ví dụ ở trên.

Nếu nhìn vào thứ tự thăm đường cắt nhưng chỉ liệt kê đỉnh thì có đỉnh được liệt kê lại vài lần, như đỉnh , hay . Còn nếu ta viết các đường cắt ra thì mọi đường cắt sẽ phân biệt.

Xét đường cắt . có vị trí tương đổi so với , còn có vị trí tương đối so với . Như vậy ta sẽ thăm trước.

Xét đường cắt , .

  • Vị trí tương đối của so với .
  • Vị trí tương đối của so với .
  • Vị trí tương đối của so với .

Như vậy thứ tự thăm sẽ là , .

Khi biểu diễn thứ tự thăm sử dụng đường cắt, ta có hệ quả thú vị: nếu như thứ tự thăm đường cắt trùng với , vậy thứ tự thăm đường cắt sẽ trùng với .

Finally, lời giải bài toán

Bài toán có yêu cầu thí sinh xử lý truy vấn thêm và xóa đường cắt. Nhưng do đề bài đã đảm bảo rằng tại một thời điểm bất kì, không có hai cặp đường cắt giao nhau. Như vậy tại một thời điểm bất kì, thứ tự thăm các đường cắt lúc nào cũng tạo ra một thứ tự Euler tour loại 2 hợp lệ!

Ở đây ta có thể suy nghĩ đến sử dụng Segment tree để duy trì thông tin của cây với Euler tour. Với mỗi đường cắt, ta có thể tìm thứ tự thăm của nó tương đối với tất cả các đường cắt còn lại. Khi một đường cắt được thêm vào đa giác, ta có thê tìm vị trí thăm đờng cắt , rồi cập nhật đoạn trên Segment tree. Cập nhật này sẽ không làm hỏng cấu trúc Euler tour vì lý do đã nêu trên.

Nếu như ta xét toàn bộ đường cắt có thể của đa giác, ta có thể giải quyết bài toán sử dụng cây Segment tree động. Tuy nhiên có những đường cắt ta sẽ không động vào, nên xét hết tất cả sẽ hơi phí phạm. Thay vào đó ta có thể thu gọn lại tập các đường cắt cần xét là các đường xét cho bởi truy vấn, và giờ số đường cắt cần xét là . Vậy ta có thể sắp xếp danh sách đường cắt và thứ tự thăm của đường cắt giờ sẽ là thứ tự của nó trong danh sác đã sắp xếp.

Giả sử ta đang cần giải truy vấn ? x y. Ta gọi là đỉnh chứa cạnh , và là đỉnh chứa cạnh . Đáp án sẽ là:

trong đó:

  • là tổng diện tích của các vùng trên đường đi từ lên gốc cây.
  • là diện tích của vùng tương ứng với đỉnh .
  • là tổ tiên chung gần nhất của .

Vì cây của ta được duy trì một cách ngầm định, nên ta không thể làm các thao tác trực tiếp được, ví dụ như tìm cha của một đỉnh. Bây giờ ta sẽ cần phát minh lại một số thao tác trên cây để có thể giải quyết được bài toán gốc.

INFO

Phần dưới là mô tả khá chi tiết, nhưng cũng sẽ hơi phức tạp. Nhưng mình tin rằng với các bạn có đủ kinh nghiệm, những thao tác ở dưới cũng sẽ là cơ bản. Nên mình cũng động viên các bạn sử dụng quan sát chính mô tả ở trên để thu được lời giải của bản thân. 🙏

Nhưng trước hết ta cần quy ước trước:

  • Ta gọi đường cắt là đường cắt nghịch đảo của đường cắt .
  • Tập hợp gồm:
    • Các đường cắt đã cho bởi truy vấn.
    • Các đường cắt nghịch đảo của các đường cắt đã cho bởi truy vấn.
    • Cạnh đa giác có dạng
    • Cạnh đa giác có dạng .
  • là thứ tự thăm đường cắt trong tập đã được sắp xếp theo thứ tự thăm.
  • , tương ứng với với nào đó ở một thời điểm bất kì.
  • , tương ứng với với nào đó ở một thời điểm bất kì.

Tìm độ sâu của môt đỉnh

Ta có thể sử dụng một cây Segment tree , cho phép tăng đoạn và truy vấn điểm để duy trì thông tin về độ sâu. Khi một đường cắt được thêm vào, ta sẽ tăng đoạn lên . Để truy vấn độ sâu của đỉnh tương ứng với vùng chứa đường cắt hoặc cạnh đa giác , ta truy vấn điểm trên cây .

Tìm cha của một đỉnh

Tất nhiên ta không định nghĩa đỉnh trên cây, mà ta chỉ duy trì các khoảng của các đỉnh trên cây. Bài toán giờ sẽ là cho cặp số thể hiện khoảng của một đỉnh , ta cần tìm khoảng tương ứng với đỉnh trên cây (ở thời điểm hiện tại).

Thao tác có thể giải quyết trong . Ta dựng interval tree , với mỗi đỉnh lưu một set là các đoạn đã qua khoảng đỉnh này quản lý, và sắp xếp các khoảng theo độ dài.

Khi có update thêm/xóa đoạn cắt , ta chỉ cần thêm/xóa trên cây . Để tìm đoạn cha tương ứng với , ta có thể tìm đoạn nhỏ nhất đi qua mà không phải là .

Mình cũng có cách làm thao tác này trong , nhưng mình sẽ trình bày sau đoạn này để tiến đến lời giải cuối cùng trước.

Tìm LCA của hai đỉnh trên cây

Giả sử ta đang cần tìm LCA tương ứng với hai đoạn cắt .

Ta có thể làm tương tự như tìm cha, tuy nhiên ta làm nó với đoạn , tức đoạn bao phủ cả hai đoạn con. Tổ tiên chung cần tìm sẽ tương ứng với đoạn có độ dài nhỏ nhất bao đoạn này.

Update diện tích của một vùng

Để làm điều này ta có thể dựng một cây Segment tree , cho phép update điểm, và truy vấn tổng đoán.

Khi thêm đoạn cắt vào đa giác (), ta cần tính diện tích của hai phần đa giác được cắt ra. Phần được cắt ra thực chất chính là cha của đỉnh tương ứng với , nên ta sẽ tìm cha tương ứng với đường cắt này trước trên cây .

Đầu tiên ta tính diện tích của vùng tương ứng với . Ta có thể tính diện tích của đa giác gồm các đỉnh (một nửa đa giác cắt bởi ) sử dụng tổng tiền tố. Sau đó ta cần trừ đi diện tích tất cả các con tương ứng với , nên ta có thể trừ đi tổng đoạn trên cây để thu được số , và đây là diện tích ta cần tìm. Ta sẽ update cây tại điểm với giá trị .

Tiếp đó ta cần update diện tích tương ứng với . Ở đây ta chỉ cần lấy diện tích đang có của (tại điểm trên cây ), trừ đi .

Độ phức tạp tổng thể của thao tác này là (không tính việc tìm cha).

Thao tác xóa cũng được làm tương tự nhưng với thứ tự ngược lại.

Tính tổng diện tích từ một đỉnh lên gốc cây (hàm )

Song song với cây , ta có thể duy trì thêm một cây nữa được sử dụng để tính tổng từ một vùng tương ứng với một đỉnh bất kì lên gốc cây.

Mỗi khi điểm của được tăng lên , ta cũng sẽ tăng lên trên cây , xong ta sẽ giảm trên cây đi . Như vậy khi ta muốn truy vấn tổng diện tích từ dỉnh tương ứng với đoạn cắt lên gốc cây, ta sẽ tính tổng đoạn trên cây .

Thao tác này cũng tốn thời gian.

Kết

Cho đến thời điểm hiện tại, mình vẫn thấy quan sát về Euler tour với đường cắt của đa giác này rất là tuyệt vời. Thực sự mà nói những khoảng khắc tìm ra những quan sát như thế này làm mình thấy yêu CP hơn, và rộng hơn thì mình cũng thấy yêu toán và khoa học máy tính hơn, dù mình cũng chỉ là dev quèn. Nhưng dù là dev quèn nhưng mình cũng biết viết blog ahihi. Hi vọng các bạn đọc xong bài này có thể thu lại được gì đó mà không thấy shock 😇.

Bonus: Tối ưu các thao tác trên cây

Mình sẽ nói đến truy vấn tìm cha. Truy vấn tìm LCA thật ra cũng tương tự, và vì blog này cũng đã quá dài rồi nên là... dành cho bạn đọc 😅.

Trước khi thực hiện thao tác này, mình cần sửa lại quy ước một chút:

  • giờ sẽ là hai lần thứ tự thăm của đường cắt . Mình muốn nhân lên để vị trí lẻ tạo ra những khoảng chắn cho giữa hai điểm liên tiếp. Điều này sẽ effectively tạo ra cấu trức Euler tour loại 3.

X_uét đường cắt và ta cần tìm khoảng là khoảng tương ứng với cha của đỉnh có khoảng . Ở đây ta sẽ không dùng cây interval tree nữa, mà ta sẽ làm mọi thứ trực tiếp trên cây (cây lưu độ sâu).

Ở đây ta đã biết được độ sâu là của , do đó ta cũng biết được độ sâu của . Có thể nhận thấy trước vị trí là điểm được phủ bởi đoạn tương ứng với , và nó sẽ mang giá trị là độ sâu của , tức . Ý tưởng ở đây là thay vì tìm vị trí , ta có thể tìm vị trí , tức là điể gần nhất về bên trái mang giá trị .

Để tìm kiếm điểm này ta có thể lợi dụng một tính chất đặt biệt của giá trị trên cây : hai điểm liên tiếp trên cây có giá trị chênh nhau không quá ! Nói cách khác, giá trị các điểm là nguyên liên tục. Với tính chất này, ta có thể tìm kiếm nhị phân trên cây : khi quyết định đi sang trái hay phải tại một đỉnh trên cây segment trê, nếu như khi đi sang phải mà có min vẫn còn nhỏ hơn, ta sẽ đi sang phải, ngược lại ta sẽ đi sang trái.

Cây như vậy sẽ chuyển thành cây cho phép update tăng/giảm đoạn và truy vấn min đoạn.

Bonus: Visualizer

Mình sẽ công nhận là ý tưởng viết visualizer là rất kì công, song cái visualizer mình sẽ treat nó là một cái side project nhỏ. Các bạn có thể sử dụng tính năng đầy đủ của visualizer tại đây.

Bonus: Validator

Mình cũng là người viết Validator cho bài này. Và kể ra Validator của bài này cũng rất khó vì kiểm tra đoạn mới được thêm vào có giao với các đoạn đã thêm trước đó hay không thật ra không đơn giản. Validator bài này có thể được tách ra làm một bài toán riêng và nó vẫn khó 😃.

Validator của mình cũng lợi dụng tính chất Euler tour của đường thăm và cũng sử dụng segment tree. Ở đây nếu như ta thêm đoạn và thì điểm và điểm cũng cần có cùng tập hợp cha. Bài toán kiểm tra hai tập hợp bằng nhau là một bài toán đã có lời giải, và ở đây mình sử dụng XOR hash: khi một đoạn được thêm, ta xor đoạn tương ứng với nó bởi một số ngẫu nhiên , và khi đoạn đó được bỏ đi ta lại xor với chính số đó.

Để tăng sự tự tin thì thay vì mình dùng số 64 bit, mình đã dùng số 256 bit. Về mặt triển khai thì không tốn nhiều công hơn.