2023 ICPC Macau
ICPC Macau
感觉是一套非常困难的题
A
可以发现选择一个 +1 与去除一个 -1 对行列的效果是一样的,所以我们可以先把所有的 -1 选上。之后改变某个数的选择状态都是对对应的行列和加一。接下来就可以贪心了:一行一行考虑,优先满足缺 +1 多的列:
void solve() {int n = 0;std::cin >> n;std::vector g(n, std::string());for (auto &s : g) {std::cin >> s;}std::vector row(n, 0);std::vector col(n, std::pair<int, int>());for (auto &i : row) {std::cin >> i;}for (int i = 0; i < n; ++i) {std::cin >> col[i].first;col[i].second = i;}std::vector ans(n, std::vector(n, 0));for (int i = 0; i < n; ++i) {for (int j = 0; j < n; ++j) {if (g[i][j] == '-') {row[i] += 1;col[j].first += 1;ans[i][j] = 1;}}}for (int i = 0; i < n; ++i) {std::sort(col.begin(), col.end(), std::greater<std::pair<int, int>>());for (int j = 0; j < n; ++j) {if (row[i] <= 0 || col[j].first <= 0) {break;}row[i] -= 1;col[j].first -= 1;ans[i][col[j].second] ^= 1;}}bool ok = true;for (int i = 0; i < n; ++i) {if (row[i] != 0 || col[i].first != 0) {std::cout << "NO\n";return;}}std::cout << "YES\n";for (auto &v : ans) {for (auto &i : v) {std::cout << i;}std::cout << '\n';}return;
}
D
首先需要观察出符合条件的连通块的类型并不多(其实数量也不多)。注意题目给出的条件:最大度不超过 3。这个条件怎么用呢?考虑一个有 \(x\) 个点且满足条件的连通块,这个连通块至少需要 \(2(x - 1)\) 条边,即单独看红边或蓝边都会形成一颗树,而在最大度不超过 3 的条件下,一个有 \(x\) 个点的连通块最多有 \(3x \over 2\),解 \(2(x - 1) \leq {2x \over 2}\) 得 \(x \leq 4\)。
接下来就是找符合条件的连通块了。
- 一个点天然符合条件
- 两个点符合条件当且仅当两个点之间同时存在两种颜色的边
- 三个点符合条件,则必然有两个点之间同时存在两种颜色的边,然后找一下这两个点是否同时连一个相同的点(注意处理边的颜色)
- 四个点符合条件,此时 4 个点的度数都是 3,而且同色边将这四个点连成一条长度为 3 的链。于是我们对一个点,尝试找以它开始的用红色边连起来的长度为 3 的链,若找得到,则记录这四个点,再看蓝色边能不能将这 4 个点连通起来。
后 3 种情况都会将同一个连通块统计两次,所以我们干脆故意将一个点形成的连通块也统计两次,最后求和除 2 就好了:
std::vector<int> adj[2][N + 5];
// i64 ans;int deg(int idx) {return adj[0][idx].size() + adj[1][idx].size();
}int find(int u, int v) {for (auto &to : adj[0][u]) {if (to != v) {return to;}}return 0;
}int node[4];bool vis[N + 5];
// 检查所枚举的连通块通过蓝边是否连通
bool chk(int cnt) {for (int i = 0; i < cnt; ++i) {vis[node[i]] = false;}std::queue<int> q;q.push(node[0]);vis[node[0]] = true;while (not q.empty()) {int cur = q.front();q.pop();for (auto &to : adj[1][cur]) {if (vis[to]) {continue;}for (int i = 0; i < cnt; ++i) {if (to == node[i]) {q.push(to);vis[to] = true;break;}}}}for (int i = 0; i < cnt; ++i) {if (not vis[node[i]]) {return false;}}return true;
}void solve() {int n = 0, m = 0;std::cin >> n >> m;for (int i = 0; i < m; ++i) {int u = 0, v = 0, w = 0;std::cin >> u >> v >> w;adj[w][u].push_back(v);adj[w][v].push_back(u);}std::array<int, 5> cnt{};for (int i = 1; i <= n; ++i) {// 一个点cnt[1] += 2;int d = deg(i);if (d <= 1) {continue;}// 两个点int j = 0;bool ok = false;for (int &u : adj[0][i]) {for (int &v : adj[1][i]) {if (u == v) {j = u;ok = true;}}}if (ok) {cnt[2] += 1;}// 三个点if (deg(j) == 3 && d == 3) {// 找第 3 个点int k = 0;int ic = 0;if (adj[ic][i].size() == 1) {ic = 1;}if (adj[ic][i][0] == j) {k = adj[ic][i][1];}else {k = adj[ic][i][0];}bool ok = false;for (auto &u : adj[ic ^ 1][j]) {if (u == k) {ok = true;break;}}if (ok) {cnt[3] += 1;}}// 四个点if (adj[0][i].size() == 1) {node[0] = i;node[1] = adj[0][i][0];node[2] = find(node[1], node[0]);node[3] = find(node[2], node[1]);if (chk(4)) {cnt[4] += 1;}}}std::cout << (cnt[1] + cnt[2] + cnt[3] + cnt[4]) / 2 << '\n';return;
}
H
树形 dp 及其优化
首先要分析合法序列的性质:
一个包含 \(1\) 到 \(n\) 每个数各一次的序列一定是合法的,在这个显然的合法的序列的基础上,考虑如何操作才能让这个序列仍然维持合法或破坏合法性。手玩一下能总结出合法序列满足:对于每个子树,序列中包含的这个子树中的点的个数(可重复)一定要大于等于这个子树的大小。而且序列的合法性与序列顺序无关
于是设计 \(f_{u, i}\) 当有 \(i\) 个点都属于 \(u\) 子树中的点时,这 \(i\) 个点的分法。
为了方便解释,我们定义 \(g_{u, j, i}\) 表示当有 \(i\) 个点都属于 \(u\) 的前 \(j\) 个儿子子树时,这 \(i\) 个点的分法,(这 i 个点不包含 \(u\))
先看朴素的转移:
第一个转移是从 \(u\) 的儿子转移到 \(u\),\(siz_j \leq y\) 是为了满足序列合法性的必要条件。第二个转移是当儿子都转移完后,加入一些数 \(u\)。除以阶乘的原因是,当我们最后求答案时,我们想直接输出 \(n! \times f_{1, n}\),但是对于相同的数,我们需要除一个阶乘,这个阶乘我们在转移的时候就给他带上。
显然以上转移一次是 \(O(n^2)\) 的,不可接受。
但实际上,第一个转移中的 \(y\) 还有一个上界,即 \(y < siz_{v_j} + depth_{v_j}\),当大于这个值时位置就不够了。结合下届,我们发现每次只用 \(depth_{v_j} \times depth_{u}\) 次转移。由于树是随机的,这个差不多是 \(log_n^2\) 的量级。
然后就是实现了:
i64 fac[N + 5], ifac[N + 5]; // 阶乘和阶乘逆元
std::vector<int> adj[N + 5];
int siz[N + 5], dep[N + 5];
std::vector<i64> f[N + 5];
i64 g[N + 5], h[N + 5]; // 辅助转移i64 qpow(i64 a, i64 p) {i64 res = 1ll;while (p) {if (p & 1) {(res *= a) %= Mod;}(a *= a) %= Mod;p >>= 1;}return res;
}
i64 inv(i64 a) {return qpow(a, Mod - 2);
}
void add(i64 &a, i64 b) {a += b;if (a >= Mod) {a -= Mod;}
}void trans(int x, int y) {// 旧区间[l, r]// 经过此次转移后得到的新区间 [l + siz[y], r + siz[y]]int l = siz[x], r = siz[x] + dep[x];for (int i = l; i <= r; ++i) {h[i] = g[i];g[i] = 0ll;}for (int i = 1; i <= siz[y]; ++i) {g[r + i] = 0;}for (int i = l; i <= r; ++i) {for (int j = siz[y]; i + j <= r + siz[y] && j < siz[y] + dep[y]; ++j) {add(g[i + j], h[i] * f[y][j - siz[y]] % Mod);}}
}void solve() {int n = 0;std::cin >> n;dep[1] = 1;for (int i = 2; i <= n; ++i) {int f = 0;std::cin >> f;adj[f].push_back(i);dep[i] = dep[f] + 1;}for (int x = n; x >= 1; --x) {siz[x] = 0;for (int i = 0; i <= dep[x]; ++i) {g[i] = 0ll;}g[0] = 1;for (auto &y : adj[x]) {trans(x, y);siz[x] += siz[y];}// 加入等于 x 的数for (int i = siz[x] + dep[x]; i > siz[x]; --i) {for (int j = siz[x]; j < i; ++j) {add(g[i], g[j] * ifac[i - j] % Mod);}}siz[x] += 1;f[x].assign(dep[x], 0ll);for (int i = 0; i < dep[x]; ++i) {f[x][i] = g[i + siz[x]];}}std::cout << f[1][0] * fac[n] % Mod << '\n';
}