ICPC Regional Hanoi 2024 Part 4 of 4

ICPC Regional Hanoi 2024 Editorial - A

Category: Challenger Tags: competitive-programming icpc

Lời giải bài hình chống full.

Đề bài

Cho một đa giác đơn không tự cắt PP gồm nn đỉnh trên mặt phẳng 2D. Có qq truy vấn độc lập. Mỗi truy vấn mô tả một cơn bão là một hình tròn di chuyển từ tâm S(xs,ys)S(x_s, y_s) với bán kính rsr_s đến tâm T(xt,yt)T(x_t, y_t) với bán kính rtr_t theo đường thẳng:

  1. Bán kính biến thiên tuyến tính theo khoảng cách di chuyển giữa SS và TT.

  2. Miền bị ảnh hưởng HH bởi cơn bão là tổng thể diện tích được bao phủ bởi hình tròn tại mọi thời điểm trong suốt hành trình di chuyển từ SS đến TT (về mặt hình học, đây là hợp của hai hình tròn tại S,TS, T và hình thang cân nối các tiếp tuyến chung ngoài của hai đường tròn này).

Với mỗi truy vấn, tính diện tích phần giao giữa đa giác đảo PP và miền ảnh hưởng HH của cơn bão.

Giới hạn: số test t≤104t \le 10^4, 3≤n≤3⋅1053 \le n \le 3 \cdot 10^5, tọa độ có trị tuyệt đối không quá 50005000, 1≤rs,rt≤1001 \le r_s, r_t \le 100, tổng n⋅qn \cdot q trên mọi test không vượt quá 10610^6. Đáp án được chấp nhận nếu sai số tuyệt đối hoặc tương đối không vượt quá 10−610^{-6}.

Lời giải

Trong kì thi chính thức không có đội nào AC bài toán này.

Ý tưởng chính

Do tổng n⋅qn \cdot q trên mọi test không vượt quá 10610^6, với mỗi truy vấn ta có thể duyệt qua toàn bộ nn cạnh của đa giác. Bài toán có hai khó khăn chính: đa giác PP có thể không lồi, và biên của miền HH có những đoạn cong. Lời giải lần lượt xử lý hai khó khăn này qua bốn bước:

  1. Ở Bước 1, ta biểu diễn đa giác PP thành tổng có dấu của nn tam giác, mỗi tam giác ứng với một cạnh của đa giác. Nhờ đó, bài toán quy về việc tính diện tích phần giao giữa một tam giác và miền HH.

  2. Ở Bước 2, ta chứng minh HH là bao lồi của hai hình tròn lúc đầu và lúc cuối, sau đó chia HH thành ba mảnh đơn giản: hai phần hình tròn và một hình thang.

  3. Ở Bước 3, ta cắt mỗi tam giác theo ba mảnh nói trên. Phần nằm trong hình thang là một đa giác lồi, nên diện tích của phần này được tính bằng công thức shoelace.

  4. Ở Bước 4, ta tính diện tích phần giao giữa một đa giác lồi và một hình tròn, áp dụng cho hai phần còn lại.

Trong lời giải, ta dùng các ký hiệu sau: A(X)\mathcal{A}(X) là diện tích có hướng của hình XX; p×q=pxqy−pyqxp \times q = p_x q_y - p_y q_x là tích có hướng của hai vector pp và qq; DSD_S là hình tròn tâm SS bán kính rsr_s và DTD_T là hình tròn tâm TT bán kính rtr_t, tức là hình tròn lúc cơn bão bắt đầu và lúc cơn bão kết thúc.

Bước 1: Biểu diễn đa giác thành tổng có dấu của các tam giác

Ta chọn một điểm OO bất kỳ làm điểm gốc (chẳng hạn trung điểm của STST). Với mỗi cạnh vivi+1v_i v_{i+1} của đa giác (quy ước vn=v0v_n = v_0), ta xét tam giác Ovivi+1O v_i v_{i+1} cùng với dấu của tam giác đó: tam giác mang dấu dương nếu các đỉnh OO, viv_i, vi+1v_{i+1} được sắp theo chiều ngược chiều kim đồng hồ, và mang dấu âm trong trường hợp ngược lại. Công thức shoelace quen thuộc chính là tổng diện tích có hướng của nn tam giác này. Tuy nhiên, để xử lý được phần giao với HH, ta cần một tính chất mạnh hơn, phát biểu cho từng điểm của mặt phẳng.

Nhận xét. Với mỗi điểm XX không nằm trên cạnh của tam giác nào, gọi w(X)w(X) là tổng dấu của các tam giác chứa XX: mỗi tam giác dương chứa XX được tính +1+1, mỗi tam giác âm chứa XX được tính −1-1. Nếu đa giác được cho theo chiều ngược chiều kim đồng hồ thì w(X)=1w(X) = 1 với mọi điểm XX nằm trong đa giác, và w(X)=0w(X) = 0 với mọi điểm XX nằm ngoài đa giác. Nếu đa giác được cho theo chiều kim đồng hồ thì giá trị ứng với các điểm nằm trong đa giác là −1-1.

Chứng minh. Gọi ℓ\ell là tia xuất phát từ XX và có cùng hướng với vector OX→\overrightarrow{OX}, tức là tia đi ra xa điểm OO. Điểm XX nằm trong tam giác Ovivi+1O v_i v_{i+1} khi và chỉ khi đường thẳng OXOX cắt cạnh vivi+1v_i v_{i+1} tại một điểm nằm xa OO hơn XX, tức là khi và chỉ khi tia ℓ\ell cắt cạnh vivi+1v_i v_{i+1}. Ngoài ra, dấu của tam giác Ovivi+1O v_i v_{i+1} cho biết cạnh vivi+1v_i v_{i+1}, đi theo chiều từ viv_i đến vi+1v_{i+1}, cắt tia ℓ\ell từ bên phải sang bên trái hay từ bên trái sang bên phải. Do đó w(X)w(X) bằng số lần biên đa giác cắt tia ℓ\ell theo chiều này trừ đi số lần cắt theo chiều kia.

