AtCoder Grand Contest 058 简要题解
从这里开始
Problem A Make it Zigzag
考虑使 这些位置后三个中的最大值在中间,最后再处理一下最后两个位置就行了。
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | #include <bits/stdc++.h> using namespace std; const int N = 2e5 + 5; int n; int a[N]; vector< int > ans; int maxp( int a, int b, int c) { if (c > max(a, b)) return 2; if (b > max(a, c)) return 1; return 0; } int main() { scanf ( "%d" , &n); for ( int i = 1; i <= (n << 1); i++) { scanf ( "%d" , a + i); } for ( int i = 1; i <= (n << 1) - 2; i += 2) { int d = maxp(a[i], a[i + 1], a[i + 2]); if (d != 1) { ans.push_back(min(i + d, i + 1)); swap(a[i + d], a[i + 1]); } } int n2 = n << 1; if (a[n2 - 1] > a[n2]) { ans.push_back(n2 - 1); } printf ( "%d\n" , ( signed ) ans.size()); for ( auto x : ans) { printf ( "%d " , x); } return 0; } |
Problem B Adjacent Chmax
考虑其实问题相当于一个较大数可以把较小数覆盖掉。
依次考虑原序列每个数在最终序列中占了多长一段。容易发现只需要满足占的那一段里没有比它大的数就可以。
然后就是个简单 dp。
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | #include <bits/stdc++.h> using namespace std; #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 = 5005; template < typename T> bool vmax(T& a, T b) { return a < b ? (a = b, true ) : false ; } int n; int vis[N]; int a[N], fa[N]; Zi f[N], g[N]; void build( int l, int r, int v) { if (l > r) { return ; } int mx = -1, mxp = 0; for ( int i = l; i <= r; i++) { if (vmax(mx, a[i])) { mxp = i; } } fa[mxp] = v; build(l, mxp - 1, mxp); build(mxp + 1, r, mxp); } int main() { scanf ( "%d" , &n); for ( int i = 1; i <= n; i++) { scanf ( "%d" , a + i); } a[0] = N, a[n + 1] = N; f[0] = 1; for ( int i = 1; i <= n; i++) { int l = i, r = i; while (a[l - 1] < a[i]) l--; while (a[r + 1] < a[i]) r++; Zi s = 0; for ( int j = l; j <= r; j++) { g[j] += (s += f[j - 1]); } for ( int j = 0; j <= n; j++) { g[j] += f[j]; f[j] = g[j]; g[j] = 0; } } printf ( "%d\n" , f[n].v); return 0; } |
Problem C Planar Tree
差点不会这题,感觉好毒瘤啊。
如果只有 1, 2 这个题瞎做就好了。
注意到如果奇数和偶数都能连,就是只有 1, 2 的情况,可是 1, 4 不能连。考虑先把这两个烦人的东西干掉。
考虑 , 相邻的时候直接连起来,然后分别当做只有一个点 2 或者 3。假如原问题有解且不存在这条边,那么显然这条边可以加上去,然后环上随便删一条边就行了。因此原问题和新问题有解性等价。
注意到如果有连续两个数是相同的,那么可以在其中 1 个连边的时候,两个一起连。考虑把这两个缩一起。和上面同样的方法证明两个问题有解性等价,只是可以加边的原因略有不同。
现在问题变成 1 不和 2 相邻,4 不和 3 相邻,相邻的数不同,目标把所有的 1, 4 连到别的点上。可以发现,树上不跨过 4 的 相连会导致至少 1 个 不能继续和 4 相连。(如果有跨过 4 的话就考虑这个 ,这样处理下去可能会得到不跨过 2 的 相连,但和这个情形是相似的)这样的情况一定是 ,考虑把这个缩成 ,继续这个操作。所以每消去一个 1 就会减少一个 3,每消去一个 4 就会减少一个 2。
如果不存在 也不存 ,那么说明不存在 相邻的情况,此时可以得到 的数量大于等于 的数量, 的数量大于等于 的数量(等于是因为可能都没有),此时显然不可能。
相反如果满足 的数量加上 的数量大于 的数量加上 的数量,同时 1 的数量大于等于 3 的数量,4 的数量大于等于 2 的数量,因为每次两个数量和同时减少,所以不可能出现不存在 相邻的情形。因此只用处理一下,然后数数量就可以了。
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | #include <bits/stdc++.h> using namespace std; const int N = 3e5 + 5; int T, n; int a[N], stk[N], cnt[5]; int merge( int x, int y) { if (x > y) swap(x, y); if (x == y) { return x; } if (x + 1 == y && x != 2) { return x == 1 ? y : x; } return 0; } void solve() { scanf ( "%d" , &n); for ( int i = 1; i <= n; i++) { scanf ( "%d" , a + i); } int pos = 1, top = 1; while (a[pos] == a[1]) pos++; rotate(a + 1, a + pos, a + n + 1); stk[top] = a[1]; for ( int i = 2, x; i <= n; i++) { x = merge(stk[top], a[i]); if (x) { stk[top] = x; } else { stk[++top] = a[i]; } } int frt = 1, tmp; while (frt < top && (tmp = merge(stk[frt], stk[top]))) { stk[top] = tmp; frt++; } memset (cnt, 0, sizeof (cnt)); for ( int i = frt; i <= top; i++) { cnt[stk[i]]++; } if (cnt[4] <= cnt[2] && cnt[1] <= cnt[3] && cnt[1] + cnt[4] < cnt[2] + cnt[3]) { puts ( "Yes" ); } else { puts ( "No" ); } } int main() { scanf ( "%d" , &T); while (T--) { solve(); } return 0; } |
Problem D Yet Another ABC String
这种题比那种无聊的猜结论题有意思多了。
朴素容斥的话相当于硬点若干个位置,然后这个位置起始长度为 3 的子串不合法。有重叠的话不好处理,主要因为 3 种字符的数量有限制。
考虑让硬点的位置后面一定不和它相连。就是 ABC 后面不是 A。相当于就是对每个这样连续的 ABCABCAB 末做一个子集容斥。
然后就是个简单组合计数。
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | #include <bits/stdc++.h> using namespace std; #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 = 3e6 + 5; int A, B, C, S; Zi fac[N], _fac[N], pw2[N]; void init( 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; } pw2[0] = 1; for ( int i = 1; i <= n; i++) { pw2[i] = pw2[i - 1] * 2; } } void cswap( int & a, int & b) { if (a > b) { swap(a, b); } } Zi comb( int n, int m) { return n < m ? 0 : fac[n] * _fac[m] * _fac[n - m]; } int main() { scanf ( "%d%d%d" , &A, &B, &C); S = A + B + C; cswap(B, C), cswap(A, B), cswap(B, C); init(S); Zi ans = 0, tmp; for ( int i = 0; i <= A; i++) { // the last one isn't included int rest = S - 3 * i; tmp = comb(rest + i - 1, i) * pw2[i] * fac[rest] * _fac[A - i] * _fac[B - i] * _fac[C - i]; // the last one is included if (i) { tmp += comb(rest + i - 1, i - 1) * pw2[i - 1] * 3 * fac[rest] * _fac[A - i] * _fac[B - i] * _fac[C - i]; } if (i & 1) { ans -= tmp; } else { ans += tmp; } } printf ( "%d\n" , ans.v); return 0; } |
Problem E Nearer Permutation
感觉这种题和码农题一样无聊,反正顺着思路做就完了,每步难度不大,就是思考的过程特别长。虽然也有可能是因为我菜。
考虑怎么找出 。
首先考虑怎么算 ,相当于将 第一个位置上的数,在 中改为 ,第二位置上的数,在 中改为 ,然后求 的逆序对数。
这个显然相当于 在两个排列中出现的位置构成的有序二元组的逆序对数。
那么现在有 2 列二元组,每列二元组只知道其中 1 个数,从小到大把 到 填进去,每次尽可能填较小的数对应的二元组。
现在问题变成判定把剩下的填进来其中 1 列的逆序对数是否不超过另一列的逆序对数。
可以发现,只有每对 在 和 中出现的位置构成的有序二元组的逆序对会对差有贡献,并且贡献为 或者 。(其实这个逆序对其实就是 在 中出现的位置 的逆序对)如果 并且 被先填上数,那么会产生 的差。(最终需要差小于等于 0)我们下面称违反一个逆序对 指先在 填数。
因此我们可以至多违反逆序对总数除以 向下取整个逆序对。
然后很容易得到一个明确的求 的做法:假设当前在确定第 个位置上的数,找尽量小的 满足上一条限制,然后填上 。
现在回到原问题。
现在我们要填 ,题目指定了上面做法填 的顺序。
当我们填了一个 且前面有没有填的 时相当于限制此时能违反的数量小于填 会违反的数量。考虑每次填的过程,允许违反的数量每次单调不增,后者至多减少 1。所以如果下一次填的位置 满足 ,那么意味着上面这个差是刚好变为 ,所以填完 后不能再违反任何逆序对。这之后只能每次填 最小的没填的位置。
我们将 分为 4 类:
- 第一类位置:满足
- 第三类位置:第一个满足 的
- 第二类位置:设第三类位置为 ,那么在 中满足 的为第二类位置
- 第四类位置:剩下的
容易发现对于第一类位置没有限制。如果一个现在要填的位置 ,之前还有没有填的位置 ,那么一定满足 ,否则能违反的限制数一定满足能先填 。因此在不考虑第三类位置的情况下,第二类位置的限制在处理出第四类位置的 的相对顺序后就已知了。
我们希望使得逆序对数减去 2 倍被违反的逆序对数为 或者 。我们先考虑这个东西的最小值。注意到每产生一个被违反的逆序对的时候对这个值的贡献为 。
在不考虑第三类位置的时候,最优方案为第一类位置依次填 ,第一个第二类位置填能填最大的数(能违反的逆序对数到这里的时候最大值就是这个位置上的限制)剩下按顺序填最小的数。
注意到你可以连续地把这个值加上 1,所以如果第三类位置在第一个第二类位置的后面就做完了。
考虑第三类位置在所有第二类位置之前的情形。
如果第三类位置填上后后面有 个数比它小,那么到这个位置的时候允许违反的逆序对数也是 。第三类位置对于倒数第 个被填上的第二类位置有一个限制:这后面至多有 个数比它小(注意求 的过程运行到这里的时候允许违反的逆序对数是 )。此时显然最优的话把第一个第二类位置填能填的最大的,剩下的没填的按顺序填能填的最小的。因此我们只用求一下最大可能的 即可。(注意这里所有的第二类位置都对这个第三类位置有限制,因为经过第一个第二类位置后,允许违反的逆序对数是 不是 )。
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | #include <bits/stdc++.h> using namespace std; template < typename T> bool vmin(T& a, T b) { return a > b ? (a = b, true ) : false ; } const int N = 3e5 + 5; typedef class Fenwick { public : int n; int a[N]; void init( int n) { this ->n = n; fill(a, a + n + 1, 0); } #define lowbit(_) ((_) & -(_)) void add( int idx, int val) { for ( ; idx <= n; idx += lowbit(idx)) { a[idx] += val; } } int query( int idx) { int ret = 0; for ( ; idx; idx -= lowbit(idx)) { ret += a[idx]; } return ret; } int query( int l, int r) { return query(r) - query(l - 1); } } Fenwick; int T, n; Fenwick fen; int a[N], les[N]; void solve() { scanf ( "%d" , &n); for ( int i = 1; i <= n; i++) { scanf ( "%d" , a + i); } int p1, p2; for (p1 = 1; p1 <= n && a[p1] == p1; p1++); if (p1 >= n) { puts ( "Yes" ); return ; } for (p2 = p1 + 1; p2 <= n && a[p2] > a[p2 - 1]; p2++); long long dif = -1ll * (n + n - p1) * (p1 - 1) / 2; fen.init(n); for ( int i = n; i >= p1; i--) { int p = a[i]; dif += fen.query(p); fen.add(p, 1); } for ( int i = 1; i <= n; i++) { les[i] = n + 1; } fen.init(n); for ( int i = p2; i <= n; i++) { int p = a[i]; if (i != p2) { les[p] = fen.query(p, n); } fen.add(p, 1); } if (a[p2] > a[p1]) { int lim = n + 1; for ( int i = p1; i <= a[p1]; i++) { vmin(lim, les[i]); } dif -= lim + (p2 - p1 - 1); } else { int lim = n - a[p2] - (p2 - p1); for ( int i = 2; i <= n; i++) { vmin(les[i], les[i - 1]); } for ( int i = p1; i < p2; i++) { vmin(lim, les[a[i]] + p2 - i - 1); } vmin(lim, les[a[p2]] - 1); dif -= lim + p2 - p1 - 1; } puts ((dif < 2) ? "Yes" : "No" ); } int main() { scanf ( "%d" , &T); while (T--) { solve(); } return 0; } |
Problem F Authentic Tree DP
别催了,在路上了.jpg
不懂就问,这题是给人做的吗?
(尝试给题解编一个思路)
显然你需要给这个式子找一个组合意义。看到 容易想到是选择某个东西。但是这个 枚举的是边。考虑每条边拆出一个新点,向原来的两个点连边。但这样变成了 。考虑给这样每个新点挂 个叶子,这样 个新点的贡献就被消掉了。
这个式子大概是在算一个概率一样的东西,同时注意到枚举的一定是新点,然后将树分成独立的两部分。如果考虑这样得到一个序列,每次在选择在最前的点,可以为这个过程编一个组合意义:求所有新点在序列中的位置在所有邻居前面的概率。
问题变成一棵树,给定边的定向,问拓扑序方案数。直接对指向根的有向边容斥做背包就完事了。
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | #include <bits/stdc++.h> using namespace std; #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 vector<Zi> Poly; const int N = 5005; Poly operator * (Poly a, Poly b) { int n = a.size(), m = b.size(); Poly c (n + m - 1, 0); for ( int i = 0; i < n; i++) { for ( int j = 0; j < m; j++) { c[i + j] += a[i] * b[j]; } } return c; } int n; Zi Inv[N]; vector< int > G[N]; void init_inv( int n) { Inv[1] = 1; for ( int i = 2; i <= n; i++) { Inv[i] = Inv[Mod % i] * (Mod - Mod / i); } } Poly dfs( int p, int fa) { Poly f {0, 1}; for ( auto e : G[p]) { if (e ^ fa) { Poly g = dfs(e, p); for ( int i = 1; i < ( signed ) g.size(); i++) { g[i] *= Inv[i]; g[0] += g[i]; g[i] = -g[i]; } f = f * g; } } for ( int i = 1; i < ( signed ) f.size(); i++) { f[i] *= Inv[i]; } return f; } int main() { scanf ( "%d" , &n); for ( int i = 1, u, v; i < n; i++) { scanf ( "%d%d" , &u, &v); G[u].push_back(v); G[v].push_back(u); } init_inv(n); auto f = dfs(1, 0); Zi ans = 0; for ( auto x : f) { ans += x; } printf ( "%d\n" , ans.v); return 0; } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
2018-08-22 Codeforces Round #505 (Div 1 + Div 2 Combined) Solution