T1:词典

题意:

给定 \(n\) 个长度为 \(m\) 的字符串 \(w_1, w_2, \cdots, w_n\)
对于每个 \(i = 1, 2, \cdots, n\) 询问是否存在 \(w_1', w_2', \cdots, w_n'\) 使得对于每个 \(j = 1, 2, \cdots, n\)\(w_j'\) 都可以由 \(w_j\) 交换字符得到,且对于 \(j \neq i\) 都有 \(w_i'\) 的字典序小于 \(w_j'\)

数据范围:

\( 1 \leqslant n \leqslant 3000 \), \( 1 \leqslant m \leqslant 3000 \), 所有字符串均由小写字母构成

算法分析

做法1:

对于每个 \(i\), 如果存在方案的话必然可以让 \(w_i'\) 字典序最小,而其他的 \(j \neq i\)\(w_j'\) 字典序最大。也就是让 \(w_i\) 的字符从小到大排序得到 \(w_i'\),让 \(w_j\) 的字符从大到小排序得到 \(w_j'\)

直接模拟这个过程的时间复杂度为 \(O(n^2m)\),期望得分 \(80\) 分。
但实际上可以拿 \(100\) 分。

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;

int main() {
    freopen("dict.in", "r", stdin);
    freopen("dict.out", "w", stdout);
    
    int n, m;
    cin >> n >> m;
    
    vector<string> w(n);
    rep(i, n) cin >> w[i];
    
    vector<string> mn(n);
    rep(i, n) {
        mn[i] = w[i];
        sort(mn[i].begin(), mn[i].end());
    }
    vector<string> mx(n);
    rep(i, n) {
        mx[i] = w[i];
        sort(mx[i].rbegin(), mx[i].rend());
    }
    
    string ans;
    rep(i, n) {
        bool ok = true;
        rep(j, n) if (j != i) {
            if (mn[i] > mx[j]) {
                ok = false;
                break;
            }
        }
        ans += ok ? '1' : '0';
    }
    
    cout << ans << '\n';
    
    return 0;
}

做法2:

实际上我们发现没有必要模拟上述过程,因为得到的 \(w_i'\) 以及 \(w_j'\) 一定是一段一段的,最多 \(\sigma = 26\) 段,使用桶存下每个字符的出现次数,之后一段一段比较即可在 \(O(nm+\sigma n^2)\) 的复杂度内解决。期望得到 \(80 \sim 100\) 分。

做法3:

不妨再深入一步,设 \(f_i\) 表示 \(w_i\) 中出现的最小字母, \(g_i\) 表示 \(w_i\) 中出现的最大字母。
如果 \(f_i < g_i\),那么显然 \(w_i'\) 的字典序小于 \(w_j'\),只需要将 \(f_i\) 作为 \(w_i'\) 的第一个字母,\(g_j\) 作为 \(w_j'\) 的第一个字母即可。
如果 \(f_i > g_j\),那么显然 \(w_i'\) 的字典序大于 \(w_j'\)
剩下最后一种情况 \(f_i = g_j\),想要 \(w_i'\) 的字典序最小,需要将 \(f_i\) 放在最前面,此时越往后 \(w_i'\) 的字母越大。想要 \(w_j'\) 的字典序最大,需要将 \(g_j\) 放在最前面,此时越往后 \(w_j'\) 的字母越小。因此此时必然有 \(w_i'\) 的字典序大于 \(w_j'\)
综上,如果 \(i\) 可能,当且仅当 \(f_i < \min\limits_{j \neq i} g_j\)。因此可以 \(O(n)\) 完成这个过程,总的时间复杂度为 \(O(nm+n^2)\)
期望得分:100分。

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;

