C. Decreasing String

发布时间 2023-10-13 23:11:48作者: onlyblues

C. Decreasing String

Recall that string $a$ is lexicographically smaller than string $b$ if $a$ is a prefix of $b$ (and $a \ne b$), or there exists an index $i$ ($1 \le i \le \min(|a|, |b|)$) such that $a_i < b_i$, and for any index $j$ ($1 \le j < i$) $a_j = b_j$.

Consider a sequence of strings $s_1, s_2, \dots, s_n$, each consisting of lowercase Latin letters. String $s_1$ is given explicitly, and all other strings are generated according to the following rule: to obtain the string $s_i$, a character is removed from string $s_{i-1}$ in such a way that string $s_i$ is lexicographically minimal.

For example, if $s_1 = \mathrm{dacb}$, then string $s_2 = \mathrm{acb}$, string $s_3 = \mathrm{ab}$, string $s_4 = \mathrm{a}$.

After that, we obtain the string $S = s_1 + s_2 + \dots + s_n$ ($S$ is the concatenation of all strings $s_1, s_2, \dots, s_n$).

You need to output the character in position $pos$ of the string $S$ (i. e. the character $S_{pos}$).

Input

The first line contains one integer $t$ — the number of test cases ($1 \le t \le 10^4$).

Each test case consists of two lines. The first line contains the string $s_1$ ($1 \le |s_1| \le 10^6$), consisting of lowercase Latin letters. The second line contains the integer $pos$ ($1 \le pos \le \frac{|s_1| \cdot (|s_1| +1)}{2}$). You may assume that $n$ is equal to the length of the given string ($n = |s_1|$).

Additional constraint on the input: the sum of $|s_1|$ over all test cases does not exceed $10^6$.

Output

For each test case, print the answer — the character that is at position $pos$ in string $S$. Note that the answers between different test cases are not separated by spaces or line breaks.

Example

input

3
cab
6
abcd
9
x
1

output

abx

 

解题思路

  我的做法跟官方的不一样,先给出我的思路。

  对于给定 $m$,假设其表示第 $k$ 个子串 $s_k$ 中的 第 $x$ 个字符,那么问题就等价于在 $s_1$ 中删除 $k-1$ 个字符后所得到的字符串中,字典序最小的字符串的第 $x$ 个字符是什么。而题目有限制 $s_{i}$ 的获得是通过删除 $s_{i-1}$ 的一个字符所得到的所有字符串中字典序最小的那个,因此我们还需要证明通过这个限制得到的 $s_k$ 一定是从 $s_1$ 中删除 $k-1$ 个字符后得到的字典序最小的字符串,才能保证上面的问题与原问题是等价的。

  归纳法,显然 $s_2$ 是从 $s_1$ 中删除 $1$ 个字符后得到的字典序最小的字符串。假设 $s_{k-1}$ 仍然满足这个条件,由于 $s_{k-1}$ 是从 $s_1$ 中删除 $k-2$ 个字符后得到的字典序最小的字符串,而 $s_k$ 是从 $s_{k-1}$ 中删除一个字符得到的字典序最小的字符串,因此 $s_k$ 一定也是从 $s_1$ 中删除 $k-1$ 字符得到的字典序最小的字符串。

  而从 $s_1$ 中删除 $k-1$ 个字符后所得到的字典序最小的字符串,还可以等价于从 $s_1$ 中选择 $n-k+1$ 个字符所构成的字典序最小的字符串。因此要在 $s_1$ 的 $[1,n]$ 中选择 $n-k+1$ 个字符,那么对于 $s_k$ 的第 $1$ 个字符串,必然是从 $s_1$ 的 $i \in [1,k]$ 中选择最靠左且最小的字符 $s_{1,i}$(否则如果选择的 $s_{1,i}$ 有 $i > k$,那么不可能选到 $n-k+1$ 个字符,因为 $n-i+1 < n-k+1$)。同理,此时要在 $s_1$ 的 $[i+1,n]$ 中选择 $n-k$ 个字符,那么对于 $s_k$ 的第 $2$ 个字符串,必然是从 $s_1$ 的 $j \in [i+1,k+1]$ 中选择最靠左且最小的字符 $s_{1,j}$,以此类推。当选择完第 $x$ 个字符时,那么这个字符就是 $S$ 中的第 $m$ 个字符。

  可以发现上面的过程本质就是求某个区间的最小值,可以用线段树来维护。

  AC 代码如下,时间复杂度为 $O(n \log{n})$:

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

const int N = 1e6 + 10;

