T1. 社团招新
\(20\) 分:\((n \leqslant 10)\)
直接用 dfs 暴力枚举每个人加入分配到哪一个社团中。在 dfs 的过程中维护当前已经得到总评分 \(sum\),以及当前每个社团的人数 \(c_1, c_2, c_3\) 。当找到一种分配方式后,判断 \(\max(c_1, c_2, c_3)\) 是否不超过 \(\frac{n}{2}\) 并用 \(sum\) 更新答案即可。时间复杂度为 \(O(3^n)\) 。
另外 \(35\) 分:\((n \leqslant 200)\)
不难发现上述 dfs 的过程中,存在大量重复的子问题,考虑用动态规划进行优化。
设 f[i][j][k] 表示当前分配了前 \(i\) 人,且 \(c_1 = j\),\(c_2 = k\),\(c_3 = i-j-k\) 时,能够得到的最大评分总和。
枚举 \(i, j, k\),然后分别考虑当前的第 \(i\) 人分配到哪个社团,可以得到转移方程:
求最终的答案时,枚举两个社团的人数 \(j, k\),然后判断三个社团是否都不超过 \(\frac{n}{2}\) 人,取满足这个条件的最大 \(f[n][j][k]\) 即可,时间复杂度为 \(O(n^3)\) 。
另外 \(5\) 分:(特殊性质 \(A\))
此时 \(a_{i,2}=a_{i,3}=0\),相当于仅有一个社团,直接取最大的 \(\frac{n}{2}\) 个 \(a_{i,1}\) 相加即可。
另外 \(10\) 分:(特殊性质 \(B\))
此时 \(a_{i,3} = 0\),相当于仅有两个社团,若不考虑“人数不超过 \(\frac{n}{2}\)” 的限制,那么答案就是 \(\max(a_{i,1}, a_{i, 2})\) 之和,即每个人取最大的社团评分相加。在此基础上考虑限制,设 \(c_1, c_2\) 为不考虑限制时两个社团的人数,那么:
- 若 \(c_1 = c_2\),则此时两个社团人数都是 \(\frac{n}{2}\) 无需做出任何改变。
- 若 \(c_1 > c_2\),则需要将第一个社团中的 \(c_1 - \frac{n}{2}\) 个人分配到第二个,已知每个人从第一个社团转移到第二个社团后,产生的贡献是 \(a_{i,2}-a_{i,1}\),那么取前 \(c_1 - \frac{n}{2}\) 大的贡献相加即可。
- 若 \(c_1 < c_2\),则处理方式与 \(c_1 > c_2\) 类似。
可以使用优先队列或是 sort 排序来得到前几大的贡献,时间复杂度为 \(O(n\log n)\) 。
另外 \(10\) 分:(特殊性质 \(C\))
此时的 \(a_{i, j}\) 是随机生成的,那么对于第 \(j(j \in \{1, 2, 3\})\) 个社团而言,\(a_{i, j}\) 是三个社团评分中最大的概率约 \(\frac{1}{3}\) 。也就是说,如果我们直接对每个人取 \(\max(a_{i, j})\) 相加,那么最终每个社团的人数约等于 \(\frac{n}{3} \leqslant \frac{n}{2}\),必然满足题目的人数限制。时间复杂度为 \(O(n)\) 。
\(100\) 分:
考虑在“特殊性质 \(B\)”的基础上进行优化,依旧是先将每个人最大的社团评分相加。设 \(p\) 表示人数最多的社团编号,那么:
- 若 \(c_p \leqslant \frac{n}{2}\),则另外两个社团人数肯定也都 \(\leqslant \frac{n}{2}\),直接输出答案。
- 若 \(c_p > \frac{n}{2}\),则此时我们需要从第 \(p\) 个社团中选出 \(c_p - \frac{n}{2}\) 个人移动到其他社团。根据贪心的思想,为了让答案减得更少,需要将这些人移动到他们原本次大评分的社团中。移动后,\(c_p = \frac{n}{2}\) 且另外两个社团的人数总和 \(= \frac{n}{2}\),那么也就是说三个社团的人数都是 \(\leqslant \frac{n}{2}\) 的,符合人数限制。
至此,我们便可以像之前一样,用优先队列或者 sort 排序来得到前 \(c_p - \frac{n}{2}\) 大的“次大减最大”的贡献,加到答案中即可,时间复杂度依然是 \(O(n\log n)\) 。
值得一提的是由于每个人只会选择前两大的社团,因此本题还可以做更多个社团的情况。
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
void solve() {
int n;
cin >> n;
vector a(n, vector<int>(3));
rep(i, n)rep(j, 3) cin >> a[i][j];
int ans = 0;
vector<int> cnt(3); int p = 0;
vector<int> mx(n), mx2(n), js(n);
rep(i, n) {
rep(j, 3) {
int x = a[i][j];
if (mx[i] < x) {
mx2[i] = mx[i];
mx[i] = x;
js[i] = j;
}
else {
mx2[i] = max(mx2[i], x);
}
}
ans += mx[i];
++cnt[js[i]];
if (cnt[js[i]] > cnt[p]) p = js[i];
}
if (cnt[p] <= n/2) {
cout << ans << '\n';
return;
}
priority_queue<int> q;
rep(i, n) {
if (js[i] != p) continue;
q.push(mx2[i]-mx[i]);
}
int k = cnt[p]-n/2;
while (k--) {
ans += q.top();
q.pop();
}
cout << ans << '\n';
}
int main() {
int t;
cin >> t;
while (t--) solve();
return 0;
}
T2. 道路修复
\(16\) 分:\((k \leqslant 0)\)
此时 \(k \leqslant 0\),不需要考虑乡镇对城市带来的影响。因此可以直接使用 \(\text{Kruskal}\) 算法来计算出最小生成树的边权总和即可。具体地,将 \(m\) 条边按照边权从小到大排序,然后依次考虑连接每一条边后是否会形成环。若不形成环,则加入这条边否则不加入。判断是否形成环,可以使用并查集。时间复杂度为 \(O(m\log m)\) 。
另外 \(32\) 分:(特殊性质 \(A\))
此时 \(c_j = 0\),且对于每个乡镇 \(j\),都存在一个城市 \(i\) 满足 \(a_{j, i} = 0\) 。这说明任何一个乡镇都可以通过一个边权为 \(0\) 的边加入到一个已经连通的图中,即需要付出的代价是 \(0\) 。而如果是中途将乡镇与未连通图中的某个连通块进行连接(前提是这个连通块中存在乡镇边权为 \(0\) 的城市),然后再通过城市之间的边将每个城市进行连通,那么仍然能够发现使所有乡镇与城市连通需要的代价还是 \(0\) 。
容易得到一个贪心的想法:既然任何一个时刻将乡镇与城市进行连通,那么不妨一开始就让所有乡镇与它对应的 \(0\) 代价城市相连接。这样一来,就可以在一开始不费任何代价地产生尽可能多的边权更小的边,这些边都是乡镇与城市之间的边,用于原图中的城市相互之间进行连接。具体地,对于乡镇 \(j\) 进行如下额外的连边:
- 令乡镇的编号为 \(n+j\),城市的编号为 \(i(1 \leqslant i \leqslant n)\) ;
- 那么在 \(n+j\) 与 \(i\) 之间连接一条边权为 \(a_{j, i}\) 的边。
随后,将这些额外的边与原本城市之间的边一同排序,进行一次 \(\text{Kruskal}\) 算法即可计算出答案,注意使用 long long 。设考虑到 \(m \geqslant nk\),且 \(n > k\),时间复杂度依然是 \(O(m\log m)\) 。
另外 \(24\) 分:\((k \leqslant 5)\)
注意到 \(k \leqslant 5\) 很小,可以考虑 \(\text{dfs}\) 枚举每个城镇是否使用。具体的做法与上述“特殊性质 \(A\)” 的做法类似,对于枚举到的每一种使用乡镇的情况,只需要把用到的乡镇对应的边额外加入进来即可,以便产生值更小的边用于城市之间的连通。\(\text{dfs}\) 部分的复杂度是 \(O(2^k)\),总时间复杂度是 \(O(2^km\log m)\) 。
\(100\) 分:
复杂度的瓶颈在于每次都将 \(m\) 条边重新排序了,需要考虑将 \(O(m\log m)\) 的排序从 \(2^k\) 中提取出来。不难发现,\(m \leqslant 10^6\) 虽然很大,但是 \(n \leqslant 10^4\) 比较小,能够构成生成树的边至多有 \(O(n)\) 条。于是我们可以先对城市之间的 \(m\) 条边进行一次 \(\text{Kruskal}\),提取出其中有用的 \(n-1\) 条树边。
这样一来,对于 \(\text{dfs}\) 枚举到的每一种乡镇情况,总边数就是 \(O(nk)\) 级别而非 \(O(m+nk)\),时间复杂度优化到了 \(O(m\log m + 2^knk\log (nk))\)。然而,这样的复杂度可能还不足以通过本题。
考虑到每次还是会对边重新排序,为了避免这种情况,我们其实完全可以在 \(\text{dfs}\) 之前,提前将所有的 \(nk\) 条边与 \(n-1\) 条树边放在一起排序。为每条边标记上这条边来自于哪一个城镇,这样一来,每次只需要枚举 \(nk+n-1\) 条边,根据边的标记来判断对应的乡镇当前是否可以使用,进而判断当前的这条边是否可以选上。时间复杂度进一步降到了 \(O(m\log m + 2^knk)\) 。
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
struct UnionFind {
vector<int> d;
UnionFind(int n = 0): d(n, -1) {}
int find(int x) {
if (d[x] < 0) return x;
return d[x] = find(d[x]);
}
bool unite(int x, int y) {
x = find(x); y = find(y);
if (x == y) return false;
if (d[x] > d[y]) swap(x, y);
d[x] += d[y];
d[y] = x;
return true;
}
bool same(int x, int y) {
return find(x) == find(y);
}
int size(int x) {
return -d[find(x)];
}
};
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int n, m, k;
cin >> n >> m >> k;
vector<tuple<int, int, int, int>> nes, es;
rep(i, m) {
int u, v, w;
cin >> u >> v >> w;
--u; --v;
nes.emplace_back(w, u, v, -1);
}
vector<int> c(k);
rep(j, k) {
cin >> c[j];
rep(i, n) {
int w;
cin >> w;
es.emplace_back(w, i, n+j, j);
}
}
sort(nes.begin(), nes.end());
UnionFind uf(n);
for (auto [w, u, v, _] : nes) {
if (!uf.unite(u, v)) continue;
es.emplace_back(w, u, v, -1);
}
sort(es.begin(), es.end());
ll ans = 1e18;
vector<bool> used(k);
auto Kruskal = [&]() {
ll res = 0;
rep(j, k) if (used[j]) res += c[j];
UnionFind uf2(n+k);
for (auto [w, u, v, id] : es) {
if (id != -1 and !used[id]) continue;
if (!uf2.unite(u, v)) continue;
res += w;
}
return res;
};
auto dfs = [&](auto& f, int i) -> void {
if (i == k) {
ans = min(ans, Kruskal());
return;
}
used[i] = true;
f(f, i+1);
used[i] = false;
f(f, i+1);
};
dfs(dfs, 0);
cout << ans << '\n';
return 0;
}
T3. 谐音替换
如果 \((s_{i,1}, s_{i,2})\) 满足条件,我们考虑这次操作对 \(t_{j,1}\) 的影响,如果 \(s_{i,1}\) 与 \(s_{i,2}\) 有一个公共的前缀,那么修改之后这两个前缀是不变的,对于后缀同理。
因此,如果我们设 \(s_{i,1} = C+P+D\),\(s_{i,2} = C+Q+D\),则 \((s_{i,1}, s_{i,2})\) 本质将 \(t_{j,1}\) 中的 \(P\) 变成了 \(Q\),因此 \(t_{j,1}\) 必然可以表示成 \(A+P+B\) 的形式,\(t_{j,2}\) 可以表示成 \(A+Q+B\) 的形式。此时 \(C\) 是 \(A\) 的后缀,\(D\) 是 \(B\) 的前缀。
由上述分析,我们使用 \((C_i, D_i, U_i, V_i)\) 来表示 \((s_{i,1}, s_{i,2}) = (C_i+U_i+D_i, C_i+V_i+D_i)\),使用 \((A_j,B_j,P_j,Q_j)\) 来表示 \((t_{j,1}, t_{j,2}) = (A_j+P_j+B_j,A_j+Q_j+B_j)\) 。
如果 \((s_{i,1}, s_{i,2})\) 满足要求,那么必然有 \((U_i, V_i) = (P_j, Q_j)\),\(C_i\) 是 \(A_j\) 的后缀,\(D_j\) 是 \(B_j\) 的前缀。
同时,经过上面的分析我们也知道,如果 \((s_{i,1}, s_{i,2})\) 满足要求,那么替换的位置是唯一的。
\(50\) 分:\(O(qL)\)
根据上面的分析,我们可以将所有的 \((s_{i,1}, s_{i,2})\) 转为 \((C_i,D_i,U_i,V_i)\) 的形式存储。
对于每个询问,先拆成 \((A,B,P,Q)\) 的形式,然后对于每个 \(i\) 检查是否 \((P,Q) = (U_i, V_i)\),如果成立进一步检查 \(C_i\) 是否为 \(A\) 的后缀,\(D_i\) 是否为 \(B\) 的前缀。
时间复杂度:\(O(qL)\),可以通过前 \(5\) 个数据,以及特殊数据 \(A(q=1)\) 的数据。
\(20\) 分:(特殊性质 \(B\):字符串由 \(ab\) 构成,且 \(b\) 恰出现一次)
延续分析的思路,此时 \((C,D,U,V)\) 中 \(c,D\) 只由 \(a\) 构成,\(U,V\) 必然以一个 \(b\) 开头,另一个以 \(b\) 结尾,因此可以分为两种:\(U\) 以 \(b\) 开头的,\(U\) 以 \(b\) 结尾的,此时进一步可以使用 \((c, d, u) = (|C|, |D|, |U|)\)。\((A,B,P,Q)\) 同理,使用 \(a, b, p\) 来描述。
对于同类型的 \((c, d, u)\) 与 \((a, b, p)\),\((c, d, u)\) 满足要求当且仅当 \(u = p\) 且 \(c \leqslant a\) 且 \(d \leqslant b\) 。
此时有很多做法:
- 将 \((c, d)\) 视为二维平面上一个点,则相当于询问一个矩形内的点数量,使用扫描线处理。时间复杂度为 \(O(L+q\log n)\) 。
- 注意到 \(\sum c \leqslant L\),因此如果将 \(c\) 离散化之后最多只有 \(O(\sqrt{L})\) 个不同的 \(c\),\(d\) 也最多只有 \(O(\sqrt{L})\) 个不同的,此时可以离散化之后直接二维前缀和。时间复杂度为 \(O(L)\) 。
- \(\cdots\)
\(100\) 分:
回到我们的分析结论,有 \(n\) 个四元组 \((C_i,D_i,U_i,V_i)\) 以及 \(q\) 个询问 \((A_j,B_j, P_j, Q_j)\),询问有多少个 \(i\) 满足 \((U_i,V_i) = (P_j,Q_j)\),且 \(C_i\) 是 \(A_j\) 的后缀,\(D_i\) 是 \(B_j\) 的前缀。
\((U_i,V_i) = (P_j,Q_j)\) 好办,直接把 \(U_i, V_i\) 拼成一个字符串 \(U_i + V_i\),\(P_j,Q_j\) 拼成一个字符串 \(P_j+Q_j\),使用 \(\text{hash}\) 或 \(\text{Trie}\) 树处理 \(U_i+V_i\),将所有 \(U_i+V_i\) 相同的视为同一类。那么我们只需要在 \(P_j+Q_j\) 这一类中寻找答案即可。
那么问题变成 \(P_j+Q_j\) 这一类中,有多少个 \((C_i, D_i)\) 满足 \(C_i\) 是 \(P_j\) 的后缀,\(D_i\) 是 \(Q_j\) 的前缀,有多种做法:
-
\(\text{AC}\) 自动机
- 使用特殊字符
#,将 \(C_i, D_i\) 以及 \(A_j, B_j\) 拼接,问题等价于有多少 \(X_i = C_i + \text{#} + D_i\) 是 \(Y_j = A_j + \text{#} + B_j\) 的子串。 - 使用 \(\text{AC}\) 自动机求解,对 \(X_i\) 建立自动机。如果 \(X_i\) 是 \(Y_j\) 的子串 \(Y_j[l:r]\),则 \(X_i\) 是 \(Y_j\) 前缀 \(Y_j[1:r]\) 的后缀,问题变成有多少个 \(Y_j\) 的前缀的后缀是 \(X_i\) 。
- 可以得到 \(Y_j\) 的每个前缀 \(Y_j[1:r]\) 在 \(\text{AC}\) 自动机上对应的点 \(t\),则满足是 \(Y_j[1:r]\) 后缀的字符串都是 \(t\) 的祖先,预处理祖先有多少个 \(X_i\) 即可。
- 由于每个 \(X_i\) 最多在 \(Y_j\) 中出现一次,这样不会算重。
- 时间复杂度:\(O(L)\) 。
- 使用特殊字符
-
二维数点
- 离线
- 对 \(D_i, B_j\) 建立 \(\text{Trie}\) 树并求出 \(\text{DFS}\) 序,则 \(D_i\) 是 \(B_j\) 的前缀等价于 \(B_j\) 在 \(D_i\) 的子树中。对 \(C_i, A_j\) 同样处理,不过需要先将字符串 \(\text{reverse}\) 。
- 问题变成对于每个 \(B_j\),\(\text{reverse}(A_j)\),询问有多少 \(D_i\) 是 \(B_j\) 的祖先,且 \(\text{reverse}(C_i)\) 是 \(\text{reverse}(A_j)\) 的祖先。将 \(D_i, \text{reverse}(C_i)\) 视为一个矩形 \([l_D, r_D] \times [l_C, r_C]\),其中 \([l_D, r_D]\) 为 \(D_i\) 子树 \(\text{DFS}\) 序,\([l_C,r_C]\) 为 \(\text{reverse}(C_i)\) 子树 \(\text{DFS}\) 序;将 \(B_j, \text{reverse}(A_j)\) 视为二维平面一个点 \((b, a)\),分别对应 \(\text{DFS}\) 序。则问题变成一个点被多少个矩形包含。
- 使用扫描线即可。时间复杂度:\(O(L+(n+q)\log (n+q))\) 。
-
\(\cdots\)
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
struct Aho {
bool inited;
using MP = map<char, int>;
vector<MP> to;
vector<int> cnt, fail;
Aho(): to(1), cnt(1) {}
int add(const string& s) {
int v = 0;
for (char c : s) {
if (!to[v].count(c)) {
to[v][c] = to.size();
to.push_back(MP());
cnt.push_back(0);
}
v = to[v][c];
}
cnt[v]++;
return v;
}
void init() {
fail = vector<int>(to.size(), -1);
queue<int> q;
q.push(0);
while (q.size()) {
int v = q.front(); q.pop();
for (auto [c, u] : to[v]) {
fail[u] = (*this)(fail[v], c);
cnt[u] += cnt[fail[u]];
q.push(u);
}
}
}
int operator()(int v, char c) const {
while (v != -1) {
auto it = to[v].find(c);
if (it != to[v].end()) return it->second;
v = fail[v];
}
return 0;
}
int operator[](int v) const { return cnt[v]; }
ll query(string s) {
ll res = 0;
int v = 0;
for (char c : s) {
v = (*this)(v, c);
res += cnt[v];
}
return res;
}
};
string f(const string& a, const string& b) {
int n = a.size();
int l = 0, r = n-1;
while (a[l] == b[l]) ++l;
while (a[r] == b[r]) --r;
return a.substr(0, l) + '#' + a.substr(l, r-l+1) + '#'
+ b.substr(l, r-l+1) + '#' + b.substr(r+1, n-r+1);
}
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int n, m;
cin >> n >> m;
Aho aho;
rep(i, n) {
string a, b;
cin >> a >> b;
if (a == b) continue;
aho.add(f(a, b));
}
aho.init();
rep(i, m) {
string a, b;
cin >> a >> b;
if (a.size() != b.size()) {
cout << 0 << '\n';
continue;
}
cout << aho.query(f(a, b)) << '\n';
}
return 0;
}
T4. 员工招聘
\(8\) 分:\(n \leqslant 10\)
\(O(n!)\) 枚举所有可能的排列。再模拟判断是否满足要求即可。
时间复杂度:\(O(n!)\) 或者 \(O(n \cdot n!)\)
\(20\) 分:\(n \leqslant 18\)
使用状压 \(\text{dp}\),设 \(f(i, S, j)\) 表示前 \(i\) 场面试,集合 \(S\) 中的面试者参加,总共通过了 \(j\) 位面试者。
转移时枚举下一位面试者 \(k\),如果 \(s_i = 0\) 或者 \(i-j \geqslant c_k\) 则这位面试者不通过,记录 \(r=0\),否则面试者通过,记录 \(r=1\),则转移为
时间复杂度:\(O(n^32^n)\)。看起来过不了,实际上 \(i = |S|\)。所以不需要将 \(i\) 加入状态,时间复杂度:\(O(n^22^n)\)。可以通过这部分。
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
const int mod = 998244353;
//const int mod = 1000000007;
struct mint {
ll x;
mint(ll x=0):x((x%mod+mod)%mod) {}
mint operator-() const {
return mint(-x);
}
mint& operator+=(const mint a) {
if ((x += a.x) >= mod) x -= mod;
return *this;
}
mint& operator-=(const mint a) {
if ((x += mod-a.x) >= mod) x -= mod;
return *this;
}
mint& operator*=(const mint a) {
(x *= a.x) %= mod;
return *this;
}
mint operator+(const mint a) const {
return mint(*this) += a;
}
mint operator-(const mint a) const {
return mint(*this) -= a;
}
mint operator*(const mint a) const {
return mint(*this) *= a;
}
mint pow(ll t) const {
if (!t) return 1;
mint a = pow(t>>1);
a *= a;
if (t&1) a *= *this;
return a;
}
// for prime mod
mint inv() const {
return pow(mod-2);
}
mint& operator/=(const mint a) {
return *this *= a.inv();
}
mint operator/(const mint a) const {
return mint(*this) /= a;
}
};
istream& operator>>(istream& is, mint& a) {
return is >> a.x;
}
ostream& operator<<(ostream& os, const mint& a) {
return os << a.x;
}
int main() {
int n, m;
string S;
cin >> n >> m >> S;
vector<int> c(n);
rep(i, n) cin >> c[i];
int n2 = 1<<n;
vector dp(n2, vector<mint>(n+1));
dp[0][0] = 1;
rep(s, n2) {
int i = __builtin_popcount(s);
rep(j, i+1) {
rep(k, n) if (~s>>k&1) {
int r = S[i] == '0' or i-j >= c[k] ? 0 : 1;
dp[s|1<<k][j+r] += dp[s][j];
}
}
}
mint ans;
for (int j = m; j <= n; ++j) ans += dp[n2-1][j];
cout << ans << '\n';
return 0;
}
\(12\) 分:\((m=1)\)
容斥处理,答案为 \(n!\) 减去所有面试者都不通过的方案数。
如果 \(s_i = 1\) 则所有面试者都通过,但实际上并没有,说明面试者拒绝面试。而根据计算目标可知前 \(i-1\) 场面试有 \(i-1\) 位面试者不通过,因此要求为 \(c_j \leqslant i-1\) 。
如果 \(s_i = 0\) 则所有面试者都不通过,此时相当于要求 \(c_j \leqslant n\) 的面试者。
因此如果 \(s_i=1\) 则 \(t_i=i-1\),否则 \(t_i = n\)。将 \(t\) 从小到大排序之后递推即可。
设 \(q_k\) 表示 \(c_j \leqslant k\) 的面试者数量,则方案数为:\(\prod (q_{t_i}-i+1)\) 。
时间复杂度:\(O(n\log n)\) 。
\(4\) 分:\(m=n\)
如果存在 \(s_i = 0\) 或者 \(c_j = 0\) 则必然不可能,方案数为 \(0\),否则必然都成立,方案数为 \(n!\) 。
时间复杂度:\(O(n)\)
\(16\) 分:(特殊性质 \(B\):\(\sum s_i \leqslant 18\))
只有少量面试者可能通过,此时枚举 \(S\) 表示集合 \(S\) 中面试的面试者通过了,其他面试者全部失败。
对于 \(s_i = 1\) 且 \(i \in S\) 的面试者,他通过了,他前面有 \(i-1 - \sum\limits_{x \in S} [x < i]\) 位面试者失败,因此要求 \(c_j > i-1-\sum\limits_{x \in S} [x < i]\) 。
对于 \(s_i = 1\) 且 \(i \notin S\) 的面试者,他拒绝面试,则要求 \(c_j \leqslant i-1-\sum\limits_{x \in S} [x < i]\) 。
对于 \(s_i = 0\),必定失败,\(c_j \leqslant n\) 。
发现对 \(c_j\) 的要求有大于还有小于等于,使用容斥,将大于号容斥成小于号,则可以使用 \(m=1\) 时的做法。
令 \(k = \sum s_i\),直接做时间复杂度为 \(O(k3^k)\) 。
继续枚举集合 \(S\),然后不再枚举 \(S\) 的子集 \(T\) 进行计算,而是使用状压 \(\text{dp}\) 计算,设 \(f(i, j)\) 表示在前 \(i\) 个 \(s\) 为 \(1\) 的位置,容斥了 \(j\) 次的方案数(\(c > x\) 等价于 \(c \leqslant n\) 减去 \(c \leqslant x\),这里的 \(j\) 表示前者的选取次数)。转移时分情况讨论:
- 如果 \(i \notin S\),则直接乘,设 \(s\) 第 \(i\) 个 \(1\) 的位置为 \(p\),则有 \(a = p-1-\sum\limits_{x \in S} [x < p]\) 位面试者失败,之前已经计算了 \(j\) 个容斥的 \(\leqslant\) 以及钦定的 \(b = i-1-\sum\limits_{x \in S} [x < p]\) 个 \(\leqslant\),因此转移为 \(f(i+1, j) \gets f(i, j)(q_a-j-b)\) 。
- 如果 \(i \in S\),若取 \(c \leqslant n\),则 \(f(i+1, j) \gets f(i, j)\);否则 \(f(i+1, j+1) \gets -f(i, j)(q_a-j-b)\) 。
最后统计答案,计算上 \(c \leqslant n\) 的即可,对于 \(f(i, j)\) 总共有 \(n-k+|S|-j\) 个 \(\leqslant n\),乘上一个阶乘即可。
时间复杂度:\(O(k^22^k)\) 。
\(100\) 分:
有两种做法:
-
容斥
- 发现特殊性质 \(B\) 中的 \(S\) 可以放入转移方程,因为我们计算只需要知道 \(\sum\limits_{x \in S} [x < p]\),这实际上就是当前 \(S\) 的大小 \(|S|\) 。
- 因此设 \(f(i, j, k)\) 表示在前 \(i\) 个 \(s\) 为 \(1\) 的位置,容斥了 \(j\) 次,此时 \(|S|=k\) 时的方案数。转移类似,不再赘述。
- 时间复杂度:\(O(n^3)\) 。
-
直接 \(\text{dp}\)
- 还可以直接 \(\text{dp}\) 。设之前有 \(j\) 位面试者不通过,则对于尚未参与面试的 \(c \leqslant j\) 的面试者无论参加 \(s\) 如何的面试均会失败,而 \(c > j\) 的面试者参加 \(s=1\) 的面试能通过。
- 此时可以将所有人分为两部分:\(c \leqslant j\) 以及 \(c > j\),对于前者我们需要知道具体每个人是否参加面试,对于后者,我们并不需要知道谁具体参加了面试(谁去都一样),只需要在 \(j\) 增大时处理一下(此时有一部分面试者从 \(c > j\) 的后者变成了前者)。
- 设 \(f(i, j, k)\) 表示前 \(i\) 场面试,有 \(j\) 位面试者未通过,还有 \(k\) 位 \(c \leqslant j\) 的面试者尚未参加面试。转移分为三种(设 \(a_x\) 表示有多少面试者 \(c=x\),\(b\) 为 \(a\) 的前缀和):
- 其一:一个 \(c \leqslant j\) 的面试者参加了第 \(i\) 场面试,此时不论 \(s\) 如何均不通过。转移到 \(f(i+1, j+1, \cdot)\),因此 \(c=j+1\) 的面试会从后者变成前者,此时需要考虑这些面试者是否参加了之前的面试。之前有 \(i\) 场面试,\(c \leqslant j\) 的面试者总共 \(b_j\) 人,有 \(k\) 人未参加面试,说明有 \(b_j-k\) 位面试者参加了面试,还有 \(i-b_j+k\) 场面试是由 \(c > j\) 的面试者参加的,这其中有 \(t\) 场是由 \(c=j+1\) 的面试者参加的,则转移之后 \(c \leqslant j+1\) 的面试者共 \(k-1+a_{j+1}-t\) 位。
- \(f(i+1, j+1, k-1+a_{j+1}-t) \gets f(i, j, k) \times k \times \dbinom{i-b_j+k}{t}\dbinom{a_{j+1}}{t}t!\)
- 其二:\(s_i=0\),一个 \(c > j\) 的面试者参加,同样枚举 \(i+1-b_j+k\) 场 \(c > j\) 的面试者参加的面试中,有 \(t\) 场是由 \(c=j+1\) 的面试者参加的。
- \(f(i+1, j+1, k+a_{j+1}-t) \gets f(i, j, k) \times \dbinom{i+1-b_j+k}{t}\dbinom{a_{j+1}}{t}t!\)
- 其三:\(s_i=1\),一个 \(c > j\) 的面试者参加,此时 \(j\) 不变,方程最简单。
- \(f(i+1, j, k) \gets f(i, j, k)\)
- 注意到当 \(j=0 \sim n\) 时 \(\sum t = \sum b_{j+1} = n\),因此时间复杂度为 \(O(n^3)\) 。
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
const int mod = 998244353;
//const int mod = 1000000007;
struct mint {
ll x;
mint(ll x=0):x((x%mod+mod)%mod) {}
mint operator-() const {
return mint(-x);
}
mint& operator+=(const mint a) {
if ((x += a.x) >= mod) x -= mod;
return *this;
}
mint& operator-=(const mint a) {
if ((x += mod-a.x) >= mod) x -= mod;
return *this;
}
mint& operator*=(const mint a) {
(x *= a.x) %= mod;
return *this;
}
mint operator+(const mint a) const {
return mint(*this) += a;
}
mint operator-(const mint a) const {
return mint(*this) -= a;
}
mint operator*(const mint a) const {
return mint(*this) *= a;
}
mint pow(ll t) const {
if (!t) return 1;
mint a = pow(t>>1);
a *= a;
if (t&1) a *= *this;
return a;
}
// for prime mod
mint inv() const {
return pow(mod-2);
}
mint& operator/=(const mint a) {
return *this *= a.inv();
}
mint operator/(const mint a) const {
return mint(*this) /= a;
}
};
istream& operator>>(istream& is, mint& a) {
return is >> a.x;
}
ostream& operator<<(ostream& os, const mint& a) {
return os << a.x;
}
int main() {
int n, m;
string S;
cin >> n >> m >> S;
m = n-m;
vector<int> a(n+1);
rep(i, n) {
int c;
cin >> c;
a[c]++;
}
auto s = a;
rep(i, n) s[i+1] += s[i];
vector comb(n+1, vector<mint>(n+1));
comb[0][0] = 1;
rep(i, n)rep(j, i+1) {
comb[i+1][j] += comb[i][j];
comb[i+1][j+1] += comb[i][j];
}
vector<mint> facs(n+1, 1);
rep(i, n) facs[i+1] = facs[i]*(i+1);
vector dp(n+1, vector<mint>(n+1));
dp[0][s[0]] = 1;
rep(i, n) {
vector old(n+1, vector<mint>(n+1));
swap(dp, old);
rep(j, m+1) {
for (int k = max(0, s[j]-i); k <= s[j]; ++k) {
if (k) {
rep(t, min(a[j+1], i-s[j]+k)+1) {
dp[j+1][k-1+a[j+1]-t] += old[j][k] * k * comb[i-s[j]+k][t] * comb[a[j+1]][t]*facs[t];
}
}
if (S[i] == '0') {
rep(t, min(a[j+1], i+1-s[j]+k)+1) {
dp[j+1][k+a[j+1]-t] += old[j][k] * comb[i+1-s[j]+k][t] * comb[a[j+1]][t] * facs[t];
}
}
else dp[j][k] += old[j][k];
}
}
}
mint ans;
rep(j, m+1) ans += dp[j][0]*facs[n-s[j]];
cout << ans << '\n';
return 0;
}
浙公网安备 33010602011771号