---
tags: VNOJ, THT, Medium Implementation, Implementation, Greedy, DP, DP-digit, Champernowne Constant, Binary Search, Stack, Online Query, SPyofgame
---
Tin học trẻ 2021 - Vòng sơ khảo - Bảng C - Dãy số tự nhiên
===
```
```
[https://oj.vnoi.info/problem/tht21_skc_seq](https://oj.vnoi.info/problem/tht21_skc_seq)
-----
###### Tags: `Medium Implementation`, `Greedy`, `DP-digit`, `Champernowne Constant`, `Binary Search`, `Stack`, `Online Query`
###### Contributor: @[WuTan](https://codeforces.com/profile/WuTan), @[Duy_e](https://codeforces.com/profile/Duy_e), @[Demen100ns](https://codeforces.com/profile/DeMen100ns)
-----
### Hướng dẫn subtask 1
**Gợi ý 1:** Chỉ cần xét $k + p$ số đầu tiên của dãy $123456789101112131415161718192021\dots$
**Gợi ý 2:** Thực hiện $k$ lần thao tác xóa, mỗi lần xóa duyệt qua $k + p$ chữ số là cùng
**Gợi ý 3:** Gọi kí tự đang xét là $x$ và kí tự bên phải nó là $y$. Xét các trường hợp $x < y$, $x = y$ và $x > y$ để xóa cho tối ưu
**Gợi ý 4:** Ưu tiên xóa các chữ số ở bên trái trước bên phải
> **Phần cài đặt xin bạn đọc xem như là bài tập để hiểu rõ hơn trước khi bước vào subtask 2**
-----
### Hướng dẫn subtask 2
Lấy ít nhất $k + p$ số đầu tiên của dãy $123456789101112131415161718192021\dots$
Gọi $S$ là đoạn giá trị lấy được, $T$ là giá trị tối đa của $S$ sau khi xóa $k$ kí tự cần tìm
Ban đầu xét $T$ rỗng. Để làm $T$ lớn hơn sau khi xóa đủ $k$ kí tự thì ta duyệt từ trái qua phải các chữ số $d$ trong $S$ và ưu tiên chọn kí tự lớn hơn để thêm. Nếu có chữ số cuối cùng của $T$ là $c$ mà
- $c \geq d$, ta thêm chữ số $d$ vào cuối $T$ vì nó không ảnh hưởng gì cả
- $c < d$, nếu ta thêm $d$ sẽ không tối ưu, nên ta xoá các chữ số cuối cùng của $T$ tới khi điều kiện không còn thỏa hoặc không còn xóa được nữa ($T$ rỗng, hoặc số lần xóa đã bằng $k$ rồi)
Cuối cùng để tránh trường hợp mà thừa $k$ ví dụ như $S$ gồm các chữ số thì ta sẽ xóa $k$ chữ số cuối của $T$
Giờ ta cần phải biết tại sao $c < d$ sẽ không tối ưu:
- Nhận thấy mình duyệt các chữ số từ trái qua phải nên nếu có một kí tự ở đầu không tối ưu thì dù số cuối có full chữ số $9$ cũng không thể lớn hơn dược.
- Cụ thể, xét $T_1 = \overline{125ABC}$ và $T_2 = \overline{124XYZ}$ là 2 cách chọn khác nhau. Thì ta có $\overline{125ABC} \geq 125000 > 124999 \geq \overline{124XYZ}$
- Nên nếu giả sử chữ số cuối của $T$ đang là $c = 4$ mà có chữ số $d$ trong $s$ là $d = 5$ thì mình sẽ xóa chữ số cuối của $T$ đi đến khi nào không xóa được nữa
-----
### Code Subtask 2
> **Time:** $O(p + k + log_{10}(max(k, p)))$
> **Space:** $O(p + k + log_{10}(max(k, p)))$
> **Algo:** Stack, Greedy
```cpp=
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
const int LIM = 1e6 + 16;
vector<int> dig[LIM];
/// Precalculation reduce significant amount of modulo you use and some other constant factors
void precal()
{
for (int n = 1; n <= LIM; ++n)
{
for (int t = n; t > 0; t /= 10) dig[n].push_back(t % 10);
reverse(dig[n].begin(), dig[n].end());
}
}
int query(int k, int p)
{
/// If this range is too large to calculate then we can stop
/// But the answer is likely to be 9 so you can return for a little score
if (k > LIM || p > LIM) return 9;
/// Take at least k + p numbers, and at most k + p + 6 digits (since log10(LIM) = 6)
/// Using reserve to boost vector faster
vector<int> T;
T.reserve(k + p + 6);
/// For each number from 1 to 1e15 (actually we limit it at 1e6)
for (int x = 1; x <= LIM; ++x)
{
/// For each digit in x
for (int d : dig[x])
{
/// While the last digit in T is not optimal
while (k > 0 && T.size() && T.back() < d) --k, T.pop_back();
/// Insert the digit to the back of T
T.push_back(d);
/// Found the answer
/// * (k == 0) -> used (k) deletion
/// * (T.size() >= p) -> found the (p)-th digit
///
if (k == 0 && T.size() >= p) return T[p - 1];
}
}
}
int main()
{
ios::sync_with_stdio(NULL);
cin.tie(NULL);
precal();
int q;
cin >> q;
while (q-->0)
{
int k, p;
cin >> k >> p;
cout << query(k, p) << "\n";
}
return 0;
}
```
-----
### Hướng dẫn subtask 3
Để cho tiện thì ta sẽ định nghĩa $F_{1, X} = \overline{123456789101112131415161718192021...X}$ là một số mà được ghép bởi từng chữ số của các số nguyên dương từ $1$ đến $X$
Tương tự ta định nghĩa tổng quát cho $F_{L, R}$
---
**Nhận xét:** Ta sẽ luôn ưu tiên xoá các chữ số nhỏ hơn chữ số $9$ vì chữ số $9$ lớn nhất trong hệ thập phân
Ta sẽ tìm vị trí $m$ đầu tiên mà có $S = F_{1, m}$ sao cho nếu ta coi $T$ là phần còn lại của $S$ sau khi xóa tối đa $k$ chữ số khác chữ số $9$ thì $|T| \geq p$.
Hay nói một cách đơn giản hơn là số $m$ nhỏ nhất mà số $S$ tạo bởi $F_{1, m}$ có độ dài ít nhất là $k + p$
Sau đó ta tìm vị trí $x$ lớn nhất không quá $m$ mà có ít nhất một chữ số $9$, sau đó ta xóa hết tất cả các chữ số khác $9$ trong đoạn $F_{1, x}$ của $S$. Nếu không tồn tại chữ số $x$ như thế, thì gán $x = 0$
- Nhận thấy rằng với mọi $x \geq 9$ thì sẽ tồn tại ít nhất một số nguyên dương trong đoạn $[x - 9, x]$ mà có chữ số $9$ ở hệ biểu diễn thập phân nên ta có thể trâu tìm kết quả
- Trong trường hợp $x < 9$, khi không tồn tại số nào có chứ số $9$, hay ta gán cụ thể là $x = 0$, ta cũng có thể chứng minh được là lúc này $m \leq 9$
- Vì độ dài của một số tối đa là $15$ (vì $x \leq 10^{15}$) nên cùng lắm cần duyệt qua $10 \times 15$ chữ số để tìm $X$ (tối ưu được nhưng không đáng kể)
Để cho tiện thì định nghĩa
- $len$ là số chữ số của $S$
- $cnt$ là số chữ số $9$ trong $S$
- $curp$ là số chữ số trong $S$ sau khi xóa $k$ kí tự
Ta có số chữ số đã bị xóa trong hành động vừa rồi là $len - cnt$
Xét trường hợp thứ nhất khi $T$ có dạng $\overline{99999\dots99}$.
- Giờ ta có xóa thêm $k - cnt$ chữ số cho đủ $k$ thì $T$ vẫn chỉ chứa chữ số $9$
- Vì trong $T$ đã chứa vị trí $p$ cần tìm nên kết quả sẽ là $9$
Ngược lại số có dạng $T$ = $\overline{99999\dots ABCDE\dots}$ với $A, B, C, D, E, \dots$ là các chữ số khác $9$
Lúc này ta sẽ bỏ qua nguyên đoạn số $9$ ở đầu để chỉ duyệt phần còn lại $Q = \overline{ABCDE\dots}$
Xét trường hợp thứ hai khi $x < m$
- Lúc này $Q = F_{x + 1, m}$
- Giờ chúng ta cần tìm chữ số thứ $p - cnt$ sau khi xóa thêm $k - (len - cnt)$ kí tự
- Ta làm tương tự như subtask $2$
Xét trường hợp thứ ba là khi $x = m$
- Ta cần tìm chữ số thứ $p$ trong dãy $|S|$ độ dài $curp$
- Làm tương tự như trường hợp thứ hai, với $Q = F_{m, m}$
-----
### Code Subtask 3
> For constant $b = 10$ is the current base
> **DP Time:** $O(f(x)) = O(log^2_{b}(max(k, p)))$
> **Query Time**: $O(g(x)) = O(log_{2}(max(k, p)) \times f(x) + log_{b}(max(k, p)) \times b)$
> **Total Time:** $O(q \times g(x)) \leq O(q \times log^3(k + p))$
> **Space:** $O(f(x))$
> **Algo:** Medium Implementation, Greedy, DP-digit, Champernowne Constant, Binary Search, Stack, Online Query
```cpp=
#include <algorithm>
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
typedef long long ll;
/// Break number into a list of digits
/// Assume V = 102109
/// Then num = {1, 0, 2, 1, 0, 9}
vector<int> num;
void pull(ll V)
{
num.clear();
do num.push_back(V % 10);
while (V /= 10);
reverse(num.begin(), num.end());
}
/// DP-digit:
/// Let S = 123456789101112131415161718192021...n
/// For n = number before break into list <num>
/// Return the number of digit 9 in |S|
///
/// DP-Function: magic(int len, int cnt, bool iln)
/// Currently have (len) length and counted (cnt) digit (9)
/// If (iln = true) means the current number is less than (n)
/// otherwise current number is not greater than (n)
///
/// Transistion: Sigma(len + 1, cnt + (d == 9), iln || (d < upper)) | d = 0 -> upper
/// d = 0 -> upper : only build the number that not greater than n
/// len -> len + 1 : build next digit
/// cnt -> cnt + (d == 9) : if selected digit is (9) then count the digit
/// iln -> iln || (d < upper) : If (iln) is already true, or (d < upper) meaning this number is smaller than n
/// For example: 123'xyzt < 124'0000 for all xyzt
///
/// Default: (0, 0, 0)
/// Currently the number has (0) length, counted (0) digit 9 and currently not greater than n
/// No leading-zero numbers are counted
ll f[18][18][2];
ll magic(int len = 0, int cnt = 0, bool iln = 0)
{
if (len >= num.size()) return cnt;
ll &res = f[len][cnt][iln];
if (res != -1) return res;
res = 0;
int upper = iln ? 9 : num[len];
for (int d = 0; d <= upper; ++d)
res += magic(len + 1, cnt + (d == 9), iln || (d < upper));
return res;
}
/// S = 123456789101112131415161718192021...n
/// return length of S
ll Champernowne_length(ll n)
{
ll cnt = 0;
for (ll x = 1; x <= n; x *= 10)
cnt += (n - x + 1);
return cnt;
}
/// S = 123456789101112131415161718192021...n
/// return number of digit 9 in S
ll Champernowne_cnt_9(ll V)
{
pull(V);
memset(f, -1, sizeof(f));
return magic();
}
/// Let n = 10^15
/// And S = 123456789101112131415161718192021...n
///
/// query >> (k, p)
/// Delete (k) digit in (S) that (S) is maximum
/// Find (p)-th digit in (S)
///
/// Example: (k = 10, p = 5)
/// (S) = 123456789101112131415...n
/// After delete (S) = 91112131415...n
///
int query(ll k, ll p)
{
/// The optimal solution is to delete all digit except (9)
///
/// We find first position (m) that S = 123456789101112131415161718192021...m
/// and the remaining length of (S) >= p after deleting at most (k) non-9 digits
/// after that we find the largest number (x) not greater than (m) that contain last digit 9
/// then we delete all digits exept (9) in 123456789101112131415161718192021...x
///
ll curp; /// Length of |S|, also the position of last digit in S after deletion
ll curk; /// The last number concated to S
ll len; /// The number of digit in (S) (no leading zeros)
ll cnt; /// The number of digit 9 in (S) (no leading zeros)
for (ll l = 1, r = 1e15; l <= r; )
{
ll m = l + r >> 1;
ll tmp_cnt = Champernowne_cnt_9(m);
ll tmp_len = Champernowne_length(m);
ll tmp_p = tmp_len - k;
if (tmp_p >= p)
{
curk = m;
curp = tmp_p;
cnt = tmp_cnt;
len = tmp_len;
r = m - 1;
}
else
{
l = m + 1;
}
}
if (k >= len - cnt)
{
/// Case #1: If the number of digit we must delete (k) is greater than or equal the number of digit (9) in S
///
/// We can delete all other digit that is not (9)
/// The remaining (S) will be 9999999...
/// Therefore the answer will be (9) (since p <= |S|)
///
return 9;
}
ll curx = curk;
while (curx >= 1 && curx % 10 != 9) --curx;
cnt = Champernowne_cnt_9(curx);
len = Champernowne_length(curx);
k -= len - cnt;
if (k >= 0)
{
/// Case #2: (x) < (m)
///
/// Let Q = (x+1)(x+2)(x+3)...m | concatenating positive integers from (x + 1) to (m)
/// Now we have to know how to find the (p)-th digit in Q after deleting (k) digits that maximize Q
/// For every (x) >= 9, it will always exist at least one number in [x - 9, x] that having digit 9 in it
/// And when (x) < 9, you can prove that (m <= 9)
/// Therefore you can just simply do a brute-forces of maximumly O(log10(m) * 10) digits
//
vector<int> S;
while (++curx <= curk)
{
/// Get digits of current number
///
pull(curx);
for (ll d : num)
{
/// We remove all smaller digits since them only make the whole number smaller
/// Example: 125XYZ > 124XYZ for whatever XYZ you choose
/// Therefore the last digits of the current stack should be as large as possible
/// Notice that you only delete exactly k digits therefore you cant make the number smaller
///
while (k && S.size() && d > S.back()) S.pop_back(), --k;
S.push_back(d);
}
}
/// You ignored the first (cnt) digits
p -= cnt;
return S[p - 1];
}
/// Case #3: (x) = (m)
///
/// The answer is in the digits of (x).
/// You have deleted exactly (k) digits
/// But dont forget that you still need to output the digit at [p]
/// Currently use are in (m)
///
pull(curk);
/// Notice that (curk) give you up to (curp) digits, therefore reduce it to the (p)-th digit
while (curp > p) num.pop_back(), --curp;
return num.back();
}
int main()
{
/// Fast input
ios_base::sync_with_stdio(NULL);
cin.tie(NULL);
ll q;
cin >> q;
/// For each query
while (q-->0)
{
ll k, p;
cin >> k >> p;
cout << query(k, p) << "\n";
}
return 0;
}
```
-----
### Bonus:
- Bạn có thể giải với độ phức tạp $O(q \times log^2(k + p))$ không ?