char s[N];
struct Node {
    int l, r, mn;
}tr[N * 4];

void build(int u, int l, int r) {
    tr[u] = {l, r};
    if (l == r) {
        tr[u].mn = l;
    }
    else {
        int mid = l + r >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
        if (s[tr[u << 1].mn] <= s[tr[u << 1 | 1].mn]) tr[u].mn = tr[u << 1].mn;
        else tr[u].mn = tr[u << 1 | 1].mn;
    }
}

int query(int u, int l, int r) {
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].mn;
    int mid = tr[u].l + tr[u].r >> 1;
    if (r <= mid) return query(u << 1, l, r);
    else if (l > mid) return query(u << 1 | 1, l, r);
    int x = query(u << 1, l, r), y = query(u << 1 | 1, l, r);
    if (s[x] <= s[y]) return x;
    return y;
}

void solve() {
    LL m;
    scanf("%s %lld", s + 1, &m);
    int n = strlen(s + 1), k;
    for (int i = 1; i <= n; i++) {    // 求出S的第m个字符是在哪个子串
        if (m <= n - i + 1) {
            k = i;
            break;
        }
        m -= n - i + 1;
    }
    build(1, 1, n);
    for (int i = 1, j = 0; i <= n - k + 1; i++) {
        j = query(1, j + 1, k + i - 1);    // 求出这个区间内最靠左且最小的字符
        if (i == m) {
            printf("%c", s[j]);
            return;
        }
    }
}

int main() {
    int t;
    scanf("%d", &t);
    while (t--) {
        solve();
    }
    
    return 0;
}

  下面给出官方的做法。

  考虑应该删除 $s_1$ 中的哪个字符,使得 $s_2$ 的字典序最小。可以证明应该选择最靠左第一个满足 $s_{1,i} > s_{1,i+1}$ 的字符 $s_{1,i}$,因为删除后得到的 $s_{2,i}$ 会变小(即变成了 $s_{1,i+1}$)。假设删除的是 $[i+1,n]$ 中的字符,那么得到的 $s_{2,i}' = s_{1,i} > s_{2,i} = s_{1,i+1}$,字典序不会变小。假设删除的是 $[1,i-1]$ 中的字符,由于有 $s_{1,1} \leq s_{1,2} \leq \cdots \leq s_{1,i}$,那么得到的 $s_{2}'$ 的前 $i-1$ 个字符中可能存在某个字符大于 $s_{2}$ 中与之对应的字符,而第 $i$ 个位置后的字符都相同,因此 $s_{2}'$ 的字典序不可能比 $s_{2}$ 小。得证。

  为此我们可以从左到右依次枚举 $s_1$ 中的字符并用栈来维护,找到所有最靠左第一个满足 $s_{1,i} > s_{1,i+1}$ 的字符 $s_{1,i}$。当枚举到 $s_{1,i}$,此时如果栈不为空且栈顶元素大于 $s_{1,i}$,那么就将栈顶元素弹出,表示删除了满足条件的字符,不断进行这个过程,直到栈为空或者栈顶元素不超过 $s_{1,i}$,然后再把 $s_{1,i}$ 压入栈。因此从栈底到栈顶元素满足非递降的关系。

  当我们从栈中弹出了 $k-1$ 个元素(这里的 $k$ 同上面方法的 $k$),则表示已经删除了 $k-1$ 个元素,此时只需把剩余的字符都压入栈,那么栈中第 $x$ 个元素就是答案。如果弹出的元素个数小于 $k-1$,为了得到字典序最小的字符串,应该继续从栈顶弹出直到达到 $k-1$ 个,然后栈顶元素就是答案(其实就是栈中第 $x$ 个元素)。

  AC 代码如下,时间复杂度为 $O(n)$:

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

const int N = 1e6 + 10;

char s[N];
char stk[N];

void solve() {
    LL m;
    scanf("%s %lld", s, &m);
    int n = strlen(s), k;
    for (int i = 0; i < n; i++) {
        if (m <= n - i) {
            k = i;
            break;
        }
        m -= n - i;
    }
    int tp = 0;
    for (int i = 0; i < n; i++) {
        while (k && tp && stk[tp] > s[i]) {
            tp--;
            k--;
        }
        stk[++tp] = s[i];
    }
    printf("%c", stk[m]);
}

int main() {
    int t;
    scanf("%d", &t);
    while (t--) {
        solve();
    }
    
    return 0;
}

 

参考资料

  Educational Codeforces Round 156 Editorial:https://codeforces.com/blog/entry/121255