int main() {
    freopen("dict.in", "r", stdin);
    freopen("dict.out", "w", stdout);
    
    int n, m;
    cin >> n >> m;
    
    vector<int> f(n, 26), g(n, -1);
    rep(i, n) {
        string w;
        cin >> w;
        rep(j, m) {
            f[i] = min(f[i], w[j]-'a');
            g[i] = max(g[i], w[j]-'a');
        }
    }
    
    string ans;
    rep(i, n) {
        bool ok = true;
        rep(j, n) if (j != i) {
            if (f[i] >= g[j]) {
                ok = false;
                break;
            }
        }
        ans += ok ? '1' : '0';
    }
    
    cout << ans << '\n';
    
    return 0;
}

T2:三值逻辑

题意:

有三种变量:\(T, F, U\) 以及一种运算 \(\neg\)。且满足运算法则:\(\neg T = F\)\(\neg F = T\)\(\neg U = U\)
已知现在有 \(n\) 个变量 \(x_1, x_2, \cdots, x_n\),有 \(m\) 条赋值语句,每条语句为以下三种之一:

  1. \(x_i \gets v\),其中 \(v\)\(T, F, U\) 三者之一
  2. \(x_i \gets x_j\)
  3. \(x_i \gets \neg x_j\)

在执行 \(m\) 条语句之前,会先对变量赋初始值。求所有使得执行 \(m\) 条语句之后,每个变量初始值与最终值相等的初始赋值中,\(U\) 变量的最小值。数据保证有解。

数据范围:

\(1 \leqslant t \leqslant 6\)\(1 \leqslant n, m \leqslant 10^5\)。多组数据,\(t\) 表示数据组数。

算法分析

做法1:

直接 \(O(3^n)\) 枚举每种可能的初始值,之后再使用 \(O(m)\) 的时间判断,可以获得 \(20\) 分。