Bây giờ ta đi dọc tia ℓ\ell từ XX ra xa vô cùng. Mỗi lần tia cắt biên, ta đi từ trong đa giác ra ngoài hoặc từ ngoài vào trong. Vì phần trong của đa giác luôn nằm về cùng một phía của các cạnh (bên trái nếu đa giác được cho ngược chiều kim đồng hồ), hai loại lần cắt này ứng với hai chiều cắt ngược nhau. Đầu xa của tia nằm ngoài đa giác. Nếu XX nằm ngoài đa giác, số lần đi vào bằng số lần đi ra, nên w(X)=0w(X) = 0. Nếu XX nằm trong đa giác, số lần đi ra nhiều hơn số lần đi vào đúng một lần, nên w(X)=±1w(X) = \pm 1. Đây cũng chính là cơ sở của thuật toán kiểm tra một điểm có nằm trong đa giác hay không bằng cách bắn tia. ■\blacksquare

Các tam giác dương trừ các tam giác âm bằng đa giác
Hình 1. Các tam giác dương (bên trái) trừ đi các tam giác âm (ở giữa) cho đúng đa giác (bên phải). Phần nằm ngoài đa giác, phía dưới cạnh đáy, được cộng một lần trong các tam giác dương và bị trừ một lần trong tam giác âm, nên bị triệt tiêu. Điều này vẫn đúng khi đa giác không lồi.

Theo nhận xét trên, khi cộng diện tích có hướng của các phần Ovivi+1∩HO v_i v_{i+1} \cap H, mỗi điểm XX thuộc HH được tính đúng w(X)w(X) lần: các điểm nằm ngoài đa giác bị triệt tiêu, còn mỗi điểm nằm trong đa giác được tính đúng một lần, với dấu phụ thuộc vào chiều của đa giác. Do đó:

A(P∩H)=∑i=0n−1A(Ovivi+1∩H)\mathcal{A}(P \cap H) = \sum_{i=0}^{n-1} \mathcal{A}(O v_i v_{i+1} \cap H)

Vì đa giác có thể được cho theo chiều kim đồng hồ, đáp án của truy vấn là trị tuyệt đối của tổng trên. Như vậy, bài toán quy về việc tính diện tích phần giao giữa một tam giác và miền HH.

Bước 2: Hình dạng của miền ảnh hưởng

Đặt d=∣ST∣d = |ST| và Δ=rt−rs\Delta = r_t - r_s. Khi tâm cơn bão đã đi được một tỉ lệ λ∈[0,1]\lambda \in [0, 1] của quãng đường STST, khoảng cách từ tâm đến SS và đến TT lần lượt là ds=λdd_s = \lambda d và dt=(1−λ)dd_t = (1 - \lambda) d. Theo công thức trong đề bài, tâm cơn bão lúc đó là điểm (1−λ)S+λT(1-\lambda)S + \lambda T, và bán kính là

dsrt+dtrsds+dt=(1−λ) rs+λ rt\frac{d_s r_t + d_t r_s}{d_s + d_t} = (1-\lambda)\, r_s + \lambda\, r_t

Như vậy, cả tâm và bán kính đều thay đổi tuyến tính theo λ\lambda. Gọi CλC_\lambda là hình tròn tại thời điểm đó. Sau khi tâm tới TT, cơn bão đứng yên nên không phủ thêm điểm nào, do đó HH là hợp của các hình tròn CλC_\lambda với λ∈[0,1]\lambda \in [0, 1].

Nhận xét. Miền HH là bao lồi của DS∪DTD_S \cup D_T.

Chứng minh. Trước hết, ta chứng minh mỗi hình tròn CλC_\lambda gồm đúng các điểm có dạng (1−λ)a+λb(1-\lambda)a + \lambda b với a∈DSa \in D_S và b∈DTb \in D_T.

  • Nếu a∈DSa \in D_S và b∈DTb \in D_T, khoảng cách từ điểm (1−λ)a+λb(1-\lambda)a + \lambda b đến tâm (1−λ)S+λT(1-\lambda)S + \lambda T bằng ∣(1−λ)(a−S)+λ(b−T)∣|(1-\lambda)(a - S) + \lambda(b - T)|. Theo bất đẳng thức tam giác, đại lượng này không vượt quá (1−λ)rs+λrt(1-\lambda)r_s + \lambda r_t, nên điểm đang xét thuộc CλC_\lambda.

  • Ngược lại, mỗi điểm của CλC_\lambda có dạng (1−λ)S+λT+w(1-\lambda)S + \lambda T + w với ∣w∣≤r|w| \le r, trong đó r=(1−λ)rs+λrtr = (1-\lambda) r_s + \lambda r_t là bán kính của CλC_\lambda. Đặt a=S+rsrwa = S + \frac{r_s}{r} w và b=T+rtrwb = T + \frac{r_t}{r} w. Khi đó a∈DSa \in D_S, b∈DTb \in D_T, và (1−λ)a+λb=(1−λ)S+λT+w(1-\lambda)a + \lambda b = (1-\lambda)S + \lambda T + w.

