<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>thầynghĩa &amp;mdash; Zumi&#39;s Blog</title>
    <link>https://blog.dtth.ch/nki/tag:thầynghĩa</link>
    <description>Just random Zumi Zoom things</description>
    <pubDate>Tue, 28 Apr 2026 23:22:42 +0200</pubDate>
    <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>
  </channel>
</rss>