做法2:(\(v\) 可能取值 \(TFU\)
此时可以发现,一个变量需要赋值为 \(U\) 当且仅当 \(m\) 条语句结束之后变量的值为 \(U\),因此我们可以记录每个变量的状态:未赋值,赋值为 \(T\),赋值为 \(F\),赋值为 \(U\)。这个可以 \(O(m)\) 得到。
期望得分:\(20\) 分。

做法3:(\(v\) 可能取值为 \(U+\)
此时与上一种情况略有区别,一个变量需要赋初值为 \(U\) 除了最终值为 \(U\) 之外,还多了某个变量最终值为 \(U\),而 \(x_i\) 的最终值为这个变量。例如 \(m = 2\)

  1. \(x_1 \gets x_2\)
  2. \(x_2 \gets U\)

此时由于 \(x_2\) 的值为 \(U\),导致 \(x_1\) 的值也必须为 \(U\)
这就启发我们要记录 \(x_i\) 的最终值是哪里来的。而这是好记录的,初始时每个 \(x_i\) 的来源都是本身,当执行 \(x_i \gets x_j\) 时,记录 \(x_i\) 的来源为 \(x_j\) 的来源即可,而对于 \(x_i \gets U\),则直接标记来源为 \(U\),如果某个值最终来源为 \(U\),则说明其最终值为 \(U\)
最终可以使用并查集(如果 \(x_i\) 的来源是 \(x_j\),说明 \(x_i\) 的最终值等于 \(x_j\) 的初始值,进一步 \(x_i\) 的初始值等于 \(x_j\) 的初始值,因此将 \(x_i\)\(x_j\) 放入一个集合),如果集合内有一个变量初始值为 \(U\),则集合内所有变量的初始值必须为 \(U\)
时间复杂度为 \(O(n+m)\)
期望得分:\(20\) 分。

做法4:

最终我们可以沿用上述思路,记录每个变量的来源(注意 \(x_i \gets \neg x_j\)\(i\) 可能等于 \(j\))以及是否被取反(也就是逻辑非运算)。
之后我们可以使用并查集维护命题,具体来说对于每个 \(x_i\) 建立两个点,\(p_i\) 表示命题 \(x_i = T\)\(p_i'\) 表命题 \(x_i = F\)。显然正常情况 \(p_i\)\(p_i'\) 是不会在同一个集合中的,而一旦它们在同一个集合中,就只能说明 \(x_i = \neg x_i\),即 \(x_i = U\)
之后对于每个 \(i\) 考虑 \(x_i\) 的来源

  1. \(U\),说明 \(x_i\) 的最终值为 \(U\),将 \(p_i\)\(p_i'\) 并入同一集合
  2. \(T\) 或者 \(F\),由于题目保证有解,而 \(x_i\) 的最终值为 \(T\) 或者 \(F\),说明不可能为 \(U\),此时我们不需要做任何处理。
  3. \(x_j\),说明 \(x_i\) 的值与 \(x_j\) 的值一致,将 \(p_i\)\(p_j\) 并入同一集合,将 \(p_i'\)\(p_j'\) 并入同一集合。
  4. \(\neg x_j\),说明 \(x_i\) 的值与 \(x_j\) 的值相反,将 \(p_i\)\(p_j'\) 并入同一集合,将 \(p_i'\)\(p_j\) 并入同一集合。

最终 \(p_i\)\(p_i'\) 在同一集合的变量 \(i\) 的个数就是所求答案。
时间复杂度为 \(O(n+m)\)
期望得分:\(100\)

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)

using namespace std;

const int N = 2e5+5;
const int U = N-1;
const int T = N-2;

int val[N], p[N];

int root(int x) {
    if (p[x] == x) return x;
    return p[x] = root(p[x]);
}

void merge(int x, int y) {
    p[root(x)] = root(y);
}

bool same(int x, int y) {
    return root(x) == root(y);
}

void solve() {
    int n, m;
    cin >> n >> m;
    
    rep(i, n) val[i] = i;
    
    rep(mi, m) {
        char v; int i, j;
        cin >> v >> i;
        if (v == '+' or v == '-') {
            cin >> j; 
            int x = val[j];
            val[i] = v == '-' ? -x : x;
        }
        else {
            val[i] = v == 'U' ? U : T;
        }
    }
    
    rep(i, n*2) p[i] = i;
    rep(i, n) {
        if (abs(val[i]) == U) merge(i, i+n);
        else if (abs(val[i]) == T) continue;
        else if (val[i] > 0) {
            merge(val[i], i); 
            merge(val[i]+n, i+n);
        }
        else if (val[i] < 0) {
            merge(-val[i], i+n);
            merge(-val[i]+n, i);
        }
    }
    
    int ans = 0;
    rep(i, n) if (same(i, i+n)) ans++;
    
    cout << ans << '\n';
}

int main() {
    freopen("tribool.in", "r", stdin);
    freopen("tribool.out", "w", stdout);
    
    int c, t;
    cin >> c >> t;
    
    while (t--) solve();
    
    return 0;
}

T3:双序列拓展

题意:

\(B = \{b_1, b_2, \cdots, b_n\}\)\(A = \{a_1, a_2, \cdots, a_m\}\) 的扩展,当且仅当存在 \(L = \{l_1, l_2, \cdots, l_m\}\) 使得 \(B\) 是将 \(A\) 中的 \(a_i\) 替换为 \(l_i\)\(a_i\) 得到的。
给出长度为 \(n\) 的序列 \(X\) 以及长度为 \(m\) 的序列 \(Y\),问是否存在 \(X\) 的拓展 \(F\),以及 \(Y\) 的拓展 \(G\),满足 \(F\)\(G\) 的长度均为 \(10^{100}\),且对于任意的 \(1 \leqslant i, j \leqslant 10^{100}\) 都有 \((f_i-g_i)(f_j-g_j) > 0\)
\(q\) 组数据,每组数据都是在原序列的基础上修改若干个元素得到的。

数据范围:

  • \(1 \leqslant n, m \leqslant 5 \times 10^5\)
  • \(1 \leqslant q \leqslant 60\)
  • \(1 \leqslant a_i, b_i \leqslant 10^9\)
  • 修改的数量之和不超过 \(5 \times 10^5\)