Lấy hợp theo mọi λ∈[0,1]\lambda \in [0, 1], ta được HH là tập các điểm (1−λ)a+λb(1-\lambda)a + \lambda b với a∈DSa \in D_S, b∈DTb \in D_T và λ∈[0,1]\lambda \in [0, 1]. Đây chính là bao lồi của hợp hai tập lồi DSD_S và DTD_T. ■\blacksquare

Nếu d≤∣Δ∣d \le |\Delta|, một hình tròn nằm trọn trong hình tròn kia, nên HH chính là hình tròn lớn hơn. Trường hợp này được xử lý riêng ở phần Cài đặt. Trong phần còn lại của Bước 2 và Bước 3, ta giả sử d>∣Δ∣d > |\Delta|. Khi đó, bao lồi của hai hình tròn được giới hạn bởi hai tiếp tuyến chung ngoài và hai cung tròn ở hai đầu (Hình 2). Có thể hình dung trực quan như sau: khoảng cách có hướng từ một điểm đến một đường thẳng là hàm bậc nhất theo tọa độ của điểm, nên khoảng cách từ tâm của CλC_\lambda đến mỗi tiếp tuyến chung ngoài thay đổi tuyến tính theo λ\lambda, bằng rsr_s khi λ=0\lambda = 0 và bằng rtr_t khi λ=1\lambda = 1. Vì vậy khoảng cách này luôn bằng bán kính của CλC_\lambda, tức là mọi hình tròn trung gian đều tiếp xúc với cả hai tiếp tuyến chung ngoài.

Để thuận tiện cho việc tính toán, ta đổi hệ tọa độ: gốc tọa độ là trung điểm OO của đoạn STST, trục OxOx cùng hướng với vector ST→\overrightarrow{ST}. Trong hệ tọa độ mới, S=(−d2,0)S = \left(-\frac{d}{2}, 0\right) và T=(d2,0)T = \left(\frac{d}{2}, 0\right).

Ta tìm tiếp tuyến chung ngoài phía trên dưới dạng n⋅p=c\mathbf{n} \cdot p = c, trong đó n\mathbf{n} là vector pháp tuyến đơn vị hướng ra phía ngoài HH. Với một điểm pp nằm cùng phía với HH so với đường thẳng này, khoảng cách từ pp đến đường thẳng bằng c−n⋅pc - \mathbf{n} \cdot p. Đường thẳng tiếp xúc với DSD_S và DTD_T khi và chỉ khi khoảng cách từ SS và TT đến đường thẳng lần lượt bằng rsr_s và rtr_t:

c−n⋅S=rs,c−n⋅T=rtc - \mathbf{n} \cdot S = r_s, \qquad c - \mathbf{n} \cdot T = r_t

Trừ vế theo vế hai đẳng thức, ta được n⋅(T−S)=rs−rt\mathbf{n} \cdot (T - S) = r_s - r_t. Vì T−S=(d,0)T - S = (d, 0), đẳng thức này cho nx=−Δdn_x = -\frac{\Delta}{d}. Do n\mathbf{n} là vector đơn vị hướng lên trên, ny=1−nx2=hdn_y = \sqrt{1 - n_x^2} = \frac{h}{d} với h=d2−Δ2h = \sqrt{d^2 - \Delta^2}. Thay vào đẳng thức thứ nhất, ta tìm được hằng số cc:

n=(−Δd, hd),c=rs+n⋅S=rs+Δ2=rs+rt2\mathbf{n} = \left(-\frac{\Delta}{d},\ \frac{h}{d}\right), \qquad c = r_s + \mathbf{n} \cdot S = r_s + \frac{\Delta}{2} = \frac{r_s + r_t}{2}

Do tính đối xứng qua trục OxOx, tiếp tuyến chung ngoài phía dưới có phương trình n′⋅p=c\mathbf{n}' \cdot p = c, với n′=(−Δd,−hd)\mathbf{n}' = \left(-\frac{\Delta}{d}, -\frac{h}{d}\right) và cùng hằng số cc.

Tiếp điểm của một đường tròn với một tiếp tuyến là chân đường vuông góc hạ từ tâm xuống tiếp tuyến đó. Vì vậy, hai tiếp điểm trên DSD_S là S+rsnS + r_s \mathbf{n} và S+rsn′S + r_s \mathbf{n}', còn hai tiếp điểm trên DTD_T là T+rtnT + r_t \mathbf{n} và T+rtn′T + r_t \mathbf{n}'. Hai vector n\mathbf{n} và n′\mathbf{n}' có cùng hoành độ, nên hai tiếp điểm trên cùng một đường tròn nằm trên cùng một đường thẳng đứng. Hoành độ của hai đường thẳng đứng này lần lượt là

xL=−d2−rsΔd,xR=d2−rtΔdx_L = -\frac{d}{2} - \frac{r_s \Delta}{d}, \qquad x_R = \frac{d}{2} - \frac{r_t \Delta}{d}

và ta có xR−xL=h2d>0x_R - x_L = \frac{h^2}{d} > 0. Hai đường thẳng đứng x=xLx = x_L và x=xRx = x_R chia HH thành ba mảnh (Hình 2):

  1. Trong nửa mặt phẳng x≤xLx \le x_L, biên của HH là cung tròn của DSD_S nối hai tiếp điểm. Do đó, phần của HH nằm trong nửa mặt phẳng này trùng với phần của DSD_S nằm trong nửa mặt phẳng đó.

  2. Trong dải xL≤x≤xRx_L \le x \le x_R, biên của HH gồm hai đoạn tiếp tuyến. Do đó, phần của HH nằm trong dải này là một hình thang cân, chính là giao của bốn nửa mặt phẳng x≥xLx \ge x_L, x≤xRx \le x_R, n⋅p≤c\mathbf{n} \cdot p \le c và n′⋅p≤c\mathbf{n}' \cdot p \le c.

  3. Tương tự, trong nửa mặt phẳng x≥xRx \ge x_R, phần của HH trùng với phần của DTD_T.

