Sofia and Strings

看这篇题解

不可逆指的是一旦操作了,就没办法回到原来的序列了,所以根据决策包容性,尽量到后面再操作

我们先来考虑一下最终的对应至少满足什么条件

假设我们现在已经获得了一个\(t\)\(s\)的单射且这个单射可以通过排序变成\(t\),我们依次考虑每一个\(t\)的字符

对于\(t_1\),我们假设其对应\(s_{i_1}\),那么对于\(s[1...i_1-1]\),比\(t_1\)小的字符一定是都会被删除的(否则\(t_1\)不可能排到第一位)

我们对每个\(t_j\)都考虑上述过程,当\(t[1...j-1]\)都被考虑之后,我们来考虑\(t_j\),假设其对应\(s_{i_j}\),对于\(s[1...i_j-1]\)的字符,如果没被删除且没有被\(t[1...j-1]\)选择且小于\(t_j\),那么我们就将其删除。注意对于每个\(t_j\)我们对应的\(s_{i_j}\)一定是没有被删除的,用反证法很容易证明

所以任意一个合法的单射一定可以通过上述测试

那我们再来考虑找一个匹配,先只考虑有删除操作(也就是说我们要按照某种方式找一个\(t\)\(s\)的单射,有可能可以通过排序来变成\(t\),而且如果按照这种方式,单射不存在那么无论按照什么方式来找单射,单射肯定都不会存在)

对于\(t\)的第一个字符\(t_1\),他在\(s\)中有若干对应,分别是\(s_{i_1},s_{i_2},...(i_1<i_2<...)\),我们假设在最终方案中选一个\(s_{i_p}\),对于\(s[1...i_p-1]\),比\(s_{i_p}\)小的字符是一定要删除的(否则\(t_1\)不可能排在第一位,这个单射就不可能通过排序来变成\(t\)了);如果我们不选择\(s_{i_1}\),那么我们就会删除更多的数(比如选择\(s_{i_2}\),首先对于选择\(s_{i_1}\)的时候,要删除的数肯定还是要删除,然后现在还多了一些介于\(i_1\)\(i_2\)之间数可能要被删除),于是如果存在合法的方案,我们在这个方案中,将选择\(s_{i_2}\)变成选择\(s_{i_1}\)一定是合法的,因为我们继续接下来的操作,如果某次操作会因为介于\(i_1\)\(i_2\)之间数没有被删除而无法进行的话,我们这个时候在删除就好了,这也是决策包容性的一种体现

利用数学归纳法,我们可以得出一种方案,假设对于\(t[1...i-1]\)我们已经选择了最佳的方案,那么对于\(t_{i}\),我们首先找到其在剩下的\(s\)中的第一个位置\(s_p\),然后对于\(s[1...p-1]\)的还没有被删除且没有被\(t[1...i-1]\)选择的字符,如果其小于\(t_i\),那么我们就删除掉。经过以上的过程,这一定是必要条件,也就是说如果上面的过程中途出现了不合法,找不到单射,那么就一定不会有单射存在了

那么对于我们找到的这个单射,我们也可以通过排序将其变成合法的序列。我们只需要在\(t_i\)对应的过程中,删除完前面该删除的字符之后,通过类似冒泡排序的邻项交换将其交换到\(t_{i-1}\)后面一位就好了

update 2026.2.18
我们可以将所有的删除操作全部放到排序操作之后(或者之前)执行,这显然是没有问题的。所以不用考虑删除操作对排序的影响
使用数学归纳法做这道题目
倒序考虑\(t\)的每一个字符,假设现在正在考虑\(i\),并且我们已经为\(t_{i+1}\sim t_m\)对应了其在\(s\)中的字符(也就是一定存在一种方案使得这种对应关系是成立的),我们用\(s_{t_j}\)来表示这种对应关系。注意\(t_i\)表示的是字符,而\(s_{t_i}\)表示的是位置
那么考虑\(t_i\)这个字符在\(s\)中还没有选择的所有位置集合\(P=\{P_1,P_2,...,P_k\}\)
显然一个位置\(P_k\)是候选位置当且仅当\(\forall j∈[i+1,m]\),如果\(s_{t_j}<P_k\),那么必须\(t_j\ge t_i\)
考虑所有满足条件的\(P_k\)\(k\)最大的一个作为\(t_i\)的对应位置即可
如果找不到这样的\(P_k\),那么就无解
可以用决策包容性证明。当然证明方法有很多
实现方法:直接为每一个字符用一个set去维护还没有选择的位置,再用一个数组去维护每个字符已经选择了的位置最靠前的位置。每次分配\(t_i\)的时候,首先找出所有小于\(t_i\)的字符中已经选择了的最靠前的位置中最小的位置,再在\(t_i\)的set中找出比这个最小的位置还要靠前的位置中最靠后的位置分配

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

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int T;
    cin >> T;
    while (T--) {
        int n, m;
        cin >> n >> m;
        string s, t;
        cin >> s >> t;

        // 每个字符维护还没选择的位置(1-index)
        array<set<int>, 26> st;
        for (int i = 0; i < n; i++) {
            st[s[i] - 'a'].insert(i + 1);
        }

        const int INF = n + 1;
        // earliest[c]:字符c已经选择的位置里最靠前(最小)的位置;未选则INF
        array<int, 26> earliest;
        earliest.fill(INF);

        bool ok = true;

        // 倒序分配 t_i
        for (int i = m - 1; i >= 0; i--) {
            int c = t[i] - 'a';

            // mn = 所有小于c的字符中“已选择的最靠前位置”的最小值
            int mn = INF;
            for (int x = 0; x < c; x++) mn = min(mn, earliest[x]);

            // 在 st[c] 中找 < mn 的最大位置
            if (st[c].empty()) { ok = false; break; }

            int pickPos = -1;
            if (mn == INF) {
                // 没有更小字符被选过,则可以随便选,取最靠后的位置
                pickPos = *prev(st[c].end());
            } else {
                auto it = st[c].lower_bound(mn); // 第一个 >= mn
                if (it == st[c].begin()) { // 没有 < mn 的元素
                    ok = false;
                    break;
                }
                --it;              // 现在是 < mn 的最大元素
                pickPos = *it;
            }

            // 选择该位置
            st[c].erase(pickPos);
            earliest[c] = min(earliest[c], pickPos);
        }

        cout << (ok ? "YES\n" : "NO\n");
    }
    return 0;
}
posted @ 2024-03-09 16:13  最爱丁珰  阅读(34)  评论(0)    收藏  举报