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
标签:字符,String,int,Decreasing,字符串,字典,个字符,string From: https://www.cnblogs.com/onlyblues/p/17763323.html