Miền ảnh hưởng H chia thành ba mảnh
Hình 2. Các hình tròn trung gian (nét chấm) đều tiếp xúc với hai tiếp tuyến chung ngoài và quét thành miền H. Hai đường thẳng đứng đi qua các tiếp điểm chia H thành hai phần hình tròn (1 và 3) và một hình thang cân (2).

Bước 3: Cắt một tam giác theo ba mảnh của miền ảnh hưởng

Xét một tam giác △=Ovivi+1\triangle = O v_i v_{i+1} ở Bước 1. Ta ký hiệu:

  • △L\triangle_L là phần của tam giác nằm trong nửa mặt phẳng x≤xLx \le x_L;

  • △M\triangle_M là phần của tam giác nằm trong hình thang ở Bước 2;

  • △R\triangle_R là phần của tam giác nằm trong nửa mặt phẳng x≥xRx \ge x_R.

Theo cách chia ở Bước 2, phần chung của △L\triangle_L và HH chính là phần chung của △L\triangle_L và DSD_S; tương tự, phần chung của △R\triangle_R và HH là phần chung của △R\triangle_R và DTD_T; còn △M\triangle_M nằm trọn trong HH. Do đó:

A(△∩H)=A(△L∩DS)+A(△M)+A(△R∩DT)\mathcal{A}(\triangle \cap H) = \mathcal{A}(\triangle_L \cap D_S) + \mathcal{A}(\triangle_M) + \mathcal{A}(\triangle_R \cap D_T)

Số hạng ở giữa là diện tích của một đa giác lồi, được tính bằng công thức shoelace. Hai số hạng còn lại là diện tích phần giao giữa một đa giác lồi và một hình tròn, sẽ được tính ở Bước 4.

Một tam giác cắt thành ba mảnh
Hình 3. Tam giác O vᵢ vᵢ₊₁ được cắt theo ba mảnh của H: phần giao với hình tròn đầu (1), phần nằm trong hình thang (2) và phần giao với hình tròn cuối (3). Phần của tam giác nằm ngoài H không đóng góp vào diện tích.

Cả ba đa giác △L\triangle_L, △M\triangle_M và △R\triangle_R đều thu được bằng cách cắt một đa giác lồi bởi một nửa mặt phẳng, lặp lại một số lần: △L\triangle_L và △R\triangle_R cần một lần cắt, còn △M\triangle_M cần bốn lần cắt, ứng với bốn nửa mặt phẳng tạo nên hình thang. Thao tác cắt này được thực hiện bằng thuật toán Sutherland–Hodgman (hàm clip_halfplane trong code), được trình bày chi tiết trong khung bên dưới. Mỗi lần cắt cho ra một đa giác lồi có cùng chiều với đa giác ban đầu, nên dấu của diện tích có hướng được giữ nguyên; đồng thời, số đỉnh tăng thêm không quá một. Vì vậy, △M\triangle_M có không quá 3+4=73 + 4 = 7 đỉnh.

Thuật toán Sutherland–Hodgman

Bài toán. Cho đa giác lồi Q=q0q1…qm−1Q = q_0 q_1 \dots q_{m-1}, với các đỉnh được liệt kê theo thứ tự trên biên, và nửa mặt phẳng {p:n⋅p≤c}\{p : \mathbf{n} \cdot p \le c\}. Ta cần tìm đa giác là phần chung của QQ và nửa mặt phẳng này.

Phân loại điểm. Đặt f(p)=n⋅p−cf(p) = \mathbf{n} \cdot p - c. Điểm pp nằm trong nửa mặt phẳng nếu f(p)≤0f(p) \le 0, và nằm ngoài nếu f(p)>0f(p) > 0. Vì ff là hàm bậc nhất, trên một cạnh xyxy giá trị của ff thay đổi tuyến tính từ f(x)f(x) đến f(y)f(y). Do đó, cạnh xyxy cắt đường thẳng biên f(p)=0f(p) = 0 khi và chỉ khi hai đầu mút xx và yy nằm ở hai phía khác nhau.

Thuật toán. Ta đi một vòng quanh biên của QQ, lần lượt xét các cạnh q0→q1q_0 \to q_1, q1→q2q_1 \to q_2, …, qm−1→q0q_{m-1} \to q_0. Với mỗi cạnh x→yx \to y, ta ghi các điểm sau vào đa giác kết quả, theo đúng thứ tự:

  1. Nếu xx nằm trong, ta ghi điểm xx.

  2. Nếu xx và yy nằm ở hai phía khác nhau, ta ghi giao điểm của cạnh xyxy với đường thẳng biên.

Giao điểm có dạng x+t(y−x)x + t(y - x) với t∈[0,1]t \in [0, 1]. Vì f(x+t(y−x))=f(x)+t(f(y)−f(x))f(x + t(y - x)) = f(x) + t\big(f(y) - f(x)\big), giao điểm ứng với giá trị

t=f(x)f(x)−f(y)t = \frac{f(x)}{f(x) - f(y)}

Ví dụ. Hình 4 minh họa việc cắt tứ giác ABCDABCD, trong đó AA và BB nằm trong nửa mặt phẳng, còn CC và DD nằm ngoài. Thuật toán xét bốn cạnh như sau:

