AtCoder Grand Contest 057 简要题解
从这里开始
两年没摸 oi,补的第一场 agc 不看题解补完了?
感觉这场 agc 可以和 agc 046 掰手腕(指题目无聊程度)
现在都听不到妹老师妹式吐槽 agc ,sad......
Problem A Antichain of Integer Strings
容易发现先选大的一定不劣。
Code
#include <bits/stdc++.h> using namespace std; int norm(int x) { if (x < 10) { return 0; } int y = 1; while (x > 9) { x /= 10; y *= 10; } return y; } int T, L, R; void solve() { scanf("%d%d", &L, &R); int nl = norm(L); int nr = norm(R); if (nl == nr) { printf("%d\n", R - L + 1); } else { int cnt = R - nr + 1; int app = nr - max(max(L, R / 10 + 1), cnt); printf("%d\n", cnt + max(app, 0)); } } int main() { scanf("%d", &T); while (T--) { solve(); } return 0; }
Problem B 2A + x
设最大值为 $a$,最小值为 $b$。如果 $2b > a$ 考虑 2 种情况:
- $a - b \geq x$,这种情况的话不可能将 $a, b$ 同时操作,否则它们的差不会减小。同时操作也不会使得 $2b - a$ 减小。
- $a - b < x$,这种情况同时操作 $a, b$ 能够让最后差变为 0。
前者显然在最大值不太大的时候就会产生最优答案。考虑每个数进行操作后可能得到的区间,拿个扫描线去跑就行了。
后者的话多做几轮,如果答案减小就输出 0 就行了。时间复杂度 $O(n\log n \log V)$
虽然感觉能直接贪心。
Code
#include <bits/stdc++.h> using namespace std; #define ll long long const ll V = 1e10; const int M = 1 << 17, S = M << 1 | 5; typedef class Event { public: ll l, r; int id; Event(ll l, ll r, int id) : l(l), r(r), id(id){ } bool operator < (Event b) const { return r > b.r; } } Event; int n, X; ll f[S]; void push_up(int p) { f[p] = max(f[p << 1], f[p << 1 | 1]); } void update(int p, ll v) { p |= M; f[p] = v; while (p >>= 1) push_up(p); } priority_queue<Event> Q; int main() { scanf("%d%d", &n, &X); for (int i = 1, x; i <= n; i++) { scanf("%d", &x); Q.push(Event(x, x, i)); update(i, x); } ll ans = V; while (!Q.empty()) { ll L = Q.top().r; if (L >= V) { break; } ans = min(ans, max(f[1] - L, 0ll)); while (!Q.empty() && Q.top().r == L) { auto e = Q.top(); Q.pop(); ll nl = e.l * 2; ll nr = e.r * 2 + X; update(e.id, nl); Q.push(Event(nl, nr, e.id)); } } ll oldans = ans; while (!Q.empty()) { ll L = Q.top().r; if (L >= 4 * V) { break; } ans = min(ans, max(f[1] - L, 0ll)); while (!Q.empty() && Q.top().r == L) { auto e = Q.top(); Q.pop(); ll nl = e.l * 2; ll nr = e.r * 2 + X; update(e.id, nl); Q.push(Event(nl, nr, e.id)); } } printf("%lld", (ans == oldans) ? ans : 0); return 0; }
Problem C Increment or Xor
可以通过打表发现一个性质:如果前 $2^{n - 1}$ 个数的最高位为 $0$,序列可以还原当且仅当可以通过一次异或操作还原。
考虑怎么翻转某个数的最高位:把低 $n - 1$ 位置为 $1$,然后执行加操作。
剩下问题变成支持全局加 $1$,全局异或,单点查询。
把所有数倒过来,用线段树维护原来倒过来的值为 $i$ 最终能变成什么值,全局加 $1$ 相当于对线段树上 log 个区间打异或标记。
现在回到开头那个性质。考虑对单位排列进行若干次操作,使得最后最高位不变,现在我们希望证明线段树上没有异或标记。
假如原来的值为 $x$ 的那个位置最高位被翻转了,那么这次 $+1$ 操作会在 $x$ 在线段树上对应的点的所有父节点打异或标记。所以这个位置的最高位在偶数次被翻转的过程都是对线段树上同样的点打标记。因此显然最终线段树上是没有标记的。
Code
#include <bits/stdc++.h> using namespace std; const int Nmx = 1 << 18; int n, N; int a[Nmx]; vector<int> ans; typedef class SegmentTree { public: int n, N, tga, tg[Nmx]; void build(int _n) { n = _n; N = 1 << n; tga = 0; for (int i = 0; i < N; tg[i++] = 0); } void modify(int p, int tga) { if (p >= N) return; int r = 1 ^ (tga & 1) ^ tg[p]; tg[p] ^= 1; modify(p << 1 | r, tga >> 1); } int rev(int x) { int y = 0; for (int i = 0; i < n; i++) { y = y << 1 | (x & 1); x >>= 1; } return y; } int query(int y) { int p = rev(y) + N, v = 1 << n; while (p >>= 1) { v >>= 1; if (tg[p]) y ^= v; } return y ^ tga; } void do_add() { modify(1, tga); } void do_xor(int x) { tga ^= x; } } SegmentTree; SegmentTree st; void do_add() { st.do_add(); ans.push_back(-1); } void do_xor(int x) { if (x) { st.do_xor(x); ans.push_back(x); } } int main() { scanf("%d", &n); N = 1 << n; for (int i = 0; i < N; i++) { scanf("%d", a + i); } int msk = (N - 1) >> 1; st.build(n); for (int i = 0; i < (N >> 1); i++) { int x = st.query(a[i]); if ((x >> (n - 1)) & 1) { do_xor((x & msk) ^ msk); do_add(); } } int x = st.query(a[0]); do_xor(x); for (int i = 0; i < N; i++) { a[i] = st.query(a[i]); if (a[i] != i) { puts("No"); return 0; } } puts("Yes"); printf("%d\n", (signed) ans.size()); for (auto x : ans) { printf("%d ", x); } return 0; }
Problem D Sum Avoidance
真的养老了,这种题也能写+调 2h
不难发现最多选出 $\left \lfloor \frac{S-1}{2} \right \rfloor$ 个数,显然全选最大的能满足这个界。再选 1 个的话会有 2 个数加起来是 $S$。
不难发现最小选出来的数一定是最小的不是 $S$ 的约数的数 $n$,容易发现它很小,不会超过 50.
考虑下一个加进来的数,如果是已有的数能凑出来的,我们不管。所以只用枚举每个模 $n$ 没有出现过的余数,用同余最短路计算这种余数最小能放的数是多少。
最后的问题就很傻逼了,所有实际选出来的数是由这不超过 $n$ 个数凑成的,所以直接二分,用同余最短路算数量。
时间复杂度 $O(T(n^3 + n\log V))$
Code
#include <bits/stdc++.h> using namespace std; #define ll long long template <typename T> void pfill(T* pst, T* ped, T val) { for ( ; pst != ped; *(pst++) = val); } template <typename T> bool vmin(T& a, T b) { return a > b ? (a = b, true) : false; } template <typename T> bool vmax(T& a, T b) { return a < b ? (a = b, true) : false; } const int N = 50; const ll llf = (signed ll) (~0ull >> 2); int T; ll S, mS, K; int n; ll f[N]; bool used[N]; vector<ll> seq; ll calc(ll mid) { ll ret = 0; for (int i = 0; i < n; i++) { if (mid >= f[i]) { ret += (mid - f[i]) / n + 1; } } return ret - 1; } void solve() { scanf("%lld%lld", &S, &K); if (K > ((S - 1) >> 1)) { puts("-1"); return; } for (n = 2; !(S % n); n++); int sr = S % n; seq.clear(); pfill(f, f + n, llf); pfill(used, used + n, false); f[0] = 0; ll nx = n; while (nx < S) { if (nx ^ n) { int w = nx % n; used[w] = true; for (int s = 0, p, q; s < n; s++) { p = s; while (vmin(f[q = (p + w) % n], f[p] + nx)) { p = q; } } } seq.push_back(nx); nx = S; for (int r = 1; r < n; r++) { if (used[r]) continue; ll L = n + r; for (int p = sr, d = 1, q; d <= n; d++, p = q) { q = (p + n - r) % n; vmax(L, (S + 1 - f[q] + d - 1) / d); } vmin(nx, (L - r + n - 1) / n * n + r); } } sort(seq.begin(), seq.end()); /* cerr << "seq:" ; for (auto x : seq) { cerr << x << " "; } cerr << endl; */ ll l = 2, r = S - 1, mid; while (l <= r) { mid = (l + r) >> 1; if (calc(mid) < K) { l = mid + 1; } else { r = mid - 1; } } printf("%lld\n", l); } int main() { scanf("%d", &T); while (T--) { solve(); } return 0; }
Problem E RowCol/ColRow Sort
先考虑只有 01 的情形。
这个排序操作相当于对所有 0 做 2048 里的往一边移动的操作。
先列排序再行排序相当于把每列 0 的数量排了个序。先行排序再列排序同理。
所以 0 在原矩阵中每行的数量以及每列的数量都是确定的。
注意到行列之间相邻关系没有意义,所以考虑每列以及每列的 0 的数量依次递减的情形。
不难发现这种情况下 0 的排布是唯一的。证明的话首先可以注意到每列 0 的数量是由每行 0 的数量决定的,第 $i$ 列 0 的数量等于 0 的数量大于等于 $i$ 的行数,所以从最后一列开始填容易证明。
所以这种情形的答案是选取和排布这些行列的方案数。
再回到原问题,每次把 $\leqslant x$ 看作 0,把 $> x$ 的看作 1。现在问题变成把 0 所在的行列塞进 1 构成的阶梯形的图形里面使得 0 的位置完全在这个阶梯形的图形的内部。
直观地讲(主要是,虽然和最自然的 dp 想法本质相同,但感觉直接讲不清楚),一个阶梯型可以通过 从下往上填行,从右往左填列(左边第一列和下面第一行是数量最多的),填一列的时候将它与之前所有行的交点加入阶梯型 构造出来。考虑用这个顺序在 1 的阶梯形里面填 0 的图形。考虑填一行的时候会导致一个后缀不能填列,考虑用 dp 数组记录最强的一个限制,现在的问题变成计算填列的时候的方案数。
你注意到每个列能填的范围是个前缀,通常情况下,从限制最强的开始计算,每个能选的方案数乘起来即可。虽然这里顺序是反的,但是你知道它对方案数的贡献系数,直接乘就好了。每日降智(1/1)
时间复杂度 $O(RCV)$
Code
#include <bits/stdc++.h> using namespace std; template <typename T> void pfill(T* pst, T* ped, T val) { for ( ; pst != ped; *(pst++) = val); } #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 bool 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 = 1505; int n, m; int B[N][N]; Zi f[N], g[N], fac[N], _fac[N]; void init_fac(int n) { fac[0] = 1; for (int i = 1; i <= n; i++) { fac[i] = fac[i - 1] * i; } _fac[n] = ~fac[n]; for (int i = n; i; i--) { _fac[i - 1] = _fac[i] * i; } } void get(int val, vector<int>& cr, vector<int>& cc) { cr = vector<int>(n, 0); cc = vector<int>(m, 0); for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { cr[i - 1] += (B[i][j] <= val); cc[j - 1] += (B[i][j] <= val); } } sort(cr.begin(), cr.end(), greater<int>()); sort(cc.begin(), cc.end(), greater<int>()); while (!cr.empty() && !cr.back()) cr.pop_back(); while (!cc.empty() && !cc.back()) cc.pop_back(); } vector<int> cr0, cc0, cr1, cc1; Zi work(int lim) { get(lim + 1, cr1, cc1); get(lim, cr0, cc0); if (!cr0[0]) { return 1; } Zi ret = 1; int R = cr1.size(), C = cc1.size(); int R0 = cr0.size(), C0 = cc0.size(); pfill(f, f + R, Zi(1)); int pr = 0, pc = C0 - 1; while (pr < R0 && pc >= 0) { int cntc = 0; while (pr < R0) { if (pr) { for (int i = 0; i < R; i++) { g[i] = f[i]; f[i] = 0; } for (int i = pr; i < R; i++) { f[i] = g[i] * (i - pr + 1); } for (int i = 1; i < R; i++) { g[i] += g[i - 1]; f[i] += g[i - 1]; } } pr++, cntc++; if (pr >= R0 || cr0[pr] != cr0[pr - 1]) { break; } } ret *= _fac[cntc]; cntc = 0; while (pc < C0) { for (int i = 0; i < R; i++) { int sec = cr1[i] - pc; f[i] *= max(sec, 0); } pc--, cntc++; if (pc < 0 || cc0[pc] != cc0[pc + 1]) { break; } } ret *= _fac[cntc]; } Zi sum = 0; for (int i = 0; i < R; i++) { sum += f[i]; } ret *= sum; // cerr << "lim: " << lim << " " << ret.v << '\n'; return ret; } int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { scanf("%d", B[i] + j); } } init_fac(max(n, m) + 3); Zi ans = 1; for (int x = 8; ~x; x--) { ans *= work(x); } printf("%d\n", ans.v); return 0; }
Problem F Reflection
首先容易观察到下面几条性质:
- 操作某个固定次数,三元组 $\{a, b, c\}(a \leqslant b \leqslant c)$ 的差值 $(b - a, c - b)$ 无视顺序的话是唯一的
- 每次操作相当于对中间的数加或减差值的最小值
- 两个不同三元组通过一次操作变成相同的三元组当且仅当它们的差值在不考虑顺序下是相同的且中间的数相同。
手玩容易发现:
- 如果某个三元组中间值为 $x$,进行一次右翻左操作,接下来一直进行左翻右操作可以使得中间值在某次操作后为 $x$
- 两个三元组中间值分别为 $x_1, x_2(x_1 < x_2)$,设 1 次操作后的中间值为 $y_1, y_2$,那么有 $y_1 \leqslant y_2$
前一条性质的证明考虑差值(不考虑顺序)为 $(x, y) (x <y)$ 的情形,分 $y \geqslant 2x$ 和 $x < y < 2x$ 两种情况分别讨论一下就能证明。
后一条可能不太显然。考虑当差值(不考虑顺序)为 $(x, y) (x < y)$ 的所有三元组,把所有不同的中间值从小到大排序,考虑相邻两个的差,进行一次操作相当于把这中间每个数减去 $2x$,然后在两两之间以及两端插入 $2x$,接着把所有 $0$ 剔除掉。考虑一个这中间某个差相当于是做了一次操作,然后下面一直往反方向操作,由前一条性质能保证这个至多会被减为 $0$。
画一个这样的图:对于每个差值(不考虑顺序)有一个数轴,把能到的三元组的中间值在上面标出来,然后一个三元组向它能直接到的三元组连边。
容易发现某个三元组 $\{a, b, c\}$ 能直接到的两个三元组 $A, B$,$A, B$ 所有能到的三元组的交集为第一次中间值回到 $b$ 三元组的所有能到的三元组。证明比较显然。
注意到 $A, B$ 的“子树”其实长得一模一样。把重复部分扣掉就独立了。所以就可以 dp 啦。直接做复杂度不太行,但是对于最小值相同的情形,转移相当于一个数列递推,套一个矩阵快速幂去优化一下就可以了。
剩下就是处理一堆无聊的细节:
- 差值里有个 $0$ 的一个中间值对应的三语组数量都是 $2$
- 差值中的两个数相等,对应的三元组数量都是 $1$
- (......)
Code
#include <bits/stdc++.h> using namespace std; template <typename T> void pfill(T* pst, T* ped, T val) { for ( ; pst != ped; *(pst++) = val); } #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 bool 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; typedef class Matrix { public: int r, c; Zi a[3][3]; Matrix(int r = 0, int c = 0) : r(r), c(c) { memset(a, 0, sizeof(a)); } Zi* operator [] (int p) { return a[p]; } Matrix operator * (Matrix b) { Matrix rt (r, b.c); for (int i = 0; i < r; i++) { for (int j = 0; j < c; j++) { for (int k = 0; k < b.c; k++) { rt[i][k] += a[i][j] * b[j][k]; } } } return rt; } } Matrix; Matrix tr (3, 3); Matrix qpow(Matrix a, ll p) { Matrix rt(3, 3); for (int i = 0; i < rt.r; i++) { rt[i][i] = 1; } for ( ; p; p >>= 1, a = a * a) { if (p & 1) { rt = rt * a; } } return rt; } Zi dp(ll a, ll b, Zi& f_1, Zi& f_2) { if (!(a % b)) { ll n = a / b; Matrix f (1, 3); f[0][0] = 9, f[0][1] = 6, f[0][2] = 1; f = f * qpow(tr, n - 2); f_1 = 3, f_2 = f[0][1]; return f[0][0]; } ll n = a / b; Matrix f (1, 3); Zi of; of = f[0][0] = dp(b, a % b, f[0][1], f_1); f[0][2] = 1; f = f * qpow(tr, n); f_2 = ((n == 1) ? of : f[0][1]); // cerr << a << " " << b << " " << f[0][0].v << endl; return f[0][0]; } void solve() { ll x, y, z, a, b; scanf("%lld%lld%lld", &x, &y, &z); a = y - x, b = z - y; if (a < b) swap(a, b); if (!b) { puts(a == b ? "1" : "2"); return; } if (a == b) { puts("5"); return; } Zi ans, foo, bar; ans = dp(a, b, foo, bar); printf("%d\n", ans.v); } int T; int main() { tr[0][0] = 2, tr[0][1] = 1; tr[1][0] = Mod - 1; tr[2][0] = 2, tr[2][2] = 1; scanf("%d", &T); while (T--) { solve(); } return 0; }