<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>vietnamese &amp;mdash; Zumi&#39;s Blog</title>
    <link>https://blog.dtth.ch/nki/tag:vietnamese</link>
    <description>Just random Zumi Zoom things</description>
    <pubDate>Tue, 28 Apr 2026 20:51:06 +0200</pubDate>
    <item>
      <title>HSGSO 2017 Editorial</title>
      <link>https://blog.dtth.ch/nki/hsgso-2017-editorial</link>
      <description>&lt;![CDATA[#editorial #hsgso #vietnamese&#xA;&#xA;Đây chắc là một trong những bài viết được chờ đợi nhất sau kì thi HSGSO 2017 vừa rồi. Ngoài chữa bài, mình sẽ đưa ra một số thống kê về đề và lượng người giải được!&#xA;&#xA;Phần chữa bài được thực hiện bởi mình và bạn Nguyễn Đinh Quang Minh. Nếu có câu hỏi gì bạn có thể hỏi mình hoặc Minh qua facebook.&#xA;&#xA;Đề bài&#xA;Các bạn có thể tải về đề bài tại đây (mirror). Trong phần comment của link cũng có chữa tóm tắt một vài bài do các bạn của dự tuyển Tổng Hợp.&#xA;&#xA;!--more--&#xA;&#xA;Lời giải&#xA;Mục lục&#xA;Ngày 1&#xA;number&#xA;race&#xA;tree&#xA;inversion&#xA;&#xA;Ngày 2&#xA;matrix&#xA;string&#xA;p2grp&#xA;turtle&#xA;&#xA;number&#xA;&#xA;Author: thầy Phương, Phạm Cao Nguyên&#xA;Tester: Nguyễn Hoàng Hải Minh&#xA;Các subtask nhỏ&#xA;Subtask 1 (\\(S \le 20\\)) có thể dễ dàng giải bằng thuật quay lui, thử tất cả các trường hợp cắt với độ phức tạp \\(O(M1 \times M2 ... \times MN)\\).&#xA;Subtask 2 (\\(S \le 500, K \le 18\\)): Do giới hạn của \\(K\\), đáp số là một số không quá giới hạn long long, vì vậy ta có thể thực hiện đơn giản phép quy hoạch động.&#xA;Gọi \\(fi\\) là số lớn nhất có thể tạo được nếu ta sử dụng các đoạn từ 1 đến \\(i\\) và lấy tổng cộng \\(j\\) chữ số.&#xA;Hiển nhiên \\(f0 = 0\\). Ta có:&#xA;\\f[i = \sum\limits\{k=0}^{\min(M\i, j)} fi - 1 \times 10^k + \overline{A\{i,1}A\{i,2}...A\{i,k}}\\]&#xA;Đáp số chính là \\(fN\\). Lưu ý độ phức tạp của thuật toán này chỉ là \\(O(SK)\\) chứ không phải \\(O(N^2K)\\).&#xA;&#xA;Tham lam 1 chữ số&#xA;Subtask 3 có một giới hạn khá lạ: Mỗi đoạn có độ dài đều là 1. Như vậy, việc chọn một đoạn chỉ đơn giản là lấy hoặc không. Ta có thể ra một thuật toán tham lam cho subtask này.&#xA;&#xA;Ta có thể thấy, ở mỗi vị trí từ trái sang phải ta sẽ luôn ưu tiên số lớn nhất ở trước. Thật vậy, dễ dàng chứng minh điều này: Khi so sánh 2 số có cùng độ dài, ta chỉ cần tìm chữ số đầu tiên khác nhau từ trái sang và so sánh chúng.&#xA;&#xA;Ý tưởng tham lam như sau. Giả sử ta đã lấy \\(x\\) chữ số, chữ số cuối cùng ta lấy ở vị trí \\(i\\). Ta có các nhận xét sau về chữ số tiếp theo cần lấy (ở vị trí \\(j\\)):&#xA;Hiển nhiên ta không thể lấy tiếp ở các vị trí \\(j \le i\\).&#xA;Hiển nhiên không kém, ta không thể lấy ở các vị trí \\(j   N - (K - x) + 1\\). Đơn giản là, nếu lấy ở các vị trí này, ta không còn đủ chữ số để dựng số có \\(K\\) chữ số nữa.&#xA;Trong các vị trí còn lại, ta luôn chọn vị trí có chữ số lớn nhất. Điều này hiển nhiên đúng, vì về sau ta chọn thế nào thì cũng ra số lớn hơn số được tạo ra nếu ta không chọn chữ số lớn nhất ở bước này.&#xA;Trong các vị trí có cùng giá trị, lấy vị trí trái nhất. Điều này đảm bảo ta có nhiều cơ hội hơn để lấy số lớn hơn ở vị trí tiếp theo.&#xA;&#xA;Như vậy subtask 3 có thể giải với độ phức tạp \\(O(10 \times N)\\). Hãy lưu ý subtask này, vì ý tưởng tham lam là chìa khóa để giải subtask cuối!&#xA;&#xA;Cải tiến quy hoạch động&#xA;2 subtask tiếp theo (4 và 5) chỉ là việc cài đặt công thức quy hoạch động của subtask 2 lên với số lớn. Dễ dàng nhận thấy ta chỉ phải cài đặt phép cộng số lớn, và đáp số không quá \\(10^K\\) nên độ phức tạp là \\(O(SK^2)\\).&#xA;&#xA;Dễ dàng cài số lớn qua subtask 4, nhưng để qua subtask 5 sẽ cần thêm một chút cải tiến để lọt time limit (vd nén 9 chữ số vào một int thay vì giữ từng chữ số, cộng dồn đoạn để giảm số phép tính,...)&#xA;&#xA;Subtask cuối&#xA;Để giải được subtask cuối, ta cần bỏ đi suy nghĩ quy hoạch động và quay lại với ý tưởng tham lam.&#xA;&#xA;Tại sao ý tưởng của subtask 3 không thể áp dụng được ngay vào subtask cuối? Bởi vì trong mỗi đoạn, ta bắt buộc phải chọn 1 đoạn tiền tố để đặt vào đáp số. Nhiều khi, chữ số phía sau lớn nhưng đoạn chữ số phía trước không tối ưu.&#xA;&#xA;Ta có thể chỉ xét chữ số tiếp theo của chuỗi đang lấy cuối cùng, hoặc đầu của các chuỗi phía sau. Đây là một nhận xét đúng. Thật vậy: Kiểu gì bạn cũng phải đặt chữ số đầu tiên của một chuỗi nếu lấy chuỗi đó. Và nếu chữ số đầu tiên có lựa chọn tốt hơn: điều đúng đắn là bỏ cả chuỗi.&#xA;&#xA;Tuy nhiên, có một vấn đề khác. Khi có nhiều lựa chọn chữ số tiếp theo giống nhau, chọn chữ số trái cùng không phải lựa chọn tối ưu. Bản chất là vì lựa chọn của bạn còn làm ảnh hưởng đến tập đáp số tối ưu phía sau:&#xA;&#xA;Nếu \\(M\i = 1\\) với mọi \\(i\\), lựa chọn của bạn luôn là \\((x\j, y\j)\\) (giá trị, vị trí) với chữ số \\(j\\) (từ trái sang) và \\(j\\) luôn tốt hơn \\(j + 1\\).&#xA;Khi điều kiện 1 không còn nữa, không phải lúc nào \\(j\\) cũng tốt hơn \\(j + 1\\). Xét trường hợp sau: 5 6 và 5 7 (\\(K = 2\\)). Nếu bạn chọn số 5 bên trái, tiếp theo bạn chỉ được chọn giữa 6 và 5 cho chữ số tiếp theo. Trong khi đó đáp số hiển nhiên là 57.&#xA;&#xA;Tuy vậy, ta nhận thấy đáp án tối ưu vẫn luôn xuất phát từ chữ số lớn nhất, và đáp án vẫn là chọn số lớn nhất có thể ở mỗi bước. Vậy ta vẫn có thể đưa số 5 vào, chỉ có điều ta chưa quyết định đấy là số 5 ở vị trí nào. Hay hiểu kiểu khác, trước tiên ta chọn 5 của 5 6, nhưng do 5 7 có đầu 5 trùng với đuôi 5 của ta nên ta xóa 5 của 5 6 đi và thêm cả 5 7 vào.&#xA;&#xA;Tóm tắt lại, thuật toán của ta như sau:&#xA;Giả sử ta đã có \\(i\\) chữ số \\(x\1, x\2, ..., x\i\\) (\\(x\i\\) ở dãy \\(j\\)), ta sẽ có tập lựa chọn cho chữ số tiếp theo (dãy \\(p\\) vị trí \\(q\\)) như sau:&#xA;Nó phải thuộc dãy \\(p \ge j\\). Hiển nhiên ta không thể lấy lại một dãy trước đó.&#xA;Sau nó phải có ít nhất \\(K - i - 1\\) chữ số (từ nó về cuối dãy, và tổng tất cả dãy phía sau).&#xA;\\(p = j\\) và nó là chữ số đứng sau \\(x\i\\), hoặc&#xA;\\(p   j\\), \\(x\{i - k} = Ap\\) với mọi \\(0 \le k &lt; q - 1\\). Khi chọn lựa chọn này, ta xóa tất cả \\(x\{i - q + 2}..x\i\\) và thêm vào \\(Ap\\).&#xA;&#xA;Dễ dàng chứng minh ở mỗi bước, ta đều lấy được chữ số lớn nhất có thể lấy được khi có prefix dựng từ bước trước đó (do vậy, nếu prefix tối ưu thì theo phép so sánh số, dãy mới là tối ưu). Do ở bước đầu (prefix rỗng), lựa chọn là tối ưu, nên theo quy nạp thuật toán tham là tối ưu.&#xA;&#xA;Ta có thể cài thuật toán này trong độ phức tạp \\(O(SK)\\).&#xA;&#xA;Một chút thống kê&#xA;&#xA;Về điểm số:&#xA;Có một bạn duy nhất được 99 điểm (Bùi Hồng Đức). Xin chúc mừng Đức đã ra được thuật toán đúng cho subtask cuối. Thật vậy, Đức chỉ còn 1 test lắt léo duy nhất: Đề bài yêu cầu in ra số tạo từ \\(K\\) chữ số đã chọn, mà đã là số thì phải không có số 0 ở đầu :( Kể ra bài đã khó, test cũng khó, sorry mọi người vì cái test quái gở này.&#xA;Có 12 bạn được 73-76 điểm. Khi sinh test mình đã tính đến việc các bạn sử dụng tham lam của subtask 3, đưa thẳng vào subtask 6 (chỉ qua nhận xét đầu tiên). Đây là số điểm các bạn nhận được :) Kể ra không cho điểm nào thì cũng hơi ác, nhưng cho nhiều điểm quá thì cũng không đáng (nhớ rằng tối ưu bignum chỉ được thêm 10 điểm). Thật sự 6 điểm khá là nhiều cho một thuật sai khá là rõ ràng, và còn vài bạn còn cài sai thuật đó nữa...&#xA;6 bạn được 70 điểm. Chúc mừng 6 bạn đã qua được mốc quan trọng của một bài khó: cắn nhiều nhất có thể. Đây là chiến thuật hữu dụng nhất khi thi quốc gia!&#xA;Còn lại 71 bạn có điểm dương, trải dài khá đều từ 1 đến 65 điểm. Rất tuyên dương cố gắng của các bạn! Hãy cố lên! Đặc biệt với những bạn được 1 điểm, mình không hiểu tại sao có thể được 1 điểm...&#xA;&#xA;Về bộ test:&#xA;Bản thân mình khá hài lòng về bộ test mình sinh ra. Có đủ điểm cho người liều lĩnh, đủ điểm cho người chắc ăn, đủ lỗi để hành người sai,...&#xA;Trong bộ test (sau này sẽ được up), có 2 test được sinh ra để giết một nhận xét tham lam của subtask 6... Đố bạn nào biết đó là nhận xét gì :)&#xA;&#xA;race&#xA;&#xA;Author: Nguyễn Đinh Quang Minh&#xA;Tester: Nguyễn Hoàng Hải Minh&#xA;&#xA;Bài toán lớp bốn và thuật toán O(N^3)&#xA;  Ba người \\(A\\), \\(B\\), \\(C\\) xuất phát ở ba vị trí \\(x\A\\), \\(x\B\\), \\(x\C\\) trên một đường thẳng, cùng đi về một hướng với vận tốc \\(v\A\\), \\(v\B\\), \\(v\C\\). Hỏi có thời điểm nào người \\(B\\) nằm ở trung điểm 2 người còn lại hay không?&#xA;&#xA;Thầy Phương từng đố mình bài toán trên. Nếu bỏ qua dữ kiện ‘đây là bài toán lớp bốn’ thì chúng ta (tất nhiên kể cả mình) đều sẽ cầm bút giải phương trình bậc nhất rồi tìm nghiệm. Nhưng hãy thử nghĩ như một học sinh lớp bốn xem sao…&#xA;&#xA;Cách giải của các bé lớp bốn cho bài toán này như sau: giả sử có thêm một người \\(D\\) di chuyển sao cho \\(D\\) luôn nằm ở trung điểm của \\(A\\) và \\(C\\), khi đó nếu \\(B\\) gặp \\(D\\) tức là \\(B\\) nằm ở trung điểm \\(AC\\). Vị trí xuất phát và vận tốc của \\(D\\) như thế nào cho hợp lý? Hiển nhiên là trung bình cộng của \\(A\\) và \\(C\\), tức là \\(x\D = \dfrac{x\A + x\C}{2}\\), \\(v\D = \dfrac{vA + vC}{2}\\).&#xA;&#xA;Khi đã có người D rồi thì có 3 trường hợp xảy ra:&#xA;&#xA;\\(B\\) luôn chạy trùng với \\(D\\): có vô hạn thời điểm thỏa mãn;&#xA;\\(B\\) không gặp \\(D\\) (kết quả tăng 0);&#xA;\\(B\\) có gặp \\(D\\) (kết quả tăng 1).&#xA;&#xA;Điều kiện cụ thể cho từng trường hợp xảy ra như sau:&#xA;&#xA;| x |      \\(v\B  v\D\\) | \\(v\B = v\D\\) |  \\(v\B  v\D\\) |&#xA;|----------|:-------------:|:------:|:-------------:|&#xA;| \\(x\B &lt; x\D\\) | 0 | 0 | 1 |&#xA;| \\(x\B = x\D\\) | 1 | vô hạn | 1 |&#xA;| \\(x\B   x\D\\) | 1 | 0 | 0 |&#xA;&#xA;Như vậy thuật giải \\(O(N^3)\\) chỉ đơn giản là duyệt tất cả các bộ ba phân biệt rồi kiểm tra theo bảng trên.&#xA;&#xA;Điều kiện trên nhìn quen quen?&#xA;Để tối ưu thuật toán, ta sẽ nghĩ đến việc duyệt 2 người \\(A\\) và \\(C\\), tính được thông tin người \\(D\\), rồi tìm số người \\(B\\) thỏa mãn. Cụ thể hơn:&#xA;Nếu tồn tại người B sao cho xB = xD, vB = vD thì trả về kết quả là infinity.&#xA;Cộng vào kết quả số người B thỏa mãn \\(x\B \le X\D\\), \\(v\B   v\D\\).&#xA;Cộng vào kết quả số người B thỏa mãn \\(x\B \ge x\D\\), \\(v\B &lt; v\D\\).&#xA;&#xA;Hai điều kiện trên (trừ điều kiện in ra infinity rất dễ) đều khá quen thuộc: ý tưởng của nó giống với bài polylines của năm ngoái. Ta có thể sort tất cả những người \\(B\\) và \\(D\\) theo chiều \\(v\\) tăng dần rồi sử dụng cấu trúc dữ liệu BIT cho chiều \\(x\\).&#xA;&#xA;Không biết cài BIT thì sao?&#xA;Thì bạn nên học cài BIT ta sẽ thử một thuật giải không sử dụng BIT mà sẽ sử dụng IT.&#xA;&#xA;Trước hết, chúng ta chỉ quan tâm đến giá trị \\(x\\) và \\(v\\) của mỗi người đứng thứ mấy nên ta hoàn toàn có thể sort các giá trị \\(x\\) và \\(v\\) lại rồi “đánh số” lại theo thứ tự của giá trị trong dãy được sort.&#xA;Bây giờ khi có một người \\(D\\) nào đó, ta có thể biết \\(x\D\\) đứng ở vị trí thứ mấy trong mảng \\(x\\) bằng chặt nhị phân, tương tự với \\(v\D\\). Điều này sẽ giúp ta giải quyết bài toán bằng mảng cộng dồn 2 chiều thay vì BIT.&#xA;&#xA;Trước hết, ta lập một mảng \\(A1..N\\), \\(Ai = 1\\) có nghĩa là tồn tại một người có giá trị \\(x\\) đứng thứ \\(i\\) và giá trị \\(v\\) đứng thứ \\(j\\), ngược lại \\(Ai = 0\\). Giả sử xét một người \\(D\\) có \\(x\D\\) đứng ở vị trí thứ \\(X\\), \\(v\D\\) đứng ở vị trí thứ \\(V\\). Như vậy, số người thỏa mãn \\(x\B \le x\D\\), \\(v\B   v\D\\) chính là tổng các số trong hình chữ nhật con có góc trên trái là \\((1, V)\\), góc dưới phải là \\((X, N)\\) và được tính trong \\(O(1)\\) bằng mảng cộng dồn.&#xA;&#xA;One more thing&#xA;Để cài đặt cho tiện, bạn nên nhân đôi tất cả tọa độ và vận tốc lên ¯\\\(ツ)\/¯&#xA;&#xA;Score Distribution!&#xA;&#xA;tree&#xA;&#xA;Author: Phạm Đức Thắng&#xA;Tester: Nguyễn Hoàng Hải Minh&#xA;&#xA;\\(N = 1\\)!&#xA;Trước tiên, cần phải để ý nếu \\(N = 1\\) thì đỉnh duy nhất phải có bậc 0. Khi đó, đường đi dài nhất là 1.&#xA;&#xA;Rất nhiều bạn đã chết test này!&#xA;&#xA;Subtask nhỏ&#xA;&#xA;Subtask 1 (\\(N \le 15\\)), bạn có thể backtrack mọi cách dựng cây?&#xA;&#xA;Dựng được cây không?&#xA;&#xA;Điều kiện sau là điều kiện cần và đủ để tồn tại cây:&#xA;Tổng bậc là \\(2N - 2\\)&#xA;Không đỉnh nào có bậc quá \\(N - 1\\).&#xA;Không đỉnh nào có bậc dưới \\(1\\).&#xA;&#xA;Dễ dàng chứng minh điều kiện này bằng quy nạp.&#xA;&#xA;Subtask 2 sinh ra để bạn kiểm tra việc dựng cây nếu không biết điều này :D&#xA;&#xA;Dựng tối ưu!&#xA;Gọi \\(x\\) là số nút có bậc lớn hơn 1 (tức không phải lá). Ta sẽ chứng minh nếu \\(N \ge 2\\) thì đáp số là \\(x + 1\\).&#xA;&#xA;Thật vậy, ta gọi tập đỉnh không phải lá là \\(A\1, A\2, ..., A\x\\). Trước tiên, dựng tập cạnh như sau:&#xA;lá - A[1] - A[2] - ... - A[x] - lá&#xA;Dễ dàng nhận thấy tổng bậc còn lại của các đỉnh không phải lá là \\((2N - 2) - (N - x) - 2x = N - x - 2\\), số lá còn lại là \\(N - x - 2\\). Như vậy ta có thể thêm lá vào các bậc còn thiếu của từng đỉnh. Cây đã dựng xong! Đường đi dài nhất trên cây này chắc chắn là \\(x + 1\\).&#xA;&#xA;Không khó để chứng minh đáp số không thể lớn hơn \\(x + 1\\) (đường đi đơn không thể đi qua 3 lá). Vì vậy \\(x + 1\\) là đáp án tối ưu.&#xA;&#xA;Phân bố điểm&#xA;Rất nhiều bạn AC bài này, tuy nhiên cũng rất nhiều người bị 98 điểm do thiếu \\(N = 1\\). Hãy cẩn thận với những trường hợp biên!&#xA;&#xA;inversion&#xA;&#xA;Author: Nguyễn Thành Vinh&#xA;Tester: Nguyễn Hoàng Hải Minh&#xA;&#xA;Chạy trâu!&#xA;For từng đoạn con, tính cặp nghịch thế trâu, \\(O(N^4)\\) ăn sub 1. Ta có thể cải tiến hơn, tính nghịch thể trong \\(O(N \log N)\\) bằng BIT, tuy nhiên vẫn chưa đủ để ăn subtask 2.&#xA;&#xA;Tịnh tiến Level 1&#xA;Nhận thấy, mỗi lần for đoạn con là một lần tính lại. Đoạn \\((i, j)\\) chỉ khác đoạn \\((i, j + 1)\\) một lượng là số phần tử trong khoảng \\((i, j)\\) mà lớn hơn \\(A[j + 1]\\). Như vậy, ta có thể for \\(i\\) rồi vừa tăng \\(j\\) vừa tính thêm, giảm độ phức tạp xuống \\(O(N^2 \log N)\\).&#xA;&#xA;Tịnh tiến Level 2&#xA;Từ việc tịnh tiến trên, ta nhận thấy rằng nếu \\(i \le j &lt; k\\) thì \\((i, j)\\) luôn có số nghịch thế không quá số nghịch thế của \\((i, k)\\).&#xA;&#xA;Đồng thời, nhận xét cũng đúng với \\((j, i)\\) và \\((k, i)\\) nếu \\(k &lt; j \le i\\).&#xA;&#xA;Giả sử \\(j\\) là số lớn nhất sao cho \\((i, j)\\) có số nghịch thế \\(&lt; K\\). Dễ dàng nhận thấy tất cả \\((i, j&#39;)\\) với \\(j &lt; j&#39; \le N\\) đều là đáp án. Như vậy với mỗi \\(i\\) ta chỉ cần tìm \\(j\\) là đủ.&#xA;&#xA;Lại có \\((i, j)\\) có số nghịch thế \\(&lt; K\\), vì thế \\((i + 1, j)\\) chắc chắn có số nghịch thế \\(&lt; K\\). Ta không cần for các giá trị nhỏ hơn \\(j\\) để kiểm tra nữa. Thay vì for lại \\(j\\), ta xóa \\(i\\) khỏi đoạn, giảm số nghịch thế đi 1 lượng là số số nhỏ hơn \\(A[i]\\).&#xA;&#xA;2 con trỏ \\(i\\) và \\(j\\) đều tăng từ 1 đến \\(N\\), mỗi lần tăng con trỏ phải update mất \\(O(\log N)\\). Độ phức tạp của thuật toán là \\(O(N \log N)\\).&#xA;&#xA;Phân bố điểm&#xA;Đây là một bài khá cơ bản, dù vậy điểm khá là thấp so với bọn mình kì vọng, chủ yếu mọi người chỉ dừng ở subtask 2.&#xA;&#xA;matrix&#xA;&#xA;Author: Nguyễn Đinh Quang Minh&#xA;Tester: Phạm Cao Nguyên, Nguyễn Hoàng Hải Minh&#xA;&#xA;Các subtask nhỏ&#xA;Subtask 1 (\\(N = 1\\)), đây là subtask cho điểm vì bản thân cả bảng đã được chứa trong mảng \\(B[]\\). Nếu xor tất cả phần tử trong mảng \\(B[]\\) bằng \\(A[1]\\) thì có 1 đáp án chính là mảng \\(B[]\\). Nếu không thì không có đáp án nào cả. Độ phức tạp \\(O(M)\\).&#xA;Subtask 2 (\\(NM \le 20\\)), từ điều kiện ta suy ra \\(\min(N, M) \le \sqrt{20} \le 5\\). &#xA;Không mất tính tổng quát ta giả sử \\(M \le 5\\) (nếu không ta xoay bảng lại), ta có thể quy hoạch động \\(fi\\) là số cách tạo các hàng từ 1 đến \\(i\\), với tổng xor từng cột được biểu diễn trong \\(mask\\). Sau đó ta có thể for tất cả các mask \\(m\\) có thể của hàng \\(i + 1\\), update vào \\(fi + 1\\). Độ phức tạp là \\(O(N \times (2^M)^2)\\). &#xA;Hoặc ta có thể backtrack giá trị của tất cả phần tử của bảng, độ phức tạp là \\(O(2^{N + M})\\).&#xA;Subtask 3 (\\(N \le 10\\), \\(M \le 2000\\)), ta sẽ cải tiến thuật toán quy hoạch động phía trên. Thay vì mỗi lần ta thêm cả cột (mất \\(O(2^N)\\)), ta chỉ thêm từng ô. &#xA;Gọi \\(fimask]\\) là số cách điền các ô từ \\((i, j)\\) trở đi (theo từng cột, rồi từng hàng), và xor các phần tử đã đặt vào trước đó của mỗi hàng được biểu diễn trong mask. Ta tính từ \\(f[i + 1mask&#39;]\\) hoặc \\(f[1[mask&#39;]\\), với điều kiện là khi điền xong mỗi cột thì xor cột đó phải bằng \\(B[j]\\). Ta có thể kiểm tra điều này bằng cách kiểm tra \\(X = B[1] \oplus B[2] \oplus ... \oplus B[j]\\) có bằng xor các bit trong \\(mask\\) không. &#xA;Độ phức tạp là \\(O(NM2^N)\\).&#xA;&#xA;Dựng một đáp án&#xA;Trước khi tìm hiểu xem có bao nhiêu đáp án thỏa mãn, ta cần phải kiểm tra xem có tồn tại đáp án không đã.&#xA;&#xA;Hiển nhiên nếu tổng xor của \\(A[]\\) khác tổng xor của \\(B[]\\) thì không thể tồn tại bảng thỏa mãn. Còn lại, ta sẽ chứng minh luôn tồn tại bảng thỏa mãn.&#xA;&#xA;Gọi tổng xor cả bảng là \\(S\\).&#xA;&#xA;Xét bảng \\(N \times M\\) được dựng như sau:&#xA;&#xA;Với \\(i &lt; N\\) và \\(j &lt; M\\): điền 0.&#xA;Với \\(i = N\\) và \\(j &lt; M\\): điền \\(B[j]\\).&#xA;Với \\(i &lt; N\\) và \\(j = M\\): điền \\(A[i]\\).&#xA;Xét ô \\((N, M)\\). Ta có giá trị \\(bN = AN] \oplus B[1] \oplus B[2] \oplus ... \oplus B[M - 1]\\), hay \\(b[N = A[N] \oplus S \oplus B[M]\\). Tất nhiên phân tích theo cột ta cũng sẽ ra công thức này, và vì \\(S\\) xác định nên có duy nhất 1 cách điền ô \\((N, M)\\). &#xA;&#xA;Vậy tồn tại bảng thỏa mãn.&#xA;&#xA;Có bao nhiêu bảng thỏa mãn?&#xA;Giả sử ta có bảng \\(b\\) thỏa mãn. &#xA;&#xA;Nếu ta flip giá trị \\(bi\\) (\\(i &lt; N\\), \\(j &lt; M\\)), ta có thể flip cả \\(bi\\), \\(bN\\) và \\(N\\) để ra một bảng mới vẫn đúng. (mỗi hàng mỗi cột ảnh hưởng đều bị flip tổng xor 2 lần).&#xA;&#xA;Nếu ta giữ nguyên \\(bi\\) với mọi \\(i &lt; N\\) và \\(j &lt; M\\), dễ dàng chứng minh cách điền các ô còn lại là duy nhất. Thật vậy, \\(bi\\) (\\(i &lt; M\\)) phải xác định duy nhất để tổng xor các ô trong hàng \\(i\\) bằng \\(Ai]\\), tương tự với \\(b[N\\) (\\(j &lt; N\\)). Còn lại ô \\((N, M)\\), tất nhiên vì hàng và cột cuối cũng có tổng xor xác định nên giá trị của ô cũng chỉ có tối đa một.&#xA;&#xA;Từ 2 nhận xét trên, ta thấy với mỗi cách điền bảng \\((N - 1)\times (M - 1)\\) ở góc trái trên ta có duy nhất 1 cách điền nốt thỏa mãn. Vậy số bảng thỏa mãn là \\(2^{(N - 1)(M - 1)}\\). &#xA;&#xA;Kiểm tra điều kiện tồn tại mất \\(O(N + M)\\), tính số bảng mất \\(O(\log(NM))\\), dựng 1 bảng thỏa mãn mất \\(O(NM)\\).&#xA;&#xA;Thống kê&#xA;&#xA;Subtask 2 là một subtask rất đơn giản (backtrack lấy 20 điểm), vậy mà số bạn làm được khá là ít. Subtask 3 đòi hỏi kỹ thuật quy hoạch động khó hơn, không nhiều bạn làm được. &#xA;&#xA;Chúc mừng 4 bạn làm được 100 điểm!&#xA;&#xA;string&#xA;&#xA;Author: Vương Hoàng Long&#xA;Tester: Nguyễn Hoàng Hải Minh&#xA;&#xA;Subtask nhỏ&#xA;Do độ dài của 2 xâu không quá \\(10^8\\) nên ta có thể thực hiện for trâu để giải subtask này. Tất nhiên có một số điều sau phải chú ý:&#xA;&#xA;Tuyệt đối không dựng cả xâu. Dựng xâu mất thời gian hằng số rất lớn, có thể làm bạn bị TLE trước cả khi thực hiện so sánh. Chỉ for các chỉ số lặp lại.&#xA;Để tăng tốc, không dùng phép mod để tính nhanh chỉ số. &#xA;&#xA;Độ phức tạp là \\(O(|A|)\\).&#xA;&#xA;Subtask lớn&#xA;Để đơn giản cho việc mod ta coi xâu đánh số từ 0.&#xA;&#xA;Nhận thấy phần tử thứ \\(i\\) của xâu \\(X\\) sẽ được so sánh với các phần tử \\(i \mod |Y|, (i + |X|) \mod |Y|, ..., (i + (n - 1)|X|) \mod |Y|\\) của \\(Y\\).&#xA;&#xA;Không khó để nhận ra dãy này có chu trình độ dài \\(|Y| / \gcd(|X|, |Y|)\\). Không khó để chứng minh điều này: \\(|X||Y| / \gcd(|X|, |Y|)\\) chính là bội chung nhỏ nhất của \\(|X|\\) và \\(|Y|\\), bội dương đầu tiên của \\(|X|\\) chia hết cho \\(|Y|\\).&#xA;&#xA;Như vậy, ta chỉ cần chia \\(Y\\) thành các chu trình tương ứng, sau đó tính trước số kí tự từng loại là có thể so sánh 1 vị trí của \\(X\\) với 1 chu trình trong \\(O(1)\\).&#xA;&#xA;Độ phức tạp là \\(O(|Y| + |X|)\\).&#xA;&#xA;Thống kê&#xA;Đây là bài dễ của ngày 2. Tuy vậy mình không nghĩ là có ít người AC như vậy. Có lẽ tại vì không nhiều người biết đến việc chu trình BCNN? &#xA;&#xA;p2grp&#xA;&#xA;Author: Nguyễn Khánh&#xA;Tester: Nguyễn Hoàng Hải Minh&#xA;&#xA;Subtask 1&#xA;Với \\(n\\) và \\(m\\) không quá 20, các cạnh không quá \\(2^{m-1}\\), chúng ta có thể dựng đồ thị rồi sử dụng thuật toán tìm đường đi ngắn nhất bất kì (Floyd, Dijkstra hoặc thậm chí là backtrack). Kết quả không vượt quá số nguyên 64 bit nên cũng không có gì bận tâm về cách cài đặt.&#xA;&#xA;Subtask 2&#xA;Thuật toán Floyd tìm đường đi ngắn nhất cho mọi cặp đỉnh chạy trong độ phức tạp \\(O(n^3)\\) nên hoàn toàn có thể vượt qua subtask này. Tuy nhiên, kết quả không còn đủ nhỏ để lưu dưới dạng số nguyên nữa, vì vậy ta cần cài thêm hàm cộng 2 dãy nhị phân.&#xA;&#xA;Subtask 3&#xA;Tất nhiên, ngoài Floyd, chúng ta có thể chạy thuật toán Dijkstra từ mỗi đỉnh của đồ thị. Mỗi lần chạy Dijkstra có độ phức tạp \\(O((n+m)  \log n)\\), nên tổng độ phức tạp sẽ là \\(O(n(n+m)\log n \times C)\\), trong đó \\(C\\) là chi phí cộng 2 dãy nhị phân.&#xA;&#xA;Nếu cộng từng bit một của 2 dãy nhị phân lại với nhau thì \\(C\\) sẽ rơi vào khoảng 1000 phép tính, không đủ để qua subtask này. Cách tốt hơn là cứ \\(x\\) bit của dãy nhị phân, ta nhóm lại thành một số rồi thực hiện cộng (hiệu quả nhất là \\(x = 63\\)).&#xA;&#xA;Subtask 4&#xA;Điều kiện \\(m &lt; n\\) cho ta biết đồ thị là một cây. Giữa 2 đỉnh bất kì trên cây chỉ có duy nhất một đường đi đơn, và hiển nhiên đó là đường đi ngắn nhất.&#xA;&#xA;Việc duyệt qua mỗi cặp đỉnh rồi tính đường đi ngắn nhất có vẻ không hiệu quả và cũng khó tối ưu được. Do đó chúng ta nghĩ đến việc thay đổi bài toán:&#xA;  Với mỗi cạnh, có bao nhiêu cặp đỉnh mà đường đi ngắn nhất đi qua cạnh đó?&#xA;&#xA;Hiển nhiên nếu ta trả lời được câu hỏi trên thì sẽ dễ dàng tính được đáp án.&#xA;&#xA;Câu trả lời thực ra cũng rất đơn giản. Mỗi cạnh trên cây, nếu cắt đi sẽ tạo ra 2 cái cây nhỏ. Dễ thấy một cặp đỉnh mà mỗi đỉnh nằm ở một cây thì đường đi giữa chúng bắt buộc phải đi qua cạnh vừa bị cắt. Do vậy số cặp đỉnh có đường đi đi qua cạnh đó sẽ bằng tích độ lớn 2 cây con.&#xA;&#xA;Ta đặt gốc cây ở đỉnh 1. Giả sử cạnh đang xét nối giữa đỉnh \\(u\\) và cha của nó, thì 1 trong 2 cây con sẽ chính là cây con gốc \\(u\\). Việc tính độ lớn cây con gốc u có thể giải quyết bằng DFS trong thời gian \\(O(n)\\).&#xA;&#xA;Những cạnh không quan trọng&#xA;Đọc kĩ lại đề bài, chúng ta phát hiện ra còn một chi tiết nữa mà cả 4 subtask trước đều chưa phải dùng đến: các cạnh có độ lớn \\(2^w\\) và có trọng số phân biệt. Liệu đây có phải mấu chốt để giải quyết subtask cuối?&#xA;&#xA;Để ý rằng trọng số của một cạnh lớn hơn hẳn tổng trọng số của các cạnh nhỏ hơn nó (vì \\(2^0 + 2^1 + ... + 2^{x-1} &lt; 2^x\\)). Điều đó chứng tỏ nếu 2 đường đi có cạnh lớn nhất khác nhau thì đường đi nào có cạnh lớn nhất nhỏ hơn chắc chắn có tổng nhỏ hơn.&#xA;&#xA;Như vậy, ta có thể lần lượt các cạnh vào đồ thị theo thứ tự trọng số tăng dần. Giả sử khi thêm cạnh \\((u, v)\\) mà giữa \\(u\\) và \\(v\\) đã có đường đi thì cạnh \\((u, v)\\) sẽ không nằm trong bất kì đường đi ngắn nhất nào (thay vì đi cạnh \\((u, v)\\) ta có thể đi đường đi ngắn nhất từ \\(u\\) đến \\(v\\) đã tìm được trước đó). Do đó, ta có thể bỏ cạnh \\((u, v)\\).&#xA;&#xA;Subtask 5 = Subtask 4 ?? :D ??&#xA;&#xA;Sau khi loại bỏ các cạnh không quan trọng, đồ thị ta thu được là một cây (!). Lí do là ta sẽ không thêm cạnh \\((u, v)\\) mà \\(u\\) đã có đường đi tới \\(v\\), tức là đồ thị sẽ không thể có chu trình. Đến đây thì subtask 5 có thể giải y hệt subtask 4 rồi :D&#xA;&#xA;Thống kê&#xA;Đây là một bài khá lằng nhằng để ăn điểm những sub nhỏ, nhưng thuật toán chuẩn không cần quá nhiều chi tiết cài đặt. Vì thế bài có nhiều người AC, nhưng lượng ăn subtask nhỏ nhỏ hơn.&#xA;&#xA;Chúc mừng 8 bạn đã AC! Fun fact: 4/8 bạn giải được bài này là của THPT Chuyên Lương Thế Vinh.&#xA;&#xA;turtle&#xA;&#xA;Author: Nguyễn Đinh Quang Minh&#xA;Tester: Nguyễn Hoàng Hải Minh&#xA;&#xA;BFS trạng thái&#xA;Subtask 1 có giới hạn \\(N \le 10\\), nên chỉ có \\(10! = 3.628.800\\) trạng thái, hơn nữa từ một trạng thái có thể đi được đến tối đa là \\(N-1\\) trạng thái khác. Vì vậy chỉ cần BFS để tìm đường đi ngắn nhất từ trạng thái hiện tại đến trạng thái đích.&#xA;&#xA;\\(N \le 20\\)?&#xA;Mình thành thật xin lỗi các bạn đã bỏ công để nghĩ cách lấy 60% số điểm từ subtask 2, vì thực ra mình cũng không có cách giải nào (không phải thuật chuẩn) mà qua được subtask này cả :( . Có lẽ một thuật backtrack đặt cận tốt có thể ăn được một vài test của subtask này, nhưng mình không chắc chắn lắm.&#xA;&#xA;pos[a[i]] = i&#xA;Thoạt nhìn, bước biến đổi hoán vị trong bài toán có vẻ khá phức tạp. Tuy nhiên, để ý kĩ bạn sẽ thấy, thực ra bước biến đổi bao gồm 2 thao tác:&#xA;&#xA;Giảm các số có giá trị từ 2 đến \\(K\\) đi 1.&#xA;Gán số đang có giá trị 1 thành \\(K\\).&#xA;&#xA;Fun fact*: Khi viết đề, mình đã cố giải thích bước biến đổi theo kiểu chú rùa 1 sẽ hút độ rùa của \\(K-1\\) chú rùa còn lại nhưng cảm thấy hơi kì kì nên thôi ¯\\\(ツ)\/¯&#xA;&#xA;Chưa nhìn ra sự kì diệu của bài toán? Gợi ý: pos[a[i]] = i là một dòng vô cùng quan trọng trong code của mình.&#xA;&#xA;Nếu bạn vẫn chưa nghĩ ra, hãy đọc tiếp phần bên dưới. Còn nếu nghĩ ra rồi thì cứ đọc tiếp để chắc chắn thuật toán của mình đúng.&#xA;&#xA;Thuật toán O(N)&#xA;&#xA;Nếu gọi \\(pos[i]\\) là vị trí của số \\(i\\) trong hoán vị, thì mảng \\(pos\\) cũng là một hoán vị. Vậy ta thử xem thao tác trong bài toán thay đổi mảng \\(pos\\) như thế nào?&#xA;&#xA;Giảm các số có giá trị từ 2 đến \\(K\\) đi 1. Như vậy thì chính là gán \\(pos[1] = pos[2], pos[2] = pos[3], ..., pos[K-1] = pos[K]\\).&#xA;Gán số đang có giá trị 1 thành \\(K\\). Vậy là gán \\(pos[K] = pos[1]\\) (cũ).&#xA;&#xA;Nói tóm lại, phép biến đổi này thay đổi hoán vị \\(pos\\) bằng cách chèn \\(pos[1]\\) vào bất kì vị trí \\(K\\) nào đó trong \\(pos\\). Ta muốn đưa hoán vị \\(pos\\) về hoán vị đơn vị (1, 2, ..., \\(N\\)). Điều đó cũng có nghĩa là ta chỉ đụng vào mỗi phần tử trong hoán vị không quá một lần, bởi vì ta chỉ cần đặt nó vào một vị trí \\(K\\) hợp lí nào đấy và không còn quan tâm đến nó nữa. &#xA;&#xA;Vậy những số nào không cần đặt vào chỗ khác?&#xA;Giả sử ta thực hiện thao tác \\(x\\) lần, tức là các số \\(pos[x+1..N]\\) không bị thay đổi. Điều đó chứng tỏ ban đầu \\(pos[x+1] &lt; pos[x+2] &lt; ... &lt; pos[N]\\). Điều ngược lại cũng đúng: nếu \\(pos[x+1] &lt; pos[x+2] &lt; ... &lt; pos[N]\\) thì ta có thể dừng thao tác ở lần thứ \\(x\\) bằng cách chèn các số \\(pos[1..x]\\) vào các vị trí hợp lý để nhận được hoán vị đơn vị.&#xA;Như vậy, thuật toán của bài này thực ra rất đơn giản, tìm vị trí \\(x\\) nhỏ nhất sao cho \\(pos[x+1] &lt; pos[x+2] &lt; ... &lt; pos[N]\\) rồi in ra \\(x\\).&#xA;&#xA;Thống kê&#xA;&#xA;In ra \\(a[1] - 1\\) được 45 điểm???&#xA;Trong lúc sinh test, mình gặp phải vấn đề sau: nếu sinh test random thì khả năng số \\(N-1\\) đứng sau số \\(N\\) (\\(pos[N-1]   pos[N]\\)) là rất lớn, vì vậy sẽ có chừng 50% số test đáp án là \\(N-1\\). Mặt khác, để đáp án là \\(K\\) thì \\(pos[K+1] &lt; pos[K+2] &lt; ... &lt; pos[N]\\), do đó sinh random thì khả năng có đáp án bé gần như bằng không. Vậy mình đã giải quyết như nào?&#xA;&#xA;Mình quyết định sinh test bằng cách: sinh trước vị trí của \\(K+1, K+2, ..., N\\) rồi sinh random các số còn lại (tất nhiên còn thêm vài test tay nữa). Nhưng một vấn đề khác lại nảy sinh: nếu \\(K\\) quá nhỏ so với \\(N\\) (giả sử, \\(K = 100\\) và \\(N = 500000\\)), thì khả năng \\(a[1] = K+1\\) là vô cùng lớn (\\(= 1 - K/N\\)). Cách cuối cùng để giải quyết vấn đề là thêm phần truy vết các thao tác. Tuy nhiên vào phút chót, mình quyết định không làm phức tạp thêm bài toán nữa vì mình cho rằng thời gian 3.5 tiếng là khá ít, cần phải khuyến khích những người nghĩ ra đáp án hơn là đánh đố họ. Dù sao thì việc in \\(a[1] - 1\\) được nhiều điểm hơn cả subtask 1 cũng làm mình khá buồn :(&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="/nki/tag:editorial" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">editorial</span></a> <a href="/nki/tag:hsgso" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">hsgso</span></a> <a href="/nki/tag:vietnamese" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">vietnamese</span></a></p>

<p>Đây chắc là một trong những bài viết được chờ đợi nhất sau kì thi HSGSO 2017 vừa rồi. Ngoài chữa bài, mình sẽ đưa ra một số thống kê về đề và lượng người giải được!</p>

<p>Phần chữa bài được thực hiện bởi mình và bạn <a href="https://www.facebook.com/HiImMing" rel="nofollow">Nguyễn Đinh Quang Minh</a>. Nếu có câu hỏi gì bạn có thể hỏi mình hoặc Minh qua facebook.</p>

<h1 id="đề-bài">Đề bài</h1>

<p>Các bạn có thể tải về đề bài tại <a href="https://www.facebook.com/groups/163215593699283/1468547496499413/" rel="nofollow">đây</a> (<a href="https://cdn.discordapp.com/attachments/676817846617243658/1108896452224229406/2017.rar" rel="nofollow">mirror</a>). Trong phần comment của link cũng có chữa tóm tắt một vài bài do các bạn của dự tuyển Tổng Hợp.</p>



<h1 id="lời-giải">Lời giải</h1>

<h2 id="mục-lục">Mục lục</h2>

<h3 id="ngày-1">Ngày 1</h3>
<ol><li><a href="#number" rel="nofollow">number</a></li>
<li><a href="#race" rel="nofollow">race</a></li>
<li><a href="#tree" rel="nofollow">tree</a></li>
<li><a href="#inversion" rel="nofollow">inversion</a></li></ol>

<h3 id="ngày-2">Ngày 2</h3>
<ol><li><a href="#matrix" rel="nofollow">matrix</a></li>
<li><a href="#string" rel="nofollow">string</a></li>
<li><a href="#p2grp" rel="nofollow">p2grp</a></li>
<li><a href="#turtle" rel="nofollow">turtle</a></li></ol>

<h2 id="number">number</h2>
<ul><li>Author: thầy Phương, Phạm Cao Nguyên</li>

<li><p>Tester: Nguyễn Hoàng Hải Minh</p>

<h3 id="các-subtask-nhỏ">Các subtask nhỏ</h3></li>

<li><p><strong>Subtask 1</strong> (\(S \le 20\)) có thể dễ dàng giải bằng thuật quay lui, thử tất cả các trường hợp cắt với độ phức tạp \(O(M<em>1 \times M</em>2 ... \times M_N)\).</p></li>

<li><p><strong>Subtask 2</strong> (\(S \le 500, K \le 18\)): Do giới hạn của \(K\), đáp số là một số không quá giới hạn <code>long long</code>, vì vậy ta có thể thực hiện đơn giản phép quy hoạch động.
Gọi \(f[i][j]\) là số lớn nhất có thể tạo được nếu ta sử dụng các đoạn từ 1 đến \(i\) và lấy tổng cộng \(j\) chữ số.
Hiển nhiên \(f[0][0] = 0\). Ta có:
\[f[i][j] = \sum\limits_{k=0}^{\min(M_i, j)} f[i – 1][j – k] \times 10^k + \overline{A_{i,1}A_{i,2}...A_{i,k}}\]
Đáp số chính là \(f[N][K]\). Lưu ý độ phức tạp của thuật toán này chỉ là \(O(SK)\) chứ không phải \(O(N^2K)\).</p></li></ul>

<h3 id="tham-lam-1-chữ-số">Tham lam 1 chữ số</h3>

<p>Subtask 3 có một giới hạn khá lạ: Mỗi đoạn có độ dài đều là 1. Như vậy, việc chọn một đoạn chỉ đơn giản là lấy hoặc không. Ta có thể ra một thuật toán tham lam cho subtask này.</p>

<p>Ta có thể thấy, ở mỗi vị trí từ trái sang phải ta sẽ luôn ưu tiên số lớn nhất ở trước. Thật vậy, dễ dàng chứng minh điều này: Khi so sánh 2 số có cùng độ dài, ta chỉ cần tìm chữ số đầu tiên khác nhau từ trái sang và so sánh chúng.</p>

<p>Ý tưởng tham lam như sau. Giả sử ta đã lấy \(x\) chữ số, chữ số cuối cùng ta lấy ở vị trí \(i\). Ta có các nhận xét sau về chữ số tiếp theo cần lấy (ở vị trí \(j\)):
– Hiển nhiên ta không thể lấy tiếp ở các vị trí \(j \le i\).
– Hiển nhiên không kém, ta không thể lấy ở các vị trí \(j &gt; N – (K – x) + 1\). Đơn giản là, nếu lấy ở các vị trí này, ta không còn đủ chữ số để dựng số có \(K\) chữ số nữa.
– Trong các vị trí còn lại, ta luôn chọn vị trí có chữ số lớn nhất. Điều này hiển nhiên đúng, vì về sau ta chọn thế nào thì cũng ra số lớn hơn số được tạo ra nếu ta không chọn chữ số lớn nhất ở bước này.
– Trong các vị trí có cùng giá trị, lấy vị trí trái nhất. Điều này đảm bảo ta có nhiều cơ hội hơn để lấy số lớn hơn ở vị trí tiếp theo.</p>

<p>Như vậy subtask 3 có thể giải với độ phức tạp \(O(10 \times N)\). Hãy lưu ý subtask này, vì ý tưởng tham lam là chìa khóa để giải subtask cuối!</p>

<h3 id="cải-tiến-quy-hoạch-động">Cải tiến quy hoạch động</h3>

<p>2 subtask tiếp theo (4 và 5) chỉ là việc cài đặt công thức quy hoạch động của subtask 2 lên với số lớn. Dễ dàng nhận thấy ta chỉ phải cài đặt phép cộng số lớn, và đáp số không quá \(10^K\) nên độ phức tạp là \(O(SK^2)\).</p>

<p>Dễ dàng cài số lớn qua subtask 4, nhưng để qua subtask 5 sẽ cần thêm một chút cải tiến để lọt time limit (vd nén 9 chữ số vào một <code>int</code> thay vì giữ từng chữ số, cộng dồn đoạn để giảm số phép tính,...)</p>

<h3 id="subtask-cuối">Subtask cuối</h3>

<p>Để giải được subtask cuối, ta cần bỏ đi suy nghĩ quy hoạch động và quay lại với ý tưởng tham lam.</p>

<p><strong>Tại sao ý tưởng của subtask 3 không thể áp dụng được ngay vào subtask cuối?</strong> Bởi vì trong mỗi đoạn, ta bắt buộc phải chọn 1 đoạn tiền tố để đặt vào đáp số. Nhiều khi, chữ số phía sau lớn nhưng đoạn chữ số phía trước không tối ưu.</p>

<p><strong>Ta có thể chỉ xét chữ số tiếp theo của chuỗi đang lấy cuối cùng, hoặc đầu của các chuỗi phía sau.</strong> Đây là một nhận xét đúng. Thật vậy: Kiểu gì bạn cũng phải đặt chữ số đầu tiên của một chuỗi nếu lấy chuỗi đó. Và nếu chữ số đầu tiên có lựa chọn tốt hơn: điều đúng đắn là bỏ cả chuỗi.</p>

<p><strong>Tuy nhiên, có một vấn đề khác.</strong> Khi có nhiều lựa chọn chữ số tiếp theo giống nhau, <strong>chọn chữ số trái cùng không phải lựa chọn tối ưu</strong>. Bản chất là vì lựa chọn của bạn còn làm ảnh hưởng đến tập đáp số tối ưu phía sau:</p>
<ul><li>Nếu \(M_i = 1\) với mọi \(i\), lựa chọn của bạn luôn là \((x_j, y_j)\) (giá trị, vị trí) với chữ số \(j\) (từ trái sang) và \(j\) luôn tốt hơn \(j + 1\).</li>
<li>Khi điều kiện 1 không còn nữa, không phải lúc nào \(j\) cũng tốt hơn \(j + 1\). Xét trường hợp sau: <code>5 6</code> và <code>5 7</code> (\(K = 2\)). Nếu bạn chọn số <code>5</code> bên trái, tiếp theo bạn chỉ được chọn giữa <code>6</code> và <code>5</code> cho chữ số tiếp theo. Trong khi đó đáp số hiển nhiên là <code>57</code>.</li></ul>

<p>Tuy vậy, ta nhận thấy đáp án tối ưu vẫn luôn xuất phát từ chữ số lớn nhất, và đáp án vẫn là chọn số lớn nhất có thể ở mỗi bước. Vậy ta vẫn có thể đưa số <code>5</code> vào, chỉ có điều <strong>ta chưa quyết định đấy là số 5 ở vị trí nào</strong>. Hay hiểu kiểu khác, trước tiên ta chọn <code>5</code> của <code>5 6</code>, nhưng do <code>5 7</code> có đầu <code>5</code> trùng với đuôi <code>5</code> của ta nên ta xóa <code>5</code> của <code>5 6</code> đi và thêm cả <code>5 7</code> vào.</p>

<p>Tóm tắt lại, thuật toán của ta như sau:
Giả sử ta đã có \(i\) chữ số \(x_1, x_2, ..., x_i\) (\(x_i\) ở dãy \(j\)), ta sẽ có tập lựa chọn cho chữ số tiếp theo (dãy \(p\) vị trí \(q\)) như sau:
– Nó phải thuộc dãy \(p \ge j\). Hiển nhiên ta không thể lấy lại một dãy trước đó.
– Sau nó phải có ít nhất \(K – i – 1\) chữ số (từ nó về cuối dãy, và tổng tất cả dãy phía sau).
– \(p = j\) và nó là chữ số đứng sau \(x_i\), <em>hoặc</em>
– \(p &gt; j\), \(x_{i – k} = A[p][q – 1 – k]\) với mọi \(0 \le k &lt; q – 1\). Khi chọn lựa chọn này, ta xóa tất cả \(x_{i – q + 2}..x_i\) và thêm vào \(A[p][1..q]\).</p>

<p>Dễ dàng chứng minh ở mỗi bước, ta đều lấy được chữ số lớn nhất có thể lấy được khi có prefix dựng từ bước trước đó (do vậy, nếu prefix tối ưu thì theo phép so sánh số, dãy mới là tối ưu). Do ở bước đầu (prefix rỗng), lựa chọn là tối ưu, nên theo quy nạp thuật toán tham là tối ưu.</p>

<p>Ta có thể cài thuật toán này trong độ phức tạp \(O(SK)\).</p>

<h3 id="một-chút-thống-kê">Một chút thống kê</h3>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108895599652249692/score_number.png" alt=""></p>

<p>Về điểm số:
– Có một bạn duy nhất được <strong>99</strong> điểm (Bùi Hồng Đức). Xin chúc mừng Đức đã ra được thuật toán đúng cho subtask cuối. Thật vậy, Đức chỉ còn 1 test lắt léo duy nhất: Đề bài yêu cầu in ra số tạo từ \(K\) chữ số đã chọn, mà đã là số thì phải không có số <code>0</code> ở đầu :( Kể ra bài đã khó, test cũng khó, sorry mọi người vì cái test quái gở này.
– Có 12 bạn được <strong>73-76</strong> điểm. Khi sinh test mình đã tính đến việc các bạn sử dụng tham lam của subtask 3, đưa thẳng vào subtask 6 (chỉ qua nhận xét đầu tiên). Đây là số điểm các bạn nhận được :) Kể ra không cho điểm nào thì cũng hơi ác, nhưng cho nhiều điểm quá thì cũng không đáng (nhớ rằng tối ưu bignum chỉ được thêm 10 điểm). Thật sự 6 điểm khá là nhiều cho một thuật sai khá là rõ ràng, và còn vài bạn còn cài sai thuật đó nữa...
– 6 bạn được <strong>70</strong> điểm. Chúc mừng 6 bạn đã qua được mốc quan trọng của một bài khó: cắn nhiều nhất có thể. Đây là chiến thuật hữu dụng nhất khi thi quốc gia!
– Còn lại 71 bạn có điểm dương, trải dài khá đều từ 1 đến 65 điểm. Rất tuyên dương cố gắng của các bạn! Hãy cố lên! Đặc biệt với những bạn được 1 điểm, mình không hiểu tại sao có thể được 1 điểm...</p>

<p>Về bộ test:
– Bản thân mình khá hài lòng về bộ test mình sinh ra. Có đủ điểm cho người liều lĩnh, đủ điểm cho người chắc ăn, đủ lỗi để hành người sai,...
– Trong bộ test (sau này sẽ được up), có 2 test được sinh ra để giết một nhận xét tham lam của subtask 6... Đố bạn nào biết đó là nhận xét gì :)</p>

<h2 id="race">race</h2>
<ul><li>Author: Nguyễn Đinh Quang Minh</li>
<li>Tester: Nguyễn Hoàng Hải Minh</li></ul>

<h3 id="bài-toán-lớp-bốn-và-thuật-toán-o-n-3">Bài toán lớp bốn và thuật toán O(N^3)</h3>

<blockquote><p>Ba người \(A\), \(B\), \(C\) xuất phát ở ba vị trí \(x_A\), \(x_B\), \(x_C\) trên một đường thẳng, cùng đi về một hướng với vận tốc \(v_A\), \(v_B\), \(v_C\). Hỏi có thời điểm nào người \(B\) nằm ở trung điểm 2 người còn lại hay không?</p></blockquote>

<p>Thầy Phương từng đố mình bài toán trên. Nếu bỏ qua dữ kiện ‘đây là bài toán lớp bốn’ thì chúng ta (tất nhiên kể cả mình) đều sẽ cầm bút giải phương trình bậc nhất rồi tìm nghiệm. Nhưng hãy thử nghĩ <em>như một học sinh lớp bốn</em> xem sao…</p>

<p>Cách giải của các bé lớp bốn cho bài toán này như sau: giả sử có thêm một người \(D\) di chuyển sao cho \(D\) <em>luôn nằm ở trung điểm</em> của \(A\) và \(C\), khi đó nếu \(B\) gặp \(D\) tức là <strong>\(B\) nằm ở trung điểm \(AC\)</strong>. Vị trí xuất phát và vận tốc của \(D\) như thế nào cho hợp lý? Hiển nhiên là trung bình cộng của \(A\) và \(C\), tức là \(x_D = \dfrac{x_A + x_C}{2}\), \(v_D = \dfrac{vA + vC}{2}\).</p>

<p>Khi đã có người D rồi thì có 3 trường hợp xảy ra:</p>
<ol><li>\(B\) luôn chạy trùng với \(D\): có vô hạn thời điểm thỏa mãn;</li>
<li>\(B\) không gặp \(D\) (kết quả tăng 0);</li>
<li>\(B\) có gặp \(D\) (kết quả tăng 1).</li></ol>

<p>Điều kiện cụ thể cho từng trường hợp xảy ra như sau:</p>

<table>
<thead>
<tr>
<th>x</th>
<th align="center">\(v_B &lt; v_D\)</th>
<th align="center">\(v_B = v_D\)</th>
<th align="center">\(v_B &gt; v_D\)</th>
</tr>
</thead>

<tbody>
<tr>
<td>\(x_B &lt; x_D\)</td>
<td align="center">0</td>
<td align="center">0</td>
<td align="center">1</td>
</tr>

<tr>
<td>\(x_B = x_D\)</td>
<td align="center">1</td>
<td align="center">vô hạn</td>
<td align="center">1</td>
</tr>

<tr>
<td>\(x_B &gt; x_D\)</td>
<td align="center">1</td>
<td align="center">0</td>
<td align="center">0</td>
</tr>
</tbody>
</table>

<p>Như vậy thuật giải \(O(N^3)\) chỉ đơn giản là duyệt tất cả các bộ ba <em>phân biệt</em> rồi kiểm tra theo bảng trên.</p>

<h3 id="điều-kiện-trên-nhìn-quen-quen">Điều kiện trên nhìn quen quen?</h3>

<p>Để tối ưu thuật toán, ta sẽ nghĩ đến việc duyệt 2 người \(A\) và \(C\), tính được thông tin người \(D\), rồi tìm số người \(B\) thỏa mãn. Cụ thể hơn:
– Nếu tồn tại người B sao cho x<em>B = x</em>D, v<em>B = v</em>D thì trả về kết quả là <code>infinity</code>.
– Cộng vào kết quả số người B thỏa mãn \(x_B \le X_D\), \(v_B &gt; v_D\).
– Cộng vào kết quả số người B thỏa mãn \(x_B \ge x_D\), \(v_B &lt; v_D\).</p>

<p>Hai điều kiện trên (trừ điều kiện in ra <code>infinity</code> rất dễ) đều khá quen thuộc: ý tưởng của nó giống với <a href="https://natsukagami.github.io/2017/05/01/HSGSO-2016-Editorial/#polylines" rel="nofollow">bài polylines của năm ngoái</a>. Ta có thể sort tất cả những người \(B\) và \(D\) theo chiều \(v\) tăng dần rồi sử dụng cấu trúc dữ liệu BIT cho chiều \(x\).</p>

<h3 id="không-biết-cài-bit-thì-sao">Không biết cài BIT thì sao?</h3>

<p>Thì <del>bạn nên học cài BIT</del> ta sẽ thử một thuật giải không sử dụng BIT <del>mà sẽ sử dụng IT</del>.</p>

<p>Trước hết, chúng ta chỉ quan tâm đến giá trị \(x\) và \(v\) của mỗi người <em>đứng thứ mấy</em> nên ta hoàn toàn có thể sort các giá trị \(x\) và \(v\) lại rồi “đánh số” lại theo thứ tự của giá trị trong dãy được sort.
Bây giờ khi có một người \(D\) nào đó, ta có thể biết \(x_D\) đứng ở vị trí thứ mấy trong mảng \(x\) bằng chặt nhị phân, tương tự với \(v_D\). Điều này sẽ giúp ta giải quyết bài toán bằng mảng cộng dồn 2 chiều thay vì BIT.</p>

<p>Trước hết, ta lập một mảng \(A[1..N][1..N]\), \(A[i][j] = 1\) có nghĩa là tồn tại một người có giá trị \(x\) đứng thứ \(i\) và giá trị \(v\) đứng thứ \(j\), ngược lại \(A[i][j] = 0\). Giả sử xét một người \(D\) có \(x_D\) đứng ở vị trí thứ \(X\), \(v_D\) đứng ở vị trí thứ \(V\). Như vậy, số người thỏa mãn \(x_B \le x_D\), \(v_B &gt; v_D\) chính là tổng các số trong hình chữ nhật con có góc trên trái là \((1, V)\), góc dưới phải là \((X, N)\) và được tính trong \(O(1)\) bằng mảng cộng dồn.</p>

<h3 id="one-more-thing">One more thing</h3>

<p>Để cài đặt cho tiện, bạn nên nhân đôi tất cả tọa độ và vận tốc lên ¯\_(ツ)_/¯</p>

<h3 id="score-distribution">Score Distribution!</h3>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108895600289783921/score_race.png" alt=""></p>

<h2 id="tree">tree</h2>
<ul><li>Author: Phạm Đức Thắng</li>
<li>Tester: Nguyễn Hoàng Hải Minh</li></ul>

<h3 id="n-1">\(N = 1\)!</h3>

<p>Trước tiên, cần phải để ý nếu \(N = 1\) thì đỉnh duy nhất phải có bậc <strong>0</strong>. Khi đó, đường đi dài nhất là 1.</p>

<p>Rất nhiều bạn đã chết test này!</p>

<h3 id="subtask-nhỏ">Subtask nhỏ</h3>
<ul><li>Subtask 1 (\(N \le 15\)), bạn có thể backtrack mọi cách dựng cây?</li></ul>

<h3 id="dựng-được-cây-không">Dựng được cây không?</h3>

<p>Điều kiện sau là điều kiện cần và đủ để tồn tại cây:
– Tổng bậc là \(2N – 2\)
– Không đỉnh nào có bậc quá \(N – 1\).
– Không đỉnh nào có bậc dưới \(1\).</p>

<p>Dễ dàng chứng minh điều kiện này bằng quy nạp.</p>

<p>Subtask 2 sinh ra để bạn kiểm tra việc dựng cây nếu không biết điều này :D</p>

<h3 id="dựng-tối-ưu">Dựng tối ưu!</h3>

<p>Gọi \(x\) là số nút có bậc lớn hơn 1 (tức không phải lá). Ta sẽ chứng minh nếu \(N \ge 2\) thì đáp số là \(x + 1\).</p>

<p>Thật vậy, ta gọi tập đỉnh không phải lá là \(A_1, A_2, ..., A_x\). Trước tiên, dựng tập cạnh như sau:</p>

<pre><code>lá - A[1] - A[2] - ... - A[x] - lá
</code></pre>

<p>Dễ dàng nhận thấy tổng bậc còn lại của các đỉnh không phải lá là \((2N – 2) – (N – x) – 2x = N – x – 2\), số lá còn lại là \(N – x – 2\). Như vậy ta có thể thêm lá vào các bậc còn thiếu của từng đỉnh. Cây đã dựng xong! Đường đi dài nhất trên cây này chắc chắn là \(x + 1\).</p>

<p>Không khó để chứng minh đáp số không thể lớn hơn \(x + 1\) (đường đi đơn không thể đi qua 3 lá). Vì vậy \(x + 1\) là đáp án tối ưu.</p>

<h3 id="phân-bố-điểm">Phân bố điểm</h3>

<p>Rất nhiều bạn AC bài này, tuy nhiên cũng rất nhiều người bị 98 điểm do thiếu \(N = 1\). Hãy cẩn thận với những trường hợp biên!</p>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108895600793108540/score_tree.png" alt=""></p>

<h2 id="inversion">inversion</h2>
<ul><li>Author: Nguyễn Thành Vinh</li>
<li>Tester: Nguyễn Hoàng Hải Minh</li></ul>

<h3 id="chạy-trâu">Chạy trâu!</h3>

<p>For từng đoạn con, tính cặp nghịch thế trâu, \(O(N^4)\) ăn sub 1. Ta có thể cải tiến hơn, tính nghịch thể trong \(O(N \log N)\) bằng BIT, tuy nhiên vẫn chưa đủ để ăn subtask 2.</p>

<h3 id="tịnh-tiến-level-1">Tịnh tiến Level 1</h3>

<p>Nhận thấy, mỗi lần for đoạn con là một lần tính lại. Đoạn \((i, j)\) chỉ khác đoạn \((i, j + 1)\) một lượng là số phần tử trong khoảng \((i, j)\) mà lớn hơn \(A[j + 1]\). Như vậy, ta có thể for \(i\) rồi vừa tăng \(j\) vừa tính thêm, giảm độ phức tạp xuống \(O(N^2 \log N)\).</p>

<h3 id="tịnh-tiến-level-2">Tịnh tiến Level 2</h3>

<p>Từ việc tịnh tiến trên, ta nhận thấy rằng nếu \(i \le j &lt; k\) thì \((i, j)\) luôn có số nghịch thế không quá số nghịch thế của \((i, k)\).</p>

<p>Đồng thời, nhận xét cũng đúng với \((j, i)\) và \((k, i)\) nếu \(k &lt; j \le i\).</p>

<p>Giả sử \(j\) là số lớn nhất sao cho \((i, j)\) có số nghịch thế \(&lt; K\). Dễ dàng nhận thấy tất cả \((i, j&#39;)\) với \(j &lt; j&#39; \le N\) đều là đáp án. Như vậy với mỗi \(i\) ta chỉ cần tìm \(j\) là đủ.</p>

<p>Lại có \((i, j)\) có số nghịch thế \(&lt; K\), vì thế \((i + 1, j)\) chắc chắn có số nghịch thế \(&lt; K\). Ta không cần for các giá trị nhỏ hơn \(j\) để kiểm tra nữa. Thay vì for lại \(j\), ta xóa \(i\) khỏi đoạn, giảm số nghịch thế đi 1 lượng là số số nhỏ hơn \(A[i]\).</p>

<p>2 con trỏ \(i\) và \(j\) đều tăng từ 1 đến \(N\), mỗi lần tăng con trỏ phải update mất \(O(\log N)\). Độ phức tạp của thuật toán là \(O(N \log N)\).</p>

<h3 id="phân-bố-điểm-1">Phân bố điểm</h3>

<p>Đây là một bài khá cơ bản, dù vậy điểm khá là thấp so với bọn mình kì vọng, chủ yếu mọi người chỉ dừng ở subtask 2.</p>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108895599027298344/score_inversion.png" alt=""></p>

<h2 id="matrix">matrix</h2>
<ul><li>Author: Nguyễn Đinh Quang Minh</li>
<li>Tester: Phạm Cao Nguyên, Nguyễn Hoàng Hải Minh</li></ul>

<h3 id="các-subtask-nhỏ-1">Các subtask nhỏ</h3>
<ul><li><strong>Subtask 1</strong> (\(N = 1\)), đây là subtask cho điểm vì bản thân cả bảng đã được chứa trong mảng \(B[]\). Nếu xor tất cả phần tử trong mảng \(B[]\) bằng \(A[1]\) thì có 1 đáp án chính là mảng \(B[]\). Nếu không thì không có đáp án nào cả. Độ phức tạp \(O(M)\).</li>
<li><strong>Subtask 2</strong> (\(NM \le 20\)), từ điều kiện ta suy ra \(\min(N, M) \le \sqrt{20} \le 5\).
Không mất tính tổng quát ta giả sử \(M \le 5\) (nếu không ta xoay bảng lại), ta có thể quy hoạch động \(f[i][mask]\) là số cách tạo các hàng từ 1 đến \(i\), với tổng xor từng cột được biểu diễn trong \(mask\). Sau đó ta có thể for tất cả các mask \(m\) có thể của hàng \(i + 1\), update vào \(f[i + 1][mask \oplus m]\). Độ phức tạp là \(O(N \times (2^M)^2)\).
Hoặc ta có thể backtrack giá trị của tất cả phần tử của bảng, độ phức tạp là \(O(2^{N + M})\).</li>
<li><strong>Subtask 3</strong> (\(N \le 10\), \(M \le 2000\)), ta sẽ cải tiến thuật toán quy hoạch động phía trên. Thay vì mỗi lần ta thêm cả cột (mất \(O(2^N)\)), ta chỉ thêm từng ô.
Gọi \(f[i][j][mask]\) là số cách điền các ô từ \((i, j)\) trở đi (theo từng cột, rồi từng hàng), và xor các phần tử đã đặt vào trước đó của mỗi hàng được biểu diễn trong mask. Ta tính từ \(f[i + 1][j][mask&#39;]\) hoặc \(f[1][j + 1][mask&#39;]\), với điều kiện là khi điền xong mỗi cột thì xor cột đó phải bằng \(B[j]\). Ta có thể kiểm tra điều này bằng cách kiểm tra \(X = B[1] \oplus B[2] \oplus ... \oplus B[j]\) có bằng xor các bit trong \(mask\) không.
Độ phức tạp là \(O(NM2^N)\).</li></ul>

<h3 id="dựng-một-đáp-án">Dựng một đáp án</h3>

<p>Trước khi tìm hiểu xem có bao nhiêu đáp án thỏa mãn, ta cần phải kiểm tra xem có tồn tại đáp án không đã.</p>

<p>Hiển nhiên nếu tổng xor của \(A[]\) khác tổng xor của \(B[]\) thì không thể tồn tại bảng thỏa mãn. Còn lại, ta sẽ chứng minh luôn tồn tại bảng thỏa mãn.</p>

<p>Gọi tổng xor cả bảng là \(S\).</p>

<p>Xét bảng \(N \times M\) được dựng như sau:</p>
<ul><li>Với \(i &lt; N\) và \(j &lt; M\): điền <code>0</code>.</li>
<li>Với \(i = N\) và \(j &lt; M\): điền \(B[j]\).</li>
<li>Với \(i &lt; N\) và \(j = M\): điền \(A[i]\).</li>
<li>Xét ô \((N, M)\). Ta có giá trị \(b[N][M] = A[N] \oplus B[1] \oplus B[2] \oplus ... \oplus B[M – 1]\), hay \(b[N][M] = A[N] \oplus S \oplus B[M]\). Tất nhiên phân tích theo cột ta cũng sẽ ra công thức này, và vì \(S\) xác định nên có duy nhất 1 cách điền ô \((N, M)\).</li></ul>

<p>Vậy tồn tại bảng thỏa mãn.</p>

<h3 id="có-bao-nhiêu-bảng-thỏa-mãn">Có bao nhiêu bảng thỏa mãn?</h3>

<p>Giả sử ta có bảng \(b[][]\) thỏa mãn.</p>

<p>Nếu ta flip giá trị \(b[i][j]\) (\(i &lt; N\), \(j &lt; M\)), ta có thể flip cả \(b[i][M]\), \(b[N][j]\) và \([N][M]\) để ra một bảng mới vẫn đúng. (mỗi hàng mỗi cột ảnh hưởng đều bị flip tổng xor 2 lần).</p>

<p>Nếu ta giữ nguyên \(b[i][j]\) với mọi \(i &lt; N\) và \(j &lt; M\), dễ dàng chứng minh cách điền các ô còn lại là duy nhất. Thật vậy, \(b[i][M]\) (\(i &lt; M\)) phải xác định duy nhất để tổng xor các ô trong hàng \(i\) bằng \(A[i]\), tương tự với \(b[N][j]\) (\(j &lt; N\)). Còn lại ô \((N, M)\), tất nhiên vì hàng và cột cuối cũng có tổng xor xác định nên giá trị của ô cũng chỉ có tối đa một.</p>

<p>Từ 2 nhận xét trên, ta thấy với mỗi cách điền bảng \((N – 1)\times (M – 1)\) ở góc trái trên ta có duy nhất 1 cách điền nốt thỏa mãn. Vậy số bảng thỏa mãn là \(2^{(N – 1)(M – 1)}\).</p>

<p>Kiểm tra điều kiện tồn tại mất \(O(N + M)\), tính số bảng mất \(O(\log(NM))\), dựng 1 bảng thỏa mãn mất \(O(NM)\).</p>

<h3 id="thống-kê">Thống kê</h3>

<p>Subtask 2 là một subtask rất đơn giản (backtrack lấy 20 điểm), vậy mà số bạn làm được khá là ít. Subtask 3 đòi hỏi kỹ thuật quy hoạch động khó hơn, không nhiều bạn làm được.</p>

<p>Chúc mừng 4 bạn làm được 100 điểm!</p>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108895599325085766/score_matrix.png" alt=""></p>

<h2 id="string">string</h2>
<ul><li>Author: Vương Hoàng Long</li>
<li>Tester: Nguyễn Hoàng Hải Minh</li></ul>

<h3 id="subtask-nhỏ-1">Subtask nhỏ</h3>

<p>Do độ dài của 2 xâu không quá \(10^8\) nên ta có thể thực hiện for trâu để giải subtask này. Tất nhiên có một số điều sau phải chú ý:</p>
<ul><li>Tuyệt đối không dựng cả xâu. Dựng xâu mất thời gian hằng số rất lớn, có thể làm bạn bị TLE trước cả khi thực hiện so sánh. Chỉ for các chỉ số lặp lại.</li>
<li>Để tăng tốc, không dùng phép mod để tính nhanh chỉ số.</li></ul>

<p>Độ phức tạp là \(O(|A|)\).</p>

<h3 id="subtask-lớn">Subtask lớn</h3>

<p>Để đơn giản cho việc mod ta coi xâu đánh số từ 0.</p>

<p>Nhận thấy phần tử thứ \(i\) của xâu \(X\) sẽ được so sánh với các phần tử \(i \mod |Y|, (i + |X|) \mod |Y|, ..., (i + (n – 1)|X|) \mod |Y|\) của \(Y\).</p>

<p>Không khó để nhận ra dãy này có chu trình độ dài \(|Y| / \gcd(|X|, |Y|)\). Không khó để chứng minh điều này: \(|X||Y| / \gcd(|X|, |Y|)\) chính là <em>bội chung nhỏ nhất</em> của \(|X|\) và \(|Y|\), bội dương đầu tiên của \(|X|\) chia hết cho \(|Y|\).</p>

<p>Như vậy, ta chỉ cần chia \(Y\) thành các chu trình tương ứng, sau đó tính trước số kí tự từng loại là có thể so sánh 1 vị trí của \(X\) với 1 chu trình trong \(O(1)\).</p>

<p>Độ phức tạp là \(O(|Y| + |X|)\).</p>

<h3 id="thống-kê-1">Thống kê</h3>

<p>Đây là bài dễ của ngày 2. Tuy vậy mình không nghĩ là có ít người AC như vậy. Có lẽ tại vì không nhiều người biết đến việc chu trình BCNN?</p>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108895600537239622/score_string.png" alt=""></p>

<h2 id="p2grp">p2grp</h2>
<ul><li>Author: Nguyễn Khánh</li>
<li>Tester: Nguyễn Hoàng Hải Minh</li></ul>

<h3 id="subtask-1">Subtask 1</h3>

<p>Với \(n\) và \(m\) không quá 20, các cạnh không quá \(2^{m-1}\), chúng ta có thể dựng đồ thị rồi sử dụng thuật toán tìm đường đi ngắn nhất bất kì (Floyd, Dijkstra hoặc thậm chí là backtrack). Kết quả không vượt quá số nguyên 64 bit nên cũng không có gì bận tâm về cách cài đặt.</p>

<h3 id="subtask-2">Subtask 2</h3>

<p>Thuật toán Floyd tìm đường đi ngắn nhất cho mọi cặp đỉnh chạy trong độ phức tạp \(O(n^3)\) nên hoàn toàn có thể vượt qua subtask này. Tuy nhiên, kết quả không còn đủ nhỏ để lưu dưới dạng số nguyên nữa, vì vậy ta cần cài thêm hàm cộng 2 dãy nhị phân.</p>

<h3 id="subtask-3">Subtask 3</h3>

<p>Tất nhiên, ngoài Floyd, chúng ta có thể chạy thuật toán Dijkstra từ mỗi đỉnh của đồ thị. Mỗi lần chạy Dijkstra có độ phức tạp \(O((n+m) * \log n)\), nên tổng độ phức tạp sẽ là \(O(n(n+m)\log n \times C)\), trong đó \(C\) là chi phí cộng 2 dãy nhị phân.</p>

<p>Nếu cộng từng bit một của 2 dãy nhị phân lại với nhau thì \(C\) sẽ rơi vào khoảng 1000 phép tính, không đủ để qua subtask này. Cách tốt hơn là cứ \(x\) bit của dãy nhị phân, ta nhóm lại thành một số rồi thực hiện cộng (hiệu quả nhất là \(x = 63\)).</p>

<h3 id="subtask-4">Subtask 4</h3>

<p>Điều kiện \(m &lt; n\) cho ta biết đồ thị là một cây. Giữa 2 đỉnh bất kì trên cây chỉ có duy nhất một đường đi đơn, và hiển nhiên đó là đường đi ngắn nhất.</p>

<p>Việc duyệt qua mỗi cặp đỉnh rồi tính đường đi ngắn nhất có vẻ không hiệu quả và cũng khó tối ưu được. Do đó chúng ta nghĩ đến việc thay đổi bài toán:
&gt; Với mỗi cạnh, có bao nhiêu cặp đỉnh mà đường đi ngắn nhất đi qua cạnh đó?</p>

<p>Hiển nhiên nếu ta trả lời được câu hỏi trên thì sẽ dễ dàng tính được đáp án.</p>

<p>Câu trả lời thực ra cũng rất đơn giản. Mỗi cạnh trên cây, nếu cắt đi sẽ tạo ra 2 cái cây nhỏ. Dễ thấy một cặp đỉnh mà mỗi đỉnh nằm ở một cây thì đường đi giữa chúng bắt buộc phải đi qua cạnh vừa bị cắt. Do vậy số cặp đỉnh có đường đi đi qua cạnh đó sẽ bằng tích độ lớn 2 cây con.</p>

<p>Ta đặt gốc cây ở đỉnh 1. Giả sử cạnh đang xét nối giữa đỉnh \(u\) và cha của nó, thì 1 trong 2 cây con sẽ chính là cây con gốc \(u\). Việc tính độ lớn cây con gốc u có thể giải quyết bằng DFS trong thời gian \(O(n)\).</p>

<h3 id="những-cạnh-không-quan-trọng">Những cạnh không quan trọng</h3>

<p>Đọc kĩ lại đề bài, chúng ta phát hiện ra còn một chi tiết nữa mà cả 4 subtask trước đều chưa phải dùng đến: các cạnh có độ lớn \(2^w\) và có trọng số <strong>phân biệt</strong>. Liệu đây có phải mấu chốt để giải quyết subtask cuối?</p>

<p>Để ý rằng trọng số của một cạnh lớn hơn hẳn tổng trọng số của các cạnh nhỏ hơn nó (vì \(2^0 + 2^1 + ... + 2^{x-1} &lt; 2^x\)). Điều đó chứng tỏ nếu 2 đường đi có cạnh lớn nhất khác nhau thì đường đi nào có cạnh lớn nhất nhỏ hơn chắc chắn có tổng nhỏ hơn.</p>

<p>Như vậy, ta có thể lần lượt các cạnh vào đồ thị theo thứ tự trọng số tăng dần. Giả sử khi thêm cạnh \((u, v)\) mà giữa \(u\) và \(v\) đã có đường đi thì cạnh \((u, v)\) sẽ không nằm trong bất kì đường đi ngắn nhất nào (thay vì đi cạnh \((u, v)\) ta có thể đi đường đi ngắn nhất từ \(u\) đến \(v\) đã tìm được trước đó). Do đó, ta có thể bỏ cạnh \((u, v)\).</p>

<h4 id="subtask-5-subtask-4-d">Subtask 5 = Subtask 4 ?? :D ??</h4>

<p>Sau khi loại bỏ các cạnh không quan trọng, đồ thị ta thu được là một cây (!). Lí do là ta sẽ không thêm cạnh \((u, v)\) mà \(u\) đã có đường đi tới \(v\), tức là đồ thị sẽ không thể có chu trình. Đến đây thì subtask 5 có thể giải y hệt subtask 4 rồi :D</p>

<h3 id="thống-kê-2">Thống kê</h3>

<p>Đây là một bài khá lằng nhằng để ăn điểm những sub nhỏ, nhưng thuật toán chuẩn không cần quá nhiều chi tiết cài đặt. Vì thế bài có nhiều người AC, nhưng lượng ăn subtask nhỏ nhỏ hơn.</p>

<p>Chúc mừng 8 bạn đã AC! Fun fact: 4/8 bạn giải được bài này là của THPT Chuyên Lương Thế Vinh.</p>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108895599954231386/score_p2grp.png" alt=""></p>

<h2 id="turtle">turtle</h2>
<ul><li>Author: Nguyễn Đinh Quang Minh</li>
<li>Tester: Nguyễn Hoàng Hải Minh</li></ul>

<h3 id="bfs-trạng-thái">BFS trạng thái</h3>

<p>Subtask 1 có giới hạn \(N \le 10\), nên chỉ có \(10! = 3.628.800\) trạng thái, hơn nữa từ một trạng thái có thể đi được đến tối đa là \(N-1\) trạng thái khác. Vì vậy chỉ cần BFS để tìm đường đi ngắn nhất từ trạng thái hiện tại đến trạng thái đích.</p>

<h3 id="n-le-20">\(N \le 20\)?</h3>

<p>Mình thành thật xin lỗi các bạn đã bỏ công để nghĩ cách lấy 60% số điểm từ subtask 2, vì thực ra mình cũng không có cách giải nào (không phải thuật chuẩn) mà qua được subtask này cả :( . Có lẽ một thuật backtrack đặt cận tốt có thể ăn được một vài test của subtask này, nhưng mình không chắc chắn lắm.</p>

<h3 id="pos-a-i-i"><code>pos[a[i]] = i</code></h3>

<p>Thoạt nhìn, bước biến đổi hoán vị trong bài toán có vẻ khá phức tạp. Tuy nhiên, để ý kĩ bạn sẽ thấy, thực ra bước biến đổi bao gồm 2 thao tác:</p>
<ol><li>Giảm các số có giá trị từ 2 đến \(K\) đi 1.</li>
<li>Gán số đang có giá trị 1 thành \(K\).</li></ol>

<p><em>Fun fact</em>: Khi viết đề, mình đã cố giải thích bước biến đổi theo kiểu chú rùa 1 sẽ hút độ rùa của \(K-1\) chú rùa còn lại nhưng cảm thấy hơi kì kì nên thôi ¯\_(ツ)_/¯</p>

<p>Chưa nhìn ra sự kì diệu của bài toán? Gợi ý: <code>pos[a[i]] = i</code> là một dòng vô cùng quan trọng trong code của mình.</p>

<p>Nếu bạn vẫn chưa nghĩ ra, hãy đọc tiếp phần bên dưới. Còn nếu nghĩ ra rồi thì cứ đọc tiếp để chắc chắn thuật toán của mình đúng.</p>

<h3 id="thuật-toán-o-n">Thuật toán O(N)</h3>

<p>Nếu gọi \(pos[i]\) là vị trí của số \(i\) trong hoán vị, thì mảng \(pos\) cũng là một hoán vị. Vậy ta thử xem thao tác trong bài toán thay đổi mảng \(pos\) như thế nào?</p>
<ol><li>Giảm các số có giá trị từ 2 đến \(K\) đi 1. Như vậy thì chính là gán \(pos[1] = pos[2], pos[2] = pos[3], ..., pos[K-1] = pos[K]\).</li>
<li>Gán số đang có giá trị 1 thành \(K\). Vậy là gán \(pos[K] = pos[1]\) (cũ).</li></ol>

<p>Nói tóm lại, phép biến đổi này thay đổi hoán vị \(pos\) bằng cách chèn \(pos[1]\) vào bất kì vị trí \(K\) nào đó trong \(pos\). Ta muốn đưa hoán vị \(pos\) về hoán vị đơn vị (1, 2, ..., \(N\)). Điều đó cũng có nghĩa là ta chỉ đụng vào mỗi phần tử trong hoán vị không quá một lần, bởi vì ta chỉ cần đặt nó vào một vị trí \(K\) hợp lí nào đấy và không còn quan tâm đến nó nữa.</p>

<p>Vậy những số nào không cần đặt vào chỗ khác?
Giả sử ta thực hiện thao tác \(x\) lần, tức là các số \(pos[x+1..N]\) không bị thay đổi. Điều đó chứng tỏ ban đầu \(pos[x+1] &lt; pos[x+2] &lt; ... &lt; pos[N]\). Điều ngược lại cũng đúng: nếu \(pos[x+1] &lt; pos[x+2] &lt; ... &lt; pos[N]\) thì ta có thể dừng thao tác ở lần thứ \(x\) bằng cách chèn các số \(pos[1..x]\) vào các vị trí hợp lý để nhận được hoán vị đơn vị.
Như vậy, thuật toán của bài này thực ra rất đơn giản, tìm vị trí \(x\) nhỏ nhất sao cho \(pos[x+1] &lt; pos[x+2] &lt; ... &lt; pos[N]\) rồi in ra \(x\).</p>

<h3 id="thống-kê-3">Thống kê</h3>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108895601007009833/score_turtles.png" alt=""></p>

<h4 id="in-ra-a-1-1-được-45-điểm">In ra \(a[1] – 1\) được 45 điểm???</h4>

<p>Trong lúc sinh test, mình gặp phải vấn đề sau: nếu sinh test random thì khả năng số \(N-1\) đứng sau số \(N\) (\(pos[N-1] &gt; pos[N]\)) là rất lớn, vì vậy sẽ có chừng 50% số test đáp án là \(N-1\). Mặt khác, để đáp án là \(K\) thì \(pos[K+1] &lt; pos[K+2] &lt; ... &lt; pos[N]\), do đó sinh random thì khả năng có đáp án bé gần như bằng không. Vậy mình đã giải quyết như nào?</p>

<p>Mình quyết định sinh test bằng cách: sinh trước vị trí của \(K+1, K+2, ..., N\) rồi sinh random các số còn lại (tất nhiên còn thêm vài test tay nữa). Nhưng một vấn đề khác lại nảy sinh: nếu \(K\) quá nhỏ so với \(N\) (giả sử, \(K = 100\) và \(N = 500000\)), thì khả năng \(a[1] = K+1\) là vô cùng lớn (\(= 1 – K/N\)). Cách cuối cùng để giải quyết vấn đề là thêm phần truy vết các thao tác. Tuy nhiên vào phút chót, mình quyết định không làm phức tạp thêm bài toán nữa vì mình cho rằng thời gian 3.5 tiếng là khá ít, cần phải khuyến khích những người nghĩ ra đáp án hơn là đánh đố họ. Dù sao thì việc in \(a[1] – 1\) được nhiều điểm hơn cả subtask 1 cũng làm mình khá buồn :(</p>
]]></content:encoded>
      <guid>https://blog.dtth.ch/nki/hsgso-2017-editorial</guid>
      <pubDate>Sat, 01 Jul 2017 15:00:00 +0000</pubDate>
    </item>
    <item>
      <title>HSGSO 2016 Editorial</title>
      <link>https://blog.dtth.ch/nki/hsgso-2016-editorial</link>
      <description>&lt;![CDATA[#editorial #hsgso #vietnamese&#xA;&#xA;Trong năm vừa rồi có kha khá nhiều bạn hỏi mình cũng như các bạn khác trong BTC HSGSO 2016 (môn Tin) về solution của contest. Vì hồi đó không có thời gian (thực ra là lười) nên bọn mình chưa có dịp chữa bài. Lần này mình quyết định làm cho chót.&#xA;&#xA;Đề bài&#xA;Các bạn có thể tải đề bài gốc tại đây.&#xA;&#xA;!--more--&#xA;&#xA;Lời giải&#xA;Mục lục&#xA;color&#xA;domino&#xA;gift&#xA;letter-o&#xA;paren&#xA;polylines&#xA;socket&#xA;zigzag&#xA;&#xA;color&#xA;Author: Nguyễn Đinh Quang Minh&#xA;Ăn điểm&#xA;Để có 10% đầu tiên cho bài này ta nhận thấy chỉ có cách tô duy nhất là so le với \\(K = 2\\), như vậy chỉ cần in \\(0\\) khi \\(K = 1\\) và \\(2\\) khi \\(K = 2 \\) là bạn đã cầm trong tay 1 điểm đầu tiên.&#xA;&#xA;Biểu diễn trạng thái&#xA;Ta sẽ tiếp cận bài toán, đầu tiên bằng phương pháp quy hoạch động. Qhđ thường là hướng giải đúng đối với những bài toán đếm. Để có thể triển khai qhđ, ta cần biết cách biểu diễn một trạng thái đang xây.&#xA;&#xA;Để ý giới hạn của bảng là \\(3 \times N\\). Vì chỉ có 3 ô nên ta hoàn toàn có thể biểu diễn trạng thái của một cột bằng một \\(K-\\)mask - một số có 3 chữ số trong đó các chữ số trong khoảng \\(0\\) đến \\(K - 1\\) (theo cơ số \\(K\\)). 2 chữ số liên tiếp phải khác nhau, nên số trạng thái thỏa mãn một cột sẽ là \\(5 \times 4 \times 4 = 80\\) trạng thái với \\(K = 5\\).&#xA;&#xA;Ta có thể có hàm quy hoạch động đơn giản \\(fi\\) là số cách điền \\(i\\) cột đầu tiên, trong đó cột cuối cùng có trạng thái là \\(mask\\). Từ \\(fi\\) ta chuyển trạng thái sang \\(fi + 1\\), for hết \\(mask&#39;\\) thỏa mãn. Ta có thể thấy cách này có độ phức tạp \\(O(80^2 N)\\), chưa đủ thỏa mãn cả subtask 2. Kể cả khi ta chỉ lọc ra các \\(mask&#39;\\) thỏa mãn và for chúng, ta vẫn có độ phức tạp \\(O(3380 N)\\), vẫn chưa thể thỏa mãn subtask 2.&#xA;&#xA;Cải tiến quy hoạch động&#xA;Thay vì chuyển trạng thái cả cột cùng lúc, ta có thể thay bằng việc mỗi lần chỉ điền 1 ô, lần lượt từ cột đầu sang cột cuối, mỗi cột điền từ trên xuống dưới. Trạng thái ta lưu lại sẽ là trạng thái của \\(K\\) ô cuối cùng ta điền.&#xA;&#xA;Tại sao lại lưu như vậy? Thực chất, khi điền lần lượt, ta chỉ cần quan tâm đến ô phía trên và bên trái nó, tức ô cách ô hiện tại \\(1\\) và \\(K\\) bước điền. Nhưng do ta cần tính cả các ô phía sau nên ta phải lưu trạng thái của cả \\(K\\) ô trước đó.&#xA;&#xA;Gọi \\(fi[mask]\\) là số cách điền các ô từ đầu đến \\((i, j)\\), với \\(K\\) cuối cùng mình điền được lưu trong \\(mask\\). Để chuyển sang ô tiếp theo, ta cần for một trong \\(K\\) màu của ô tiếp theo ô \\((i, j)\\). Như vậy đpt sẽ là \\(O(100  N  K  3)\\) (\\(5^2 \times 4\\) trạng thái, do khi lưu như này có thể tồn tại 2 ô liên tiếp cùng màu), vừa khít qua subtask 2. Code hơi trâu nhưng không sao, worth it, vì ta đã có 50% số điểm.&#xA;&#xA;Bạn có thể xem thêm 1 bài mình đã chữa có cách qhđ tương tự ở đây (bài Domino).&#xA;&#xA;Nhân ma trận&#xA;Đối với những bạn đã biết về nhân ma trận, ta có thể bỏ qua phần quy hoạch động cải tiến phía trên và thay vào đó, cải tiến qhđ \\(O(80^2 N)\\) thành nhân ma trận \\(O(80^3 \log N)\\). Việc chuyển đổi không khó, thực chất ta chỉ cần dựng bảng chuyển đổi \\(80 \times 80\\) xem 2 trạng thái nào có thể chuyển được cho nhau, mũ \\(N-1\\) lần lên rồi lấy ma trận \\(1 \times 80\\) toàn 1 (thể hiện hàng đầu) nhân cùng tích ban nãy, ra được một ma trận \\(1 \times 80\\) mới, đáp số chính là tổng các phần tử.&#xA;&#xA;Việc nhân ma trận như nào chỉ là kĩ thuật cơ bản nên mình sẽ không nói nhiều.&#xA;&#xA;Kiến thức nhân ma trận, so với tối ưu quy hoạch động như trên, là phổ thông hơn nhiều, vì thế subtask 3 không cho nhiều điểm như subtask 2.&#xA;&#xA;1 tỉ màu??&#xA;Đọc đến subtask 4 hẳn tất cả sẽ ngạc nhiên khi \\(K\\) thay đổi đáng ngạc nhiên: từ \\(\le 5\\) và là mấu chốt giải bài toán, thành \\(10^9\\) - không còn đưa được vào độ phức tạp nữa. Để đào sâu vào subtask này, ta cần có một nhận xét về tương quan các màu giữa các cột.&#xA;&#xA;Tương quan các màu&#xA;Trong một cột, chỉ có 2 loại tương quan sau:&#xA; \\(a,b,c\\) - tức 3 ô trong cột khác nhau&#xA; \\(a,b,a\\) - tức 2 ô đầu và cuối giống nhau&#xA;&#xA;Hơn nữa, số cách chọn màu cho cột \\(i + 1\\) chỉ phụ thuộc vào tương quan của hàng \\(i\\), theo bảng sau:&#xA;Từ \\(a, b, c\\):&#xA;&#x9;Sang \\(a, b, c\\): có \\((K-1)+2(K-2)^2+ (K-3)(K-1)+(K-3)(K-2)^2\\) cách.&#xA;&#x9;Sang \\(a, b, a\\): có \\((K-1)+(K-3)(K-2)\\) cách.&#xA;Từ \\(a, b, a\\):&#xA;&#x9;Sang \\(a, b, c\\): có \\((K-1)+(K-3)(K-2)\\) cách.&#xA;&#x9;Sang \\(a, b, a\\): có \\((K-1)+(K-2)^2\\) cách.&#xA;&#xA;Việc chứng minh chỉ là công thức tổ hợp, mình sẽ không chứng minh để chống dài dòng.&#xA;&#xA;Như vậy ta không cần lưu cụ thể các màu, mà chỉ cần tương quan giữa các màu, tức chỉ còn 2 trạng thái để quản lí chứ không nhiều như trưóc. Việc qhđ để ăn sub 4 (độ phức tạp \\(O(2^2 N)\\)) hay nhân ma trận để ăn sub 5 (độ phức tạp \\(O(2^3 \log N)\\)) có thể được thực hiện đơn giản.&#xA;&#xA;domino&#xA;Author: thầy Hồ Đắc Phương&#xA;Backtrack&#xA;Ở subtask 1, đơn giản ta chỉ cần backtrack tất cả các cách đặt domino. Do bảng chỉ có \\(2 \times 20\\) nên không có đến \\(2^{20}\\) cách đặt là tối đa. Độ phức tạp sẽ là \\(O(2^N)\\).&#xA;&#xA;Bổ đề: lát gạch cơ bản&#xA;Đếm số cách lát gạch vào bảng \\(2 \times N\\)&#xA;&#xA;Có lẽ đây là bài toán nổi tiếng trong giới VNOI. Cách giải khá đơn giản: quy hoạch động \\(f[i]\\) là số cách lát \\(i\\) cột đầu tiên. Ta có 2 cách lát: 1 viên dọc (chuyển xuống \\(f[i - 1]\\)) hoặc 2 viên ngang (chuyển xuống \\(f[i - 2]\\)). Độ phức tạp là \\(O(N)\\), hoặc vì đây là phương trình đệ quy tuyến tính nên ta có thể nhân ma trận \\(O(2^3 \log N)\\).&#xA;&#xA;Không khó để nhận ra \\(f[i]\\) cũng chính là số fibonacci, ta cũng có 1 số cách tính chính xác khác trong \\(O(\log N)\\).&#xA;&#xA;Cách điền duy nhất?&#xA;Để giải tất cả subtask sau, ta cần có chút quan sát về các ô cấm:&#xA;Nếu một cột bị cấm cả 2 ô, hiển nhiên ta có bên trái và bên phải là 2 bài toán riêng biệt, ta chỉ cần tính riêng 2 bên rồi nhân vào nhau.&#xA;Nếu một cột bị chặn 1 ô, hiển nhiên ô đó thuộc một viên domino ngang. Ta sẽ phải lựa chọn viên ngang đó nằm lệch về bên trái hay bên phải.&#xA;&#xA;Ngạc nhiên thay, cách chọn ô đó là duy nhất. Để hiểu rõ tại sao, hãy xét 3 trường hợp sau:&#xA;&#xA;Nhận 1 bên&#xA;TH 1. Ô đỏ là ô cấm&#xA;&#xA;Hiển nhiên trong trường hợp này, số cách điền là 0. Đơn là vì có lẻ ô.&#xA;&#xA;Nhận 2 bên, điền được&#xA;TH 2, cách điền duy nhất&#xA;&#xA;Xét cột thứ nhất, ta có cách điền domino duy nhất. Con domino này chắn 1 ô của cột thứ hai, làm cho bài toán đệ quy xuống. Tại mỗi bước chỉ có một cách điền duy nhất nên với cả đoạn cũng chỉ tồn tại 1 cách điền.&#xA;&#xA;Nhận 2 bên, không điền được&#xA;TH 3&#xA;&#xA;Giống như trường hợp trên, nhưng vì khi đặt con domino cuối cùng, ta bị đặt trùng lên ô cấm, nên ta không thể điền trường hợp này.&#xA;&#xA;Phân biệt với TH 2 như nào? Ta điền được khi và chỉ khi:&#xA;&#xA;Cả đoạn độ dài chẵn và 2 ô cấm cùng hàng, hoặc&#xA;Cả đoạn độ dài lẻ và 2 ô cấm khác hàng&#xA;&#xA;Cách tính&#xA;Xét một đoạn bị chắn 2 đầu là cột 2 ô cấm (hoặc biên, ta có thể coi 2 biên là 2 cột 2 ô cấm). Gọi \\(P\1, P\2, ..., P\K\\) là vị trí các cột có 1 ô cấm, từ trái sang phải.&#xA;&#xA;Hiển nhiên nếu \\(K\\) lẻ thì có 0 cách điền vì có lẻ ô.&#xA;&#xA;\\(P\1 = 3\\), \\(P\2 = 5\\), số cách điền là \\(f[2] \times 1 \times f[3]\\)&#xA;&#xA;Ta có:&#xA;Xét đoạn \\(1..P\1\\): Hiển nhiên viên ở \\(P\1\\) sẽ đặt sang phải vì nếu không ta sẽ có TH 1. Số cách điền đoạn \\(1..P\1-1\\) là \\(f[P\1 - 1]\\).&#xA;Xét đoạn \\(P\1..P\2\\): Vì viên ở \\(P\1\\) đặt trọn trong đoạn nên \\(P\2\\) cũng vậy, nếu không sẽ bị TH 1. Số cách điền là 0 hoặc 1 phụ thuộc vào nó là TH 2 hay 3.&#xA;Xét đoạn \\(P\2..P\3\\), vì viên ở \\(P\2\\) nằm trọn bên trái nên trường hợp này như đoạn \\(1..P\1\\), số cách chọn là \\(f[P\3 - P\2 - 1]\\).&#xA;Vân vân, xét đến khi ta gặp \\(P\{2k}..N\\) thì cũng như đoạn đầu, số cách là \\(f[N - 2k]\\).&#xA;&#xA;Số cách điền cả đoạn sẽ là tích số cách điền từng đoạn con.&#xA;&#xA;Độ phức tạp là \\(O(N \log 10^9)\\), vì ta tính \\(f[i]\\) trong \\(O(\log i)\\).&#xA;&#xA;Tại sao lại chia subtask như vậy?&#xA;Với \\(K = 4\\), bạn có thể mập mờ nhìn ra tính chất trên khi chia tất cả trường hợp 4 ô cấm. Bọn mình muốn hướng suy nghĩ phải theo mạch tự nhiên, không bị gò bó.&#xA;&#xA;gift&#xA;Author: Nguyễn Đức Duy&#xA;Thuật toán backtrack&#xA;Với \\(N \le 10\\), ta có thể thực hiện backtrack, mỗi bước cho phần tử \\(i\\) cho Alice, Bob hoặc giữ lại. Đến cuối, nếu có đáp án, ta in ra và thoát chương trình.&#xA;&#xA;Do mỗi bước ta có 3 lựa chọn nên độ phức tạp là \\(O(3^N)\\).&#xA;&#xA;22 phần tử&#xA;Với \\(N\\) lớn hơn, hẳn là lượng tập con càng lớn so với giới hạn, vì thể xác suất tồn tại đáp số càng lớn.&#xA;Ta sẽ chứng minh chỉ với 22 phần tử, luôn luôn tồn tại đáp số.&#xA;&#xA;Hiển nhiên, với 22 phần tử, ta có \\(2^{22} - 1\\) tập con không rỗng. Đồng thời, các tổng nằm trong khoảng \\(1.. 22 \times 10^5\\). Vì \\(2^22 - 1   22 \times 10^5\\) nên theo định lí Dirichlet ta luôn có 2 tập có cùng tổng.&#xA;&#xA;Gọi 2 tập này là \\(x\\) và \\(y\\). Chắc chắn 2 tập này không phải tập con của nhau, vì mỗi phần tử đều lớn hơn 0. Như vậy chắc chắn tồn tại ít nhất 1 phần tử của mỗi tập mà không tồn tại trong tập kia.&#xA;&#xA;Ta loại đi các phần tử có trong cả 2 (vì chúng cùng trừ cả 2 bên đi 1 lượng), và còn lại 2 tập không rỗng. Đây chính là đáp số.&#xA;&#xA;Như vậy, với \\(N \ge 22\\), ta chỉ cần bốc ra 22 phần tử rồi tính tất cả tổng tập con, lấy 2 tập bằng nhau và loại đi các phần tử trùng là sẽ ra đáp số.&#xA;&#xA;Độ phức tạp là \\(O(2^{22})\\).&#xA;&#xA;\\(N\\) &#34;ất ơ&#34;&#xA;Vậy với \\(10 &lt; N &lt; 22\\) thì sao? Rất tiếc bọn mình không chuẩn bị được test mà giết được thuật ở trên. Tuy nhiên, ta vẫn có thể backtrack gặp nhau ở giữa, lần lượt backtrack \\(3^{10}\\) trường hợp ở đầu và \\(3^{N - 10}\\) trường hợp ở cuối, sau đó ghép 2 số có hiệu trái dấu.&#xA;Như vậy độ phức tạp sẽ không quá \\(O(3^{N / 2})\\).&#xA;&#xA;letter-o&#xA;Author: mình&#xA;&#xA;Lưu ý đây là bài output-only, vì vậy bạn có 5 tiếng để chạy chứ không phải 1 giây.&#xA;Thuật toán \\(O(N^4)\\)&#xA;Thực ra thuật toán \\(O(N^4)\\) khá đơn giản, ta chỉ cần for 2 góc của hình chữ nhật và kiểm tra liệu 4 cạnh của chúng có chứa toàn cùng kí tự không. Để kiểm tra ta có thể tính trước mảng cộng dồn \\(O(N^2)\\).&#xA;&#xA;Lấy thuật \\(O(N^4)\\) có thể chạy 1s đến input 5, và ăn 50% số điểm. Bài thật là dễ!&#xA;&#xA;Thuật toán \\(O(N^3)\\)&#xA;Ta nhận thấy, nếu ta for trước 2 cạnh song song của hình chữ nhật, thì chỉ cần xét các vị trí mà có toàn kí tự x nào đó trong cả đoạn nằm giữa 2 cạnh. Ta sẽ chọn 2 điểm xa nhau nhất mà 2 điểm đó dọc 2 cạnh đều là các kí tự giống nhau.&#xA;&#xA;11111&#xA;10021&#xA;10311&#xA;11111&#xA;10231&#xA;11111&#xA;10230&#xA;11111&#xA;Xét ví dụ trên, chọn 2 cột đầu cuối. Ta thấy chỉ có các hàng 1, 4, 6 và 8 có thể làm 2 cạnh ngang của hình chữ nhật. Ngoài ra, chỉ có 1, 4, 6 được nối với nhau. Ta chọn hình lớn nhất (1 - 6).&#xA;&#xA;Việc lựa chọn có thể được thực hiện chỉ trong \\(O(N)\\) bằng một vòng for lưu max. Như vậy ta có thuật \\(O(N^3)\\) đủ ăn input 6.&#xA;&#xA;Sức mạnh của input!&#xA;Đối với input 7 và 8, đáp số được đảm bảo là lớn, nên bạn có thể sử dụng chiến thuật chỉ bài output-only mới có: sử dụng mắt.&#xA;&#xA;Với mỗi số ta có thể in ra vị trí của chúng (và để trống những vị trí khác). Việc nhìn bằng mắt cũng sẽ cho ta thấy một số hình lớn, chỉ việc thử vào đáp số.&#xA;&#xA;Tìm kiếm pattern&#xA;Nếu nhìn kỹ, bạn có thể nhận ra input 9 có pattern khá dị, khi chỉ có một số hình chữ nhật. Bạn có thể nhìn tay và chỉ chạy các miền có hình chữ nhật thỏa mãn.&#xA;&#xA;... hoặc không&#xA;Để giải input 10, bạn cần phải nhận ra quy tắc quan trọng nhất: bạn không bị giới hạn bởi thời gian chạy của máy chấm. Vì thế hãy nhập input 10 vào, đặt cận đáp số và chờ 15-20 phút cho máy chạy. Tính trên máy trường mình, chỉ mất 1h để chạy tất cả input với \\(O(N^3)\\) đặt cận đáp số! Bạn có 5 tiếng cơ mà, chạy trâu rồi làm bài khác... đó là chiến thuật của bài này.&#xA;&#xA;paren&#xA;Author: thầy Hồ Đắc Phương &amp; Phạm Tùng Dương.&#xA;&#xA;Đệ quy&#xA;Thực chất đây chỉ là một bài tính toán có chút lằng nhằng. Phương thức tính toán như sau, xét đoạn ngoặc \\(l..r\\) là 1 cặp ngoặc:&#xA;&#xA;Tính tất cả các cặp ngoặc con \\(l\1..r\1, ..., l\p..r\p\\)&#xA;Độ cao của \\(l..r\\) là max độ cao của các cặp ngoặc con, cộng 1&#xA;Độ dài của \\(l..r\\) là tổng độ dài của các cặp ngoặc con, cộng \\(p-1\\) khoảng cách ở giữa, cộng 2 hoặc 4 tùy loại ngoặc của \\(l..r\\)&#xA;Phần tô màu của \\(l..r\\) là:&#xA;&#x9;Nếu viền ngoài cùng của \\(l..r\\) là đen: độ dài \\(\times\\) độ cao \\(-\\) diện tích các hình con&#xA;&#x9;Nếu không thì là 0&#xA;&#x9;Sau đó cộng thêm phần tô màu các hình con&#xA;&#xA;Dựng cây&#xA;Để có thể dựng quan hệ cha - con và tính đệ quy trong \\(O(N)\\), ta sẽ cần dựng cây bằng stack. Cách dựng như sau:&#xA;&#xA;Duy trì 1 stack, lúc đầu stack rỗng&#xA;Đi từ trái sang phải, giả sử kí tự ta có là \\(S\x\\):&#xA;&#x9;Nếu \\(S\x\\) là mở ngoặc: Nếu stack không rỗng, thì cặp ngoặc \\(x\\) có cha là đỉnh stack. Push \\(x\\) vào stack.&#xA;&#x9;Nếu \\(x\\) là đóng ngoặc: xóa đỉnh stack.&#xA;&#xA;Tổng độ phức tạp là \\(O(N)\\).&#xA;&#xA;polylines&#xA;Author: mình&#xA;Quy hoạch động trâu cơ bản&#xA;Để đơn giản ta coi điểm xuất phát là \\(0\\), đích là \\(M + 1\\).&#xA;&#xA;Ta có công thức quy hoạch động: Gọi \\(f[i]\\) là số đường đi kết thúc ở \\(i\\). Ta có&#xA;\\(f[0] = 1\\)&#xA;\\(f[i] = \sum\limits\{X\j \le X\i, Y\j \le Y\i, i \neq j}f[j]\\)&#xA;Đáp số là \\(f[M + 1]\\) - 1.&#xA;&#xA;Để có thứ tự qhđ ta chỉ cần sort các điểm theo cả 2 tọa độ tăng dần. Chỉ đơn giản vậy ta có thuật toán \\(O(N^2)\\).&#xA;&#xA;Tăng tốc!&#xA;Nhìn vào điều kiện của \\(j\\) ở hàm qhđ, ta nhận thấy hoàn toàn có thể lấy nhanh tổng các \\(f[j]\\) bằng 1 cấu trúc dữ liệu nào đó.&#xA;&#xA;Nhận thấy, khi sort các phần tử theo \\(X\\) rồi lấy các phần tử đứng trước, ta chỉ còn cần lọc điều kiện \\(Y\\) là đủ. Việc này ta hoàn toàn có thể sử dụng BIT để lấy nhanh, sort tọa độ BIT theo \\(Y\\) rồi get prefix, update điểm.&#xA;&#xA;Độ phức tạp là \\(O(N \log N)\\).&#xA;&#xA;socket&#xA;Author: Nguyễn Đức Duy&#xA;Tìm đáp án&#xA;Không khó để nhận ra nếu chỉ có thể xếp được \\(K\\) thiết bị, ta luôn lấy \\(K\\) thiết bị có độ yêu cầu cao nhất. Vì vậy ta có thể sort thiết bị theo yêu cầu giảm dần rồi chặt nhị phân, kiểm tra xem có thể đặt \\(K\\) thiết bị đầu tiên không.&#xA;&#xA;Xếp ổ điện như nào?&#xA;Ta có thể coi hệ thống ổ điện như một cây, trong đó gốc nối với nguồn. Xét 2 ổ điện \\(i\\) và \\(j\\), trong đó \\(i\\) gần gốc hơn \\(j\\). Nếu \\(A\i\\) &lt; \\(A\j\\), ta hoàn toàn có thể đổi chỗ \\(i\\) và \\(j\\) và đáp án không thể nhỏ hơn ban đầu. Vì thế, để xây cây từ gốc, ta đặt các ổ điện theo thứ tự \\(A\i\\) giảm dần.&#xA;&#xA;Với nhận xét trên, ta coi như \\(A\i\\) đã được xếp giảm dần. Giờ ta BFS theo từng tầng, dễ dàng nhận thấy khi xét tầng \\(x\\):&#xA;Nếu tồn tại \\(B\i = x\\), lập tức phải đặt \\(i\\) vào tầng đó. Nếu không đặt được thì kiểm tra fail.&#xA;Mỗi lần ở tầng \\(x\\) ta thêm ổ \\(j\\) vào, thì bớt 1 chỗ ở tầng \\(x\\) và thêm \\(A\j\\) chỗ ở tầng \\(x + 1\\). Vì \\(A\j\\) dương nên sau khi thêm ta luôn có nhiều chỗ ở tầng \\(x + 1\\) cho các \\(B\i   x\\) hơn ở tầng \\(x\\). Vì vậy,&#xA;Nếu \\(B\i   x\\), ta nhường cho ổ điện nếu còn, nếu không ta sẽ xét sau.&#xA;&#xA;Tóm tắt thuật toán&#xA;Ta chặt nhị phân \\(K\\), kiểm tra xem có thể xếp \\(K\\) thiết bị \\(B[1..K]\\) vào không.&#xA;&#xA;Để kiểm tra:&#xA;Lúc đầu ở tầng 0 ta có 1 vị trí đặt (ổ điện)&#xA;Nếu có nhiều \\(B[i] = x\\) hơn số vị trí đặt, kiểm tra fail. Nếu không, đặt hết \\(B[i] = x\\).&#xA;Nếu còn chỗ ở tầng \\(x\\) và còn ổ điện, đặt ổ điện cho tầng \\(x + 1\\).&#xA;Nếu còn chỗ, coi như chúng của tầng \\(x + 1\\).&#xA;&#xA;Độ phức tạp sẽ là \\(O((N + M) \log M)\\), do các bước kiểm tra chỉ là \\(O(N + M)\\).&#xA;&#xA;zigzag&#xA;Trường hợp \\(K = 1\\)&#xA;Không khó để nhận thấy với 10 chữ số và điều kiện phải thăm mỗi số ít nhất 1 lần và không cần đúng thứ tự, ta sẽ cần sử dụng đến bitmask. Từ ngôi nhà nguồn, ta cần bfs đến các đỉnh, tìm đường đi ngắn nhất dựng ra đủ mask.&#xA;&#xA;Trên đồ thị ta dựng ra các đỉnh \\((i, j, mask)\\), tức đứng ở ô \\((i, j)\\) và tập các số đã đi qua là \\(mask\\). Từ đỉnh \\((i, j)\\) ta đi đến các đỉnh lân cận, thêm mask của đỉnh đó vào nếu cần, mất 1 bước. Đáp số là khoảng cách đến đỉnh gần nhất có mask đầy đủ 10 bit.&#xA;&#xA;Độ phức tạp là \\(O(NM2^{10})\\).&#xA;&#xA;\\(K\\) lớn hơn&#xA;Với \\(K\\) lớn đến \\(M \times N\\), ta không thể chỉ đơn giản là chạy thuật toán trên \\(K\\) lần, vì như vậy là không thỏa mãn giới hạn bài toán.&#xA;&#xA;Thay vào đó, ta cần một cách để có thể chạy tất cả các truy vấn một lúc.&#xA;&#xA;Lật ngược yêu cầu&#xA;Đề bài yêu cầu từ một ngôi nhà, ta đến một ô bất kì, miễn là đủ mask trên đường đi. Ta sẽ lật ngược yêu cầu lại, cho phép xuất phát từ đỉnh bất kì, đi thoải mái, với điều kiện kết thúc ở nhà và đủ mask trên đường đi.&#xA;&#xA;Vậy điểm khác biệt là gì? Với bài toán không quan trọng đích với mỗi nguồn, ta cần BFS với từng nguồn riêng biệt. Tuy nhiên, với bài toán không quan trọng nguồn, ta có thể thực hiện BFS song song nhiều nguồn, để tính khoảng cách từ nguồn gần nhất* tới mỗi đỉnh, với độ phức tạp chỉ bằng 1 lần BFS.&#xA;&#xA;Nói cách khác, thay vì ta xuất phát từ \\((Xi, Yi, 0)\\), ta xuất phát từ tất cả các đỉnh \\((i, j, 0)\\) và tìm đường từ đỉnh bất kì đến \\((Xi, Yi, 1023)\\). Để chạy song song, tưởng tượng có một nguồn ảo nối đến tất cả nguồn thật với trọng số 0. Như vậy, vì chỉ có 1 nguồn (ảo), nên độ phức tạp chỉ là \\(O(NM2^{10})\\).&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="/nki/tag:editorial" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">editorial</span></a> <a href="/nki/tag:hsgso" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">hsgso</span></a> <a href="/nki/tag:vietnamese" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">vietnamese</span></a></p>

<p>Trong năm vừa rồi có kha khá nhiều bạn hỏi mình cũng như các bạn khác trong BTC HSGSO 2016 (môn Tin) về solution của contest. Vì hồi đó không có thời gian (<em>thực ra là lười</em>) nên bọn mình chưa có dịp chữa bài. Lần này mình quyết định làm cho chót.</p>

<h1 id="đề-bài">Đề bài</h1>

<p>Các bạn có thể tải đề bài gốc tại <a href="https://drive.google.com/file/d/0ByCMlnXUqIAIVmpSUWh6dHo1a2c/view" rel="nofollow">đây</a>.</p>



<h1 id="lời-giải">Lời giải</h1>

<h2 id="mục-lục">Mục lục</h2>
<ol><li><a href="#color" rel="nofollow">color</a></li>
<li><a href="#domino" rel="nofollow">domino</a></li>
<li><a href="#gift" rel="nofollow">gift</a></li>
<li><a href="#letter-o" rel="nofollow">letter-o</a></li>
<li><a href="#paren" rel="nofollow">paren</a></li>
<li><a href="#polylines" rel="nofollow">polylines</a></li>
<li><a href="#socket" rel="nofollow">socket</a></li>
<li><a href="#zigzag" rel="nofollow">zigzag</a></li></ol>

<h2 id="color">color</h2>

<p><strong>Author</strong>: Nguyễn Đinh Quang Minh</p>

<h3 id="ăn-điểm">Ăn điểm</h3>

<p>Để có 10% đầu tiên cho bài này ta nhận thấy chỉ có cách tô duy nhất là so le với \(K = 2\), như vậy chỉ cần in \(0\) khi \(K = 1\) và \(2\) khi \(K = 2 \) là bạn đã cầm trong tay 1 điểm đầu tiên.</p>

<h3 id="biểu-diễn-trạng-thái">Biểu diễn trạng thái</h3>

<p>Ta sẽ tiếp cận bài toán, đầu tiên bằng phương pháp quy hoạch động. Qhđ thường là hướng giải đúng đối với những bài toán đếm. Để có thể triển khai qhđ, ta cần biết cách biểu diễn một trạng thái đang xây.</p>

<p>Để ý giới hạn của bảng là \(3 \times N\). Vì chỉ có 3 ô nên ta hoàn toàn có thể biểu diễn trạng thái của một cột bằng một \(K-\)mask – một số có 3 chữ số trong đó các chữ số trong khoảng \(0\) đến \(K – 1\) (theo cơ số \(K\)). 2 chữ số liên tiếp phải khác nhau, nên số trạng thái thỏa mãn một cột sẽ là \(5 \times 4 \times 4 = 80\) trạng thái với \(K = 5\).</p>

<p>Ta có thể có hàm quy hoạch động đơn giản \(f[i][mask]\) là số cách điền \(i\) cột đầu tiên, trong đó cột cuối cùng có trạng thái là \(mask\). Từ \(f[i][mask]\) ta chuyển trạng thái sang \(f[i + 1][mask&#39;]\), for hết \(mask&#39;\) thỏa mãn. Ta có thể thấy cách này có độ phức tạp \(O(80^2 N)\), chưa đủ thỏa mãn cả subtask 2. Kể cả khi ta chỉ lọc ra các \(mask&#39;\) thỏa mãn và for chúng, ta vẫn có độ phức tạp \(O(3380 N)\), vẫn chưa thể thỏa mãn subtask 2.</p>

<h3 id="cải-tiến-quy-hoạch-động">Cải tiến quy hoạch động</h3>

<p>Thay vì chuyển trạng thái cả cột cùng lúc, ta có thể thay bằng việc mỗi lần chỉ điền 1 ô, lần lượt từ cột đầu sang cột cuối, mỗi cột điền từ trên xuống dưới. Trạng thái ta lưu lại sẽ là trạng thái của \(K\) ô cuối cùng ta điền.</p>

<p>Tại sao lại lưu như vậy? Thực chất, khi điền lần lượt, ta chỉ cần quan tâm đến ô phía trên và bên trái nó, tức ô cách ô hiện tại \(1\) và \(K\) bước điền. Nhưng do ta cần tính cả các ô phía sau nên ta phải lưu trạng thái của cả \(K\) ô trước đó.</p>

<p>Gọi \(f[i][j][mask]\) là số cách điền các ô từ đầu đến \((i, j)\), với \(K\) cuối cùng mình điền được lưu trong \(mask\). Để chuyển sang ô tiếp theo, ta cần for một trong \(K\) màu của ô tiếp theo ô \((i, j)\). Như vậy đpt sẽ là \(O(100 * N * K * 3)\) (\(5^2 \times 4\) trạng thái, do khi lưu như này có thể tồn tại 2 ô liên tiếp cùng màu), vừa khít qua subtask 2. Code hơi trâu nhưng không sao, worth it, vì ta đã có 50% số điểm.</p>

<p>Bạn có thể xem thêm 1 bài mình đã chữa có cách qhđ tương tự ở <a href="https://natsukagami.github.io/2017/04/21/2017-04-20-Training/" rel="nofollow">đây (bài Domino)</a>.</p>

<h3 id="nhân-ma-trận">Nhân ma trận</h3>

<p>Đối với những bạn đã biết về nhân ma trận, ta có thể bỏ qua phần quy hoạch động cải tiến phía trên và thay vào đó, cải tiến qhđ \(O(80^2 N)\) thành nhân ma trận \(O(80^3 \log N)\). Việc chuyển đổi không khó, thực chất ta chỉ cần dựng bảng chuyển đổi \(80 \times 80\) xem 2 trạng thái nào có thể chuyển được cho nhau, mũ \(N-1\) lần lên rồi lấy ma trận \(1 \times 80\) toàn 1 (thể hiện hàng đầu) nhân cùng tích ban nãy, ra được một ma trận \(1 \times 80\) mới, đáp số chính là tổng các phần tử.</p>

<p>Việc nhân ma trận như nào chỉ là kĩ thuật cơ bản nên mình sẽ không nói nhiều.</p>

<p>Kiến thức nhân ma trận, so với tối ưu quy hoạch động như trên, là phổ thông hơn nhiều, vì thế subtask 3 không cho nhiều điểm như subtask 2.</p>

<h3 id="1-tỉ-màu">1 tỉ màu??</h3>

<p>Đọc đến subtask 4 hẳn tất cả sẽ ngạc nhiên khi \(K\) thay đổi đáng ngạc nhiên: từ \(\le 5\) và là mấu chốt giải bài toán, thành \(10^9\) – không còn đưa được vào độ phức tạp nữa. Để đào sâu vào subtask này, ta cần có một nhận xét về tương quan các màu giữa các cột.</p>

<h4 id="tương-quan-các-màu">Tương quan các màu</h4>

<p>Trong một cột, chỉ có 2 loại tương quan sau:
 – \(a,b,c\) – tức 3 ô trong cột khác nhau
 – \(a,b,a\) – tức 2 ô đầu và cuối giống nhau</p>

<p>Hơn nữa, số cách chọn màu cho cột \(i + 1\) chỉ phụ thuộc vào tương quan của hàng \(i\), theo bảng sau:
– Từ \(a, b, c\):
    – Sang \(a, b, c\): có \((K-1)+2(K-2)^2+ (K-3)(K-1)+(K-3)(K-2)^2\) cách.
    – Sang \(a, b, a\): có \((K-1)+(K-3)(K-2)\) cách.
– Từ \(a, b, a\):
    – Sang \(a, b, c\): có \((K-1)+(K-3)(K-2)\) cách.
    – Sang \(a, b, a\): có \((K-1)+(K-2)^2\) cách.</p>

<p>Việc chứng minh chỉ là công thức tổ hợp, mình sẽ không chứng minh để chống dài dòng.</p>

<p>Như vậy ta không cần lưu cụ thể các màu, mà chỉ cần tương quan giữa các màu, tức chỉ còn 2 trạng thái để quản lí chứ không nhiều như trưóc. Việc qhđ để ăn sub 4 (độ phức tạp \(O(2^2 N)\)) hay nhân ma trận để ăn sub 5 (độ phức tạp \(O(2^3 \log N)\)) có thể được thực hiện đơn giản.</p>

<h2 id="domino">domino</h2>

<p><strong>Author</strong>: thầy Hồ Đắc Phương</p>

<h3 id="backtrack">Backtrack</h3>

<p>Ở subtask 1, đơn giản ta chỉ cần backtrack tất cả các cách đặt domino. Do bảng chỉ có \(2 \times 20\) nên không có đến \(2^{20}\) cách đặt là tối đa. Độ phức tạp sẽ là \(O(2^N)\).</p>

<h3 id="bổ-đề-lát-gạch-cơ-bản">Bổ đề: lát gạch cơ bản</h3>

<p><strong>Đếm số cách lát gạch vào bảng \(2 \times N\)</strong></p>

<p>Có lẽ đây là bài toán nổi tiếng trong giới VNOI. Cách giải khá đơn giản: quy hoạch động \(f[i]\) là số cách lát \(i\) cột đầu tiên. Ta có 2 cách lát: 1 viên dọc (chuyển xuống \(f[i – 1]\)) hoặc 2 viên ngang (chuyển xuống \(f[i – 2]\)). Độ phức tạp là \(O(N)\), hoặc vì đây là phương trình đệ quy tuyến tính nên ta có thể nhân ma trận \(O(2^3 \log N)\).</p>

<p>Không khó để nhận ra \(f[i]\) cũng chính là số fibonacci, ta cũng có 1 số cách tính chính xác khác trong \(O(\log N)\).</p>

<h3 id="cách-điền-duy-nhất">Cách điền duy nhất?</h3>

<p>Để giải <em>tất cả subtask sau</em>, ta cần có chút quan sát về các ô cấm:
– Nếu một cột bị cấm cả 2 ô, hiển nhiên ta có bên trái và bên phải là 2 bài toán riêng biệt, ta chỉ cần tính riêng 2 bên rồi nhân vào nhau.
– Nếu một cột bị chặn 1 ô, hiển nhiên ô đó thuộc một viên domino ngang. Ta sẽ phải lựa chọn viên ngang đó nằm lệch về bên trái hay bên phải.</p>

<p>Ngạc nhiên thay, cách chọn ô đó là duy nhất. Để hiểu rõ tại sao, hãy xét 3 trường hợp sau:</p>

<h5 id="nhận-1-bên">Nhận 1 bên</h5>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108894215737454612/domino_one.png" alt="TH 1. Ô đỏ là ô cấm"></p>

<p>Hiển nhiên trong trường hợp này, số cách điền là 0. Đơn là vì có lẻ ô.</p>

<h5 id="nhận-2-bên-điền-được">Nhận 2 bên, điền được</h5>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108894215984922774/domino_two.png" alt="TH 2, cách điền duy nhất"></p>

<p>Xét cột thứ nhất, ta có cách điền domino duy nhất. Con domino này chắn 1 ô của cột thứ hai, làm cho bài toán đệ quy xuống. Tại mỗi bước chỉ có một cách điền duy nhất nên với cả đoạn cũng chỉ tồn tại 1 cách điền.</p>

<h5 id="nhận-2-bên-không-điền-được">Nhận 2 bên, không điền được</h5>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108894216249167892/domino_three.png" alt="TH 3"></p>

<p>Giống như trường hợp trên, nhưng vì khi đặt con domino cuối cùng, ta bị đặt trùng lên ô cấm, nên ta không thể điền trường hợp này.</p>

<p>Phân biệt với TH 2 như nào? Ta điền được khi và chỉ khi:</p>
<ul><li>Cả đoạn độ dài chẵn <strong>và</strong> 2 ô cấm cùng hàng, <em>hoặc</em></li>
<li>Cả đoạn độ dài lẻ <strong>và</strong> 2 ô cấm khác hàng</li></ul>

<h3 id="cách-tính">Cách tính</h3>

<p>Xét một đoạn bị chắn 2 đầu là cột 2 ô cấm (hoặc biên, ta có thể coi 2 biên là 2 cột 2 ô cấm). Gọi \(P_1, P_2, ..., P_K\) là vị trí các cột có 1 ô cấm, từ trái sang phải.</p>

<p>Hiển nhiên nếu \(K\) lẻ thì có 0 cách điền vì có lẻ ô.</p>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108894216513400922/domino_all.png" alt="\\(P\_1 = 3\\), \\(P\_2 = 5\\), số cách điền là \\(f[2] \times 1 \times f[3]\\)"></p>

<p>Ta có:
– Xét đoạn \(1..P_1\): Hiển nhiên viên ở \(P_1\) sẽ đặt sang phải vì nếu không ta sẽ có TH 1. Số cách điền đoạn \(1..P_1-1\) là \(f[P_1 – 1]\).
– Xét đoạn \(P_1..P_2\): Vì viên ở \(P_1\) đặt trọn trong đoạn nên \(P_2\) cũng vậy, nếu không sẽ bị TH 1. Số cách điền là 0 hoặc 1 phụ thuộc vào nó là TH 2 hay 3.
– Xét đoạn \(P_2..P_3\), vì viên ở \(P_2\) nằm trọn bên trái nên trường hợp này như đoạn \(1..P_1\), số cách chọn là \(f[P_3 – P_2 – 1]\).
– Vân vân, xét đến khi ta gặp \(P_{2k}..N\) thì cũng như đoạn đầu, số cách là \(f[N – 2k]\).</p>

<p>Số cách điền cả đoạn sẽ là tích số cách điền từng đoạn con.</p>

<p>Độ phức tạp là \(O(N \log 10^9)\), vì ta tính \(f[i]\) trong \(O(\log i)\).</p>

<h3 id="tại-sao-lại-chia-subtask-như-vậy">Tại sao lại chia subtask như vậy?</h3>

<p>Với \(K = 4\), bạn có thể mập mờ nhìn ra tính chất trên khi chia tất cả trường hợp 4 ô cấm. Bọn mình muốn hướng suy nghĩ phải theo mạch tự nhiên, không bị gò bó.</p>

<h2 id="gift">gift</h2>

<p><strong>Author</strong>: Nguyễn Đức Duy</p>

<h3 id="thuật-toán-backtrack">Thuật toán backtrack</h3>

<p>Với \(N \le 10\), ta có thể thực hiện backtrack, mỗi bước cho phần tử \(i\) cho Alice, Bob hoặc giữ lại. Đến cuối, nếu có đáp án, ta in ra và thoát chương trình.</p>

<p>Do mỗi bước ta có 3 lựa chọn nên độ phức tạp là \(O(3^N)\).</p>

<h3 id="22-phần-tử">22 phần tử</h3>

<p>Với \(N\) lớn hơn, hẳn là lượng tập con càng lớn so với giới hạn, vì thể xác suất tồn tại đáp số càng lớn.
Ta sẽ chứng minh chỉ với 22 phần tử, luôn luôn tồn tại đáp số.</p>

<p>Hiển nhiên, với 22 phần tử, ta có \(2^{22} – 1\) tập con không rỗng. Đồng thời, các tổng nằm trong khoảng \(1.. 22 \times 10^5\). Vì \(2^22 – 1 &gt; 22 \times 10^5\) nên theo định lí Dirichlet ta luôn có 2 tập có cùng tổng.</p>

<p>Gọi 2 tập này là \(x\) và \(y\). Chắc chắn 2 tập này không phải tập con của nhau, vì mỗi phần tử đều lớn hơn 0. Như vậy chắc chắn tồn tại ít nhất 1 phần tử của mỗi tập mà không tồn tại trong tập kia.</p>

<p>Ta loại đi các phần tử có trong cả 2 (vì chúng cùng trừ cả 2 bên đi 1 lượng), và còn lại 2 tập không rỗng. Đây chính là đáp số.</p>

<p>Như vậy, với \(N \ge 22\), ta chỉ cần bốc ra 22 phần tử rồi tính tất cả tổng tập con, lấy 2 tập bằng nhau và loại đi các phần tử trùng là sẽ ra đáp số.</p>

<p>Độ phức tạp là \(O(2^{22})\).</p>

<h3 id="n-ất-ơ">\(N\) “ất ơ”</h3>

<p>Vậy với \(10 &lt; N &lt; 22\) thì sao? Rất tiếc bọn mình không chuẩn bị được test mà giết được thuật ở trên. Tuy nhiên, ta vẫn có thể backtrack gặp nhau ở giữa, lần lượt backtrack \(3^{10}\) trường hợp ở đầu và \(3^{N – 10}\) trường hợp ở cuối, sau đó ghép 2 số có hiệu trái dấu.
Như vậy độ phức tạp sẽ không quá \(O(3^{N / 2})\).</p>

<h2 id="letter-o">letter-o</h2>

<p><strong>Author</strong>: mình</p>

<p>Lưu ý đây là bài output-only, vì vậy bạn có 5 tiếng để chạy chứ không phải 1 giây.</p>

<h3 id="thuật-toán-o-n-4">Thuật toán \(O(N^4)\)</h3>

<p>Thực ra thuật toán \(O(N^4)\) khá đơn giản, ta chỉ cần for 2 góc của hình chữ nhật và kiểm tra liệu 4 cạnh của chúng có chứa toàn cùng kí tự không. Để kiểm tra ta có thể tính trước mảng cộng dồn \(O(N^2)\).</p>

<p>Lấy thuật \(O(N^4)\) có thể chạy 1s đến input 5, và ăn 50% số điểm. Bài thật là dễ!</p>

<h3 id="thuật-toán-o-n-3">Thuật toán \(O(N^3)\)</h3>

<p>Ta nhận thấy, nếu ta for trước 2 cạnh song song của hình chữ nhật, thì chỉ cần xét các vị trí mà có toàn kí tự <code>x</code> nào đó trong cả đoạn nằm giữa 2 cạnh. Ta sẽ chọn 2 điểm xa nhau nhất mà 2 điểm đó dọc 2 cạnh đều là các kí tự giống nhau.</p>

<pre><code>11111
10021
10311
11111
10231
11111
10230
11111
</code></pre>

<p>Xét ví dụ trên, chọn 2 cột đầu cuối. Ta thấy chỉ có các hàng 1, 4, 6 và 8 có thể làm 2 cạnh ngang của hình chữ nhật. Ngoài ra, chỉ có 1, 4, 6 được nối với nhau. Ta chọn hình lớn nhất (1 – 6).</p>

<p>Việc lựa chọn có thể được thực hiện chỉ trong \(O(N)\) bằng một vòng for lưu max. Như vậy ta có thuật \(O(N^3)\) đủ ăn input 6.</p>

<h3 id="sức-mạnh-của-input">Sức mạnh của input!</h3>

<p>Đối với input 7 và 8, đáp số được đảm bảo là lớn, nên bạn có thể sử dụng chiến thuật chỉ bài output-only mới có: sử dụng mắt.</p>

<p>Với mỗi số ta có thể in ra vị trí của chúng (và để trống những vị trí khác). Việc nhìn bằng mắt cũng sẽ cho ta thấy một số hình lớn, chỉ việc thử vào đáp số.</p>

<h3 id="tìm-kiếm-pattern">Tìm kiếm pattern</h3>

<p>Nếu nhìn kỹ, bạn có thể nhận ra input 9 có pattern khá dị, khi chỉ có một số hình chữ nhật. Bạn có thể nhìn tay và chỉ chạy các miền có hình chữ nhật thỏa mãn.</p>

<h3 id="hoặc-không">... hoặc không</h3>

<p>Để giải input 10, bạn cần phải nhận ra quy tắc quan trọng nhất: bạn không bị giới hạn bởi thời gian chạy của máy chấm. Vì thế hãy nhập input 10 vào, đặt cận đáp số và chờ 15-20 phút cho máy chạy. Tính trên máy trường mình, chỉ mất 1h để chạy tất cả input với \(O(N^3)\) đặt cận đáp số! Bạn có 5 tiếng cơ mà, chạy trâu rồi làm bài khác... đó là chiến thuật của bài này.</p>

<h2 id="paren">paren</h2>

<p><strong>Author</strong>: thầy Hồ Đắc Phương &amp; Phạm Tùng Dương.</p>

<h3 id="đệ-quy">Đệ quy</h3>

<p>Thực chất đây chỉ là một bài tính toán có chút lằng nhằng. Phương thức tính toán như sau, xét đoạn ngoặc \(l..r\) là 1 cặp ngoặc:</p>
<ul><li>Tính tất cả các cặp ngoặc con \(l_1..r_1, ..., l_p..r_p\)</li>
<li>Độ cao của \(l..r\) là max độ cao của các cặp ngoặc con, cộng 1</li>
<li>Độ dài của \(l..r\) là tổng độ dài của các cặp ngoặc con, cộng \(p-1\) khoảng cách ở giữa, cộng 2 hoặc 4 tùy loại ngoặc của \(l..r\)</li>
<li>Phần tô màu của \(l..r\) là:
<ul><li>Nếu viền ngoài cùng của \(l..r\) là đen: độ dài \(\times\) độ cao \(-\) diện tích các hình con</li>
<li>Nếu không thì là 0</li>
<li>Sau đó cộng thêm phần tô màu các hình con</li></ul></li></ul>

<h3 id="dựng-cây">Dựng cây</h3>

<p>Để có thể dựng quan hệ cha – con và tính đệ quy trong \(O(N)\), ta sẽ cần dựng cây bằng stack. Cách dựng như sau:</p>
<ul><li>Duy trì 1 stack, lúc đầu stack rỗng</li>
<li>Đi từ trái sang phải, giả sử kí tự ta có là \(S_x\):
<ul><li>Nếu \(S_x\) là mở ngoặc: Nếu stack không rỗng, thì cặp ngoặc \(x\) có cha là đỉnh stack. Push \(x\) vào stack.</li>
<li>Nếu \(x\) là đóng ngoặc: xóa đỉnh stack.</li></ul></li></ul>

<p>Tổng độ phức tạp là \(O(N)\).</p>

<h2 id="polylines">polylines</h2>

<p><strong>Author</strong>: mình</p>

<h3 id="quy-hoạch-động-trâu-cơ-bản">Quy hoạch động trâu cơ bản</h3>

<p>Để đơn giản ta coi điểm xuất phát là \(0\), đích là \(M + 1\).</p>

<p>Ta có công thức quy hoạch động: Gọi \(f[i]\) là số đường đi kết thúc ở \(i\). Ta có
– \(f[0] = 1\)
– \(f[i] = \sum\limits_{X_j \le X_i, Y_j \le Y_i, i \neq j}f[j]\)
– Đáp số là \(f[M + 1]\) – 1.</p>

<p>Để có thứ tự qhđ ta chỉ cần sort các điểm theo cả 2 tọa độ tăng dần. Chỉ đơn giản vậy ta có thuật toán \(O(N^2)\).</p>

<h3 id="tăng-tốc">Tăng tốc!</h3>

<p>Nhìn vào điều kiện của \(j\) ở hàm qhđ, ta nhận thấy hoàn toàn có thể lấy nhanh tổng các \(f[j]\) bằng 1 cấu trúc dữ liệu nào đó.</p>

<p>Nhận thấy, khi sort các phần tử theo \(X\) rồi lấy các phần tử đứng trước, ta chỉ còn cần lọc điều kiện \(Y\) là đủ. Việc này ta hoàn toàn có thể sử dụng BIT để lấy nhanh, sort tọa độ BIT theo \(Y\) rồi get prefix, update điểm.</p>

<p>Độ phức tạp là \(O(N \log N)\).</p>

<h2 id="socket">socket</h2>

<p><strong>Author</strong>: Nguyễn Đức Duy</p>

<h3 id="tìm-đáp-án">Tìm đáp án</h3>

<p>Không khó để nhận ra nếu chỉ có thể xếp được \(K\) thiết bị, ta luôn lấy \(K\) thiết bị có độ yêu cầu cao nhất. Vì vậy ta có thể sort thiết bị theo yêu cầu giảm dần rồi chặt nhị phân, kiểm tra xem có thể đặt \(K\) thiết bị đầu tiên không.</p>

<h3 id="xếp-ổ-điện-như-nào">Xếp ổ điện như nào?</h3>

<p>Ta có thể coi hệ thống ổ điện như một cây, trong đó gốc nối với nguồn. Xét 2 ổ điện \(i\) và \(j\), trong đó \(i\) gần gốc hơn \(j\). Nếu \(A_i\) &lt; \(A_j\), ta hoàn toàn có thể đổi chỗ \(i\) và \(j\) và đáp án không thể nhỏ hơn ban đầu. Vì thế, để xây cây từ gốc, ta đặt các ổ điện theo thứ tự \(A_i\) giảm dần.</p>

<p>Với nhận xét trên, ta coi như \(A_i\) đã được xếp giảm dần. Giờ ta BFS theo từng tầng, dễ dàng nhận thấy khi xét tầng \(x\):
– Nếu tồn tại \(B_i = x\), lập tức phải đặt \(i\) vào tầng đó. Nếu không đặt được thì kiểm tra fail.
– Mỗi lần ở tầng \(x\) ta thêm ổ \(j\) vào, thì bớt 1 chỗ ở tầng \(x\) và thêm \(A_j\) chỗ ở tầng \(x + 1\). Vì \(A_j\) dương nên sau khi thêm ta luôn có nhiều chỗ ở tầng \(x + 1\) cho các \(B_i &gt; x\) hơn ở tầng \(x\). <em>Vì vậy,</em>
– Nếu \(B_i &gt; x\), ta nhường cho ổ điện nếu còn, nếu không ta sẽ xét sau.</p>

<h3 id="tóm-tắt-thuật-toán">Tóm tắt thuật toán</h3>

<p>Ta chặt nhị phân \(K\), kiểm tra xem có thể xếp \(K\) thiết bị \(B[1..K]\) vào không.</p>

<p>Để kiểm tra:
– Lúc đầu ở tầng 0 ta có 1 vị trí đặt (ổ điện)
– Nếu có nhiều \(B[i] = x\) hơn số vị trí đặt, kiểm tra fail. Nếu không, đặt hết \(B[i] = x\).
– Nếu còn chỗ ở tầng \(x\) và còn ổ điện, đặt ổ điện cho tầng \(x + 1\).
– Nếu còn chỗ, coi như chúng của tầng \(x + 1\).</p>

<p>Độ phức tạp sẽ là \(O((N + M) \log M)\), do các bước kiểm tra chỉ là \(O(N + M)\).</p>

<h2 id="zigzag">zigzag</h2>

<h3 id="trường-hợp-k-1">Trường hợp \(K = 1\)</h3>

<p>Không khó để nhận thấy với 10 chữ số và điều kiện phải thăm mỗi số ít nhất 1 lần và không cần đúng thứ tự, ta sẽ cần sử dụng đến bitmask. Từ ngôi nhà nguồn, ta cần bfs đến các đỉnh, tìm đường đi ngắn nhất dựng ra đủ mask.</p>

<p>Trên đồ thị ta dựng ra các đỉnh \((i, j, mask)\), tức đứng ở ô \((i, j)\) và tập các số đã đi qua là \(mask\). Từ đỉnh \((i, j)\) ta đi đến các đỉnh lân cận, thêm mask của đỉnh đó vào nếu cần, mất 1 bước. Đáp số là khoảng cách đến đỉnh gần nhất có mask đầy đủ 10 bit.</p>

<p>Độ phức tạp là \(O(NM2^{10})\).</p>

<h3 id="k-lớn-hơn">\(K\) lớn hơn</h3>

<p>Với \(K\) lớn đến \(M \times N\), ta không thể chỉ đơn giản là chạy thuật toán trên \(K\) lần, vì như vậy là không thỏa mãn giới hạn bài toán.</p>

<p>Thay vào đó, ta cần một cách để có thể chạy tất cả các truy vấn một lúc.</p>

<h3 id="lật-ngược-yêu-cầu">Lật ngược yêu cầu</h3>

<p>Đề bài yêu cầu từ một ngôi nhà, ta đến một ô bất kì, miễn là đủ mask trên đường đi. Ta sẽ lật ngược yêu cầu lại, cho phép xuất phát từ đỉnh bất kì, đi thoải mái, với điều kiện kết thúc ở nhà và đủ mask trên đường đi.</p>

<p>Vậy điểm khác biệt là gì? Với bài toán không quan trọng đích với mỗi nguồn, ta cần BFS với từng nguồn riêng biệt. Tuy nhiên, với bài toán không quan trọng nguồn, ta có thể thực hiện BFS song song nhiều nguồn, để tính khoảng cách từ <em>nguồn gần nhất</em> tới mỗi đỉnh, với độ phức tạp chỉ bằng 1 lần BFS.</p>

<p>Nói cách khác, thay vì ta xuất phát từ \((X<em>i, Y</em>i, 0)\), ta xuất phát từ tất cả các đỉnh \((i, j, 0)\) và tìm đường từ đỉnh bất kì đến \((X<em>i, Y</em>i, 1023)\). Để chạy song song, tưởng tượng có một nguồn ảo nối đến tất cả nguồn thật với trọng số 0. Như vậy, vì chỉ có 1 nguồn (ảo), nên độ phức tạp chỉ là \(O(NM2^{10})\).</p>
]]></content:encoded>
      <guid>https://blog.dtth.ch/nki/hsgso-2016-editorial</guid>
      <pubDate>Mon, 01 May 2017 15:00:00 +0000</pubDate>
    </item>
    <item>
      <title>2017/04/28 Training</title>
      <link>https://blog.dtth.ch/nki/yyd5ub7sbm</link>
      <description>&lt;![CDATA[#training #apio #vietnamese #thầyHoàng #anhKhuê&#xA;&#xA;Hôm nay có hai phần: bài thầy Hoàng và bài anh Khuê. Cả 2 đều cần một sự tay to nhất định.&#xA;&#xA;!--more--&#xA;&#xA;Tóm tắt đề bài&#xA;select (thầy Hoàng)&#xA;Cho dãy $S$ gồm các kí tự d và x. 2 người lần lượt chơi, mỗi lượt bốc 1 kí tự, kí tự này phải đứng cạnh 1 vị trí đã bị chọn trước đó (trừ nước đầu tiên được chọn thoải mái). Khi trò chơi kết thúc, người đi trước thắng khi có nhiều d hơn hẳn người kia. Đếm số vị trí ban đầu mà người đi trước có thể chọn mà vẫn đảm bảo chiến thắng?&#xA;&#xA;Giới hạn&#xA;\\(1 \le |S| \le 1000\\).&#xA;&#xA;eureka (thầy Hoàng)&#xA;Cho một hệ thống cân đĩa được biểu diễn như sau:&#xA;&#xA;Nếu chỉ gồm -1: Đây là một vị trí đặt quả cân.&#xA;Là một cái cân có dạng x y A B trong đó \\(x\\) và \\(y\\) lần lượt là độ dài cánh tay đòn bên trái và bên phải của cân; \\(A\\) và \\(B\\) là 2 hệ thống cân được treo vào bên trái và bên phải của cân.&#xA;&#xA;Hình dưới biểu diễn hệ thống cân được biểu diễn bằng dãy 12 18 4 2 -1 -1 10 8 3 3 -1 -1 6 4 -1 -1:&#xA;&#xA;Trọng lượng các quả cân đều phải là số nguyên dương.&#xA;&#xA;Hãy tìm trọng lượng cho các quả cân của từng vị trí đặt sao cho tất cả các cân đều thăng bằng và tổng trọng lượng các quả cân phải đặt là nhỏ nhất. Giả sử cân không có trọng lượng.&#xA;&#xA;In ra tổng nhỏ nhất lấy dư cho \\(123456789\\).&#xA;&#xA;Giới hạn&#xA;Miêu tả được cho bởi dãy \\(A[1..N]\\).&#xA;&#xA;\\(1 \le N \le 3 \times 10^5\\), \\(A[i] = -1\\) hoặc \\(1 \le A[i] \le 100\\).&#xA;&#xA;liondance (thầy Hoàng)&#xA;Cho 2 dãy số \\(A[1..N]\\) và \\(B[1..N]\\). Tìm dãy con chung dài nhất sao cho 2 phần tử liên tiếp chênh lệch nhau không quá \\(d\\).&#xA;Nếu có nhiều dãy, in ra dãy có thứ tự từ điển lớn nhất.&#xA;&#xA;Giới hạn&#xA;\\(1 \le N \le 4000\\), \\(1 \le A[i], B[i] \le 10^9\\)&#xA;&#xA;desert (thầy Hoàng)&#xA;Trên mặt phẳng cho \\(N\\) điểm. Tìm đường đi từ \\(1\\) đến \\(N\\) sao cho khoảng cách Manhattan lớn nhất giữa 2 điểm liên tiếp đi qua là nhỏ nhất.&#xA;&#xA;Giới hạn&#xA;\\(1 \le N \le 10^5\\), tọa độ \\(-10^9 \le X\i, Y\i\le 10^9\\)&#xA;&#xA;Trốn việc (anh Khuê)&#xA;Cho dãy số \\(A[1..3N]\\), chọn ra tập số có tổng lớn nhất sao cho trong \\(N\\) số liên tiếp bất kì có không quá \\(K\\) số được chọn.&#xA;&#xA;Giới hạn&#xA;\\(1 \le N \le 200\\), \\(1 \le K \le 10\\), \\(1 \le A[i] \le 10^6\\)&#xA;&#xA;Party1 (anh Khuê)&#xA;Cho \\(N\\) bạn nam và \\(M\\) bạn nữ, và \\(K\\) mối quan hệ nam - nữ. Liệt kê tất cả bạn nam và bạn nữ chắc chắn sẽ xuất hiện trong cặp ghép cực đại.&#xA;&#xA;Giới hạn&#xA;\\(1 \le N, M \le 10^4\\), \\(1 \le K \le 10^5\\).&#xA;&#xA;Party2 (anh Khuê)&#xA;Cho đồ thị \\(N\\) đỉnh \\(M\\) cạnh xanh \\(K\\) cạnh đỏ, đỉnh \\(i\\) có trọng số \\(A[i]\\). Chọn một tập điểm có tổng trọng số lớn nhất thỏa mãn:&#xA; 2 đỉnh có cạnh xanh nối giữa thì không được cùng chọn. - 2 đỉnh có cạnh đỏ nối giữa thì cùng được chọn, hoặc cùng không được chọn.&#xA;&#xA;Đồng thời, đếm số cách chọn tối ưu.&#xA;&#xA;Giới hạn&#xA;\\(1 \le N \le 250\\), \\(0 \le M \le \frac{N(N-1)}{6}\\), \\(\frac{N(N-1)}{3} \le K \le \frac{N(N-1)}{2}\\)&#xA;&#xA;Lời giải&#xA;select&#xA;Chuyển đổi bài toán&#xA;Rất khó quản lí trạng thái thắng thua khi bài toán yêu cầu so sánh số lượng. Vì vậy, ta sẽ biến đổi bài toán một chút - mặc dù tính chất thắng - thua không thay đổi.&#xA;&#xA;Thay vì so sánh số lượng d của từng người, ta lấy số lượng d của người đi trước trừ đi người đi sau. Hiển nhiên người đi trước muốn hiệu dương - tức đối đa hóa nó, và người đi sau muốn tối thiểu hóa nó.&#xA;&#xA;Bởi vì đây là trò chơi hữu hạn bước, và 2 người đều phải chơi tối ưu, nên ta sẽ chỉ đi tìm một kết quả duy nhất: hiệu tối ưu của trò chơi, với mỗi cách chọn bước đầu.&#xA;&#xA;Quy hoạch động&#xA;Gọi \\(fl\\) là hiệu tối ưu của trò chơi, nếu trò chơi bắt đầu với trạng thái đoạn \\(l..r\\) bị khuyết. Ta có:&#xA;Hiển nhiên \\(fl = 0\\) nếu \\(l..r\\) đã phủ toàn bộ vòng tròn.&#xA;Ta có thể hiểu độ dài \\(l..r\\) sẽ tương đương với số bước đã xảy ra, vì vậy ta có thể biết được lượt đi tiếp theo là của ai.&#xA;Nếu đây là lượt của người đi trước, hẳn hắn ta sẽ muốn \\(fl\\) lớn nhất có thể, tức hắn sẽ chọn vị trí \\(l - 1\\) hay \\(r + 1\\) sao cho \\(fl - 1\\) hoặc \\(fl\\), cộng vị trí hắn chọn nếu nó màu đỏ, sao cho tổng ấy lớn nhất có thể. Nói cách khác ta có:&#xA;\\ f[l = \max(fl - 1 +&#xA;(\text{Sl - 1] == &#39;d&#39;}), f[l +&#xA;(\text{S[r + 1] == &#39;d&#39;}))\\]&#xA;Ngược lại, người đi sau sẽ muốn lựa chọn của mình là nhỏ nhất có thể, tức:&#xA;\\ f[l = \min(fl - 1 -&#xA;(\text{Sl - 1] == &#39;d&#39;}), f[l -&#xA;(\text{S[r + 1] == &#39;d&#39;}))\\]&#xA;Lưu ý dấu -, bởi ta đang tối ưu hiệu người đi trước trừ người đi sau.&#xA;&#xA;Ta có thể tính tất cả các hàm \\(f1\\), \\(f2\\), ..., \\(fN\\) trong độ phức tạp \\(O(N^2)\\).&#xA;&#xA;Đếm vị trí tốt&#xA;Một vị trí \\(i\\) thỏa mãn đầu bài nếu như hiệu tối ưu lớn hơn \\(0\\), bởi khi đó người thứ nhất luôn có thể làm cho mình có nhiều d hơn. Như vậy, ta chỉ cần thử hết các vị trí \\(i\\) và kiểm tra xem \\(fi   0\\) đúng không.&#xA;&#xA;Tổng độ phức tạp của bài toán là \\(O(N^2)\\).&#xA;&#xA;eureka&#xA;Tính chất của tổng trọng lượng&#xA;Xét một hệ thống cân, không khó để chứng minh tổng trọng lượng của nó phải là một bội của số \\(X\\) tương ứng với từng hệ thống.&#xA;&#xA;Nhiệm vụ của ta là đi tìm \\(X\\) đó cho cả hệ thống lớn, bằng cách tính từ các hệ thống con.&#xA;&#xA;Quy nạp&#xA;Hiển nhiên quả cân có thể nhận trọng lượng bất kì, vậy quả cân là hệ thống cân có \\(X\\) là 1.&#xA;&#xA;Ta xét hệ thống cân \\(a, b, X\l, X\r\\) trong đó \\(X\l, X\r\\) là các giá trị mình đã tính trước đó cho hệ thống cân bên trái và phải.&#xA;Ta có:&#xA;Tồn tại \\((p, q) = 1\\) sao cho \\(paX\l = qbX\r\\). Hiển nhiên đây là \\(p\\) và \\(q\\) có tổng nhỏ nhất thỏa mãn.&#xA;Khi đó, \\(X = pX\l + qX\r\\).&#xA;&#xA;Ta có thể nhận thấy \\(\frac{p}{q} = \frac{bX\r}{aX\l}\\), vậy \\(p = bX\r / d\\) và \\(q = aX\l / d\\) với \\(d = \gcd(aX\l, bX\r)\\).&#xA;Như vậy \\(X = \dfrac{X\l X\r(a + b)}{d}\\).&#xA;&#xA;Số lớn&#xA;Ta nhận thấy từ công thức trên rằng đáp số có thể rất lớn. Ta không cần phải in số lớn, tuy vậy các phép tính như \\(\gcd\\) không thể được tính khi số đã bị mod.&#xA;&#xA;Tuy nhiên ta cũng không cần phải cài số lớn: Mỗi lần ta nhân thêm một số \\(a + b \le 200\\), nên thay vì lưu số lớn ta sẽ lưu tập ước nguyên tố và số mũ. Khi đó, các phép nhân (cộng số mũ), chia (trừ số mũ), lấy \\(\gcd\\) (lấy min số mũ) trở nên đơn giản, với độ phức tạp \\(O(A[i])\\).&#xA;&#xA;Ta có thuật toán đáp số với độ phức tạp&#xA;\\(O(N  A[i])\\).&#xA;&#xA;Cài đặt&#xA;Một cách cài đặt ngắn gọn là sử dụng đệ quy, vừa đọc vừa làm.&#xA;Ta thực hiện lần lượt:&#xA;Đọc \\(a\\) và kiểm tra xem có phải quả cân không. Nếu có trả về 1.&#xA;Đọc \\(b\\).&#xA;Đệ quy giải bên trái \\(Xl\\).&#xA;Đệ quy giải bên phải \\(Xr\\).&#xA;Tính và trả về \\(X\\).&#xA;&#xA;liondance&#xA;Quy hoạch động&#xA;Về cơ bản, đây chỉ là bài toán LCS có thêm điều kiện, vì vậy hướng suy nghĩ của ta tất nhiên là cải thiện thuật toán quy hoạch động LCS, vì độ phức tạp yêu cầu cũng tương đương.&#xA;&#xA;Vậy làm sao để quản lí trạng thái vẫn là \\(i\\) khi ta còn cần thông tin về 2 phần tử liên tiếp? Ta sẽ cần &#34;gói&#34; nhiều thông tin hơn vào trạng thái \\((i, j)\\).&#xA;&#xA;Thêm thông tin!&#xA;Ta sẽ định nghĩa hàm quy hoạch động như sau:&#xA;Gọi \\(fi\\) là dãy con chung dài nhất khi sử dụng đoạn \\(A[i..N]\\), đoạn \\(B[i + 1..N]\\) và \\(B[j]\\) là phần tử cuối cùng được thêm ở đầu đoạn (để đơn giản, ta không tính cặp \\(B[j]\\) - ?? vào đáp số, cũng như coi \\( B[0]\\) là phần tử có thể ghép với mọi thứ).&#xA;&#xA;Tại sao lại định nghĩa như vậy?&#xA;Nhờ có việc đánh dấu \\(B[j]\\) là số được thêm vào cuối cùng, ta có thể quản lí giá trị có thể thêm vào tiếp theo.&#xA;Khi có \\(fi\\), và \\(A[i] = B[k]\\) (\\(k \ge j + 1\\)), điều kiện duy nhất đẻ kiểm tra chỉ là \\(|B[j] - B[k]| \le d\\).&#xA;&#xA;Tại sao lại từ cuối?&#xA;Để giúp cho việc truy vết thứ tự từ điển, sau này mình sẽ nói đến.&#xA;&#xA;Ta có thể tính hàm quy hoạch động này như sau - \\(fi\\) có thể được tính từ các trạng thái:&#xA;&#xA;\\(fi + 1\\), hiển nhiên&#xA;\\(fi + 1 + 1\\), với \\(k \ge j + 1\\) và \\(A[i] = B[k]\\).&#xA;&#xA;Độ phức tạp là \\(O(N^3)\\). Ta sẽ cần một chút quan sát để hạ độ phức tạp.&#xA;&#xA;Nhảy xuống \\(O(N^2)\\)&#xA;Khi xét \\(fi\\), ta có thể thấy nếu \\(k \le k&#39;\\) và \\(Ai] = B[k] = B[k&#39;]\\) thì \\(f[i + 1 \ge fi + 1\\). Chứng minh rất đơn giản: \\(B[k + 1..N]\\) dài hơn \\(B[k&#39; + 1..N]\\), mà điều kiện ghép không đổi.&#xA;&#xA;Vì vậy, thực chất ta chỉ cần tìm \\(k\\) nhỏ nhất thỏa mãn \\(k \ge j + 1\\) và \\(Ai] = B[k]\\) để cập nhật vào \\(f[i\\). Việc này có thể thực hiện bằng việc đánh dấu khi for ngược \\(i\\) và \\(j\\), nên độ phức tạp chỉ còn \\(O(N^2)\\).&#xA;&#xA;Thứ tự từ điển&#xA;Khi quy hoạch động \\(fi\\), ta sẽ lưu thêm \\(nxi\\) là chỉ số \\(B[k]\\) mà mình chọn làm kí tự tiếp theo. Do điều kiện của dãy đáp số là phụ thuộc vào giá trị* chứ không phải chỉ số, ta sẽ phải thêm vào các điều kiện sau đây để đảm bảo lựa chọn chỉ số tiếp theo cho truy vết:&#xA;Hiển nhiên nếu \\(fi + 1 \neq fi + 1\\) thì ta chọn cái nào lớn hơn, vì được dãy dài hơn.&#xA;Nếu bằng nhau, hiển nhiên trong \\(Bnx[i + 1]\\) và \\(B[k]\\) cái nào lớn hơn ta chọn.&#xA;Nếu vẫn bằng nhau, ta nhận thấy chắc chắn \\(nxi + 1 \ge k\\), do vậy từ \\((i + 1, k)\\) ta có nhiều lựa chọn hơn (và có tất cả lựa chọn của) \\((i + 1, nxi + 1)\\), nên ta sẽ chọn \\((i + 1, k)\\).&#xA;&#xA;Khi đã có mảng \\(nxi\\), việc truy vết trở thành đơn giản.&#xA;&#xA;desert&#xA;Cây khung Manhattan&#xA;Bản chất của việc dựng đường đi sao cho cạnh \\(\max\\) là \\(\min\\) chính là dựng cây khung nhỏ nhất của đồ thị.&#xA;&#xA;Từ việc chăm chỉ đọc wiki, ta cũng biết có thể dựng lên cây khung giữa các đỉnh theo khoảng cách Manhattan với độ phức tạp \\(O(N \log N)\\). Thực chất đây chỉ là bài yêu cầu implement thuật toán đó.&#xA;&#xA;Dựng cây khung như nào?&#xA;Ta có thể chứng minh rằng, để dựng cây khung Manhattan, với mỗi&#xA;điểm ta chỉ cần nối cạnh đến đỉnh gần nó nhất trong mỗi góc phần tám).&#xA;Vậy tìm chúng như thế nào?&#xA;&#xA;Tìm điểm gần nhất trong góc phần tám&#xA;Giả sử ta sẽ giải bài toán cho góc phần tám có \\(X \le X\0\\), \\(Y \le Y\0\\) và \\(X - Y \le X\0 - Y\0\\).&#xA;&#xA;Ta sắp xếp các điểm theo thứ tự \\(X\i - Y\i\\) giảm dần, và thêm vào một IT có tọa độ được xếp theo \\(X\i\\) giá trị \\(X\i + Y\i\\) lấy \\(\max\\).&#xA;&#xA;Với mỗi điểm, ta get trong IT giá trị \\(X\j + Y\j\\) lớn nhất với \\(X\j \le X\i\\), rồi cập nhật điểm đó vào IT. Điểm get ra sẽ là điểm gần nhất theo góc phần tám này.&#xA;&#xA;Để hiểu tại sao sắp xếp như vậy lại đúng, xem hình dưới.&#xA;&#xA;Phần được gọi khi xét điểm \\(A\\). Phần tô màu đã được thêm vào IT, phần màu xanh thể hiện phần được get trong IT&#xA;&#xA;Dễ dàng làm với góc phần tám đối diện, chỉ cần for tập điểm ngược lại.&#xA;&#xA;Giải nhanh tất cả các phần&#xA;Thay vì cài tất cả các trường hợp góc phần tám, ta có thể chỉ cần giải 2 góc đối diện như trên, rồi thực hiện phép quay 90 độ cho tất cả các điểm theo gốc, và tiếp tục giải.&#xA;&#xA;Hiển nhiên sau khi xoay 3 lần ta sẽ giải đủ 8 góc phần tám.&#xA;&#xA;Trốn việc&#xA;Liên tưởng&#xA;Trong một hệ thống \\(N\\) máy song song, ta phải đảm bảo trong khoảng thời gian \\(M\\) bất kì không có quá \\(N\\) thao tác được thực hiện trên bất cứ máy nào. Ta sẽ làm như nào?&#xA;&#xA;Một cách đơn giản là ra lệnh cho một máy, sau khi thực hiện một thao tác, sẽ ngủ trong \\(M\\) giây. Như vậy, không có khoảng thời gian \\(M\\) nào có một máy chạy 2 lần, vì thế không có chuyện có nhiều hơn \\(N\\) thao tác được chạy.&#xA;&#xA;Ta sẽ sử dụng ý tưởng này cho bài toán.&#xA;&#xA;Biến đổi bài toán&#xA;Đề bài yêu cầu ta chọn một tập lớn nhất sao cho cứ \\(N\\) phần tử liên tiếp bất kì có không quá \\(K\\) phần tử được chọn. Ta sẽ biến bài toán thành: Cho phép chạy \\(K\\) máy lựa chọn song song, mỗi phần tử chỉ được cho một máy lựa chọn, trong khoảng \\(N\\) bất kì không có máy nào chọn liên tiếp 2 phần tử.&#xA;&#xA;Nếu \\(K = 1\\), ta có thể sử dụng quy hoạch động \\(f[i] = \max(f[i - 1], f[i - N] + A[i])\\). Với \\(K\\) lớn hơn, ta sẽ phải tìm cách khác để đảm bảo mỗi phần tử chỉ được tối đa một máy lựa chọn.&#xA;&#xA;Hãy tưởng tượng một đường ống \\(3N + 1\\) đoạn liên tiếp nối thành một đường thẳng. Ngoài ra đường ống thứ \\(i\\) còn được nối với \\(\min( i + N, 3N + 1)\\). Từ đỉnh 1 ta cho \\(K\\) robot chạy về hướng \\(3N + 1\\). Các robot có thể đi đường \\(i\\) - \\(i + 1\\) thoải mái, nhưng mỗi đường đi \\(i\\) - \\(i + N\\) chỉ có thể cho 1 robot đi qua, đồng thời sẽ thu về \\(A[i]\\) đồng tiền. Ta cần thu về nhiều tiền nhất có thể.&#xA;&#xA;Nghe rất giống một bài luồng max cost.&#xA;&#xA;Dựng luồng&#xA;Ta áp dụng toàn bộ ý tưởng của đường ống vào luồng.&#xA;&#xA;\\(3N + 2\\) đỉnh, \\(0\\) là nguồn \\(3N + 1\\) là đích.&#xA;\\(0\\) =  \\(1\\): cap = \\(K\\), cost = \\(0\\)&#xA;Với \\(i   0\\), \\(i\\) =  \\(i + 1\\): cap = \\(inf\\), cost = \\(0\\)&#xA;Với \\(i   0\\), \\(i\\) =  \\(\min(i + N, 3N + 1)\\): cap = \\(1\\), cost = \\(A[i]\\)&#xA;&#xA;Trên mạng, ta tìm luồng max cost. Hiển nhiên luồng cực đại là \\(K\\), nhưng ta chỉ cần quan tâm đến cost tối đa. Cost chính là đáp số.&#xA;&#xA;Độ phức tạp là O(luồng 600 đỉnh 1000 cạnh).&#xA;&#xA;Party1&#xA;Thay đổi mục tiêu&#xA;Thay vì đi tìm những đỉnh mà xuất hiện trong mọi cặp ghép cực đại, ta sẽ tìm những đỉnh không có tính chất đó. Thật vậy, ta sẽ cần tìm những đỉnh mà khi bỏ nó đi, kích cỡ cặp ghép cực đại vẫn không thay đổi.&#xA;&#xA;Tính chất của cặp ghép&#xA;Đầu tiên, ta dựng cặp ghép cực đại trên đồ thị đã cho. Hiển nhiên các đỉnh không thuộc cặp ghép là các đỉnh cần tìm. Ta sẽ chỉ xét đến các đỉnh thuộc cặp ghép.&#xA;&#xA;Giả sử \\(v\0\\) là một đỉnh không được ghép. Xét đường tăng \\(v\0, v\1,..., v\{2k}\\). Hiển nhiên độ dài đường tăng phải chẵn, nếu không ta có thể tăng số cặp ghép, không thỏa mãn tính chất cặp ghép cực đại.&#xA;&#xA;Ta nhận thấy, nếu xóa đi 1 trong các đỉnh \\(v\2, v\4, ...,v\{2k}\\), ta sẽ tách đường tăng hiện tại ra thành 1 đường tăng lẻ, đồng thời số cặp ghép giảm đi 1. Tuy nhiến, do tồn tại đường tăng lẻ nên ta có thể đảo lại, làm tăng số cặp ghép về như cũ. Vì vậy \\(v\2, v\4, ..., v\{2k}\\) đều là các đỉnh cần tìm.&#xA;&#xA;Nếu một đỉnh \\(x\\) khi xóa đi không tạo ra đường tăng lẻ nào (nhưng làm giảm số cặp ghép), chắc chắn nó không phải đỉnh cần tìm.&#xA;&#xA;Các cạnh xanh là cạnh cặp ghép. Xét đường tăng DHCGBFA (tím xanh), nếu bỏ B, đường tăng DHCG sẽ là đường lẻ, tăng cặp ghép về như cũ. Tương tự với C và A. Đường tăng JEI (xanh dương-xanh) cũng vậy.&#xA;&#xA;Thuật toán&#xA;Trước tiên, tìm cặp ghép. Sau đó, ta BFS từ các đỉnh không được cặp ghép, đi theo các đường tăng, đánh dấu các đỉnh cùng phía được thăm.&#xA;&#xA;Do tính chất của đồ thị 2 phía nên ta chỉ cần 2 lần BFS, mỗi lần xuất phát từ tất cả các đính không được thăm trên cùng 1 phía.&#xA;&#xA;Độ phức tạp sẽ là \\(O(K \sqrt{N + M})\\) do cặp ghép.&#xA;&#xA;Party2&#xA;Nén đồ thị&#xA;Với tập đỉnh đỏ, ta sẽ gộp tất cả các đỉnh liên thông lại thành một, cộng tất cả các trọng số lại. Bài toán trở thành tìm tập độc lập cực đại trên đồ thị cạnh xanh.&#xA;&#xA;Đây là một bài NP-Hard, chưa thể giải với giới hạn \\(N \le 250\\).&#xA;&#xA;Số lượng đỉnh&#xA;Một chi tiết quan trọng là số lượng cạnh đỏ rất lớn. Với \\(N = 250\\) và \\(K = \frac{N(N-1)}{3}\\), chỉ có tối đa \\(46\\) đỉnh đã gộp (bao gồm 205 đỉnh có clique và 45 đỉnh bậc 0).&#xA;&#xA;Từ đây, ta có chút hi vọng với thuật backtrack.&#xA;&#xA;Đặt cận!&#xA;Đây là danh sách cận cần thiết để hi vọng qua được đống test(?):&#xA;Sort các đỉnh theo bậc rồi backtrack dần&#xA;Xử lí nhanh các đỉnh bị cấm&#xA;Loại bỏ các trường hợp khi tổng các đỉnh còn lại không lớn hơn max hiện tại&#xA;Tính riêng các tplt rồi nhân với nhau&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="/nki/tag:training" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">training</span></a> <a href="/nki/tag:apio" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">apio</span></a> <a href="/nki/tag:vietnamese" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">vietnamese</span></a> <a href="/nki/tag:th%E1%BA%A7yHo%C3%A0ng" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">thầyHoàng</span></a> <a href="/nki/tag:anhKhu%C3%AA" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">anhKhuê</span></a></p>

<p>Hôm nay có hai phần: bài thầy Hoàng và bài anh Khuê. Cả 2 đều cần một sự tay to nhất định.</p>



<h1 id="tóm-tắt-đề-bài">Tóm tắt đề bài</h1>

<h2 id="select-thầy-hoàng">select (thầy Hoàng)</h2>

<p>Cho dãy $S$ gồm các kí tự <code>d</code> và <code>x</code>. 2 người lần lượt chơi, mỗi lượt bốc 1 kí tự, kí tự này phải đứng cạnh 1 vị trí đã bị chọn trước đó (trừ nước đầu tiên được chọn thoải mái). Khi trò chơi kết thúc, người đi trước thắng khi có nhiều <code>d</code> <strong>hơn hẳn</strong> người kia. Đếm số vị trí ban đầu mà người đi trước có thể chọn mà vẫn đảm bảo chiến thắng?</p>

<h4 id="giới-hạn">Giới hạn</h4>

<p>\(1 \le |S| \le 1000\).</p>

<h2 id="eureka-thầy-hoàng">eureka (thầy Hoàng)</h2>

<p>Cho một hệ thống cân đĩa được biểu diễn như sau:</p>
<ul><li>Nếu chỉ gồm <code>-1</code>: Đây là một vị trí đặt quả cân.</li>
<li>Là một cái cân có dạng <code>x y A B</code> trong đó \(x\) và \(y\) lần lượt là độ dài cánh tay đòn bên trái và bên phải của cân; \(A\) và \(B\) là 2 hệ thống cân được treo vào bên trái và bên phải của cân.</li></ul>

<p>Hình dưới biểu diễn hệ thống cân được biểu diễn bằng dãy <code>12 18 4 2 -1 -1 10 8 3 3 -1 -1 6 4 -1 -1</code>:</p>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108881431742124143/eureka_exp.png" alt=""></p>

<p>Trọng lượng các quả cân đều phải là số nguyên dương.</p>

<p>Hãy tìm trọng lượng cho các quả cân của từng vị trí đặt sao cho tất cả các cân đều thăng bằng và tổng trọng lượng các quả cân phải đặt là nhỏ nhất. Giả sử cân không có trọng lượng.</p>

<p>In ra tổng nhỏ nhất lấy dư cho \(123456789\).</p>

<h4 id="giới-hạn-1">Giới hạn</h4>

<p>Miêu tả được cho bởi dãy \(A[1..N]\).</p>

<p>\(1 \le N \le 3 \times 10^5\), \(A[i] = -1\) hoặc \(1 \le A[i] \le 100\).</p>

<h2 id="liondance-thầy-hoàng">liondance (thầy Hoàng)</h2>

<p>Cho 2 dãy số \(A[1..N]\) và \(B[1..N]\). Tìm dãy con chung dài nhất sao cho 2 phần tử liên tiếp chênh lệch nhau không quá \(d\).
Nếu có nhiều dãy, in ra dãy có <strong>thứ tự từ điển</strong> lớn nhất.</p>

<h4 id="giới-hạn-2">Giới hạn</h4>

<p>\(1 \le N \le 4000\), \(1 \le A[i], B[i] \le 10^9\)</p>

<h2 id="desert-thầy-hoàng">desert (thầy Hoàng)</h2>

<p>Trên mặt phẳng cho \(N\) điểm. Tìm đường đi từ \(1\) đến \(N\) sao cho khoảng cách Manhattan lớn nhất giữa 2 điểm liên tiếp đi qua là nhỏ nhất.</p>

<h4 id="giới-hạn-3">Giới hạn</h4>

<p>\(1 \le N \le 10^5\), tọa độ \(-10^9 \le X_i, Y_i\le 10^9\)</p>

<h2 id="trốn-việc-anh-khuê">Trốn việc (anh Khuê)</h2>

<p>Cho dãy số \(A[1..3N]\), chọn ra tập số có tổng lớn nhất sao cho trong \(N\) số liên tiếp bất kì có không quá \(K\) số được chọn.</p>

<h4 id="giới-hạn-4">Giới hạn</h4>

<p>\(1 \le N \le 200\), \(1 \le K \le 10\), \(1 \le A[i] \le 10^6\)</p>

<h2 id="party1-anh-khuê">Party1 (anh Khuê)</h2>

<p>Cho \(N\) bạn nam và \(M\) bạn nữ, và \(K\) mối quan hệ nam – nữ. Liệt kê tất cả bạn nam và bạn nữ chắc chắn sẽ xuất hiện trong cặp ghép cực đại.</p>

<h4 id="giới-hạn-5">Giới hạn</h4>

<p>\(1 \le N, M \le 10^4\), \(1 \le K \le 10^5\).</p>

<h2 id="party2-anh-khuê">Party2 (anh Khuê)</h2>

<p>Cho đồ thị \(N\) đỉnh \(M\) cạnh xanh \(K\) cạnh đỏ, đỉnh \(i\) có trọng số \(A[i]\). Chọn một tập điểm có tổng trọng số lớn nhất thỏa mãn:
 – 2 đỉnh có cạnh xanh nối giữa thì không được cùng chọn. – 2 đỉnh có cạnh đỏ nối giữa thì cùng được chọn, hoặc cùng không được chọn.</p>

<p>Đồng thời, đếm số cách chọn tối ưu.</p>

<h4 id="giới-hạn-6">Giới hạn</h4>

<p>\(1 \le N \le 250\), \(0 \le M \le \frac{N(N-1)}{6}\), \(\frac{N(N-1)}{3} \le K \le \frac{N(N-1)}{2}\)</p>

<h1 id="lời-giải">Lời giải</h1>

<h2 id="select">select</h2>

<h3 id="chuyển-đổi-bài-toán">Chuyển đổi bài toán</h3>

<p>Rất khó quản lí trạng thái thắng thua khi bài toán yêu cầu so sánh số lượng. Vì vậy, ta sẽ biến đổi bài toán một chút – mặc dù tính chất thắng – thua không thay đổi.</p>

<p>Thay vì so sánh số lượng <code>d</code> của từng người, ta lấy số lượng <code>d</code> của người đi trước trừ đi người đi sau. Hiển nhiên người đi trước muốn hiệu dương – tức <em>đối đa hóa</em> nó, và người đi sau muốn tối thiểu hóa nó.</p>

<p>Bởi vì đây là trò chơi hữu hạn bước, và 2 người đều phải chơi tối ưu, nên ta sẽ chỉ đi tìm một kết quả duy nhất: hiệu tối ưu của trò chơi, với mỗi cách chọn bước đầu.</p>

<h3 id="quy-hoạch-động">Quy hoạch động</h3>

<p>Gọi \(f[l][r]\) là hiệu tối ưu của trò chơi, nếu trò chơi bắt đầu với trạng thái đoạn \(l..r\) bị khuyết. Ta có:
– Hiển nhiên \(f[l][r] = 0\) nếu \(l..r\) đã phủ toàn bộ vòng tròn.
– Ta có thể hiểu độ dài \(l..r\) sẽ tương đương với số bước đã xảy ra, vì vậy ta có thể biết được lượt đi tiếp theo là của ai.
– Nếu đây là lượt của người đi trước, hẳn hắn ta sẽ muốn \(f[l][r]\) lớn nhất có thể, tức hắn sẽ chọn vị trí \(l – 1\) hay \(r + 1\) sao cho \(f[l – 1][r]\) hoặc \(f[l][r + 1]\), cộng vị trí hắn chọn nếu nó màu đỏ, sao cho tổng ấy lớn nhất có thể. Nói cách khác ta có:
\[ f[l][r] = \max(f[l – 1][r] +
(\text{S[l – 1] == &#39;d&#39;}), f[l][r + 1] +
(\text{S[r + 1] == &#39;d&#39;}))\]
– Ngược lại, người đi sau sẽ muốn lựa chọn của mình là nhỏ nhất có thể, tức:
\[ f[l][r] = \min(f[l – 1][r] -
(\text{S[l – 1] == &#39;d&#39;}), f[l][r + 1] -
(\text{S[r + 1] == &#39;d&#39;}))\]
Lưu ý dấu <code>-</code>, bởi ta đang tối ưu hiệu người đi trước trừ người đi sau.</p>

<p>Ta có thể tính tất cả các hàm \(f[1][1]\), \(f[2][2]\), ..., \(f[N][N]\) trong độ phức tạp \(O(N^2)\).</p>

<h3 id="đếm-vị-trí-tốt">Đếm vị trí tốt</h3>

<p>Một vị trí \(i\) thỏa mãn đầu bài nếu như hiệu tối ưu lớn hơn \(0\), bởi khi đó người thứ nhất luôn có thể làm cho mình có nhiều <code>d</code> hơn. Như vậy, ta chỉ cần thử hết các vị trí \(i\) và kiểm tra xem \(f[i][i] &gt; 0\) đúng không.</p>

<p>Tổng độ phức tạp của bài toán là \(O(N^2)\).</p>

<h2 id="eureka">eureka</h2>

<h3 id="tính-chất-của-tổng-trọng-lượng">Tính chất của tổng trọng lượng</h3>

<p>Xét một hệ thống cân, không khó để chứng minh tổng trọng lượng của nó phải là một bội của số \(X\) tương ứng với từng hệ thống.</p>

<p>Nhiệm vụ của ta là đi tìm \(X\) đó cho cả hệ thống lớn, bằng cách tính từ các hệ thống con.</p>

<h3 id="quy-nạp">Quy nạp</h3>

<p>Hiển nhiên quả cân có thể nhận trọng lượng bất kì, vậy quả cân là hệ thống cân có \(X\) là 1.</p>

<p>Ta xét hệ thống cân \(a, b, X_l, X_r\) trong đó \(X_l, X_r\) là các giá trị mình đã tính trước đó cho hệ thống cân bên trái và phải.
Ta có:
– Tồn tại \((p, q) = 1\) sao cho \(paX_l = qbX_r\). Hiển nhiên đây là \(p\) và \(q\) có tổng nhỏ nhất thỏa mãn.
– Khi đó, \(X = pX_l + qX_r\).</p>

<p>Ta có thể nhận thấy \(\frac{p}{q} = \frac{bX_r}{aX_l}\), vậy \(p = bX_r / d\) và \(q = aX_l / d\) với \(d = \gcd(aX_l, bX_r)\).
Như vậy \(X = \dfrac{X_l X_r(a + b)}{d}\).</p>

<h3 id="số-lớn">Số lớn</h3>

<p>Ta nhận thấy từ công thức trên rằng đáp số có thể rất lớn. Ta không cần phải in số lớn, tuy vậy các phép tính như \(\gcd\) không thể được tính khi số đã bị mod.</p>

<p>Tuy nhiên ta cũng không cần phải cài số lớn: Mỗi lần ta nhân thêm một số \(a + b \le 200\), nên thay vì lưu số lớn ta sẽ lưu tập ước nguyên tố và số mũ. Khi đó, các phép nhân (cộng số mũ), chia (trừ số mũ), lấy \(\gcd\) (lấy min số mũ) trở nên đơn giản, với độ phức tạp \(O(A[i])\).</p>

<p>Ta có thuật toán đáp số với độ phức tạp
\(O(N * A[i])\).</p>

<h3 id="cài-đặt">Cài đặt</h3>

<p>Một cách cài đặt ngắn gọn là sử dụng đệ quy, vừa đọc vừa làm.
Ta thực hiện lần lượt:
– Đọc \(a\) và kiểm tra xem có phải quả cân không. Nếu có trả về 1.
– Đọc \(b\).
– Đệ quy giải bên trái \(X<em>l\).
– Đệ quy giải bên phải \(X</em>r\).
– Tính và trả về \(X\).</p>

<h2 id="liondance">liondance</h2>

<h3 id="quy-hoạch-động-1">Quy hoạch động</h3>

<p>Về cơ bản, đây chỉ là bài toán LCS có thêm điều kiện, vì vậy hướng suy nghĩ của ta tất nhiên là cải thiện thuật toán quy hoạch động LCS, vì độ phức tạp yêu cầu cũng tương đương.</p>

<p>Vậy làm sao để quản lí trạng thái vẫn là \([i][j]\) khi ta còn cần thông tin về 2 phần tử liên tiếp? Ta sẽ cần “gói” nhiều thông tin hơn vào trạng thái \((i, j)\).</p>

<h3 id="thêm-thông-tin">Thêm thông tin!</h3>

<p>Ta sẽ định nghĩa hàm quy hoạch động như sau:
Gọi \(f[i][j]\) là dãy con chung dài nhất khi sử dụng đoạn \(A[i..N]\), đoạn \(B[i + 1..N]\) <strong>và \(B[j]\) là phần tử cuối cùng được thêm ở đầu đoạn</strong> (để đơn giản, ta không tính cặp \(B[j]\) – ?? vào đáp số, cũng như coi \( B[0]\) là phần tử có thể ghép với mọi thứ).</p>

<h4 id="tại-sao-lại-định-nghĩa-như-vậy">Tại sao lại định nghĩa như vậy?</h4>

<p>Nhờ có việc đánh dấu \(B[j]\) là số được thêm vào cuối cùng, ta có thể quản lí giá trị có thể thêm vào tiếp theo.
Khi có \(f[i][j]\), và \(A[i] = B[k]\) (\(k \ge j + 1\)), điều kiện duy nhất đẻ kiểm tra chỉ là \(|B[j] – B[k]| \le d\).</p>

<h4 id="tại-sao-lại-từ-cuối">Tại sao lại từ cuối?</h4>

<p>Để giúp cho việc truy vết thứ tự từ điển, sau này mình sẽ nói đến.</p>

<p>Ta có thể tính hàm quy hoạch động này như sau – \(f[i][j]\) có thể được tính từ các trạng thái:</p>
<ul><li>\(f[i + 1][j]\), hiển nhiên</li>
<li>\(f[i + 1][k] + 1\), với \(k \ge j + 1\) và \(A[i] = B[k]\).</li></ul>

<p>Độ phức tạp là \(O(N^3)\). Ta sẽ cần một chút quan sát để hạ độ phức tạp.</p>

<h3 id="nhảy-xuống-o-n-2">Nhảy xuống \(O(N^2)\)</h3>

<p>Khi xét \(f[i][j]\), ta có thể thấy nếu \(k \le k&#39;\) và \(A[i] = B[k] = B[k&#39;]\) thì \(f[i + 1][k] \ge f[i + 1][k&#39;]\). Chứng minh rất đơn giản: \(B[k + 1..N]\) dài hơn \(B[k&#39; + 1..N]\), mà điều kiện ghép không đổi.</p>

<p>Vì vậy, thực chất ta chỉ cần tìm \(k\) nhỏ nhất thỏa mãn \(k \ge j + 1\) và \(A[i] = B[k]\) để cập nhật vào \(f[i][j]\). Việc này có thể thực hiện bằng việc đánh dấu khi for ngược \(i\) và \(j\), nên độ phức tạp chỉ còn \(O(N^2)\).</p>

<h3 id="thứ-tự-từ-điển">Thứ tự từ điển</h3>

<p>Khi quy hoạch động \(f[i][j]\), ta sẽ lưu thêm \(nx[i][j]\) là chỉ số \(B[k]\) mà mình chọn làm kí tự tiếp theo. Do điều kiện của dãy đáp số là phụ thuộc vào <em>giá trị</em> chứ không phải chỉ số, ta sẽ phải thêm vào các điều kiện sau đây để đảm bảo lựa chọn chỉ số tiếp theo cho truy vết:
– Hiển nhiên nếu \(f[i + 1][j] \neq f[i + 1][k]\) thì ta chọn cái nào lớn hơn, vì được dãy dài hơn.
– Nếu bằng nhau, hiển nhiên trong \(B[nx[i + 1][j]]\) và \(B[k]\) cái nào lớn hơn ta chọn.
– Nếu vẫn bằng nhau, ta nhận thấy chắc chắn \(nx[i + 1][j] \ge k\), do vậy từ \((i + 1, k)\) ta có nhiều lựa chọn hơn (và có tất cả lựa chọn của) \((i + 1, nx[i + 1][j])\), nên ta sẽ chọn \((i + 1, k)\).</p>

<p>Khi đã có mảng \(nx[i][j]\), việc truy vết trở thành đơn giản.</p>

<h2 id="desert">desert</h2>

<h3 id="cây-khung-manhattan">Cây khung Manhattan</h3>

<p>Bản chất của việc dựng đường đi sao cho cạnh \(\max\) là \(\min\) chính là dựng cây khung nhỏ nhất của đồ thị.</p>

<p>Từ việc chăm chỉ đọc wiki, ta cũng biết có thể dựng lên cây khung giữa các đỉnh theo khoảng cách Manhattan với độ phức tạp \(O(N \log N)\). Thực chất đây chỉ là bài yêu cầu implement thuật toán đó.</p>

<h3 id="dựng-cây-khung-như-nào">Dựng cây khung như nào?</h3>

<p>Ta có thể <a href="https://www.topcoder.com/community/data-science/data-science-tutorials/line-sweep-algorithms/" rel="nofollow">chứng minh</a> rằng, để dựng cây khung Manhattan, với mỗi
điểm ta chỉ cần nối cạnh đến đỉnh gần nó nhất trong mỗi <a href="https://www.wikiwand.com/en/Octant_(plane_geometry)" rel="nofollow">góc phần tám</a>.
Vậy tìm chúng như thế nào?</p>

<h4 id="tìm-điểm-gần-nhất-trong-góc-phần-tám">Tìm điểm gần nhất trong góc phần tám</h4>

<p>Giả sử ta sẽ giải bài toán cho góc phần tám có \(X \le X_0\), \(Y \le Y_0\) và \(X – Y \le X_0 – Y_0\).</p>

<p>Ta sắp xếp các điểm theo thứ tự \(X_i – Y_i\) giảm dần, và thêm vào một IT có tọa độ được xếp theo \(X_i\) giá trị \(X_i + Y_i\) lấy \(\max\).</p>

<p>Với mỗi điểm, ta <code>get</code> trong IT giá trị \(X_j + Y_j\) lớn nhất với \(X_j \le X_i\), rồi cập nhật điểm đó vào IT. Điểm <code>get</code> ra sẽ là điểm gần nhất theo góc phần tám này.</p>

<p>Để hiểu tại sao sắp xếp như vậy lại đúng, xem hình dưới.</p>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108881675724783716/desert_tree_upd.png" alt="Phần được gọi khi xét điểm \\(A\\). Phần tô màu đã được thêm vào IT, phần màu xanh thể hiện phần được get trong IT"></p>

<p>Dễ dàng làm với góc phần tám đối diện, chỉ cần for tập điểm ngược lại.</p>

<h4 id="giải-nhanh-tất-cả-các-phần">Giải nhanh tất cả các phần</h4>

<p>Thay vì cài tất cả các trường hợp góc phần tám, ta có thể chỉ cần giải 2 góc đối diện như trên, rồi thực hiện <a href="https://quizlet.com/6704360/rotation-rules-for-mrs-nelsons-geometry-flash-cards/" rel="nofollow">phép quay 90 độ</a> cho tất cả các điểm theo gốc, và tiếp tục giải.</p>

<p>Hiển nhiên sau khi xoay 3 lần ta sẽ giải đủ 8 góc phần tám.</p>

<h2 id="trốn-việc">Trốn việc</h2>

<h3 id="liên-tưởng">Liên tưởng</h3>

<p>Trong một hệ thống \(N\) máy song song, ta phải đảm bảo trong khoảng thời gian \(M\) bất kì không có quá \(N\) thao tác được thực hiện trên bất cứ máy nào. Ta sẽ làm như nào?</p>

<p>Một cách đơn giản là ra lệnh cho một máy, sau khi thực hiện một thao tác, sẽ ngủ trong \(M\) giây. Như vậy, không có khoảng thời gian \(M\) nào có một máy chạy 2 lần, vì thế không có chuyện có nhiều hơn \(N\) thao tác được chạy.</p>

<p>Ta sẽ sử dụng ý tưởng này cho bài toán.</p>

<h3 id="biến-đổi-bài-toán">Biến đổi bài toán</h3>

<p>Đề bài yêu cầu ta chọn một tập lớn nhất sao cho cứ \(N\) phần tử liên tiếp bất kì có không quá \(K\) phần tử được chọn. Ta sẽ biến bài toán thành: Cho phép chạy \(K\) máy lựa chọn song song, mỗi phần tử chỉ được cho một máy lựa chọn, trong khoảng \(N\) bất kì không có máy nào chọn liên tiếp 2 phần tử.</p>

<p>Nếu \(K = 1\), ta có thể sử dụng quy hoạch động \(f[i] = \max(f[i – 1], f[i – N] + A[i])\). Với \(K\) lớn hơn, ta sẽ phải tìm cách khác để đảm bảo mỗi phần tử chỉ được tối đa một máy lựa chọn.</p>

<p>Hãy tưởng tượng một đường ống \(3N + 1\) đoạn liên tiếp nối thành một đường thẳng. Ngoài ra đường ống thứ \(i\) còn được nối với \(\min( i + N, 3N + 1)\). Từ đỉnh 1 ta cho \(K\) robot chạy về hướng \(3N + 1\). Các robot có thể đi đường \(i\) – \(i + 1\) thoải mái, nhưng mỗi đường đi \(i\) – \(i + N\) chỉ có thể cho 1 robot đi qua, đồng thời sẽ thu về \(A[i]\) đồng tiền. Ta cần thu về nhiều tiền nhất có thể.</p>

<p>Nghe rất giống một bài luồng max cost.</p>

<h3 id="dựng-luồng">Dựng luồng</h3>

<p>Ta áp dụng toàn bộ ý tưởng của đường ống vào luồng.</p>
<ul><li>\(3N + 2\) đỉnh, \(0\) là nguồn \(3N + 1\) là đích.</li>
<li>\(0\) =&gt; \(1\): cap = \(K\), cost = \(0\)</li>
<li>Với \(i &gt; 0\), \(i\) =&gt; \(i + 1\): cap = \(inf\), cost = \(0\)</li>
<li>Với \(i &gt; 0\), \(i\) =&gt; \(\min(i + N, 3N + 1)\): cap = \(1\), cost = \(A[i]\)</li></ul>

<p>Trên mạng, ta tìm luồng max cost. Hiển nhiên luồng cực đại là \(K\), nhưng ta chỉ cần quan tâm đến cost tối đa. Cost chính là đáp số.</p>

<p>Độ phức tạp là <code>O(luồng 600 đỉnh 1000 cạnh)</code>.</p>

<h2 id="party1">Party1</h2>

<h3 id="thay-đổi-mục-tiêu">Thay đổi mục tiêu</h3>

<p>Thay vì đi tìm những đỉnh mà xuất hiện trong mọi cặp ghép cực đại, ta sẽ tìm những đỉnh không có tính chất đó. Thật vậy, ta sẽ cần tìm những đỉnh mà khi bỏ nó đi, kích cỡ cặp ghép cực đại vẫn không thay đổi.</p>

<h3 id="tính-chất-của-cặp-ghép">Tính chất của cặp ghép</h3>

<p>Đầu tiên, ta dựng cặp ghép cực đại trên đồ thị đã cho. Hiển nhiên các đỉnh không thuộc cặp ghép là các đỉnh cần tìm. Ta sẽ chỉ xét đến các đỉnh thuộc cặp ghép.</p>

<p>Giả sử \(v_0\) là một đỉnh không được ghép. Xét đường tăng \(v_0, v_1,..., v_{2k}\). Hiển nhiên độ dài đường tăng phải chẵn, nếu không ta có thể tăng số cặp ghép, không thỏa mãn tính chất cặp ghép cực đại.</p>

<p>Ta nhận thấy, nếu xóa đi 1 trong các đỉnh \(v_2, v_4, ...,v_{2k}\), ta sẽ tách đường tăng hiện tại ra thành 1 đường tăng lẻ, đồng thời số cặp ghép giảm đi 1. Tuy nhiến, do tồn tại đường tăng lẻ nên ta có thể đảo lại, làm tăng số cặp ghép về như cũ. Vì vậy \(v_2, v_4, ..., v_{2k}\) đều là các đỉnh cần tìm.</p>

<p>Nếu một đỉnh \(x\) khi xóa đi không tạo ra đường tăng lẻ nào (nhưng làm giảm số cặp ghép), chắc chắn nó không phải đỉnh cần tìm.</p>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108881748743434240/party1_exp.png" alt="Các cạnh xanh là cạnh cặp ghép. Xét đường tăng DHCGBFA (tím xanh), nếu bỏ B, đường tăng DHCG sẽ là đường lẻ, tăng cặp ghép về như cũ. Tương tự với C và A. Đường tăng JEI (xanh dương-xanh) cũng vậy."></p>

<h3 id="thuật-toán">Thuật toán</h3>

<p>Trước tiên, tìm cặp ghép. Sau đó, ta BFS từ các đỉnh không được cặp ghép, đi theo các đường tăng, đánh dấu các đỉnh cùng phía được thăm.</p>

<p>Do tính chất của đồ thị 2 phía nên ta chỉ cần 2 lần BFS, mỗi lần xuất phát từ tất cả các đính không được thăm trên cùng 1 phía.</p>

<p>Độ phức tạp sẽ là \(O(K \sqrt{N + M})\) do cặp ghép.</p>

<h2 id="party2">Party2</h2>

<h3 id="nén-đồ-thị">Nén đồ thị</h3>

<p>Với tập đỉnh đỏ, ta sẽ gộp tất cả các đỉnh liên thông lại thành một, cộng tất cả các trọng số lại. Bài toán trở thành tìm tập độc lập cực đại trên đồ thị cạnh xanh.</p>

<p>Đây là một bài <strong>NP-Hard</strong>, chưa thể giải với giới hạn \(N \le 250\).</p>

<h3 id="số-lượng-đỉnh">Số lượng đỉnh</h3>

<p>Một chi tiết quan trọng là số lượng cạnh đỏ rất lớn. Với \(N = 250\) và \(K = \frac{N(N-1)}{3}\), chỉ có tối đa \(46\) đỉnh đã gộp (bao gồm 205 đỉnh có clique và 45 đỉnh bậc 0).</p>

<p>Từ đây, ta có chút hi vọng với thuật backtrack.</p>

<h3 id="đặt-cận">Đặt cận!</h3>

<p>Đây là danh sách cận cần thiết để hi vọng qua được đống test(?):
– Sort các đỉnh theo bậc rồi backtrack dần
– Xử lí nhanh các đỉnh bị cấm
– Loại bỏ các trường hợp khi tổng các đỉnh còn lại không lớn hơn max hiện tại
– Tính riêng các tplt rồi nhân với nhau</p>
]]></content:encoded>
      <guid>https://blog.dtth.ch/nki/yyd5ub7sbm</guid>
      <pubDate>Fri, 28 Apr 2017 18:00:00 +0000</pubDate>
    </item>
    <item>
      <title>2017/04/21 Training</title>
      <link>https://blog.dtth.ch/nki/2017-04-21-training</link>
      <description>&lt;![CDATA[#training #apio #vietnamese #thầyNghĩa&#xA;&#xA;Tóm tắt đề bài&#xA;SEQUENCE&#xA;Cho dãy số A[1..N]. Mỗi lần xóa ta sẽ xóa tất cả các số mang một giá trị x nào đó. Hỏi dãy dài nhất có thể tạo ra được mà không tồn tại i &lt; j &lt; k thỏa mãn A[i] == A[k] &amp;&amp; A[i] != A[j]?&#xA;Giới hạn&#xA;1 &lt;= N &lt;= 10^5, 1 &lt;= A[i] &lt;= 100&#xA;AVTOGAME&#xA;Cho xâu S. Mỗi bước ta có thể chọn một đoạn l &lt; r sao cho S[l] == S[r] và xóa đoạn đó khỏi xâu. Hỏi xâu ngắn nhất và dài nhất có thể tạo được (mà không thể xóa được tiếp) là bao nhiêu?&#xA;Giới hạn&#xA;10 test, 1 &lt;= |S| &lt;= 100, &#39;a&#39; &lt;= S[i] &lt;= &#39;p&#39;&#xA;DISKGAME&#xA;Cho một đĩa gồm N tầng xoay, mỗi tầng có K nấc xoay như hình dưới.&#xA;&#xA;Một đĩa có 3 tầng, mỗi tầng có 8 nấc&#xA;&#xA;Mỗi bước ta được xoay 1 tầng sang trái hoặc phải 1 nấc. Hỏi số bước nhỏ nhất để tạo ra 1 cột có các số bằng nhau là bao nhiêu?&#xA;&#xA;Một cách giải hình trên&#xA;Giới hạn&#xA;1 &lt;= N, K &lt;= 2000&#xA;&#xA;!--more--&#xA;&#xA;Lời giải&#xA;SEQUENCE&#xA;Điều kiện của dãy số&#xA;Ta có thể thấy, 2 số x và y không được cùng tồn tại trong đáp án nếu số x bị &#34;kẹp giữa&#34; số y hoặc ngược lại. Ta cũng có thể dễ dàng chứng minh một dãy không tồn tại cặp x, y nào như vậy là một dãy thỏa mãn.&#xA;&#xA;Ví dụ&#xA;1, 2, 1, 3, 1, 4 không thỏa mãn vì số 2 bị kẹp giữa 2 lần số 1.&#xA;&#xA;Bài toán của ta trở thành đi tìm một dãy không có 2 số nào &#34;kẹp&#34; nhau.&#xA;&#xA;Đầu và đuôi&#xA;Xét ví dụ ở trên, ta có thể thấy 2 bị kẹp giữa bởi 2 số 1 ở vị trí 1 và 3. Ta cũng có thể nói 2 bị kẹp giữa bởi 2 số 1 ở 1 và 5.&#xA;&#xA;Giả sử x kẹp giữa y ở 2 vị trí a &lt;= b, ta cũng có thể nói x kẹp ở 2 vị trí first[x] &lt;= a và b &lt;= last[x] (2 lần xuất hiện đầu và cuối của x). Như vậy điều kiện để x kẹp giữa y chỉ là tồn tại y nằm giữa 2 vị trí xa nhau nhất chứa x.&#xA;&#xA;Đi xa hơn, ta có thể thấy xét trên trục 1 chiều, tồn tại cặp x, y kẹp nhau khi và chỉ khi 2 đoạn (first[x], last[x]) và (first[y], last[y]) giao nhau.&#xA;&#xA;Quy hoạch động&#xA;Như vậy, ta chỉ cần tìm 1 tập số sao cho tập (first[x], last[x]) của các số không giao nhau. Đây là bài toán quy hoạch động cơ bản, có thể thực hiện quy hoạch động trong O(N + M) với M là số phần tử khác nhau.&#xA;&#xA;Gọi f[i] là số đoạn thẳng nhiều nhất ta có thể chọn trong khoảng 1..i. Từ đây, ta có 2 lựa chọn:&#xA;Thêm khoảng không, cập nhật f[i] cho f[i + 1].&#xA;Thêm một đoạn (i + 1..j). Ta duyệt tất cả các đoạn thẳng có đầu mút trái là i + 1 và cập nhật f[i] + 1 cho f[j].&#xA;&#xA;Đáp số là f[N].&#xA;&#xA;AVTOGAME&#xA;Có thể xóa 1 đoạn?&#xA;Hiển nhiên các đoạn ta xóa sẽ không giao nhau, nên chỉ có 2 khả năng xảy ra để xóa đoạn [a &lt; b]:&#xA;Nếu S[a] == S[b] ta xóa cả đoạn trong 1 bước.&#xA;Chọn 1 vị trí a &lt; k &lt; b - 1, xóa đoạn a..k rồi xóa đoạn k+1..b.&#xA;&#xA;Dựa vào nhận xét này, ta dễ dàng dựng nên mảng canErasel (có thể xóa đoạn l..r không?) trong O(N^3):&#xA;&#xA;for (int l = 1; l &lt;= N; ++l) {&#xA;  for (int r = l + 1; r &lt;= N; ++r) {&#xA;    if (Sl] == S[r]) canErase[l = 1;&#xA;    for (int k = l + 1; k + 1 &lt; r; ++k) {&#xA;      canErasel = canErasel || (canErasel &amp;&amp; canErasek + 1);&#xA;    }&#xA;  }&#xA;}&#xA;&#xA;Chi phí xóa hết nhỏ nhất&#xA;Thay vì giải bài toán xâu ngắn nhất còn lại, ta sẽ thay đổi bài toán bằng cách cho phép một kiểu xóa nữa: xóa 1 kí tự với chi phí 1. Sau đó ta đi tìm chi phí nhỏ nhất để xóa cả dãy. Ta có thể thấy tính chất các bước xóa rời nhau không thay đổi.&#xA;&#xA;Hiển nhiên chi phí sẽ bằng đáp án, vì ta không bao giờ xóa đơn lẻ 2 kí tự giống nhau.&#xA;&#xA;Để giải được bài toán này, ta cải tiến thuật toán kiểm tra tính xóa được phía trên, thành chi phí nhỏ nhất để xóa đoạn l..r. Hiển nhiên costi = 1 vì chỉ có 1 kí tự. Với đoạn l..r ta có 2 cách xóa:&#xA;Nếu S[l] == S[r] ta xóa cả đoạn với chi phí 0.&#xA;Chọn l &lt;= k &lt; r rồi xóa 2 đoạn l..k và k + 1..r với tổng chi phí costl + costk + 1.&#xA;&#xA;Đáp số là cost1, độ phức tạp là O(N^3).&#xA;&#xA;Các kí tự còn lại&#xA;Để giải được bài toán dãy còn lại dài nhất, ta cần phải thấy tính chất của dãy còn lại. Tính chất khá đơn giản: không tồn tại 2 kí tự giống nhau trong xâu. Như vậy, ta cần nhặt ra 1 tập kí tự khác nhau sao cho các phần ở giữa có thể xóa được.&#xA;&#xA;Điều kiện chỉ có 16 kí tự khác nhau cho ta một gợi ý: sử dụng bitmask để quản lí các kí tự đã lấy.&#xA;&#xA;Quy hoạch động&#xA;Gọi bool fi là tính khả thi của việc chọn ra tập kí tự thỏa mãn mask trong đoạn 1..i và xóa hết các kí tự còn lại, trong đó kí tự cuối ta chọn chính là S[i]. Ta có 2 lựa chọn:&#xA;Chọn cả kí tự Si - 1], với điều kiện S[i - 1] != S[i] và mask có S[i - 1]. Ta lùi về trạng thái f[i - 1].&#xA;Chọn một vị trí j &lt; i và lấy kí tự này là kí tự đứng ngay trước Si]. Điều kiện là S[j] != S[i], mask có S[j] và j + 1..i - 1 xóa được. Ta lùi về trạng thái f[j].&#xA;&#xA;Độ phức tạp sẽ là O(N^2  2^16), chưa thể thỏa mãn bài toán. Ta cần một chút cải tiến để xóa bớt N.&#xA;&#xA;Nhảy, chọn và xóa&#xA;Ta sẽ chỉnh sửa hàm quy hoạch động một chút: xóa bỏ điều kiện Si] là kí tự cuối cùng chọn. Thay vào đó, ta &#34;nhảy&#34; từng bước, chọn hoặc sử dụng duy nhất một phép xóa. Cụ thể, từ trạng thái f[i, ta có:&#xA;Si] là kí tự được chọn. Điều kiện là mask chứa S[i]. Lùi về f[i - 1].&#xA;Si] là kí tự cuối cùng bị xóa. Vậy ta cần một vị trí j &lt; i sao cho S[i] == S[j], và lùi về f[j - 1.&#xA;&#xA;Thoáng qua, vẫn là O(N^2  2^16). Làm sao để cải tiến? Ta thấy, trong trường hợp 2, điều kiện duy nhất là Sj] == S[i], mà chỉ có 16 loại kí tự, vậy ta hoàn toàn có thể lưu lại tất cả các trường hợp f[j - 1 với mỗi lọai S[j] khác nhau.&#xA;&#xA;Gọi gi là tổng kết tất cả các trường hợp fj thỏa mãn Sj + 1] == i. Ta có thể vừa đi vừa cập nhật g[S[i + 1], đồng thời trong trường hợp 2 ta chỉ cần lấy giá trị của gS[i] trong O(1).&#xA;&#xA;Độ phức tạp giảm xuống còn O(N  2^16), thỏa mãn bài toán.&#xA;&#xA;DISKGAME&#xA;Chi phí xoay của 1 đĩa&#xA;Hãy phân tích chi phí xoay của 1 đĩa để có số n ở vị trí p. Hiển nhiên chi phí là min(|x - p|) với x là các vị trí xuất hiện của n trong đĩa.&#xA;&#xA;Thực chất ta chỉ cần xét đến 2 vị trí gần nhất bên trái và bên phải của p. Ta tạm gọi là x và y (để đơn giản ta coi x &lt;= p &lt;= y). Chi phí sẽ là min(p - x, y - p). Dễ dàng nhận thấy p - x là hàm tăng 1 đơn vị, y - p là hàm giảm 1 đơn vị với x &lt;= p &lt;= y. min của 2 hàm này sẽ là &#34;núi&#34; góc 45 độ có chóp ở trung điểm của x và y (hoặc có chóp ngang nếu trung điểm không nguyên).&#xA;&#xA;Nếu ta xét tất cả các cặp vị trí liên tiếp của số, thì chi phí sẽ là nhiều &#34;ngọn núi&#34; như vậy.&#xA;&#xA;Ta có thể thấy chi phí là một hàm như hình dưới, cho dãy 1 2 3 1 2 3 5 1 5 với n = 1. Lưu ý đoạn 8, 9, 1 cũng là 1 &#34;ngọn núi&#34;, vì thực chất đĩa là hình tròn.&#xA;&#xA;Hàm chi phí&#xA;&#xA;Ta có thể cắt hàm thành các đường chéo tăng và giảm 45 độ để đơn giản hóa việc tính toán chi phí cho tất cả các đĩa.&#xA;&#xA;Tổng cộng 1 đĩa sẽ bị cắt thành 2K đường chéo.&#xA;&#xA;Tính tổng chi phí cho mọi đĩa&#xA;Với mỗi vị trí p và một số n, ta cần tính tổng chi phí xoay với mọi đĩa trong O(1). Biết chúng là tổng các đường chéo, làm sao để tính nhanh?&#xA;&#xA;Ta sẽ vận dụng tính chất chúng đều có dạng x + b hoặc -x + b và sử dụng đường quét để tính với mỗi n.&#xA;&#xA;Ta thấy, khi có k đoạn x + b[i], chi phí là kx + sum(b[i]) với bước tăng là k. Vì vậy thực chất với mỗi vị trí ta chỉ cần biết số đoạn tăng và tổng phần hằng số của chúng. Ta hoàn toàn có thể làm điều này khi quét bằng cách xét 2 đầu mút đầu (thêm đoạn) và cuối (xóa đoạn) sau đó xử lí từ trái sang phải.&#xA;&#xA;Điều tương tự cũng đúng với hàm giảm.&#xA;&#xA;int b[N  K + 1]; // tất cả b[i] của các đường tăng&#xA;vectorint add[K + 2], remove[K + 2]; // các mốc thêm xóa&#xA;&#xA;void addSegment(int l, int r, int id) {&#xA;  // thêm đoạn [l..r] = x + b[id]&#xA;  add[l].pushback(id);&#xA;  remove[r + 1].push_back(id);&#xA;}&#xA;&#xA;void scan() {&#xA;  int value = 0, cnt = 0;&#xA;  for (int i = 1; i &lt;= K; ++i) {&#xA;    for (auto p: add[i]) {&#xA;      value += b[p] + i - 1; // giá trị của i trước đó&#xA;      ++cnt;&#xA;    }&#xA;    for (auto p: remove[i]) {&#xA;      value -= b[p] + i - 1;&#xA;      --cnt;&#xA;    }&#xA;    value += cnt;&#xA;    // value là tổng ở vị trí i&#xA;  }&#xA;}&#xA;&#xA;Ta có thể thấy độ phức tạp với mỗi n là O(K + số đoạn của n). Vì thế tổng độ phức tạp là O(K^2 + NK), do có 2NK đoạn tất cả.&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="/nki/tag:training" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">training</span></a> <a href="/nki/tag:apio" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">apio</span></a> <a href="/nki/tag:vietnamese" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">vietnamese</span></a> <a href="/nki/tag:th%E1%BA%A7yNgh%C4%A9a" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">thầyNghĩa</span></a></p>

<h1 id="tóm-tắt-đề-bài">Tóm tắt đề bài</h1>

<h2 id="sequence">SEQUENCE</h2>

<p>Cho dãy số <code>A[1..N]</code>. Mỗi lần xóa ta sẽ xóa tất cả các số mang một giá trị <code>x</code> nào đó. Hỏi dãy dài nhất có thể tạo ra được mà không tồn tại <code>i &lt; j &lt; k</code> thỏa mãn <code>A[i] == A[k] &amp;&amp; A[i] != A[j]</code>?</p>

<h4 id="giới-hạn">Giới hạn</h4>

<p><code>1 &lt;= N &lt;= 10^5</code>, <code>1 &lt;= A[i] &lt;= 100</code></p>

<h2 id="avtogame">AVTOGAME</h2>

<p>Cho xâu <code>S</code>. Mỗi bước ta có thể chọn một đoạn <code>l &lt; r</code> sao cho <code>S[l] == S[r]</code> và xóa đoạn đó khỏi xâu. Hỏi xâu ngắn nhất và dài nhất có thể tạo được (mà không thể xóa được tiếp) là bao nhiêu?</p>

<h4 id="giới-hạn-1">Giới hạn</h4>

<p>10 test, <code>1 &lt;= |S| &lt;= 100</code>, <code>&#39;a&#39; &lt;= S[i] &lt;= &#39;p&#39;</code></p>

<h2 id="diskgame">DISKGAME</h2>

<p>Cho một đĩa gồm <code>N</code> tầng xoay, mỗi tầng có <code>K</code> nấc xoay như hình dưới.</p>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108884401510682674/diskgame_exp.png" alt="Một đĩa có 3 tầng, mỗi tầng có 8 nấc"></p>

<p>Mỗi bước ta được xoay 1 tầng sang trái hoặc phải 1 nấc. Hỏi số bước nhỏ nhất để tạo ra 1 cột có các số bằng nhau là bao nhiêu?</p>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108884401774940251/diskgame_sol.png" alt="Một cách giải hình trên"></p>

<h4 id="giới-hạn-2">Giới hạn</h4>

<p><code>1 &lt;= N, K &lt;= 2000</code></p>



<h1 id="lời-giải">Lời giải</h1>

<h2 id="sequence-1">SEQUENCE</h2>

<h3 id="điều-kiện-của-dãy-số">Điều kiện của dãy số</h3>

<p>Ta có thể thấy, 2 số <code>x</code> và <code>y</code> không được cùng tồn tại trong đáp án nếu số <code>x</code> bị “kẹp giữa” số <code>y</code> hoặc ngược lại. Ta cũng có thể dễ dàng chứng minh một dãy không tồn tại cặp <code>x, y</code> nào như vậy là một dãy thỏa mãn.</p>

<h4 id="ví-dụ">Ví dụ</h4>

<p><code>1, 2, 1, 3, 1, 4</code> không thỏa mãn vì số <code>2</code> bị kẹp giữa 2 lần số <code>1</code>.</p>

<p>Bài toán của ta trở thành đi tìm một dãy không có 2 số nào “kẹp” nhau.</p>

<h3 id="đầu-và-đuôi">Đầu và đuôi</h3>

<p>Xét ví dụ ở trên, ta có thể thấy <code>2</code> bị kẹp giữa bởi 2 số <code>1</code> ở vị trí 1 và 3. Ta cũng có thể nói <code>2</code> bị kẹp giữa bởi 2 số <code>1</code> ở 1 và 5.</p>

<p>Giả sử <code>x</code> kẹp giữa <code>y</code> ở 2 vị trí <code>a &lt;= b</code>, ta cũng có thể nói <code>x</code> kẹp ở 2 vị trí <code>first[x] &lt;= a</code> và <code>b &lt;= last[x]</code> (2 lần xuất hiện đầu và cuối của <code>x</code>). Như vậy điều kiện để <code>x</code> kẹp giữa <code>y</code> chỉ là tồn tại <code>y</code> nằm giữa 2 vị trí xa nhau nhất chứa <code>x</code>.</p>

<p>Đi xa hơn, ta có thể thấy xét trên trục 1 chiều, tồn tại cặp <code>x</code>, <code>y</code> kẹp nhau khi và chỉ khi 2 đoạn <code>(first[x], last[x])</code> và <code>(first[y], last[y])</code> giao nhau.</p>

<h3 id="quy-hoạch-động">Quy hoạch động</h3>

<p>Như vậy, ta chỉ cần tìm 1 tập số sao cho tập <code>(first[x], last[x])</code> của các số không giao nhau. Đây là bài toán quy hoạch động cơ bản, có thể thực hiện quy hoạch động trong <code>O(N + M)</code> với <code>M</code> là số phần tử khác nhau.</p>

<p>Gọi <code>f[i]</code> là số đoạn thẳng nhiều nhất ta có thể chọn trong khoảng <code>1..i</code>. Từ đây, ta có 2 lựa chọn:
– Thêm khoảng không, cập nhật <code>f[i]</code> cho <code>f[i + 1]</code>.
– Thêm một đoạn <code>(i + 1..j)</code>. Ta duyệt tất cả các đoạn thẳng có đầu mút trái là <code>i + 1</code> và cập nhật <code>f[i] + 1</code> cho <code>f[j]</code>.</p>

<p>Đáp số là <code>f[N]</code>.</p>

<h2 id="avtogame-1">AVTOGAME</h2>

<h3 id="có-thể-xóa-1-đoạn">Có thể xóa 1 đoạn?</h3>

<p>Hiển nhiên các đoạn ta xóa sẽ không giao nhau, nên chỉ có 2 khả năng xảy ra để xóa đoạn <code>[a &lt; b]</code>:
– Nếu <code>S[a] == S[b]</code> ta xóa cả đoạn trong 1 bước.
– Chọn 1 vị trí <code>a &lt; k &lt; b - 1</code>, xóa đoạn <code>a..k</code> rồi xóa đoạn <code>k+1..b</code>.</p>

<p>Dựa vào nhận xét này, ta dễ dàng dựng nên mảng <code>canErase[l][r]</code> (có thể xóa đoạn <code>l..r</code> không?) trong <code>O(N^3)</code>:</p>

<pre><code class="language-cpp">for (int l = 1; l &lt;= N; ++l) {
  for (int r = l + 1; r &lt;= N; ++r) {
    if (S[l] == S[r]) canErase[l][r] = 1;
    for (int k = l + 1; k + 1 &lt; r; ++k) {
      canErase[l][r] = canErase[l][r] || (canErase[l][k] &amp;&amp; canErase[k + 1][r]);
    }
  }
}
</code></pre>

<h3 id="chi-phí-xóa-hết-nhỏ-nhất">Chi phí xóa hết nhỏ nhất</h3>

<p>Thay vì giải bài toán xâu ngắn nhất còn lại, ta sẽ thay đổi bài toán bằng cách cho phép một kiểu xóa nữa: xóa <strong>1 kí tự</strong> với chi phí 1. Sau đó ta đi tìm chi phí nhỏ nhất để xóa cả dãy. Ta có thể thấy tính chất các bước xóa rời nhau không thay đổi.</p>

<p>Hiển nhiên chi phí sẽ bằng đáp án, vì ta không bao giờ xóa đơn lẻ 2 kí tự giống nhau.</p>

<p>Để giải được bài toán này, ta cải tiến thuật toán kiểm tra tính xóa được phía trên, thành chi phí nhỏ nhất để xóa đoạn <code>l..r</code>. Hiển nhiên <code>cost[i][i] = 1</code> vì chỉ có 1 kí tự. Với đoạn <code>l..r</code> ta có 2 cách xóa:
– Nếu <code>S[l] == S[r]</code> ta xóa cả đoạn với chi phí 0.
– Chọn <code>l &lt;= k &lt; r</code> rồi xóa 2 đoạn <code>l..k</code> và <code>k + 1..r</code> với tổng chi phí <code>cost[l][k] + cost[k + 1][r]</code>.</p>

<p>Đáp số là <code>cost[1][N]</code>, độ phức tạp là <code>O(N^3)</code>.</p>

<h3 id="các-kí-tự-còn-lại">Các kí tự còn lại</h3>

<p>Để giải được bài toán dãy còn lại dài nhất, ta cần phải thấy tính chất của dãy còn lại. Tính chất khá đơn giản: không tồn tại 2 kí tự giống nhau trong xâu. Như vậy, ta cần nhặt ra 1 tập kí tự khác nhau sao cho các phần ở giữa có thể xóa được.</p>

<p>Điều kiện chỉ có 16 kí tự khác nhau cho ta một gợi ý: sử dụng bitmask để quản lí các kí tự đã lấy.</p>

<h3 id="quy-hoạch-động-1">Quy hoạch động</h3>

<p>Gọi <code>bool f[i][mask]</code> là tính khả thi của việc chọn ra tập kí tự thỏa mãn <code>mask</code> trong đoạn <code>1..i</code> và xóa hết các kí tự còn lại, trong đó kí tự cuối ta chọn chính là <code>S[i]</code>. Ta có 2 lựa chọn:
– Chọn cả kí tự <code>S[i - 1]</code>, với điều kiện <code>S[i - 1] != S[i]</code> và <code>mask</code> có <code>S[i - 1]</code>. Ta lùi về trạng thái <code>f[i - 1][mask ^ S[i]]</code>.
– Chọn một vị trí <code>j &lt; i</code> và lấy kí tự này là kí tự đứng ngay trước <code>S[i]</code>. Điều kiện là <code>S[j] != S[i]</code>, <code>mask</code> có <code>S[j]</code> và <code>j + 1..i - 1</code> xóa được. Ta lùi về trạng thái <code>f[j][mask ^ S[i]]</code>.</p>

<p>Độ phức tạp sẽ là <code>O(N^2 * 2^16)</code>, chưa thể thỏa mãn bài toán. Ta cần một chút cải tiến để xóa bớt <code>N</code>.</p>

<h3 id="nhảy-chọn-và-xóa">Nhảy, chọn và xóa</h3>

<p>Ta sẽ chỉnh sửa hàm quy hoạch động một chút: xóa bỏ điều kiện <code>S[i]</code> là kí tự cuối cùng chọn. Thay vào đó, ta “nhảy” từng bước, chọn hoặc sử dụng duy nhất một phép xóa. Cụ thể, từ trạng thái <code>f[i][mask]</code>, ta có:
– <code>S[i]</code> là kí tự được chọn. Điều kiện là <code>mask</code> chứa <code>S[i]</code>. Lùi về <code>f[i - 1][mask ^ S[i]]</code>.
– <code>S[i]</code> là kí tự cuối cùng bị xóa. Vậy ta cần một vị trí <code>j &lt; i</code> sao cho <code>S[i] == S[j]</code>, và lùi về <code>f[j - 1][mask]</code>.</p>

<p>Thoáng qua, vẫn là <code>O(N^2 * 2^16)</code>. Làm sao để cải tiến? Ta thấy, trong trường hợp 2, điều kiện duy nhất là <code>S[j] == S[i]</code>, mà chỉ có 16 loại kí tự, vậy ta hoàn toàn có thể lưu lại tất cả các trường hợp <code>f[j - 1][mask]</code> với mỗi lọai <code>S[j]</code> khác nhau.</p>

<p>Gọi <code>g[i][mask]</code> là tổng kết tất cả các trường hợp <code>f[j][mask]</code> thỏa mãn <code>S[j + 1] == i</code>. Ta có thể vừa đi vừa cập nhật <code>g[S[i + 1]][mask]</code>, đồng thời trong trường hợp 2 ta chỉ cần lấy giá trị của <code>g[S[i]][mask]</code> trong <code>O(1)</code>.</p>

<p>Độ phức tạp giảm xuống còn <code>O(N * 2^16)</code>, thỏa mãn bài toán.</p>

<h2 id="diskgame-1">DISKGAME</h2>

<h3 id="chi-phí-xoay-của-1-đĩa">Chi phí xoay của 1 đĩa</h3>

<p>Hãy phân tích chi phí xoay của 1 đĩa để có số <code>n</code> ở vị trí <code>p</code>. Hiển nhiên chi phí là <code>min(|x - p|)</code> với <code>x</code> là các vị trí xuất hiện của <code>n</code> trong đĩa.</p>

<p>Thực chất ta chỉ cần xét đến 2 vị trí gần nhất bên trái và bên phải của <code>p</code>. Ta tạm gọi là <code>x</code> và <code>y</code> (để đơn giản ta coi <code>x &lt;= p &lt;= y</code>). Chi phí sẽ là <code>min(p - x, y - p)</code>. Dễ dàng nhận thấy <code>p - x</code> là hàm tăng 1 đơn vị, <code>y - p</code> là hàm giảm 1 đơn vị với <code>x &lt;= p &lt;= y</code>. min của 2 hàm này sẽ là “núi” góc 45 độ có chóp ở trung điểm của <code>x</code> và <code>y</code> (hoặc có chóp ngang nếu trung điểm không nguyên).</p>

<p>Nếu ta xét tất cả các cặp vị trí liên tiếp của số, thì chi phí sẽ là nhiều “ngọn núi” như vậy.</p>

<p>Ta có thể thấy chi phí là một hàm như hình dưới, cho dãy <code>1 2 3 1 2 3 5 1 5</code> với <code>n = 1</code>. Lưu ý đoạn <code>8, 9, 1</code> cũng là 1 “ngọn núi”, vì thực chất đĩa là hình tròn.</p>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108884473422041138/diskgame_func.png" alt="Hàm chi phí"></p>

<p>Ta có thể cắt hàm thành các đường chéo tăng và giảm 45 độ để đơn giản hóa việc tính toán chi phí cho tất cả các đĩa.</p>

<p>Tổng cộng 1 đĩa sẽ bị cắt thành <code>2K</code> đường chéo.</p>

<h3 id="tính-tổng-chi-phí-cho-mọi-đĩa">Tính tổng chi phí cho mọi đĩa</h3>

<p>Với mỗi vị trí <code>p</code> và một số <code>n</code>, ta cần tính tổng chi phí xoay với mọi đĩa trong <code>O(1)</code>. Biết chúng là tổng các đường chéo, làm sao để tính nhanh?</p>

<p>Ta sẽ vận dụng tính chất chúng đều có dạng <code>x + b</code> hoặc <code>-x + b</code> và sử dụng đường quét để tính với mỗi <code>n</code>.</p>

<p>Ta thấy, khi có <code>k</code> đoạn <code>x + b[i]</code>, chi phí là <code>kx + sum(b[i])</code> với bước tăng là <code>k</code>. Vì vậy thực chất với mỗi vị trí ta chỉ cần biết số đoạn tăng và tổng phần hằng số của chúng. Ta hoàn toàn có thể làm điều này khi quét bằng cách xét 2 đầu mút đầu (thêm đoạn) và cuối (xóa đoạn) sau đó xử lí từ trái sang phải.</p>

<p>Điều tương tự cũng đúng với hàm giảm.</p>

<pre><code class="language-cpp">int b[N * K + 1]; // tất cả b[i] của các đường tăng
vector&lt;int&gt; add[K + 2], remove[K + 2]; // các mốc thêm xóa

void addSegment(int l, int r, int id) {
  // thêm đoạn [l..r] = x + b[id]
  add[l].push_back(id);
  remove[r + 1].push_back(id);
}

void scan() {
  int value = 0, cnt = 0;
  for (int i = 1; i &lt;= K; ++i) {
    for (auto p: add[i]) {
      value += b[p] + i - 1; // giá trị của i trước đó
      ++cnt;
    }
    for (auto p: remove[i]) {
      value -= b[p] + i - 1;
      --cnt;
    }
    value += cnt;
    // value là tổng ở vị trí i
  }
}
</code></pre>

<p>Ta có thể thấy độ phức tạp với mỗi <code>n</code> là <code>O(K + số đoạn của n)</code>. Vì thế tổng độ phức tạp là <code>O(K^2 + NK)</code>, do có <code>2NK</code> đoạn tất cả.</p>
]]></content:encoded>
      <guid>https://blog.dtth.ch/nki/2017-04-21-training</guid>
      <pubDate>Sun, 23 Apr 2017 06:11:00 +0000</pubDate>
    </item>
    <item>
      <title>2017/04/20 Training</title>
      <link>https://blog.dtth.ch/nki/2017-04-20-training</link>
      <description>&lt;![CDATA[#training #apio #vietnamese #thầyĐông&#xA;&#xA;Lưu ý: Mình đã tóm tắt đề bài ở trên, ai không muốn bị spoil thì đừng&#xA;kéo xuống lời giải vội.&#xA;&#xA;Hôm nay có 5 bài của thầy Đông. Do mình không được nghe thầy chữa buổi chiều nên solution là của mình, mặc dù 99% là đúng nhưng không đảm bảo.&#xA;Thực chất bài không phải là khó quá.&#xA;&#xA;!--more--&#xA;&#xA;Tóm tắt đề bài&#xA;ACM&#xA;Có N đội, mỗi đội có 11 chỉ số Ai. Chọn 3 đội sao cho sumi = 1..11[i])] (tổng của max chỉ số từng loại của 3 đội) là lớn nhất.&#xA;Giới hạn&#xA;1 &lt;= N &lt;= 30000&#xA;DOMINO (Bài toán thứ nhất)&#xA;Cho một bảng M x N với K ô cấm. Điền 0 hoặc 1 vào các ô không bị cấm sao cho với ô (i, j):&#xA;Nếu i + j lẻ, thì ô (i, j) không nhỏ hơn các ô không bị cấm xung quanh.&#xA;Nếu i + j chẵn, thì (i, j) không lớn hơn các ô không bị cấm xung quanh.&#xA;&#xA;Giới hạn&#xA;1 &lt;= M, N &lt;= 16&#xA;DOMINO (Bài toán thứ hai)&#xA;Lần này không có ô cấm. Điều kiện như bài toán thứ nhất.&#xA;Giới hạn&#xA;1 &lt;= N &lt;= 8, 1 &lt;= M &lt;= 10^6&#xA;GAMES&#xA;Cho một dãy bit N phần tử chưa xác định và M điều kiện có dạng xor của A[l..r] là 0 hay 1. Tìm vị trí x đầu tiên mà điều kiện x mâu thuẫn với các điều kiện trên.&#xA;Giới hạn&#xA;1 &lt;= N &lt;= 10^9, 1 &lt;= M &lt;= 10^5&#xA;HANOI&#xA;Cho thuật toán giải bài toán tháp Hà Nội:&#xA;def HanoiTower(height, From, Temp, To):&#xA;  if height == 1:&#xA;    # Move one from From to To&#xA;    return&#xA;  HanoiTower(height - 1, From, To, Temp)&#xA;  HanoiTower(1, From, Temp, To)&#xA;  HanoiTower(height - 1, Temp, From, To)&#xA;&#xA;Call the function&#xA;HanoiTower(N, &#39;A&#39;, &#39;B&#39;, &#39;C&#39;)&#xA;Tìm trạng thái của 3 tháp sau P lần gọi hàm.&#xA;Cho trạng thái của 3 tháp, tính số bước đã gọi hàm P, hoặc in -1 nếu không tồn tại trạng thái đó khi giải.&#xA;&#xA;Giới hạn&#xA;1 &lt;= N &lt;= 100. Lưu ý P có thể là số lớn.&#xA;WG&#xA;Cho một xâu P và một tập xâu S[1..N]. Dựng xâu T như sau:&#xA;Đầu tiên chọn một xâu trong tập S[] và thêm vào T&#xA;Sau đó, tìm xâu Si] bất kì thỏa mãn S[0] == T.back() rồi thêm S[i vào.&#xA;&#xA;Hỏi xâu T ngắn nhất chứa dãy con không liên tiếp P là gì?&#xA;&#xA;Giới hạn&#xA;1 &lt;= |P| &lt;= 250, 1 &lt;= |S[i]| &lt;= 10, 1 &lt;= N &lt;= 1000&#xA;&#xA;Lời giải&#xA;ACM&#xA;Tóm tắt đề bài&#xA;Có N đội, mỗi đội có 11 chỉ số Ai. Chọn 3 đội sao cho sumi = 1..11[i])] (tổng của max chỉ số từng loại của 3 đội) là lớn nhất.&#xA;Giới hạn&#xA;1 &lt;= N &lt;= 30000&#xA;&#34;Phức tạp hóa&#34; bài toán&#xA;Rất khó để thực hiện việc chọn nếu mình sử dụng việc lấy max của từng chỉ số. Vì thế ta có thể thay đổi bài toán thành chọn 3 đội rồi mỗi chỉ số lấy của một đội. Ta có thể thấy khi có quản lí đơn giản hơn: chỉ cần mỗi đội một bitmask lựa chọn chỉ số.&#xA;&#xA;Hiển nhiên khi đã xét tất cả trường hợp thì trường hợp tốt nhất luôn là lấy max.&#xA;&#xA;Ghép bitmask&#xA;Giả sử ta đã chọn 3 đội i, j và k. Ta sẽ gán lần lượt 3 mask x, y, z cho 3 đội này. Các mask sẽ thỏa mãn:&#xA;Đôi một rời rạc (x &amp; y == 0, y &amp; z == 0, z &amp; x == 0)&#xA;Ghép lại thì có đầy đủ (x | y | z == (1 &lt;&lt; 11) - 1)&#xA;Tổng chỉ số tương ứng lớn nhất.&#xA;&#xA;Ta có thể thấy, thực chất ta không cần quan tâm các đội được chọn là đội nào. Với mask x được chọn trước, ta chỉ cần tính xem trong các đội thì đội nào có tổng tương ứng mask x là lớn nhất.&#xA;Việc 2 mask x và y bị chọn trùng đội không quan trọng: Ta có thể coi như khi đó có 1 người được chọn với mask 0.&#xA;&#xA;Ta có thể tính trước max[x] với mask x trong O(N  2^11).&#xA;&#xA;Chọn 3 phần tử&#xA;Trước tiên, ta có thể thấy nếu chỉ chọn 2 phần tử, ta có thể for tất cả cặp mask, kiểm tra trong O((2^11)^2). Hiển nhiên do 2 mask đều chỉ có 11 bit nên khi or lại với nhau (ghép bộ) thì mask mới vẫn chỉ có 11 bit. Vậy để chọn 3 phần tử, ta có thể tiếp tục ghép cặp tập đã or với tập max[x].&#xA;&#xA;int two[1 &lt;&lt; 11];&#xA;int three[1 &lt;&lt; 11];&#xA;&#xA;for (int i = 0; i &lt; (1 &lt;&lt; 11); ++i) {&#xA;  for (int j = 0; j &lt; (1 &lt;&lt; 11); ++j) {&#xA;    if (!(i &amp; j)) // không có phần tử trùng&#xA;      two[i | j] = max(two[i | j], max[i] + max[j]);&#xA;  }&#xA;}&#xA;for (int i = 0; i &lt; (1 &lt;&lt; 11); ++i) {&#xA;  for (int j = 0; j &lt; (1 &lt;&lt; 11); ++j) {&#xA;    if (!(i &amp; j)) // không có phần tử trùng&#xA;      three[i | j] = max(three[i | j], two[i] + max[j]);&#xA;  }&#xA;}&#xA;&#xA;Đáp số chính là three[(1 &lt;&lt; 11) - 1]. Độ phức tạp sẽ là O((2 ^ 11) ^ 2).&#xA;&#xA;DOMINO (Bài toán thứ nhất)&#xA;Tóm tắt đề bài&#xA;Cho một bảng M x N với K ô cấm. Điền 0 hoặc 1 vào các ô không bị cấm sao cho với ô (i, j):&#xA;Nếu i + j lẻ, thì ô (i, j) không nhỏ hơn các ô không bị cấm xung quanh.&#xA;Nếu i + j chẵn, thì (i, j) không lớn hơn các ô không bị cấm xung quanh.&#xA;&#xA;Giới hạn&#xA;1 &lt;= M, N &lt;= 16&#xA;Quy hoạch động&#xA;Thực chất đây là một bài toán quy hoạch động bitmask cơ bản. Nhận xét rằng với mỗi ô ta chỉ cần để ý bit ô bên trái và bên trên nó để có thể xét điều kiện thỏa mãn.&#xA;&#xA;Khi quy hoạch động ta đi từng ô theo từng cột, trên xuống dưới trái qua phải. Gọi fi[mask] là số cách lát kể từ ô (i, j) đến cuối cùng, với mask là trạng thái N ô cuối cùng đã đến trước (i, j) (tức các ô (i - 1, j), (i - 1, j + 1), ..., (i, j - 1)). Xem hính dưới:&#xA;&#xA;Bài Domino&#xA;&#xA;Hình thể hiện trạng thái khi đã đến ô (i, j). Ô màu xanh lá là các ô đã lát, ô màu vàng thể hiện mask đang bị quản lí bởi bit tương ứng trong mask, ô màu xanh dương thể hiện ô sắp điền, ô màu đỏ thể hiện các ô chưa lát.&#xA;&#xA;Để chuyển trạng thái ta xác định bit của ô (i, j), nếu nó thỏa mãn điều kiện với ô trái và trên thì gọi đến trạng thái tiếp theo (fimask mới] hoặc f[i + 1[mask mới] nếu đó là ô cuối của cột).&#xA;&#xA;Chuyển mask như nào?&#xA;Ta để ý trên hình, bit 3, kể từ ô tiếp theo, không cần biết đến nữa. Ta có thể xóa bit này và đẩy lên, cho bit của (i, j) vào cuối. Như vậy trạng thái của mình luôn có N bit.&#xA;&#xA;Độ phức tạp là O(N  M  2^N).&#xA;&#xA;Cài đặt như nào?&#xA;Nên gọi đệ quy có nhớ.&#xA;&#xA;int f20[1 &lt;&lt; 16];&#xA;bool visited20[1 &lt;&lt; 16];&#xA;&#xA;int cal(int i, int j, int mask) {&#xA;  if (i   M) return 1; // trường hợp biên&#xA;  if (visitedi[mask]) // đã tính&#xA;    return fi[mask];&#xA;  visitedi[mask] = 1;&#xA;  for (int ch = 0; ch &lt; 2; ++ch) {&#xA;    if (/kiểm tra điều kiện đặt bit ch ở (i, j)/) {&#xA;      fi[mask] += cal(i + (j == N), j % N + 1, (mask &lt;&lt; 1) &amp; ((1 &lt;&lt; N) - 1) + ch);&#xA;    }&#xA;  }&#xA;  return fi[mask];&#xA;}&#xA;&#xA;// x &amp; ((1 &lt;&lt; N) - 1) để lấy x % (1 &lt;&lt; N), N bit cuối của x.&#xA;// (x &lt;&lt; 1) == x  2, đẩy các bit sang phải 1 đơn vị.&#xA;&#xA;int ans = cal(1, 1, 0);&#xA;&#xA;DOMINO (Bài toán thứ hai)&#xA;Lần này không có ô cấm. Điều kiện như bài toán thứ nhất.&#xA;Giới hạn&#xA;1 &lt;= N &lt;= 8, 1 &lt;= M &lt;= 10^6&#xA;&#xA;Nhân ma trận&#xA;Lần này không có ô cấm, nên với mỗi hàng ta chỉ cần quan tâm mask của nó là gì. Từ đây ta có thể nghĩ đến việc nhân ma trận.&#xA;&#xA;Giới hạn N nhỏ, M lớn cũng mang đến cho ta gợi ý này.&#xA;&#xA;Số trạng thái&#xA;O((2 ^ N)^3  log(M)) chưa thể thỏa mãn bài toán. Ta phân tích thêm một chút: với mỗi cột, ta có thể loại ra các trạng thái không thỏa mãn các điều kiện giữa 2 ô liên tiếp trên cùng cột.&#xA;&#xA;Việc thử nghiệm cho thấy với N = 8 cũng chỉ có 55 trạng thái, có thể nhân ma trận.&#xA;&#xA;Điều kiện theo i + j&#xA;Khi chuyển từ cột 2i sang cột 2i + 1, điều kiện bị thay đổi: thứ tự các ô trong cột đang từ lẻ, chẵn, lẻ, ... thành chẵn, lẻ, chẵn,... Điều này làm cho việc chuyển trạng thái không thể thực hiện đơn thuần.&#xA;&#xA;Ta có thể sửa điều này bằng cách thêm 1 bit cho trạng thái của cột, chỉ xem đây là trạng thái cho cột lẻ hay chẵn.&#xA;&#xA;Bảng chuyển đổi của mình sẽ chỉ cho phép chuyển từ cột lẻ sang chẵn và ngược lại.&#xA;&#xA;Để đơn giản từ giờ ta gọi số trạng thái là P (P &lt;= 110).&#xA;&#xA;Ma trận gốc và đáp số&#xA;Hiển nhiên ta trận gốc là một ma trận 1 x P, trong đó các ô thể hiện trạng thái cột lẻ sẽ là 1. Ta nhân ma trận gốc với bảng chuyển đổi đã lũy thừa M - 1, nhận được ma trận đáp số 1 x P. Đáp án chính là tổng các phần tử trong ma trận.&#xA;&#xA;Độ phức tạp là O(P ^ 3  log(M)).&#xA;&#xA;GAMES&#xA;Tóm tắt đề bài&#xA;Cho một dãy bit N phần tử chưa xác định và M điều kiện có dạng xor của A[l..r] là 0 hay 1. Tìm vị trí x đầu tiên mà điều kiện x mâu thuẫn với các điều kiện trên.&#xA;Giới hạn&#xA;1 &lt;= N &lt;= 10^9, 1 &lt;= M &lt;= 10^5&#xA;Chặt nhị phân&#xA;Để tìm vị trí đầu tiên mâu thuẫn, ta chặt nhị phân x để tìm vị trí xa nhất mà vẫn tồn tại một dãy thỏa mãn các điều kiện từ 1 đến x.&#xA;&#xA;Bài toán trở thành kiểm tra xem có một dãy tồn tại không.&#xA;Tính chất mảng dồn&#xA;Nếu ta xét mảng dồn S[1..N], thì điều kiện tổng xor l..r bằng 0 hay 1 tương đương với S[l - 1] với S[r] bằng nhau hay khác nhau.&#xA;&#xA;Ngoài ra, S[i] có thể nhận được bất kí giá trị nào không phụ thuộc vào S[i - 1] nên ta có thể thoải mái gán một giá trị bất kì, nhưng chỉ một mà thôi.&#xA;&#xA;Bài toán trở thành, liệu có thể gán dãy S[1..N] thỏa mãn các điều kiện các nhau không?&#xA;&#xA;2 giá trị cho 1 biến&#xA;Ta có thể coi mảng S[] như một đồ thị N đỉnh. Gộp các đỉnh cùng giá trị, ta thấy việc gán giá trị 0-1 cho các đỉnh còn lại giống như tô màu 2 phía.&#xA;&#xA;Như vậy, ta có thể kiểm tra xem đồ thị có phải 2 phía không.&#xA;&#xA;Độ phức tạp sẽ là O(M + N).&#xA;&#xA;Giảm số lượng đỉnh&#xA;Có tận 10^9 đỉnh, tuy nhiên chỉ có 10^5 cạnh. Vì thế chỉ có tối đa 2  10^5 đỉnh có bậc khác 0, ta chỉ cần quan tâm tới các đỉnh này.&#xA;&#xA;Độ phức tạp chỉ còn O(M), mang lại thuật toán O(M  log(M)).&#xA;&#xA;HANOI&#xA;Tóm tắt đề bài&#xA;Cho thuật toán giải bài toán tháp Hà Nội:&#xA;def HanoiTower(height, From, Temp, To):&#xA;  if height == 1:&#xA;    # Move one from From to To&#xA;    return&#xA;  HanoiTower(height - 1, From, To, Temp)&#xA;  HanoiTower(1, From, Temp, To)&#xA;  HanoiTower(height - 1, Temp, From, To)&#xA;&#xA;Call the function&#xA;HanoiTower(N, &#39;A&#39;, &#39;B&#39;, &#39;C&#39;)&#xA;Tìm trạng thái của 3 tháp sau P lần gọi hàm.&#xA;Cho trạng thái của 3 tháp, tính số bước đã gọi hàm P, hoặc in -1 nếu không tồn tại trạng thái đó khi giải.&#xA;&#xA;Giới hạn&#xA;1 &lt;= N &lt;= 100. Lưu ý P có thể là số lớn.&#xA;Các bước của thuật toán&#xA;Ta có thể tóm tắt thuật toán trong 3 bước:&#xA; Chuyển tháp N - 1 từ A sang B dùng C làm đệm&#xA; Chuyển đĩa N từ A sang C&#xA; Chuyển tháp N - 1 từ B sang C dùng A làm đệm&#xA;&#xA;Từ thuật toán, ta có thể xác định mình đang ở bước nào bằng cách xét vị trí của đĩa N.&#xA;Nếu N còn ở A thì ta ở bước 1.&#xA;Nếu không ta ở bước 2 hoặc 3.&#xA;&#xA;Sau khi xác định được vị trí của N, ta có thể bỏ nó đi và đệ quy xuống bước dưới, coi như ta đang giải bài toán chuyển tháp N - 1.&#xA;&#xA;Tìm trạng thái từ P&#xA;Ta biết để chuyển tháp x sẽ mất 2^x - 1 bước, nên khi xét vị trí đĩa N ta có thể xác định xem ta đang ở bước mấy của việc chuyển tháp N:&#xA;Nếu P &lt; 2^x thì ta đang ở bước 1.&#xA;Nếu P = 2^x thì ta đang ở bước 2.&#xA;Nếu P   2^x thì ta đang ở lượt P - 2^x của bước 3.&#xA;&#xA;Tùy theo bước ta xác định vị trí của đĩa N rồi đệ quy xuống bước tương ứng. Độ phức tạp là O(N). Code khá giống bò trên BST.&#xA;&#xA;Tìm P từ trạng thái&#xA;Việc tìm P không khác gì tìm trạng thái. Khi xét tháp N, ta kiểm tra xem mình ở bước nào tùy theo vị trí của đĩa N:&#xA;Nếu N ở A, thì ta ở bước 1. Đệ quy vào bước 1.&#xA;Nếu N ở C, ta ở bước 2 hoặc 3. Cộng P thêm 2^x (cho bước 1+2) rồi đệ quy vào 3.&#xA;&#xA;Độ phức tạp cũng là O(N).&#xA;&#xA;WG&#xA;Tóm tắt đề bài&#xA;Cho một xâu P và một tập xâu S[1..N]. Dựng xâu T như sau:&#xA;Đầu tiên chọn một xâu trong tập S[] và thêm vào T&#xA;Sau đó, tìm xâu Si] bất kì thỏa mãn S[0] == T.back() rồi thêm S[i vào.&#xA;&#xA;Hỏi xâu T ngắn nhất chứa dãy con không liên tiếp P là gì?&#xA;&#xA;Giới hạn&#xA;1 &lt;= |P| &lt;= 250, 1 &lt;= |S[i]| &lt;= 10, 1 &lt;= N &lt;= 1000&#xA;&#xA;Tham lam dựng xâu&#xA;Khi đã có xâu T có thể lấy ra được dãy con là prefix x của P, ta có thể xác định số lượng kí tự ghép thêm khi thêm xâu S[i] bằng cách đi từ trái sang phải, tham lam kí tự tiếp theo cần ghép.&#xA;&#xA;Từ đó ta tính trước được mảng nxi, khi thêm xâu i với j kí tự đã ghép thì trạng thái mới là bao nhiêu. Độ phức tạp sẽ là O(N  |P|  |S[i]|).&#xA;&#xA;Quy hoạch động&#xA;Ta có thể quy hoạch động fi là độ dài xâu T ngắn nhất sao cho xâu cuối cùng là i và đã ghép được j kí tự đầu tiên của P. Ta chọn thêm một xâu Sk] mới và chuyển trạng thái sang f[kj]]. Điều kiện là S[i].back() == S[k và nxk != j.&#xA;&#xA;Độ phức tạp sẽ là O(N^2  |P|), chưa thỏa mãn bài toán.&#xA;&#xA;Kí tự cuối&#xA;Thực chất ta không cần lưu chiều i là xâu cuối cùng, vì ta chỉ cần quan tâm đến kí tự cuối cùng của T, nên thay vào đó ta có thể chỉ lưu i là kí tự cuối cùng.&#xA;&#xA;Độ phức tạp giảm xuống còn O(26  |P| * N), thỏa mãn bài toán.&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="/nki/tag:training" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">training</span></a> <a href="/nki/tag:apio" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">apio</span></a> <a href="/nki/tag:vietnamese" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">vietnamese</span></a> <a href="/nki/tag:th%E1%BA%A7y%C4%90%C3%B4ng" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">thầyĐông</span></a></p>

<p><strong>Lưu ý</strong>: Mình đã tóm tắt đề bài ở trên, ai không muốn bị spoil thì đừng
kéo xuống lời giải vội.</p>

<p>Hôm nay có 5 bài của thầy Đông. Do mình không được nghe thầy chữa buổi chiều nên solution là của mình, mặc dù 99% là đúng nhưng không đảm bảo.
Thực chất bài không phải là khó quá.</p>



<h1 id="tóm-tắt-đề-bài">Tóm tắt đề bài</h1>

<h2 id="acm">ACM</h2>

<p>Có <code>N</code> đội, mỗi đội có 11 chỉ số <code>A[i][1..11]</code>. Chọn 3 đội sao cho <code>sum[i = 1..11][max(A[x, y, z][i])]</code> (tổng của max chỉ số từng loại của 3 đội) là lớn nhất.</p>

<h4 id="giới-hạn">Giới hạn</h4>

<p><code>1 &lt;= N &lt;= 30000</code></p>

<h2 id="domino-bài-toán-thứ-nhất">DOMINO (Bài toán thứ nhất)</h2>

<p>Cho một bảng <code>M x N</code> với <code>K</code> ô cấm. Điền 0 hoặc 1 vào các ô không bị cấm sao cho với ô <code>(i, j)</code>:
– Nếu <code>i + j</code> lẻ, thì ô <code>(i, j)</code> không nhỏ hơn các ô không bị cấm xung quanh.
– Nếu <code>i + j</code> chẵn, thì <code>(i, j)</code> không lớn hơn các ô không bị cấm xung quanh.</p>

<h4 id="giới-hạn-1">Giới hạn</h4>

<p><code>1 &lt;= M, N &lt;= 16</code></p>

<h2 id="domino-bài-toán-thứ-hai">DOMINO (Bài toán thứ hai)</h2>

<p>Lần này không có ô cấm. Điều kiện như bài toán thứ nhất.</p>

<h4 id="giới-hạn-2">Giới hạn</h4>

<p><code>1 &lt;= N &lt;= 8</code>, <code>1 &lt;= M &lt;= 10^6</code></p>

<h2 id="games">GAMES</h2>

<p>Cho một dãy bit <code>N</code> phần tử chưa xác định và <code>M</code> điều kiện có dạng xor của <code>A[l..r]</code> là 0 hay 1. Tìm vị trí <code>x</code> đầu tiên mà điều kiện <code>x</code> mâu thuẫn với các điều kiện trên.</p>

<h4 id="giới-hạn-3">Giới hạn</h4>

<p><code>1 &lt;= N &lt;= 10^9</code>, <code>1 &lt;= M &lt;= 10^5</code></p>

<h2 id="hanoi">HANOI</h2>

<p>Cho thuật toán giải bài toán tháp Hà Nội:</p>

<pre><code class="language-python">def HanoiTower(height, From, Temp, To):
  if height == 1:
    # Move one from `From` to `To`
    return
  HanoiTower(height - 1, From, To, Temp)
  HanoiTower(1, From, Temp, To)
  HanoiTower(height - 1, Temp, From, To)

# Call the function
HanoiTower(N, &#39;A&#39;, &#39;B&#39;, &#39;C&#39;)
</code></pre>
<ul><li>Tìm trạng thái của 3 tháp sau <code>P</code> lần gọi hàm.</li>
<li>Cho trạng thái của 3 tháp, tính số bước đã gọi hàm <code>P</code>, hoặc in <code>-1</code> nếu không tồn tại trạng thái đó khi giải.</li></ul>

<h4 id="giới-hạn-4">Giới hạn</h4>

<p><code>1 &lt;= N &lt;= 100</code>. Lưu ý <code>P</code> có thể là số lớn.</p>

<h2 id="wg">WG</h2>

<p>Cho một xâu <code>P</code> và một tập xâu <code>S[1..N]</code>. Dựng xâu <code>T</code> như sau:
– Đầu tiên chọn một xâu trong tập <code>S[]</code> và thêm vào <code>T</code>
– Sau đó, tìm xâu <code>S[i]</code> bất kì thỏa mãn <code>S[0] == T.back()</code> rồi thêm <code>S[i][1..]</code> vào.</p>

<p>Hỏi xâu <code>T</code> ngắn nhất chứa dãy con không liên tiếp <code>P</code> là gì?</p>

<h4 id="giới-hạn-5">Giới hạn</h4>

<p><code>1 &lt;= |P| &lt;= 250, 1 &lt;= |S[i]| &lt;= 10, 1 &lt;= N &lt;= 1000</code></p>

<h1 id="lời-giải">Lời giải</h1>

<h2 id="acm-1">ACM</h2>

<h3 id="tóm-tắt-đề-bài-1">Tóm tắt đề bài</h3>

<p>Có <code>N</code> đội, mỗi đội có 11 chỉ số <code>A[i][1..11]</code>. Chọn 3 đội sao cho <code>sum[i = 1..11][max(A[x, y, z][i])]</code> (tổng của max chỉ số từng loại của 3 đội) là lớn nhất.</p>

<h4 id="giới-hạn-6">Giới hạn</h4>

<p><code>1 &lt;= N &lt;= 30000</code></p>

<h3 id="phức-tạp-hóa-bài-toán">“Phức tạp hóa” bài toán</h3>

<p>Rất khó để thực hiện việc chọn nếu mình sử dụng việc lấy max của từng chỉ số. Vì thế ta có thể thay đổi bài toán thành <strong>chọn 3 đội rồi mỗi chỉ số lấy của một đội</strong>. Ta có thể thấy khi có quản lí đơn giản hơn: chỉ cần mỗi đội một bitmask lựa chọn chỉ số.</p>

<p>Hiển nhiên khi đã xét tất cả trường hợp thì trường hợp tốt nhất luôn là lấy max.</p>

<h3 id="ghép-bitmask">Ghép bitmask</h3>

<p>Giả sử ta đã chọn 3 đội <code>i</code>, <code>j</code> và <code>k</code>. Ta sẽ gán lần lượt 3 mask <code>x</code>, <code>y</code>, <code>z</code> cho 3 đội này. Các mask sẽ thỏa mãn:
– Đôi một rời rạc (<code>x &amp; y == 0</code>, <code>y &amp; z == 0</code>, <code>z &amp; x == 0</code>)
– Ghép lại thì có đầy đủ (<code>x | y | z == (1 &lt;&lt; 11) - 1</code>)
– Tổng chỉ số tương ứng lớn nhất.</p>

<p>Ta có thể thấy, thực chất ta không cần quan tâm các đội được chọn là đội nào. Với mask <code>x</code> được chọn trước, ta chỉ cần tính xem trong các đội thì đội nào có tổng tương ứng mask <code>x</code> là lớn nhất.
Việc 2 mask <code>x</code> và <code>y</code> bị chọn trùng đội không quan trọng: Ta có thể coi như khi đó có 1 người được chọn với mask <code>0</code>.</p>

<p>Ta có thể tính trước <code>max[x]</code> với mask <code>x</code> trong <code>O(N * 2^11)</code>.</p>

<h3 id="chọn-3-phần-tử">Chọn 3 phần tử</h3>

<p>Trước tiên, ta có thể thấy nếu chỉ chọn 2 phần tử, ta có thể for tất cả cặp mask, kiểm tra trong <code>O((2^11)^2)</code>. Hiển nhiên do 2 mask đều chỉ có 11 bit nên khi <code>or</code> lại với nhau (ghép bộ) thì mask mới vẫn chỉ có 11 bit. Vậy để chọn 3 phần tử, ta có thể tiếp tục ghép cặp tập đã or với tập <code>max[x]</code>.</p>

<pre><code class="language-cpp">int two[1 &lt;&lt; 11];
int three[1 &lt;&lt; 11];

for (int i = 0; i &lt; (1 &lt;&lt; 11); ++i) {
  for (int j = 0; j &lt; (1 &lt;&lt; 11); ++j) {
    if (!(i &amp; j)) // không có phần tử trùng
      two[i | j] = max(two[i | j], max[i] + max[j]);
  }
}
for (int i = 0; i &lt; (1 &lt;&lt; 11); ++i) {
  for (int j = 0; j &lt; (1 &lt;&lt; 11); ++j) {
    if (!(i &amp; j)) // không có phần tử trùng
      three[i | j] = max(three[i | j], two[i] + max[j]);
  }
}
</code></pre>

<p>Đáp số chính là <code>three[(1 &lt;&lt; 11) - 1]</code>. Độ phức tạp sẽ là <code>O((2 ^ 11) ^ 2)</code>.</p>

<h2 id="domino-bài-toán-thứ-nhất-1">DOMINO (Bài toán thứ nhất)</h2>

<h3 id="tóm-tắt-đề-bài-2">Tóm tắt đề bài</h3>

<p>Cho một bảng <code>M x N</code> với <code>K</code> ô cấm. Điền 0 hoặc 1 vào các ô không bị cấm sao cho với ô <code>(i, j)</code>:
– Nếu <code>i + j</code> lẻ, thì ô <code>(i, j)</code> không nhỏ hơn các ô không bị cấm xung quanh.
– Nếu <code>i + j</code> chẵn, thì <code>(i, j)</code> không lớn hơn các ô không bị cấm xung quanh.</p>

<h4 id="giới-hạn-7">Giới hạn</h4>

<p><code>1 &lt;= M, N &lt;= 16</code></p>

<h3 id="quy-hoạch-động">Quy hoạch động</h3>

<p>Thực chất đây là một bài toán quy hoạch động bitmask cơ bản. Nhận xét rằng với mỗi ô ta chỉ cần để ý bit ô bên trái và bên trên nó để có thể xét điều kiện thỏa mãn.</p>

<p>Khi quy hoạch động ta đi từng ô theo từng cột, trên xuống dưới trái qua phải. Gọi <code>f[i][j][mask]</code> là số cách lát kể từ ô <code>(i, j)</code> đến cuối cùng, với <code>mask</code> là trạng thái <code>N</code> ô cuối cùng đã đến trước <code>(i, j)</code> (tức các ô <code>(i - 1, j)</code>, <code>(i - 1, j + 1)</code>, ..., <code>(i, j - 1)</code>). Xem hính dưới:</p>

<p><img src="https://cdn.discordapp.com/attachments/676817846617243658/1108885514007879702/20170420_domino.png" alt="Bài Domino"></p>

<p>Hình thể hiện trạng thái khi đã đến ô <code>(i, j)</code>. Ô màu xanh lá là các ô đã lát, ô màu vàng thể hiện mask đang bị quản lí bởi bit tương ứng trong mask, ô màu xanh dương thể hiện ô sắp điền, ô màu đỏ thể hiện các ô chưa lát.</p>

<p>Để chuyển trạng thái ta xác định bit của ô <code>(i, j)</code>, nếu nó thỏa mãn điều kiện với ô trái và trên thì gọi đến trạng thái tiếp theo (<code>f[i][j + 1][mask mới]</code> hoặc <code>f[i + 1][1][mask mới]</code> nếu đó là ô cuối của cột).</p>

<h4 id="chuyển-mask-như-nào">Chuyển mask như nào?</h4>

<p>Ta để ý trên hình, bit <code>3</code>, kể từ ô tiếp theo, không cần biết đến nữa. Ta có thể xóa bit này và đẩy lên, cho bit của <code>(i, j)</code> vào cuối. Như vậy trạng thái của mình luôn có <code>N</code> bit.</p>

<p>Độ phức tạp là <code>O(N * M * 2^N)</code>.</p>

<h3 id="cài-đặt-như-nào">Cài đặt như nào?</h3>

<p>Nên gọi đệ quy có nhớ.</p>

<pre><code class="language-cpp">int f[20][20][1 &lt;&lt; 16];
bool visited[20][20][1 &lt;&lt; 16];

int cal(int i, int j, int mask) {
  if (i &gt; M) return 1; // trường hợp biên
  if (visited[i][j][mask]) // đã tính
    return f[i][j][mask];
  visited[i][j][mask] = 1;
  for (int ch = 0; ch &lt; 2; ++ch) {
    if (/*kiểm tra điều kiện đặt bit ch ở (i, j)*/) {
      f[i][j][mask] += cal(i + (j == N), j % N + 1, (mask &lt;&lt; 1) &amp; ((1 &lt;&lt; N) - 1) + ch);
    }
  }
  return f[i][j][mask];
}

// x &amp; ((1 &lt;&lt; N) - 1) để lấy x % (1 &lt;&lt; N), N bit cuối của x.
// (x &lt;&lt; 1) == x * 2, đẩy các bit sang phải 1 đơn vị.

int ans = cal(1, 1, 0);
</code></pre>

<h2 id="domino-bài-toán-thứ-hai-1">DOMINO (Bài toán thứ hai)</h2>

<p>Lần này không có ô cấm. Điều kiện như bài toán thứ nhất.</p>

<h4 id="giới-hạn-8">Giới hạn</h4>

<p><code>1 &lt;= N &lt;= 8</code>, <code>1 &lt;= M &lt;= 10^6</code></p>

<h3 id="nhân-ma-trận">Nhân ma trận</h3>

<p>Lần này không có ô cấm, nên với mỗi hàng ta chỉ cần quan tâm mask của nó là gì. Từ đây ta có thể nghĩ đến việc nhân ma trận.</p>

<p>Giới hạn <code>N</code> nhỏ, <code>M</code> lớn cũng mang đến cho ta gợi ý này.</p>

<h3 id="số-trạng-thái">Số trạng thái</h3>

<p><code>O((2 ^ N)^3 * log(M))</code> chưa thể thỏa mãn bài toán. Ta phân tích thêm một chút: với mỗi cột, ta có thể loại ra các trạng thái không thỏa mãn các điều kiện giữa 2 ô liên tiếp trên cùng cột.</p>

<p>Việc thử nghiệm cho thấy với <code>N = 8</code> cũng chỉ có <strong>55</strong> trạng thái, có thể nhân ma trận.</p>

<h3 id="điều-kiện-theo-i-j">Điều kiện theo <code>i + j</code></h3>

<p>Khi chuyển từ cột <code>2i</code> sang cột <code>2i + 1</code>, điều kiện bị thay đổi: thứ tự các ô trong cột đang từ <em>lẻ, chẵn, lẻ, ...</em> thành <em>chẵn, lẻ, chẵn,...</em> Điều này làm cho việc chuyển trạng thái không thể thực hiện đơn thuần.</p>

<p>Ta có thể sửa điều này bằng cách thêm 1 bit cho trạng thái của cột, chỉ xem đây là trạng thái cho cột lẻ hay chẵn.</p>

<p>Bảng chuyển đổi của mình sẽ chỉ cho phép chuyển từ cột lẻ sang chẵn và ngược lại.</p>

<p>Để đơn giản từ giờ ta gọi số trạng thái là <code>P</code> (<code>P &lt;= 110</code>).</p>

<h3 id="ma-trận-gốc-và-đáp-số">Ma trận gốc và đáp số</h3>

<p>Hiển nhiên ta trận gốc là một ma trận <code>1 x P</code>, trong đó các ô thể hiện trạng thái cột lẻ sẽ là <code>1</code>. Ta nhân ma trận gốc với bảng chuyển đổi đã lũy thừa <code>M - 1</code>, nhận được ma trận đáp số <code>1 x P</code>. Đáp án chính là tổng các phần tử trong ma trận.</p>

<p>Độ phức tạp là <code>O(P ^ 3 * log(M))</code>.</p>

<h2 id="games-1">GAMES</h2>

<h3 id="tóm-tắt-đề-bài-3">Tóm tắt đề bài</h3>

<p>Cho một dãy bit <code>N</code> phần tử chưa xác định và <code>M</code> điều kiện có dạng xor của <code>A[l..r]</code> là 0 hay 1. Tìm vị trí <code>x</code> đầu tiên mà điều kiện <code>x</code> mâu thuẫn với các điều kiện trên.</p>

<h4 id="giới-hạn-9">Giới hạn</h4>

<p><code>1 &lt;= N &lt;= 10^9</code>, <code>1 &lt;= M &lt;= 10^5</code></p>

<h3 id="chặt-nhị-phân">Chặt nhị phân</h3>

<p>Để tìm vị trí đầu tiên mâu thuẫn, ta chặt nhị phân <code>x</code> để tìm vị trí xa nhất mà vẫn tồn tại một dãy thỏa mãn các điều kiện từ 1 đến <code>x</code>.</p>

<p>Bài toán trở thành kiểm tra xem có một dãy tồn tại không.</p>

<h3 id="tính-chất-mảng-dồn">Tính chất mảng dồn</h3>

<p>Nếu ta xét mảng dồn <code>S[1..N]</code>, thì điều kiện tổng xor <code>l..r</code> bằng 0 hay 1 tương đương với <code>S[l - 1]</code> với <code>S[r]</code> bằng nhau hay khác nhau.</p>

<p>Ngoài ra, <code>S[i]</code> có thể nhận được bất kí giá trị nào không phụ thuộc vào <code>S[i - 1]</code> nên ta có thể thoải mái gán một giá trị bất kì, nhưng chỉ <strong>một</strong> mà thôi.</p>

<p>Bài toán trở thành, liệu có thể gán dãy <code>S[1..N]</code> thỏa mãn các điều kiện các nhau không?</p>

<h3 id="2-giá-trị-cho-1-biến">2 giá trị cho 1 biến</h3>

<p>Ta có thể coi mảng <code>S[]</code> như một đồ thị <code>N</code> đỉnh. Gộp các đỉnh cùng giá trị, ta thấy việc gán giá trị 0-1 cho các đỉnh còn lại giống như tô màu 2 phía.</p>

<p>Như vậy, ta có thể kiểm tra xem đồ thị có phải 2 phía không.</p>

<p>Độ phức tạp sẽ là <code>O(M + N)</code>.</p>

<h3 id="giảm-số-lượng-đỉnh">Giảm số lượng đỉnh</h3>

<p>Có tận <code>10^9</code> đỉnh, tuy nhiên chỉ có <code>10^5</code> cạnh. Vì thế chỉ có tối đa <code>2 * 10^5</code> đỉnh có bậc khác 0, ta chỉ cần quan tâm tới các đỉnh này.</p>

<p>Độ phức tạp chỉ còn <code>O(M)</code>, mang lại thuật toán <code>O(M * log(M))</code>.</p>

<h2 id="hanoi-1">HANOI</h2>

<h3 id="tóm-tắt-đề-bài-4">Tóm tắt đề bài</h3>

<p>Cho thuật toán giải bài toán tháp Hà Nội:</p>

<pre><code class="language-python">def HanoiTower(height, From, Temp, To):
  if height == 1:
    # Move one from `From` to `To`
    return
  HanoiTower(height - 1, From, To, Temp)
  HanoiTower(1, From, Temp, To)
  HanoiTower(height - 1, Temp, From, To)

# Call the function
HanoiTower(N, &#39;A&#39;, &#39;B&#39;, &#39;C&#39;)
</code></pre>
<ul><li>Tìm trạng thái của 3 tháp sau <code>P</code> lần gọi hàm.</li>
<li>Cho trạng thái của 3 tháp, tính số bước đã gọi hàm <code>P</code>, hoặc in <code>-1</code> nếu không tồn tại trạng thái đó khi giải.</li></ul>

<h4 id="giới-hạn-10">Giới hạn</h4>

<p><code>1 &lt;= N &lt;= 100</code>. Lưu ý <code>P</code> có thể là số lớn.</p>

<h3 id="các-bước-của-thuật-toán">Các bước của thuật toán</h3>

<p>Ta có thể tóm tắt thuật toán trong 3 bước:
 – Chuyển tháp <code>N - 1</code> từ A sang B dùng C làm đệm
 – Chuyển đĩa <code>N</code> từ A sang C
 – Chuyển tháp <code>N - 1</code> từ B sang C dùng A làm đệm</p>

<p>Từ thuật toán, ta có thể xác định mình đang ở bước nào bằng cách xét vị trí của đĩa <code>N</code>.
– Nếu <code>N</code> còn ở <code>A</code> thì ta ở bước 1.
– Nếu không ta ở bước 2 hoặc 3.</p>

<p>Sau khi xác định được vị trí của <code>N</code>, ta có thể bỏ nó đi và đệ quy xuống bước dưới, coi như ta đang giải bài toán chuyển tháp <code>N - 1</code>.</p>

<h3 id="tìm-trạng-thái-từ-p">Tìm trạng thái từ <code>P</code></h3>

<p>Ta biết để chuyển tháp <code>x</code> sẽ mất <code>2^x - 1</code> bước, nên khi xét vị trí đĩa <code>N</code> ta có thể xác định xem ta đang ở bước mấy của việc chuyển tháp <code>N</code>:
– Nếu <code>P &lt; 2^x</code> thì ta đang ở bước 1.
– Nếu <code>P = 2^x</code> thì ta đang ở bước 2.
– Nếu <code>P &gt; 2^x</code> thì ta đang ở lượt <code>P - 2^x</code> của bước 3.</p>

<p>Tùy theo bước ta xác định vị trí của đĩa <code>N</code> rồi đệ quy xuống bước tương ứng. Độ phức tạp là <code>O(N)</code>. Code khá giống bò trên BST.</p>

<h3 id="tìm-p-từ-trạng-thái">Tìm <code>P</code> từ trạng thái</h3>

<p>Việc tìm <code>P</code> không khác gì tìm trạng thái. Khi xét tháp <code>N</code>, ta kiểm tra xem mình ở bước nào tùy theo vị trí của đĩa <code>N</code>:
– Nếu <code>N</code> ở A, thì ta ở bước 1. Đệ quy vào bước 1.
– Nếu <code>N</code> ở C, ta ở bước 2 hoặc 3. Cộng <code>P</code> thêm <code>2^x</code> (cho bước 1+2) rồi đệ quy vào 3.</p>

<p>Độ phức tạp cũng là <code>O(N)</code>.</p>

<h2 id="wg-1">WG</h2>

<h3 id="tóm-tắt-đề-bài-5">Tóm tắt đề bài</h3>

<p>Cho một xâu <code>P</code> và một tập xâu <code>S[1..N]</code>. Dựng xâu <code>T</code> như sau:
– Đầu tiên chọn một xâu trong tập <code>S[]</code> và thêm vào <code>T</code>
– Sau đó, tìm xâu <code>S[i]</code> bất kì thỏa mãn <code>S[0] == T.back()</code> rồi thêm <code>S[i][1..]</code> vào.</p>

<p>Hỏi xâu <code>T</code> ngắn nhất chứa dãy con không liên tiếp <code>P</code> là gì?</p>

<h4 id="giới-hạn-11">Giới hạn</h4>

<p><code>1 &lt;= |P| &lt;= 250, 1 &lt;= |S[i]| &lt;= 10, 1 &lt;= N &lt;= 1000</code></p>

<h3 id="tham-lam-dựng-xâu">Tham lam dựng xâu</h3>

<p>Khi đã có xâu <code>T</code> có thể lấy ra được dãy con là prefix <code>x</code> của <code>P</code>, ta có thể xác định số lượng kí tự ghép thêm khi thêm xâu <code>S[i]</code> bằng cách đi từ trái sang phải, tham lam kí tự tiếp theo cần ghép.</p>

<p>Từ đó ta tính trước được mảng <code>nx[i][j]</code>, khi thêm xâu <code>i</code> với <code>j</code> kí tự đã ghép thì trạng thái mới là bao nhiêu. Độ phức tạp sẽ là <code>O(N * |P| * |S[i]|)</code>.</p>

<h3 id="quy-hoạch-động-1">Quy hoạch động</h3>

<p>Ta có thể quy hoạch động <code>f[i][j]</code> là độ dài xâu <code>T</code> ngắn nhất sao cho xâu cuối cùng là <code>i</code> và đã ghép được <code>j</code> kí tự đầu tiên của <code>P</code>. Ta chọn thêm một xâu <code>S[k]</code> mới và chuyển trạng thái sang <code>f[k][nx[k][j]]</code>. Điều kiện là <code>S[i].back() == S[k][0]</code> và <code>nx[k][j] != j</code>.</p>

<p>Độ phức tạp sẽ là <code>O(N^2 * |P|)</code>, chưa thỏa mãn bài toán.</p>

<h3 id="kí-tự-cuối">Kí tự cuối</h3>

<p>Thực chất ta không cần lưu chiều <code>i</code> là xâu cuối cùng, vì ta chỉ cần quan tâm đến kí tự cuối cùng của <code>T</code>, nên thay vào đó ta có thể chỉ lưu <code>i</code> là kí tự cuối cùng.</p>

<p>Độ phức tạp giảm xuống còn <code>O(26 * |P| * N)</code>, thỏa mãn bài toán.</p>
]]></content:encoded>
      <guid>https://blog.dtth.ch/nki/2017-04-20-training</guid>
      <pubDate>Fri, 21 Apr 2017 16:36:25 +0000</pubDate>
    </item>
    <item>
      <title>2017-04-19 Training</title>
      <link>https://blog.dtth.ch/nki/2017-04-19-training</link>
      <description>&lt;![CDATA[#training #apio #vietnamese #thầyPhương&#xA;&#xA;Thầy Phương cho 3 bài của FARIO 2017. Bài 1 đã làm rồi, bài 3 là bài approximate nên mình chỉ chữa bài 2.&#xA;&#xA;!--more--&#xA;&#xA;Pyramid Cake&#xA;Tóm tắt đề bài&#xA;Cho một chiếc hộp có đáy chữ nhật M x N, vị trí (i, j) có độ cao là Hi. Ta dựng một chiếc bánh nhiều tần thỏa mãn:&#xA; Các tầng là các hình chữ nhật chứa đỉnh (1,1)&#xA; Tầng trên phải nằm bên trong mặt phẳng của tầng dưới&#xA; Không ô nào cao hơn vị trí tương ứng của hộp&#xA; Thể tích bánh là lớn nhất.&#xA;Giới hạn&#xA;1 &lt;= N, M &lt;= 1000, 1 &lt;= Hi &lt;= 10^8&#xA;&#xA;Lựa chọn tầng&#xA;Do mọi tầng đề chứa (1, 1), bản chất ta chỉ cần tọa độ của góc còn lại là có thể xác định được duy nhất tầng hiện tại.&#xA;&#xA;Tại sao ta chỉ cần quan tâm số tầng mà không phải độ cao? Hiển nhiên, với tầng (i, j) ta biết độ cao của tầng đó (nếu tính cả các tầng dưới nó) sẽ là Mi = min(Hi&#39;) với i&#39; &lt;= i, j&#39; &lt;= j.&#xA;&#xA;Khi nén tầng như vậy, ta có thể coi như tầng sau luôn nhỏ hơn tầng dưới, làm cho tập trạng thái không có chu trình và ta có thể quy hoạch động được.&#xA;&#xA;Gọi fi là diện tích lớn nhất của hình có tầng dưới cùng là (i, j). Hiển nhiên ta sẽ đặt tầng dưới cùng độ dày Mi. Sau đó, ta chọn một tầng nhỏ hơn để qhđ:&#xA;&#xA;int cur = Mi  i  j; // Diện tích phần đáy&#xA;for (int k = 1; k &lt;= i; ++k)&#xA;&#x9;for (int l = 1; l &lt;= j; ++l)&#xA;&#x9;&#x9;if (k != i || l != j)&#xA;&#x9;&#x9;&#x9;fi = max(fi, cur + fk - Mi  k  l);&#xA;&#xA;Tại sao ta trừ đi Mi  k  l? Bởi phần diện tích này đã được tính vào đáy của hình dưới cùng.&#xA;&#xA;Đáp số sẽ là max(fi), độ phức tạp là O(N^4), chưa đủ để giải quyết bài toán.&#xA;&#xA;Thử tất cả?&#xA;&#xA;Ta có thể thấy, mỗi hình chữ nhật con đều nhỏ hơn đáy ban đầu ít nhất 1 hàng hoặc 1 cột. Vậy tại sao mình không chọn 1 trong 2 hình to nhất ((i, j - 1) và (i - 1, j)) để làm đáy tiếp theo?&#xA;&#xA;Rất có thể mọi người sẽ nghĩ việc này không đúng vì có thể không điền đc thêm tầng nào - nhưng nếu ta nghĩ theo cách nhìn khác - ta thêm 0 tầng, thì lựa chọn vẫn hợp lí.&#xA;&#xA;Có thể việc này không tối ưu không? Giả sử, lựa chọn (x, y) (x &lt; i, y &lt; j) là tối ưu. Hiển nhiên nó cũng sẽ tối ưu cho (i, j - 1). Vậy ta hoàn toàn có thể chọn (i, j - 1) và nó sẽ chứa cả (x, y), kết quả không đổi.&#xA;&#xA;Cải thiện thuật toán&#xA;Ta rút việc chọn tất cả cặp thành chọn một trong hai hình chữ nhật con lớn nhất. Độ phức tạp giảm xuống còn O(N^2), thỏa mãn bài toán.&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="/nki/tag:training" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">training</span></a> <a href="/nki/tag:apio" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">apio</span></a> <a href="/nki/tag:vietnamese" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">vietnamese</span></a> <a href="/nki/tag:th%E1%BA%A7yPh%C6%B0%C6%A1ng" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">thầyPhương</span></a></p>

<p>Thầy Phương cho 3 bài của <a href="http://orac.amt.edu.au/cgi-bin/train/hub.pl?expand=fario17#fario17" rel="nofollow">FARIO 2017</a>. Bài 1 đã làm rồi, bài 3 là bài approximate nên mình chỉ chữa bài 2.</p>



<h2 id="pyramid-cake">Pyramid Cake</h2>

<h3 id="tóm-tắt-đề-bài">Tóm tắt đề bài</h3>

<p>Cho một chiếc hộp có đáy chữ nhật <code>M x N</code>, vị trí <code>(i, j)</code> có độ cao là <code>H[i][j]</code>. Ta dựng một chiếc bánh nhiều tần thỏa mãn:
 – Các tầng là các hình chữ nhật chứa đỉnh <code>(1,1)</code>
 – Tầng trên phải nằm bên trong mặt phẳng của tầng dưới
 – Không ô nào cao hơn vị trí tương ứng của hộp
 – Thể tích bánh là lớn nhất.</p>

<h4 id="giới-hạn">Giới hạn</h4>

<p><code>1 &lt;= N, M &lt;= 1000</code>, <code>1 &lt;= H[i][j] &lt;= 10^8</code></p>

<h3 id="lựa-chọn-tầng">Lựa chọn tầng</h3>

<p>Do mọi tầng đề chứa <code>(1, 1)</code>, bản chất ta chỉ cần tọa độ của góc còn lại là có thể xác định được duy nhất tầng hiện tại.</p>

<p>Tại sao ta chỉ cần quan tâm số tầng mà không phải độ cao? Hiển nhiên, với tầng <code>(i, j)</code> ta biết độ cao của tầng đó (nếu tính cả các tầng dưới nó) sẽ là <code>M[i][j] = min(H[i&#39;][j&#39;])</code> với <code>i&#39; &lt;= i, j&#39; &lt;= j</code>.</p>

<p>Khi nén tầng như vậy, ta có thể coi như tầng sau luôn nhỏ hơn tầng dưới, làm cho tập trạng thái không có chu trình và ta có thể quy hoạch động được.</p>

<p>Gọi <code>f[i][j]</code> là diện tích lớn nhất của hình có tầng dưới cùng là <code>(i, j)</code>. Hiển nhiên ta sẽ đặt tầng dưới cùng độ dày <code>M[i][j]</code>. Sau đó, ta chọn một tầng nhỏ hơn để qhđ:</p>

<pre><code class="language-cpp">int cur = M[i][j] * i * j; // Diện tích phần đáy
for (int k = 1; k &lt;= i; ++k)
	for (int l = 1; l &lt;= j; ++l)
		if (k != i || l != j)
			f[i][j] = max(f[i][j], cur + f[k][l] - M[i][j] * k * l);
</code></pre>

<p>Tại sao ta trừ đi <code>M[i][j] * k * l</code>? Bởi phần diện tích này đã được tính vào đáy của hình dưới cùng.</p>

<p>Đáp số sẽ là <code>max(f[i][j])</code>, độ phức tạp là <code>O(N^4)</code>, chưa đủ để giải quyết bài toán.</p>

<h3 id="thử-tất-cả">Thử tất cả?</h3>

<p>Ta có thể thấy, mỗi hình chữ nhật con đều nhỏ hơn đáy ban đầu ít nhất 1 hàng hoặc 1 cột. Vậy tại sao mình không chọn 1 trong 2 hình to nhất (<code>(i, j - 1)</code> và <code>(i - 1, j)</code>) để làm đáy tiếp theo?</p>

<p>Rất có thể mọi người sẽ nghĩ việc này không đúng vì có thể không điền đc thêm tầng nào – nhưng nếu ta nghĩ theo cách nhìn khác – ta thêm <strong>0</strong> tầng, thì lựa chọn vẫn hợp lí.</p>

<p>Có thể việc này không tối ưu không? Giả sử, lựa chọn <code>(x, y)</code> (<code>x &lt; i, y &lt; j</code>) là tối ưu. Hiển nhiên nó cũng sẽ tối ưu cho <code>(i, j - 1)</code>. Vậy ta hoàn toàn có thể chọn <code>(i, j - 1)</code> và nó sẽ chứa cả <code>(x, y)</code>, kết quả không đổi.</p>

<h3 id="cải-thiện-thuật-toán">Cải thiện thuật toán</h3>

<p>Ta rút việc chọn tất cả cặp thành chọn một trong hai hình chữ nhật con lớn nhất. Độ phức tạp giảm xuống còn <code>O(N^2)</code>, thỏa mãn bài toán.</p>
]]></content:encoded>
      <guid>https://blog.dtth.ch/nki/2017-04-19-training</guid>
      <pubDate>Wed, 19 Apr 2017 13:00:00 +0000</pubDate>
    </item>
  </channel>
</rss>