Subset Equality

USACO 2022 US Open Contest, Silver

Problem Link: usaco.org/index.php?page=viewproblem2&cpid=1231

算法讲解与题解

题目大意

给定两个字符串 $S$ 和 $T$(只包含 'a'-'r',长度 $\le 10^5$)。

有 $Q$ 个询问($Q \le 10^5$)。

每个询问给出一个字符集(例如 "abc")。我们需要判断:

如果只保留 $S$ 和 $T$ 中属于该字符集的字符,剩下的字符串是否完全相等

示例演示

Original $S$: u s a c o
Original $T$: u s c a o

Query: "ac" (保留 a, c)

S: u s a c o → "ac" T: u s c a o → "ca" ≠ NO

朴素思路 (Naive Approach)

对于每个 Query,直接模拟构建新字符串并比较。


for(string query_str : queries) {
    string s_prime = "", t_prime = "";
    for(char c : S) if(in_query(c)) s_prime += c;
    for(char c : T) if(in_query(c)) t_prime += c;
    if(s_prime == t_prime) print("Y");
    else print("N");
}
                    
  • 复杂度:$O(Q \times N)$
  • 数据范围:$N, Q \le 10^5$
  • 总运算量:$10^{10}$ $\to$ TLE (超时)

核心观察 (Key Observations)

如何快速判断 $S'$ 和 $T'$ 是否相等?

  1. 数量一致性
    对于 Query 中的每个字符 $c$,它在 $S$ 中的出现次数必须等于在 $T$ 中的出现次数。
    (例如:S中有3个'a',T中必须也有3个'a')
  2. 两两相对顺序 (Pairwise Order)
    如果 $S'$ 和 $T'$ 相等,那么对于 Query 中任意两个字符 $(u, v)$,它们在 $S$ 中构成的子序列必须等于在 $T$ 中构成的子序列。
重要结论:
若 Query 中所有字符对 $(u, v)$ 都没有发生“错位”,且数量一致,则整个集合相等。

两两配对原理

只需预处理所有字符对 (a,b), (a,c)... 是否冲突。
字符集 'a'-'r' 只有 18 个字符。
如果 Query = "abc" Check (a, b) Check (a, c) Check (b, c) 如果任何一对有冲突 (Mismatch) ⇒ 整个 Query 结果为 N

总对数:$18 \times 18 = 324$ 对。预处理非常快。

算法流程

Step 1: 预处理 (Preprocessing)

  • 计算 $S$ 和 $T$ 中每个字符的出现次数:cnt[2][char]
  • 对于每一对字符 $(i, j)$($0 \le i, j < 18$):
    • 从 $S$ 中提取只包含 $i, j$ 的子串 $S_0$。
    • 从 $T$ 中提取只包含 $i, j$ 的子串 $T_0$。
    • 如果 $S_0 \neq T_0$,标记 mis[i][j] = true

Step 2: 回答询问 (Query)

  • 检查 Query 中每个字符数量是否一致。
  • 检查 Query 中每两个字符是否在 mis 表中冲突。
  • 全通过则输出 'Y',否则 'N'。

复杂度分析

  • 字符集大小 $C = 18$ ('a' through 'r')
  • 字符串长度 $N = 10^5$
  • Step 1 预处理:
    • 遍历所有对 $(i, j)$:$C^2$ 对。
    • 每对扫描整个字符串:$O(N)$。
    • 总计:$O(C^2 \cdot N) \approx 324 \times 10^5 \approx 3 \times 10^7$ (可接受)
  • Step 2 询问:
    • 单次询问检查所有对:$O(C^2)$。
    • 总计:$O(Q \cdot C^2)$。

代码实现 - 变量定义


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

int q, n;
string s, t;
string s0, t0;

// cnt[0] 存 S 的计数,cnt[1] 存 T 的计数
int cnt[2][20]; 

// mis[i][j] = true 表示字符 i 和 j 发生错位
bool mis[20][20];
                

代码实现 - 预处理


int main() {
    cin >> s >> t;
    // 1. 统计单字符数量
    for (char c : s) cnt[0][c-'a']++;
    for (char c : t) cnt[1][c-'a']++;

    // 2. 预处理所有字符对的错位情况
    for (int i = 0; i < 18; i++) {
        for (int j = 0; j < 18; j++) {
            s0.clear(); t0.clear();
            // 只提取 i 和 j 两个字符构建子串
            for (char c : s) if (c == 'a'+i || c == 'a'+j) s0 += c;
            for (char c : t) if (c == 'a'+i || c == 'a'+j) t0 += c;
            
            if (s0 != t0) mis[i][j] = true;
        }
    }
                

代码实现 - 回答询问


    cin >> q;
    for (int i = 0; i < q; i++) {
        string p;
        cin >> p;
        bool ok = true;
        
        // 检查1: 数量是否一致
        for (char c : p)
            if (cnt[0][c-'a'] != cnt[1][c-'a']) {
                ok = false; break;
            }
            
        // 检查2: 两两是否冲突
        for (char c : p)
            for (char d : p)
                if (c != d && mis[c-'a'][d-'a']) { // O(1) 查询
                    ok = false; break;
                }
        cout << (ok ? "Y" : "N");
    }
    return 0;
}
                

总结

Key Takeaways:

  • 直接模拟 $O(QN)$ 会超时。
  • 字符串子集相等 $\iff$ 字符计数相等 + 所有字符对子序列相等。
  • 利用字符集较小 ($C=18$) 的特点,我们可以花费 $O(C^2 N)$ 进行预处理。
  • 这是一种典型的 "从局部性质推导全局性质" 的思维方式。

谢谢大家

Questions?