AtCoder Grand Contest 041 简要题解
从这里开始
Problem A Table Tennis Training
如果两个人位置奇偶性相同,那么一定是两个人同时往中间走。
否则是两个人走到边上使得奇偶性相同,然后再像上面那样做。
Code
#include <bits/stdc++.h> using namespace std; typedef bool boolean; #define ll long long ll n, a, b; int main() { scanf("%lld%lld%lld", &n, &a, &b); ll ans = -1; if (!((b - a) & 1)) { ans = (b - a) >> 1; } else { ans = min(a, n - b + 1) + ((b - a - 1) >> 1); } printf("%lld\n", ans); return 0; }
Problem B Voting Judges
不难发现,初试分数越大比越小的题目有更大的可能性可行。考虑二分答案,问题是怎么 check。
条件相当于是要求严格比它的大的至多有 $P - 1$ 个。
考虑先找最大的 $P - 1$ 个每个人都投。剩下的题目是让尽可能多的人投。
每次分配投的人的时候让已经投的题数最少的人投,这样至多有 2 种投的题数的人。
Code
#include <bits/stdc++.h> using namespace std; typedef bool boolean; const int N = 1e5 + 5; int n, m, V, P; int a[N]; boolean check(int mid) { int hig = V - 1, r1 = m, r2 = 0; int cnt = 0, r = n; for (r = n; r && cnt < P - 1; r--) { if (r == mid) continue; hig--, cnt++; } for (int i = 1; i <= r; i++) { if (i == mid) continue; if (a[i] > a[mid] + m) return false; int need = min(m, a[mid] + m - a[i]); if (r1 > need) { r1 -= need; r2 += need; } else { r1 = need - r1; r2 = m - r1; swap(r1, r2); hig--; } } return hig <= 0; } int main() { scanf("%d%d%d%d", &n, &m, &V, &P); for (int i = 1; i <= n; i++) { scanf("%d", a + i); } sort(a + 1, a + n + 1); int l = 1, r = n; while (l <= r) { int mid = (l + r) >> 1; if (check(mid)) { r = mid - 1; } else { l = mid + 1; } } printf("%d\n", n - r); return 0; }
Problem C Domino Quality
当 $n < 3$ 的时候显然无解。
手玩 $n = 3, 4, 5, 6, 7$,除了 $n = 3$ 的时候要求每行每列恰好有 1 个,剩下都要求有 3 个,然后剩下可以拿它们凑一下就行了。
Code
#include <bits/stdc++.h> using namespace std; typedef bool boolean; const int N = 1005; char ans3[12][12] = {"aa.", "..a", "..a"}; char ans4[12][12] = {"aacd", "bbcd", "dcaa", "dcbb"}; char ans5[12][12] = {"aabc.", "..bcd", "iij.d", "n.jaa", "nkkbb"}; char ans7[12][12] = {"aade...", "bbde...", "ccffg..", "..i.gee", "..ihhdd", "....cba", "....cba"}; char ans10[12][12] = {"aacd......", "bbcd......", "..eeab....", "..ffab....", "ef..cc....", "ef..dd....", "......aacd", "......bbcd", "......dcaa", "......dcbb"}; int n; char ans[N][N]; void put(char (*T)[12], int x, int d) { for (int i = 0; i < d; i++) { for (int j = 0; j < d; j++) { ans[x + i][x + j] = T[i][j]; } } } int main() { scanf("%d", &n); for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { ans[i][j] = '.'; } } if (n == 1 || n == 2) { puts("-1"); return 0; } else if (!(n % 3)) { for (int i = 0; i < n; i += 3) put(ans3, i, 3); } else if (n == 4) { put(ans4, 0, 4); } else if (n == 5) { put(ans5, 0, 5); } else if (n == 7) { put(ans7, 0, 7); } else if (n == 10) { put(ans10, 0, 10); } else if (n == 11) { put(ans4, 0, 4); put(ans7, 4, 7); } else { int c4 = 0, t = 0; while ((n - c4 * 4) % 5) c4++; for (int i = 0; i < c4; i++, t += 4) put(ans4, t, 4); while (t < n) put(ans5, t, 5), t += 5; } for (int i = 0; i < n; i++) puts(ans[i]); return 0; }
Problem D Problem Scores
一. (材料分析题) 请根据下列材料回答问题
材料1 神仙 jerome_wei 已经手持 2 枚国际金牌,而 yyf 有 0 块。
材料2 神仙 jerome_wei 在 csp-s 2019 上取得了全国前 10,四川第一的优异成绩
1. 神仙 jerome_wei 过了 D 后,跑来对 yyf 说:
请简要分析,这句话体现神仙 jerome_wei 怎样的特点。
2. 请简要分析造成这样现象的原因。
条件可以转化成长度为 $\left \lceil \frac{n - 1}{2} \right \rceil + 1$ 的前缀的和大于长度为 $\left \lceil \frac{n - 1}{2} \right \rceil$ 的后缀的和。
枚举中间的数的值 $x$,然后把所有数先填成这个值,然后左侧可以减少一些值,右侧可以加上一些值,它们的改变量之和小于 $x$。
问题相当于将 $x$ 拆成不超过 某个接近 $n/2$ 的数 个不超过 $y$ 的数的方案数。
不考虑个数限制是个 trivial dp。因为这个数加一的和的两倍比 $n - 1$ 大,所以可以枚举有多少个数,把它们都减 1,这样就能算不合法方案数。
Code
#include <bits/stdc++.h> using namespace std; typedef bool boolean; #define ll long long int Mod; typedef class Zi { public: int v; Zi() : v(0) { } Zi(int v) : v(v) { } Zi(ll x) : v(x % Mod) { } friend Zi operator + (Zi a, Zi b) { return ((a.v += b.v) >= Mod) ? (a.v - Mod) : a; } friend Zi operator - (Zi a, Zi b) { return ((a.v -= b.v) < 0) ? (a.v + Mod) : a; } friend Zi operator * (Zi a, Zi b) { return 1ll * a.v * b.v; } } Zi; const int N = 5e3 + 5; int n; Zi f[N][N], g[N][N]; void work(Zi (*f)[N], int lim) { f[0][0] = 1; for (int i = 1; i <= n; i++) { for (int j = 0; j < i; j++) f[i][j] = f[i - 1][j]; for (int j = i; j <= n; j++) { f[i][j] = f[i - 1][j] + f[i][j - i]; } } for (int i = n; i; i--) { Zi sum = 0; for (int j = lim + 1; j <= n; j++) { sum = sum + f[i - 1][j - lim - 1]; f[i][j] = f[i][j] - sum; } } } int main() { scanf("%d%d", &n, &Mod); int hn = (n + 1) >> 1; Zi ans = 0; if (n & 1) { work(f, hn - 1); work(g, hn - 1); } else { work(f, hn); work(g, hn - 1); } for (int i = 0; i <= n; i++) { for (int j = 1; j <= n; j++) { g[i][j] = g[i][j] + g[i][j - 1]; } } for (int i = 1; i <= n; i++) { for (int j = 0; j < i; j++) { ans = ans + f[i - 1][j] * g[n - i][i - j - 1]; } } printf("%d\n", ans.v); return 0; }
Problem E Balancing Network
第一问大概就是考虑最终能到一根线的所有起始位置,如果它不是全集就无解,否则存在解。如果有一条在 $(x, y)$ 间的边,最终到了 $x$,那么可能的起始位置是两边的并,求这个可以用 bitset 简单搞一搞,求一下最后经过第 $i$ 条边所有的起始位置,然后简单构造一下。
语文不太好,讲不清楚,sad.....
第二问就是你发现当 $n = 3$ 的时候一定有解,只考虑前三根线。假设当前在 $a, b$ 两根线的最终会到不同线,并且下一条边不是 $(a, b)$,考虑下下一条边如果是 $(a, b)$,你就让这一根线做一点调整就行了,显然它仍然满足这个限制。
时间复杂度 $O(m)$
另外简单说一下神仙 jerome_wei 的做法。考虑建 $n$ 个点,表示初始在每个线,然后给每条边建一个点。对于每条线向第一次遇到的边连边,每条边向两端下一个会到的边连边。建一个源点和汇点,源点向初始的 $n$ 个点连边,如果某个点之后不会再遇到任何边,那么向汇点连边。
问题可以转化为求两条除去起点终点外点不相交的路径。
Code
#include <bits/stdc++.h> using namespace std; typedef bool boolean; #define pii pair<int, int> const int N = 5e4 + 5; const int M = 1e5 + 5; int n, m, type; char ans[M]; int us[M], vs[M]; namespace subtask1 { bitset<N> f[M]; boolean vis[M]; int h[N], A[M], B[M]; void F(int p, int x) { if (!p || vis[p]) { return; } vis[p] = true; if (x == us[p]) { F(A[p], x); F(B[p], vs[p]); } else { ans[p] = 'v'; F(B[p], x); F(A[p], us[p]); } } void solve() { for (int i = 1, u, v; i <= m; i++) { u = us[i], v = vs[i]; if (!h[u]) { f[i].set(u); } else { f[i] = f[h[u]]; } swap(u, v); if (!h[u]) { f[i].set(u); } else { f[i] |= f[h[u]]; } swap(u, v); A[i] = h[u], B[i] = h[v]; h[u] = h[v] = i; } for (int i = 1; i <= m; i++) ans[i] = '^'; for (int i = 1; i <= m; i++) { if ((signed) f[i].count() == n) { F(i, us[i]); puts(ans + 1); return; } } puts("-1"); } } namespace subtask2 { const int C = 451; set<pii> S; int h[C]; int nxt[M][C]; bitset<C> vis[M][2]; boolean dfs(int p, int x, int y) { if (x == y) { return false; } if (p == m + 1) { return true; } int sx = -1, sy = -1; if (x == us[p] || y == us[p]) { sx = 0, sy = x ^ y ^ us[p]; } else { sx = 1, sy = x ^ y ^ vs[p]; } if (vis[p][sx].test(sy)) return false; vis[p][sx].set(sy); // down int nx = (x == us[p]) ? vs[p] : x; int ny = (y == us[p]) ? vs[p] : y; if (dfs(min(nxt[p][nx], nxt[p][ny]), nx, ny)) { ans[p] = 'v'; return true; } nx = (x == vs[p]) ? us[p] : x; ny = (y == vs[p]) ? us[p] : y; if (dfs(min(nxt[p][nx], nxt[p][ny]), nx, ny)) { ans[p] = '^'; return true; } return false; } void solve() { for (int i = 1; i <= m; i++) { S.insert(pii(us[i], vs[i])); } if ((signed) S.size() < 1ll * n * (n - 1) / 2) { for (int i = 1; i <= n; i++) { for (int j = i + 1; j <= n; j++) { if (!S.count(pii(i, j))) { for (int k = 1; k <= m; k++) { if (us[k] == i || us[k] == j) { ans[k] = '^'; } else { ans[k] = 'v'; } } puts(ans + 1); return; } } } assert(false); return; } assert(n < C); for (int i = 1; i <= m; i++) ans[i] = 'v'; for (int i = 1; i <= n; i++) h[i] = m + 1; for (int i = m; i; i--) { memcpy(nxt[i], h, sizeof(h)); h[us[i]] = h[vs[i]] = i; } for (int i = 1; i <= n; i++) { for (int j = i + 1; j <= n; j++) { if (dfs(min(h[i], h[j]), i, j)) { puts(ans + 1); return; } } } puts("-1"); } } int main() { scanf("%d%d%d", &n, &m, &type); for (int i = 1; i <= m; i++) { scanf("%d%d", us + i, vs + i);; } if (type == 1) { subtask1::solve(); } else { subtask2::solve(); } return 0; }
Problem F Histogram Rooks
平方比立方好想,心情简单.jpg。
考虑容斥,枚举哪些位置一定没有被覆盖,剩下的位置任意放置或不放置。
朴素 dp 是建笛卡尔树,设 $f_{i, j, k}$ 表示考虑到第 $i$ 个点,硬点的位置一共占据了 $j$ 列,其中 $k$ 列已经填了。
考虑一个点表示的矩形内的转移,转移有三种情况,一种是这一行什么都不干,一种是改变 $k$ 的值的情况下填数,一种是不会改变 $k$ 的转一下填数。
写一下转移式子不难发现,第二种转移当且仅当 $k = 0$ 的时候转移系数非 0。因此只有 $j = k$ 或者 $k = 0$ 的状态有用。
剩下的转移只有背包合并,它的复杂度和树上背包复杂度相同。
所以总时间复杂度 $O(n^2)$
三方做法好像是不容斥,考虑自底向上填,维护填上面的方案数,记录有多少列必须填,一些可能要填的数量,可以发现其中某个数量为 0。
Code
#include <bits/stdc++.h> using namespace std; typedef bool boolean; #define ll long long void exgcd(int a, int b, int& x, int& y) { if (!b) { x = 1, y = 0; } else { exgcd(b, a % b, y, x); y -= (a / b) * x; } } int inv(int a, int n) { int x, y; exgcd(a, n, x, y); return (x < 0) ? (x + n) : (x); } const int Mod = 998244353; template <const int Mod = :: Mod> class Z { public: int v; Z() : v(0) { } Z(int x) : v(x){ } Z(ll x) : v(x % Mod) { } friend Z operator + (const Z& a, const Z& b) { int x; return Z(((x = a.v + b.v) >= Mod) ? (x - Mod) : (x)); } friend Z operator - (const Z& a, const Z& b) { int x; return Z(((x = a.v - b.v) < 0) ? (x + Mod) : (x)); } friend Z operator * (const Z& a, const Z& b) { return Z(a.v * 1ll * b.v); } friend Z operator ~(const Z& a) { return inv(a.v, Mod); } friend Z operator - (const Z& a) { return Z(0) - a; } Z& operator += (Z b) { return *this = *this + b; } Z& operator -= (Z b) { return *this = *this - b; } Z& operator *= (Z b) { return *this = *this * b; } friend boolean operator == (const Z& a, const Z& b) { return a.v == b.v; } }; Z<> qpow(Z<> a, int p) { Z<> rt = Z<>(1), pa = a; for ( ; p; p >>= 1, pa = pa * pa) { if (p & 1) { rt = rt * pa; } } return rt; } typedef Z<> Zi; const int N = 805; int m, n; int h[N]; Zi pw2[N * N]; Zi f[N][N][2]; Zi comb[N][N]; int L[N], R[N]; void prepare(int n) { comb[0][0] = 1; for (int i = 1; i <= n; i++) { comb[i][0] = comb[i][i] = 1; for (int j = 1; j < i; j++) { comb[i][j] = comb[i - 1][j] + comb[i - 1][j - 1]; } } pw2[0] = 1; for (int i = 1; i <= n * n + 3; i++) { pw2[i] = pw2[i - 1] + pw2[i - 1]; } } int build(int l, int r, int bh) { static Zi g[N][2]; int p = ++n; L[p] = l, R[p] = r; int mi = h[l]; for (int i = l; i <= r; i++) { mi = min(mi, h[i]); } int ls = l - 1; vector<int> son; int cnew = 0; for (int i = l; i <= r; i++) { if (h[i] == mi) { if (i - 1 >= ls + 1) son.push_back(build(ls + 1, i - 1, mi)); ls = i, cnew++; } } if (ls < r) son.push_back(build(ls + 1, r, mi)); for (int i = 0; i <= cnew; i++) f[p][i][i == 0] = comb[cnew][i]; int lenp = cnew; for (auto e : son) { int lene = R[e] - L[e] + 1; for (int i = 0; i <= lenp + lene; i++) g[i][0] = g[i][1] = 0; for (int i = 0; i <= lenp; i++) { for (int j = 0; j <= lene; j++) { g[i + j][0] += f[p][i][0] * f[e][j][0]; g[i + j][1] += f[p][i][1] * f[e][j][1]; } } for (int i = 1; i <= lenp; i++) { g[i][0] += f[p][i][0] * f[e][0][1]; } for (int i = 1; i <= lene; i++) { g[i][0] += f[e][i][0] * f[p][0][1]; } lenp += lene; for (int i = 0; i <= lenp; i++) f[p][i][0] = g[i][0], f[p][i][1] = g[i][1]; } int hei = mi - bh, len = r - l + 1; for (int t = 1; t <= hei; t++) { f[p][0][1] *= pw2[len]; for (int i = 1; i <= len; i++) { Zi coef = (i & 1) ? (Mod - 1) : 1; f[p][i][1] = (f[p][i][1] * (pw2[len - i] - 1) + f[p][i][0] * coef); f[p][i][0] *= pw2[len - i]; } } /* cerr << p << " [" << l << ", " << r << "]:\n"; for (int i = 0; i <= len; i++) cerr << f[p][i][0].v << " "; cerr << '\n'; for (int i = 0; i <= len; i++) cerr << f[p][i][1].v << " "; cerr << '\n'; */ return p; } int main() { scanf("%d", &m); for (int i = 1; i <= m; i++) { scanf("%d", h + i); } prepare(m); build(1, m, 0); Zi ans = 0; for (int i = 0; i <= m; i++) ans += f[1][i][1]; printf("%d\n", ans.v); return 0; }