CạnhĐầu cạnhCuối cạnhCác điểm được ghi
A→BA \to BtrongtrongAA
B→CB \to CtrongngoàiBB, sau đó giao điểm I1I_1
C→DC \to Dngoàingoàikhông có
D→AD \to Angoàitronggiao điểm I2I_2

Kết quả là tứ giác ABI1I2A B I_1 I_2. Các đỉnh nằm trong (AA và BB) được giữ nguyên theo thứ tự cũ, còn phần biên nằm ngoài (I1→C→D→I2I_1 \to C \to D \to I_2) được thay bằng đoạn thẳng I1I2I_1 I_2 nằm trên đường thẳng biên.

Cắt tứ giác ABCD bằng một nửa mặt phẳng
Hình 4. Các mũi tên chỉ chiều duyệt các cạnh. Hai đỉnh A, B nằm trong nửa mặt phẳng nên được giữ lại; hai đỉnh C, D nằm ngoài nên bị loại; I₁ và I₂ là hai giao điểm được thêm vào. Đa giác kết quả là phần được tô màu.

Tính đúng đắn. Phần chung của QQ và nửa mặt phẳng là giao của hai tập lồi, nên cũng là một đa giác lồi. Nếu mọi đỉnh của QQ nằm trong nửa mặt phẳng, kết quả là toàn bộ QQ; nếu mọi đỉnh nằm ngoài, kết quả là tập rỗng; thuật toán cho đúng kết quả trong cả hai trường hợp này. Trong trường hợp còn lại, đường thẳng biên cắt biên của QQ tại đúng hai điểm và chia biên của QQ thành hai đường gấp khúc: một đường nằm trong nửa mặt phẳng và một đường nằm ngoài. Biên của đa giác kết quả gồm đường gấp khúc nằm trong, được khép lại bởi đoạn thẳng nối hai giao điểm. Khi đi một vòng quanh QQ, thuật toán ghi lại đúng các đỉnh của đường gấp khúc nằm trong, cùng với hai giao điểm ở hai đầu, theo thứ tự xuất hiện trên biên. Vì vậy, đa giác kết quả có cùng chiều với QQ. Hơn nữa, có ít nhất một đỉnh của QQ bị loại và chỉ có hai giao điểm được thêm vào, nên đa giác kết quả có không quá (m−1)+2=m+1(m - 1) + 2 = m + 1 đỉnh.

Để cắt một đa giác bởi nhiều nửa mặt phẳng, ta áp dụng thuật toán lần lượt với từng nửa mặt phẳng. Hàm clip_halfplane trong code cài đặt đúng thuật toán trên, kèm theo hai chi tiết để tránh sai số: một điểm nằm ngoài nhưng rất gần đường thẳng biên (sai lệch cỡ 10−1610^{-16}, tính tương đối theo độ lớn của các giá trị) vẫn được coi là nằm trong, và tham số tt được giới hạn trong đoạn [0,1][0, 1].

Bước 4: Diện tích giao giữa đa giác lồi và hình tròn

Cho đa giác lồi Q=q0q1…qm−1Q = q_0 q_1 \dots q_{m-1} và hình tròn DD có tâm CC, bán kính rr. Nhận xét ở Bước 1 đúng với mọi điểm gốc và mọi hình được đem giao, nên ta có thể áp dụng nhận xét đó với điểm gốc là tâm CC và hình DD thay cho HH:

A(Q∩D)=∑j=0m−1A(Cqjqj+1∩D)\mathcal{A}(Q \cap D) = \sum_{j=0}^{m-1} \mathcal{A}(C q_j q_{j+1} \cap D)

Như vậy, ta chỉ cần tính diện tích có hướng của phần giao giữa hình tròn DD và một tam giác CABCAB có một đỉnh là tâm CC. Ta đặt gốc tọa độ tại CC. Đoạn ABAB gồm các điểm A+t(B−A)A + t(B - A) với t∈[0,1]t \in [0, 1], và các giao điểm của đoạn ABAB với đường tròn ứng với các nghiệm t∈(0,1)t \in (0, 1) của phương trình bậc hai

∣A+t(B−A)∣2=r2|A + t(B - A)|^2 = r^2

Phương trình này có nhiều nhất hai nghiệm, nên các giao điểm chia đoạn ABAB thành nhiều nhất ba đoạn con. Mỗi đoạn con pqpq không chứa giao điểm nào ở bên trong, nên nằm hoàn toàn bên trong hoặc hoàn toàn bên ngoài hình tròn. Tương ứng, tam giác CABCAB được chia thành các tam giác nhỏ CpqCpq (Hình 5), và ta tính phần giao của từng tam giác nhỏ với hình tròn:

  • Nếu đoạn pqpq nằm trong hình tròn, cả ba điểm CC, pp, qq đều thuộc hình tròn. Vì hình tròn là một tập lồi, toàn bộ tam giác CpqCpq nằm trong hình tròn, nên phần giao có diện tích có hướng bằng 12(p×q)\frac{1}{2}(p \times q).

  • Nếu đoạn pqpq nằm ngoài hình tròn, mỗi tia xuất phát từ CC và đi qua một điểm của đoạn pqpq sẽ ra khỏi hình tròn trước khi chạm tới đoạn pqpq. Do đó, phần giao chính là hình quạt giới hạn bởi hai tia CpCp và CqCq, có diện tích có hướng bằng 12r2θ\frac{1}{2} r^2 \theta, trong đó θ=atan2⁡(p×q, p⋅q)\theta = \operatorname{atan2}(p \times q,\ p \cdot q) là góc có hướng từ vector pp đến vector qq.

