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\) 条赋值语句,每条语句为以下三种之一:
- \(x_i \gets v\),其中 \(v\) 是 \(T, F, U\) 三者之一
- \(x_i \gets x_j\)
- \(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\):
- \(x_1 \gets x_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\) 的来源
- 为 \(U\),说明 \(x_i\) 的最终值为 \(U\),将 \(p_i\) 与 \(p_i'\) 并入同一集合
- 为 \(T\) 或者 \(F\),由于题目保证有解,而 \(x_i\) 的最终值为 \(T\) 或者 \(F\),说明不可能为 \(U\),此时我们不需要做任何处理。
- 为 \(x_j\),说明 \(x_i\) 的值与 \(x_j\) 的值一致,将 \(p_i\) 与 \(p_j\) 并入同一集合,将 \(p_i'\) 与 \(p_j'\) 并入同一集合。
- 为 \(\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\)