Tam giác CAB giao hình tròn tâm C
Hình 5. Phần giao giữa tam giác CAB và hình tròn tâm C gồm hai hình quạt (1 và 3), ứng với hai đoạn con nằm ngoài hình tròn, và một tam giác (2), ứng với đoạn con nằm trong hình tròn.

Để biết một đoạn con nằm trong hay nằm ngoài hình tròn, ta chỉ cần kiểm tra một điểm bất kỳ ở bên trong đoạn đó. Code kiểm tra hai điểm, tại vị trí 13\frac{1}{3} và 23\frac{2}{3} của đoạn, để hạn chế ảnh hưởng của sai số.

Cài đặt

Với mỗi truy vấn, thuật toán thực hiện như sau:

  1. Nếu d≤∣Δ∣d \le |\Delta|, miền HH là hình tròn lớn hơn trong hai hình tròn. Ta chọn OO là tâm của hình tròn này và cộng các đại lượng A(Ovivi+1∩H)\mathcal{A}(O v_i v_{i+1} \cap H), trong đó mỗi đại lượng được tính bằng Bước 4.

  2. Trong trường hợp còn lại, ta đổi sang hệ tọa độ ở Bước 2 và tính xLx_L, xRx_R, n\mathbf{n}, n′\mathbf{n}' và cc. Với mỗi ii, ta cắt tam giác Ovivi+1O v_i v_{i+1} thành ba phần như ở Bước 3, rồi cộng ba đại lượng tương ứng vào tổng.

  3. Đáp án là trị tuyệt đối của tổng.

Mỗi tam giác chỉ cần một số lượng phép tính không đổi, nên mỗi truy vấn có độ phức tạp O(n)\mathcal{O}(n), và độ phức tạp của toàn bộ thuật toán là O(∑n⋅q)\mathcal{O}\left(\sum n \cdot q\right).

Độ chính xác: Mỗi số hạng A(Ovivi+1∩H)\mathcal{A}(O v_i v_{i+1} \cap H) có thể lớn tới cỡ diện tích của HH (khoảng 3⋅1063 \cdot 10^6), và các số hạng dương, âm triệt tiêu lẫn nhau, trong khi đáp án có thể rất nhỏ nhưng vẫn cần sai số tuyệt đối không quá 10−610^{-6}. Code xử lý vấn đề này bằng ba cách:

  1. Dùng kiểu long double thay cho double. Trên máy chấm Linux x86, long double có khoảng 19 chữ số có nghĩa, so với khoảng 16 chữ số của double.

  2. Cộng dồn đáp án bằng tổng Kahan (xem khung bên dưới), nhờ đó sai số gần như không tích lũy theo số lượng số hạng.

  3. Giải phương trình bậc hai theo cách ổn định số. Xét phương trình αt2+βt+γ=0\alpha t^2 + \beta t + \gamma = 0. Khi β2\beta^2 lớn hơn nhiều so với ∣4αγ∣|4\alpha\gamma|, ta có β2−4αγ≈∣β∣\sqrt{\beta^2 - 4\alpha\gamma} \approx |\beta|, nên trong công thức nghiệm −β±β2−4αγ2α\frac{-\beta \pm \sqrt{\beta^2 - 4\alpha\gamma}}{2\alpha}, một trong hai nghiệm là hiệu của hai số gần bằng nhau, và phép trừ này làm mất gần hết các chữ số có nghĩa. Để tránh điều đó, code chỉ tính trực tiếp nghiệm ứng với phép cộng hai số cùng dấu, rồi suy ra nghiệm còn lại từ định lý Viète t1t2=γαt_1 t_2 = \frac{\gamma}{\alpha}:

    u=−12(β+sgn⁡(β)β2−4αγ),t1=uα,t2=γuu = -\frac{1}{2}\left(\beta + \operatorname{sgn}(\beta)\sqrt{\beta^2 - 4\alpha\gamma}\right), \qquad t_1 = \frac{u}{\alpha}, \qquad t_2 = \frac{\gamma}{u}
Tổng Kahan

Vấn đề. Máy tính lưu mỗi số thực với một số lượng chữ số có nghĩa cố định (với double là 53 bit, tương đương khoảng 16 chữ số thập phân). Vì vậy, kết quả của mỗi phép cộng đều bị làm tròn. Khi cộng một số nhỏ vào một tổng lớn, các chữ số ở hàng thấp của số nhỏ không được lưu lại. Khi cộng liên tiếp nn số, các sai số làm tròn này có thể tích lũy, và trong trường hợp xấu nhất, sai số tăng tỉ lệ với nn.

Giả sử máy tính chỉ lưu được 4 chữ số có nghĩa (theo hệ thập phân), và ta cần tính 1000+1,4+1,4+1,41000 + 1{,}4 + 1{,}4 + 1{,}4. Kết quả chính xác là 1004,21004{,}2; khi làm tròn tới 4 chữ số có nghĩa, kết quả tốt nhất có thể đạt được là 10041004. Ở phép cộng đầu tiên, 1000+1,4=1001,41000 + 1{,}4 = 1001{,}4, nhưng máy chỉ lưu được 10011001, nên phần 0,40{,}4 bị mất (Hình 6). Nếu cộng theo cách thông thường, cả ba phép cộng đều mất 0,40{,}4, và kết quả cuối cùng là 10031003, lệch 1,21{,}2 so với kết quả chính xác.

Phần bị làm tròn mất khi cộng
Hình 6. Khi tổng có độ lớn cỡ 1000, máy chỉ lưu được phần nguyên, nên chữ số 4 sau dấu phẩy bị làm tròn mất. Thuật toán Kahan tính lại chính phần bị mất này.

Ý tưởng. Sau mỗi phép cộng, thuật toán Kahan tính lại phần vừa bị làm tròn mất và bù phần đó vào phép cộng tiếp theo. Biến correction lưu phần bị mất của phép cộng gần nhất:

C++
void add(ld x) {
ld y = x - correction;
ld next = sum + y;
correction = (next - sum) - y;
sum = next;
}

Ý nghĩa của từng dòng như sau:

  • y = x - correction là số hạng mới, đã được bù phần bị mất ở phép cộng trước.

  • next = sum + y là tổng mới; giá trị này đã bị làm tròn.

  • next - sum là phần thực sự đã được cộng vào tổng. Vì next và sum có giá trị gần nhau, phép trừ này thường không bị làm tròn.

  • correction = (next - sum) - y là phần thực sự đã được cộng trừ đi phần cần cộng, tức là sai số của phép cộng vừa thực hiện. Đại lượng này âm nếu kết quả bị làm tròn xuống, và dương nếu kết quả bị làm tròn lên. Ở phép cộng tiếp theo, đại lượng này được trừ khỏi số hạng mới, nghĩa là phần bị mất được cộng bù trở lại.

Bảng sau chạy lại ví dụ trên với máy tính chỉ lưu 4 chữ số có nghĩa:

Lần cộngCộng thông thườngynextcorrectionTổng Kahan
1100110011,41{,}410011001−0,4-0{,}410011001
2100210021,4+0,4=1,81{,}4 + 0{,}4 = 1{,}8100310030,20{,}210031003
3100310031,4−0,2=1,21{,}4 - 0{,}2 = 1{,}210041004−0,2-0{,}210041004

Tổng Kahan cho kết quả 10041004, đúng bằng kết quả chính xác sau khi làm tròn, trong khi cách cộng thông thường cho 10031003. Tổng quát, gọi ε\varepsilon là sai số tương đối của một phép làm tròn. Khi cộng nn số x1,x2,…,xnx_1, x_2, \dots, x_n, sai số của cách cộng thông thường bị chặn bởi khoảng nε∑∣xi∣n \varepsilon \sum |x_i|, còn sai số của tổng Kahan bị chặn bởi khoảng 2ε∑∣xi∣2 \varepsilon \sum |x_i|, gần như không phụ thuộc vào nn. Trong bài này, mỗi truy vấn cộng tới 9⋅1059 \cdot 10^5 số hạng (ba số hạng cho mỗi tam giác), và các số hạng có giá trị lớn, triệt tiêu lẫn nhau, nên tổng Kahan giúp đáp án chính xác hơn đáng kể.

Lưu ý. Không biên dịch chương trình với cờ -ffast-math, cũng như không dùng #pragma GCC optimize("Ofast"). Với các tùy chọn này, trình biên dịch được phép biến đổi biểu thức theo các quy tắc đại số, chẳng hạn rút gọn (next - sum) - y thành 00 vì next = sum + y, làm mất hoàn toàn phần bù.

Code

C++
#include <bits/stdc++.h>
using namespace std;
using ld = long double;
const ld DISC_EPS = 2e-15L;
struct Point {
ld x = 0, y = 0;
Point operator+(Point o) const { return {x + o.x, y + o.y}; }
Point operator-(Point o) const { return {x - o.x, y - o.y}; }
Point operator*(ld k) const { return {x * k, y * k}; }
};
ld dot(Point a, Point b) { return a.x * b.x + a.y * b.y; }
ld cross(Point a, Point b) { return a.x * b.y - a.y * b.x; }
ld norm2(Point a) { return dot(a, a); }
struct Polygon {
array<Point, 12> p{};
int n = 0;
};
struct KahanSum {
ld sum = 0;
ld correction = 0;
void add(ld x) {
ld y = x - correction;
ld next = sum + y;
correction = (next - sum) - y;
sum = next;
}
};
Polygon clip_halfplane(const Polygon &a, Point normal, ld c) {
Polygon b;
if (!a.n)
return b;
auto value = [&](Point p) { return dot(normal, p) - c; };
for (int i = 0; i < a.n; ++i) {
Point x = a.p[i];
Point y = a.p[(i + 1) % a.n];
ld dx = value(x);
ld dy = value(y);
ld eps_x = 1e-16L * (1 + fabsl(c) + fabsl(dot(normal, x)));
ld eps_y = 1e-16L * (1 + fabsl(c) + fabsl(dot(normal, y)));
bool inside_x = dx <= eps_x;
bool inside_y = dy <= eps_y;
if (inside_x) b.p[b.n++] = x;
if (inside_x != inside_y) {
ld t = dx / (dx - dy);
t = clamp(t, 0.0L, 1.0L);
b.p[b.n++] = x + (y - x) * t;
}
}
return b;
}
ld polygon_area(const Polygon &p) {
ld result = 0;
for (int i = 0; i < p.n; ++i)
result += cross(p.p[i], p.p[(i + 1) % p.n]);
return result / 2;
}
int segment_circle_roots(Point a, Point b, ld r, ld roots[2]) {
Point v = b - a;
ld A = norm2(v);
if (A == 0) return 0;
ld B = 2 * dot(a, v);
ld C = norm2(a) - r * r;
ld four_ac = 4 * A * C;
ld discriminant = fmal(B, B, -four_ac);
ld scale = fabsl(B * B) + fabsl(four_ac) + 1;
if (discriminant < -DISC_EPS * scale) return 0;
discriminant = max((ld)0, discriminant);
ld root = sqrtl(discriminant);
if (root <= 1e-24L * sqrtl(scale)) {
roots[0] = -B / (2 * A);
return 1;
}
ld q = -0.5L * (B + copysignl(root, B));
if (fabsl(q) < 1e-30L) {
roots[0] = (-B - root) / (2 * A);
roots[1] = (-B + root) / (2 * A);
} else {
roots[0] = q / A;
roots[1] = C / q;
}
if (roots[0] > roots[1]) swap(roots[0], roots[1]);
return 2;
}
ld edge_circle_area(Point a, Point b, ld r) {
array<ld, 4> cuts{};
int count = 0;
cuts[count++] = 0;
cuts[count++] = 1;
ld roots[2];
int root_count = segment_circle_roots(a, b, r, roots);
for (int i = 0; i < root_count; ++i) {
if (roots[i] > 1e-16L && roots[i] < 1 - 1e-16L) {
cuts[count++] = roots[i];
}
}
sort(cuts.begin(), cuts.begin() + count);
int unique_count = 0;
for (int i = 0; i < count; ++i) {
if (!unique_count || fabsl(cuts[i] - cuts[unique_count - 1]) > 1e-15L) {
cuts[unique_count++] = cuts[i];
}
}
Point direction = b - a;
ld answer = 0;
ld r2 = r * r;
for (int i = 0; i + 1 < unique_count; ++i) {
ld left = cuts[i];
ld right = cuts[i + 1];
Point p = a + direction * left;
Point q = a + direction * right;
Point sample1 = a + direction * ((2 * left + right) / 3);
Point sample2 = a + direction * ((left + 2 * right) / 3);
ld tolerance = 1e-15L * (r2 + norm2(sample1) + norm2(sample2) + 1);
bool inside = norm2(sample1) <= r2 + tolerance && norm2(sample2) <= r2 + tolerance;
if (inside) {
answer += cross(p, q) / 2;
} else {
answer += r2 * atan2l(cross(p, q), dot(p, q)) / 2;
}
}
return answer;
}
ld polygon_circle_area(const Polygon &p, Point center, ld r) {
ld answer = 0;
for (int i = 0; i < p.n; ++i) {
Point a = p.p[i] - center;
Point b = p.p[(i + 1) % p.n] - center;
answer += edge_circle_area(a, b, r);
}
return answer;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int tests;
cin >> tests;
cout << fixed << setprecision(12);
while (tests--) {
int n;
cin >> n;
vector<Point> polygon(n);
for (Point &p : polygon) {
long long x, y;
cin >> x >> y;
p = {static_cast<ld>(x), static_cast<ld>(y)};
}
int q;
cin >> q;
while (q--) {
long long xs, ys, rs_input;
long long xt, yt, rt_input;
cin >> xs >> ys >> rs_input >> xt >> yt >> rt_input;
Point start{static_cast<ld>(xs), static_cast<ld>(ys)};
Point target{static_cast<ld>(xt), static_cast<ld>(yt)};
ld rs = rs_input;
ld rt = rt_input;
Point delta = target - start;
ld distance2 = norm2(delta);
ld radius_delta = rt - rs;
bool is_circle = distance2 <= radius_delta * radius_delta + 1e-24L;
Point origin;
Point axis_x{1, 0};
Point axis_y{0, 1};
Point s, t;
ld radius = 0;
ld distance = 0;
ld height = 0;
ld left_x = 0;
ld right_x = 0;
Point normal_top;
Point normal_bottom;
ld tangent_constant = 0;
if (is_circle) {
if (rs >= rt) {
origin = start;
radius = rs;
} else {
origin = target;
radius = rt;
}
} else {
distance = sqrtl(distance2);
origin = (start + target) * 0.5L;
axis_x = delta * (1 / distance);
axis_y = {-axis_x.y, axis_x.x};
s = {-distance / 2, 0};
t = {distance / 2, 0};
height = sqrtl(max((ld)0, distance2 - radius_delta * radius_delta));
left_x = -distance / 2 - rs * radius_delta / distance;
right_x = distance / 2 - rt * radius_delta / distance;
normal_top = {-radius_delta / distance, height / distance};
normal_bottom = {-radius_delta / distance, -height / distance};
tangent_constant = (rs + rt) / 2;
}
vector<Point> transformed(n);
for (int i = 0; i < n; ++i) {
Point v = polygon[i] - origin;
transformed[i] = {dot(v, axis_x), dot(v, axis_y)};
}
KahanSum answer;
for (int i = 0; i < n; ++i) {
Polygon triangle;
triangle.n = 3;
triangle.p[0] = {0, 0};
triangle.p[1] = transformed[i];
triangle.p[2] = transformed[(i + 1) % n];
if (is_circle) {
answer.add(polygon_circle_area(triangle, {0, 0}, radius));
continue;
}
Polygon left = clip_halfplane(triangle, {1, 0}, left_x);
answer.add(polygon_circle_area(left, s, rs));
Polygon middle = clip_halfplane(triangle, {-1, 0}, -left_x);
middle = clip_halfplane(middle, {1, 0}, right_x);
middle = clip_halfplane(middle, normal_top, tangent_constant);
middle = clip_halfplane(middle, normal_bottom, tangent_constant);
answer.add(polygon_area(middle));
Polygon right = clip_halfplane(triangle, {-1, 0}, -right_x);
answer.add(polygon_circle_area(right, t, rt));
}
cout << fabsl(answer.sum) << '\n';
}
}
}

Visionary

🎧MƯA ĐỢI CHỜ - MELODIC EDM VESION | VIRAL 2026 | GEN Z music VN

ZACE HYPER

0:00 0:00

Visionary