Codechef MARCH20A: March Challenge 2020 Division 1
题目不一定按照难度排序。点我获取所有中文题面。
LAZER
很容易转化为二维偏序。时间复杂度为 \(\mathcal O ((N + Q) \log N)\)。
#include <cstdio>
#include <algorithm>
const int MN = 100005, MQ = 100005;
int N, Q, A[MN], lb[MQ], rb[MQ], y[MQ], qp[MQ], Ans[MQ];
int op[MN * 2], id[MN * 2], yp[MN * 2], pp[MN * 2], C;
int bit[MN];
inline void Add(int i, int x) { for (; i < N; i += i & -i) bit[i] += x; }
inline int Qur(int i) { int a = 0; for (; i; i -= i & -i) a += bit[i]; return a; }
int main() {
int Tests;
scanf("%d", &Tests);
while (Tests--) {
scanf("%d%d", &N, &Q), C = 0;
for (int i = 1; i <= N; ++i) scanf("%d", &A[i]);
for (int i = 1; i <= Q; ++i) scanf("%d%d%d", &lb[i], &rb[i], &y[i]), qp[i] = i;
for (int i = 1; i < N; ++i)
op[++C] = 0, id[C] = i, yp[C] = std::min(A[i], A[i + 1]), pp[C] = C,
op[++C] = 1, id[C] = i, yp[C] = std::max(A[i], A[i + 1]), pp[C] = C;
std::sort(pp + 1, pp + C + 1, [](int i, int j) { return yp[i] == yp[j] ? op[i] < op[j] : yp[i] < yp[j]; });
std::sort(qp + 1, qp + Q + 1, [](int i, int j) { return y[i] < y[j]; });
for (int i = 1; i < N; ++i) bit[i] = 0;
for (int I = 1, J = 1; I <= Q; ++I) {
while (J <= C && (yp[pp[J]] < y[qp[I]] || (yp[pp[J]] == y[qp[I]] && op[pp[J]] == 0)))
Add(id[pp[J]], op[pp[J]] ? -1 : 1), ++J;
Ans[qp[I]] = Qur(rb[qp[I]] - 1) - Qur(lb[qp[I]] - 1);
}
for (int i = 1; i <= Q; ++i) printf("%d\n", Ans[i]);
}
return 0;
}
LAZERTST
弱智题,第一个子任务输出 \(M - 1\),后两个子任务,对于每个问题,询问 \(M / 2\) 下取整,然后直接返回答案。
#include <cstdio>
const int MQ = 15;
int M, Q, L[MQ], R[MQ], Ans[MQ];
int main() {
int Tests;
scanf("%d", &Tests);
while (Tests--) {
scanf("%*d%d%*d%d", &M, &Q);
for (int i = 1; i <= Q; ++i) scanf("%d%d", &L[i], &R[i]);
for (int i = 1; i <= Q; ++i) {
if (M <= 10 && R[i] - L[i] >= 1000) {
Ans[i] = M - 1;
} else {
printf("1 %d %d %d\n", L[i], R[i], M / 2);
fflush(stdout);
scanf("%d", &Ans[i]);
}
}
printf("2");
for (int i = 1; i <= Q; ++i) printf(" %d", Ans[i]);
printf("\n"), fflush(stdout);
scanf("%*d");
}
return 0;
}
CHEFDAG
使用 \(\mathcal O (K N^2)\) 的时间,可以确定出所有可以选的边。
观察到最终连边条件,是需要每个连通块形成一棵内向树。
但是如果对树进行树链剖分,断开链之间的连边,其实也是不影响无入度的节点(叶子)个数的。
那么其实就是寻找一个最小链覆盖,众所周知 DAG 的最小链覆盖等于 \(n\) 减去转化成二分图的匹配数。
使用 Dinic 算法求解匹配,时间复杂度为 \(\mathcal O (N^2 \sqrt{N})\)。
#include <cstdio>
#include <algorithm>
namespace DinicFlows {
typedef long long LL;
const LL Inf = 0x3f3f3f3f3f3f3f3f;
const int MN = 4005, MM = 1999005;
int N, S, T;
int h[MN], iter[MN], nxt[MM * 2], to[MM * 2], tot; LL w[MM * 2];
inline void SetST(int _S, int _T) { S = _S, T = _T; }
inline void Init(int _N) {
N = _N, tot = 1;
for (int i = 1; i <= N; ++i) h[i] = 0;
SetST(_N - 1, _N);
}
inline void ins(int u, int v, LL x) {
if (tot + 1 >= MM * 2) { puts("Error : too many edges."); return ; }
nxt[++tot] = h[u], to[tot] = v, w[tot] = x, h[u] = tot;
}
inline void insw(int u, int v, LL w1 = Inf, LL w2 = 0) {
if (!u) u = S; if (!v) v = T;
ins(u, v, w1), ins(v, u, w2);
}
int lv[MN], que[MN], l, r;
inline bool Lvl() {
for (int i = 1; i <= N; ++i) lv[i] = 0;
lv[S] = 1;
que[l = r = 1] = S;
while (l <= r) {
int u = que[l++];
for (int i = h[u]; i; i = nxt[i])
if (w[i] && !lv[to[i]]) {
lv[to[i]] = lv[u] + 1;
que[++r] = to[i];
}
}
return lv[T] != 0;
}
LL Flow(int u, LL f) {
if (u == T) return f;
LL d = 0, s = 0;
for (int &i = iter[u]; i; i = nxt[i])
if (w[i] && lv[to[i]] == lv[u] + 1) {
d = Flow(to[i], std::min(f, w[i]));
f -= d, s += d;
w[i] -= d, w[i ^ 1] += d;
if (!f) break;
}
return s;
}
inline LL Dinic() {
LL Ans = 0;
while (Lvl()) {
for (int i = 1; i <= N; ++i) iter[i] = h[i];
Ans += Flow(S, Inf);
}
return Ans;
}
}
using DinicFlows::insw;
using DinicFlows::to;
const int MN = 2005;
int N, K, C[MN][MN], Z[MN];
int main() {
int Tests;
scanf("%d", &Tests);
while (Tests--) {
scanf("%d%d", &N, &K);
for (int i = 1; i <= N; ++i)
for (int j = 1; j <= N; ++j)
C[i][j] = 0;
for (int k = 1; k <= K; ++k) {
for (int i = 1; i <= N; ++i) scanf("%d", &Z[i]);
for (int i = 1; i < N; ++i)
for (int j = i + 1; j <= N; ++j)
++C[Z[i]][Z[j]];
}
DinicFlows::Init(2 * N + 2);
for (int i = 1; i <= N; ++i) insw(0, i, 1), insw(N + i, 0, 1);
for (int i = 1; i <= N; ++i)
for (int j = 1; j <= N; ++j)
if (C[i][j] == K) insw(i, N + j, 1);
printf("%lld\n", N - DinicFlows::Dinic());
for (int i = 1; i <= N; ++i) {
int ans = 0;
for (int j = DinicFlows::h[i], u; j; j = DinicFlows::nxt[j])
if ((u = DinicFlows::to[j] - N) <= N && !DinicFlows::w[j]) { ans = u; break; }
printf("%d ", ans);
}
puts("");
}
return 0;
}
MDSWIN2
也就是每次询问一个区间,对这个区间做组合游戏。
对于在这个区间中出现过的元素,它可以被看作 Nim 游戏中的一堆石子,个数为它在这个区间中的出现次数。
使用莫队算法可以在 \(\mathcal O (Q + N \sqrt{Q})\) 的时间内维护每一堆石子的个数,也能维护 \(\mathrm{SG}\) 值。
如果 \(\mathrm{SG}\) 值为 \(0\),则答案自然为 \(0\),因为先手必败。
否则我们考虑,答案应该等于 \(\displaystyle \sum_{i} \binom{c_i}{c_i \oplus \mathrm{SG}}\)。
也就是枚举每一堆石子,需要把这一堆石子的个数 \(c_i\) 变为 \(c_i \oplus \mathrm{SG}\),才能让新的 \(\mathrm{SG}\) 值变为 \(0\),所以方案数为 \(\displaystyle \binom{c_i}{c_i \oplus \mathrm{SG}}\)。
这个东西不好维护,考虑按大小分块。
对于所有在原数组中出现次数不超过 \(S = \mathcal O (\sqrt{N})\) 的数,称它对应的石子堆为小堆,否则为大堆。
那么我们使用一个大小为 \(S\) 的桶维护当前石子数恰好为某个值的小堆石子个数,初始区间为空时所有小堆石子的个数都为 \(0\)。
对于大堆石子,其个数不超过 \(n / S = \mathcal O (\sqrt{N})\) 个,可以直接计算。
以上两个信息在莫队过程中也是可以 \(\mathcal O (1)\) 维护的,只有在处理询问时进行计算。
预处理阶乘和阶乘的逆元,在询问时遍历桶计算每个对应石子数量的答案并乘上相对应的石子堆个数,然后对于大堆石子暴力计算。
时间复杂度为 \(\mathcal O (N \sqrt{Q} + Q \sqrt{N})\)。
#include <cstdio>
#include <cmath>
#include <algorithm>
typedef long long LL;
const int Mod = 998244353;
const int MN = 100005, MQ = 100005, MS = 325;
inline int qPow(int b, int e) {
int a = 1;
for (; e; e >>= 1, b = (LL)b * b % Mod)
if (e & 1) a = (LL)a * b % Mod;
return a;
}
int Fac[MN], iFac[MN];
void Init(int N) {
Fac[0] = 1;
for (int i = 1; i <= N; ++i) Fac[i] = (LL)Fac[i - 1] * i % Mod;
iFac[N] = qPow(Fac[N], Mod - 2);
for (int i = N; i >= 1; --i) iFac[i - 1] = (LL)iFac[i] * i % Mod;
}
inline int Binom(int N, int M) {
if (M < 0 || M > N) return 0;
return (LL)Fac[N] * iFac[M] % Mod * iFac[N - M] % Mod;
}
int N, S, blk[MN], A[MN], B[MN], D[MN], C;
int Q, ql[MQ], qr[MQ], qp[MQ], Ans[MQ];
int vis[MN], buk[MS], seq[MN], cnt, SG;
inline void Mdf(int x, int t) {
SG ^= vis[x];
if (B[x] < S) {
--buk[vis[x]];
vis[x] += t;
++buk[vis[x]];
} else vis[x] += t;
SG ^= vis[x];
}
inline int Num(int x) { return Binom(x, x ^ SG); }
int main() {
Init(100000);
int Tests;
scanf("%d", &Tests);
while (Tests--) {
scanf("%d", &N), SG = cnt = C = 0, S = sqrt(N);
for (int i = 1; i <= N; ++i) scanf("%d", &A[i]), D[++C] = A[i];
std::sort(D + 1, D + C + 1), C = std::unique(D + 1, D + C + 1) - D - 1;
for (int i = 1; i <= C; ++i) vis[i] = B[i] = 0;
for (int i = 1; i <= N; ++i) ++B[A[i] = std::lower_bound(D + 1, D + C + 1, A[i]) - D];
for (int i = 1; i <= C; ++i) if (B[i] >= S) seq[++cnt] = i;
for (int i = 1; i <= N; ++i) blk[i] = (i - 1) / S + 1;
for (int i = 1; i < S; ++i) buk[i] = 0;
scanf("%d", &Q);
for (int i = 1; i <= Q; ++i) scanf("%d%d", &ql[i], &qr[i]), qp[i] = i, Ans[i] = 0;
std::sort(qp + 1, qp + Q + 1, [](int i, int j) {
if (blk[ql[i]] == blk[ql[j]])
return blk[ql[i]] & 1 ? qr[i] < qr[j] : qr[i] > qr[j];
return blk[ql[i]] < blk[ql[j]];
});
int L = 1, R = 0;
for (int I = 1; I <= Q; ++I) {
int i = qp[I], l = ql[i], r = qr[i];
while (L > l) Mdf(A[--L], 1);
while (R < r) Mdf(A[++R], 1);
while (L < l) Mdf(A[L++], -1);
while (R > r) Mdf(A[R--], -1);
if (!SG) { Ans[i] = 0; continue; }
for (int j = 1; j < S; ++j) Ans[i] = (Ans[i] + (LL)buk[j] * Num(j)) % Mod;
for (int j = 1; j <= cnt; ++j) Ans[i] = (Ans[i] + Num(vis[seq[j]])) % Mod;
}
for (int i = 1; i <= Q; ++i) printf("%d\n", Ans[i]);
}
return 0;
}
INVXOR
考虑我们已知一个序列,如何计算它的漂亮程度?
按二进制位分别考虑,如果序列中的所有数,在该位上不全为 \(0\),那么那个为 \(1\) 的位置就能够任意决定结果,所以贡献一个 \(2^{N - 1}\),再乘上该位的位值。
通俗地说,令这个序列中的所有数的 \(\mathrm{or}\)(按位或)值为 \(x\),则漂亮程度为 \(2^{N - 1} \cdot x\)。
那么反过来,如何计算漂亮程度为 \(B\) 的序列个数?也就是计算 \(F(N, B)\)。
首先,如果 \(B\) 不是 \(2^{N - 1}\) 的倍数,则 \(F(N, B)\) 为 \(0\)。
否则令 \(X = B / 2^{N - 1}\),则 \(\mathrm{or}\) 值就为 \(X\),如果 \(N\) 个数或起来等于 \(X\),要满足什么条件?
对于 \(X\) 为 \(1\) 的位,不同位互不影响,每个位的方案数为 \((2^{N} - 1)\)。
所以总方案数为 \({(2^N - 1)}^{\mathrm{popcount}(X)}\),即 \(F(N, B) = {(2^N - 1)}^{\mathrm{popcount}(X)}\)。
题目中给定了 \(F(N, B) \equiv X \pmod{M}\),并要求出最小的 \(B\)。
令 \(K = \mathrm{popcount}(X)\),则在 \(K\) 确定的前提下,取 \(X = 2^K - 1\) 可以让 \(X\) 最小,进而让 \(B\) 最小。
所以只需考虑最小的 \(K\) 就行了。
那么就是解方程 \({(2^N - 1)}^K \equiv X \pmod{M}\),这是标准的离散对数问题,使用 ExBSGS 算法求解即可。
解得了 \(K\) 后,对应的 \(B\) 就为 \(2^{N - 1} (2^K - 1)\),然后就做完了。
因为 \(N\) 是高精度整数,所以做关于它的运算的时候要考虑取模,解离散对数的时候对 \(M\) 取模,算答案的时候对 \(998244353\) 取模。
但是这题的一个坑点在于,如果 \(B\) 不是 \(2^{N - 1}\) 的倍数,则 \(F(N, B) = 0\),这种情况是需要考虑的。
当 \(N \ge 2\) 且 \(X = 0\) 且 \(M \ge 2\) 的时候这种情况就会发生,需要输出 \(1\)。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <unordered_map>
typedef long long LL;
const int Mod = 998244353, Inv2 = (Mod + 1) / 2;
inline int qPow(int b, int e) {
int a = 1;
for (; e; e >>= 1, b = (LL)b * b % Mod)
if (e & 1) a = (LL)a * b % Mod;
return a;
}
int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }
int exgcd(int a, int b, int &x, int &y) {
if (!b) return x = 1, y = 0, a;
int d = exgcd(b, a % b, y, x);
return y -= a / b * x, d;
}
std::unordered_map<int, int> buk;
inline int BSGS(int a, int b, int m) {
int S = sqrt(m - 1) + 1;
int A = 1, f = -1;
buk.clear();
for (int i = 0; i < S; ++i) {
buk[(LL)b * A % m] = i;
A = (LL)A * a % m;
}
int C = 1;
for (int i = 1; !~f && i <= S; ++i)
if (buk.count(C = (LL)C * A % m))
f = i * S - buk[C];
return f;
}
inline int ExBSGS(int a, int b, int m) {
int o = 0, A = 1 % m, d = 1, nd, x, y;
while (1) {
if (d == (nd = gcd((LL)A * a % m, m))) break;
if (A == b) return o;
++o, A = (LL)A * a % m, d = nd;
}
if (b % d) return -1;
m /= d, b /= d, A /= d;
exgcd(A, m, x, y);
b = (LL)b * (x + m) % m;
x = b == 1 % m ? 0 : BSGS(a, b, m);
if (!~x) return -1;
return o + x;
}
const int MS = 10005;
char S[MS];
int A[MS], Len, X, M;
int pw2[10];
inline void Init() {
scanf("%s%d%d", S + 1, &X, &M), Len = strlen(S + 1);
for (int i = 1; i <= Len; ++i) A[i] = S[i] - '0';
pw2[0] = 1 % M;
for (int i = 1; i < 10; ++i) pw2[i] = pw2[i - 1] * 2 % M;
}
int main() {
int Tests;
scanf("%d", &Tests);
while (Tests--) {
Init();
if (X == 1 % M) { puts("0"); continue; }
if (X == 0 && (Len >= 2 || A[1] >= 2)) { puts("1"); continue; }
int V = 1 % M;
for (int i = 1; i <= Len; ++i) {
int _2 = (LL)V * V % M;
int _5 = (LL)_2 * _2 % M * V % M;
V = (LL)_5 * _5 % M * pw2[A[i]] % M;
}
V = (V ? V : M) - 1;
int K = ExBSGS(V, X, M);
if (!~K) { puts("-1"); continue; }
int Ans = 1;
for (int i = 1; i <= Len; ++i) {
int _2 = (LL)Ans * Ans % Mod;
int _5 = (LL)_2 * _2 % Mod * Ans % Mod;
Ans = (((LL)_5 * _5 % Mod) << A[i]) % Mod;
}
printf("%lld\n", (LL)Ans * Inv2 % Mod * (qPow(2, K) - 1) % Mod);
}
return 0;
}
EGGFREE
梳理一下题面,要求为无向边定向,使得是个 DAG,且不存在 \(x \to y\) 且 \(x \to z\) 但 \(y, z\) 之间没有连边。
那么很自然地,我们考虑到拓扑序,拓扑序在前的点强制向拓扑序在后的点连边。
那么这个限制就是说,只考虑一个点向后的连边,则连向的点形成一个团。
这是赤裸裸的弦图判定,构造完美消除序列,贴一个最大势算法的板子上去就做完了(我不会告诉你我是现学现卖的)。
#include <cstdio>
#include <vector>
const int MN = 200005, MM = 200005;
int N, M, eu[MM], ev[MM];
std::vector<int> G[MN], V[MN];
int deg[MN], id[MN], seq[MN], cnt;
int pre[MN], col[MN];
int main() {
int Tests;
scanf("%d", &Tests);
while (Tests--) {
scanf("%d%d", &N, &M);
for (int i = 1; i <= N; ++i) G[i].clear(), deg[i] = id[i] = 0;
for (int i = 1; i <= M; ++i)
scanf("%d%d", &eu[i], &ev[i]),
G[eu[i]].push_back(ev[i]),
G[ev[i]].push_back(eu[i]);
for (int i = 0; i < N; ++i) V[i].clear();
for (int i = 1; i <= N; ++i) V[0].push_back(i);
int best = 0;
for (int i = 1, u; i <= N; ++i, ++best) {
while (V[best].empty() || id[V[best].back()])
if (V[best].empty()) --best;
else V[best].pop_back();
id[seq[i] = u = V[best].back()] = i;
for (int v : G[u]) if (!id[v]) V[++deg[v]].push_back(v);
}
for (int i = 1; i <= N; ++i) V[i].clear();
for (int i = 1; i <= N; ++i) {
pre[i] = 0;
for (int u : G[i]) if (id[u] < id[i])
if (id[pre[i]] < id[u]) pre[i] = u;
if (!pre[i]) continue;
for (int u : G[i]) if (id[u] < id[i])
if (u != pre[i]) V[pre[i]].push_back(u);
}
int ok = 1;
for (int i = 1; i <= N; ++i) {
int u = seq[i];
for (int v : G[u]) col[v] = u;
for (int v : V[u]) if (col[v] != u) ok = 0;
}
if (!ok) { puts("No solution"); continue; }
for (int i = 1; i <= M; ++i) putchar(id[eu[i]] < id[ev[i]] ? 'v' : '^');
putchar('\n');
}
return 0;
}
BREAK
第一个子任务,要在一个回合中打完所有牌。
推导一下就发现最小的牌必打,对面最好的决策也是出最小的牌来应对(因为要考虑到你的(严格)第二小的牌需要一个能打出的基础,但是在那之间的牌也不得不打)。
那么只要排个序,然后判断一下是否每一张牌打出时都是合法的就行。
第二个子任务我没有写代码,这里给出官方题解,实际上和我的几个猜测几乎吻合,猜结论才是硬道理。
- 对于 \(N \le 2\) 的情况特判。
- 如果存在一种牌出现超过了 \(N\) 次(也就是大于一半),因为每次是丢掉不同的两张牌,所以答案是
NO
。 - 如果己方的所有牌都一样且权值比对方的所有牌都要大(或相等),则因为无论送给对方多少张牌都没法进行一次丢弃并转换攻防,所以答案是
NO
。 - 如果对方的所有牌都一样且权值比己方的所有牌都要小(或相等),则答案是
NO
,理由同上。 - 其它所有情况答案是
YES
(Itst 的博客给出的证明)。
\(50\) 分代码:
#include <cstdio>
#include <algorithm>
const int MN = 100005;
int N, A[MN], B[MN], D[MN * 2], C;
inline void Z(int *Ar) {
for (int i = 1, j = 1; i <= N; ++i) {
while (j < C && D[j] < Ar[i]) ++j;
Ar[i] = j;
}
}
inline void Init() {
scanf("%d", &N);
for (int i = 1; i <= N; ++i) scanf("%d", &A[i]);
for (int i = 1; i <= N; ++i) scanf("%d", &B[i]);
std::sort(A + 1, A + N + 1), std::sort(B + 1, B + N + 1);
std::merge(A + 1, A + N + 1, B + 1, B + N + 1, D + 1);
C = std::unique(D + 1, D + 2 * N + 1) - D - 1, Z(A), Z(B);
}
int buk[MN * 2];
inline int check() {
for (int i = 1; i <= N; ++i) if (A[i] >= B[i]) return 0;
for (int i = 1; i <= C; ++i) buk[i] = 0;
for (int i = 1; i < N; ++i) {
buk[A[i]] = buk[B[i]] = 1;
if (!buk[A[i + 1]]) return 0;
}
return 1;
}
int main() {
int Tests, Type;
scanf("%d%d", &Tests, &Type);
while (Tests--) {
Init();
int Ans = check();
if (Ans) { puts("YES"); continue; }
if (Type == 1) { puts("NO"); continue; }
// printf("\t\tA : "); for (int i = 1; i <= N; ++i) printf("%d, ", A[i]); puts("");
// printf("\t\tB : "); for (int i = 1; i <= N; ++i) printf("%d, ", B[i]); puts("");
}
return 0;
}
GOODSEGS
序列上连续段问题,在不久之前只有三类做法,其中单调栈 + 线段树的做法可拓展性最强。
但是由刘承奥发明的析合树的横空出世打破了这种局面,析合树让连续段问题的抽象结构提升到了更高的层次。
如果你对析合树比较熟悉,可以发现两个相交但不包含的连续段 \([a, b]\) 和 \([c, d]\)(\(a < c \le b < d\))分离出的三个区间 \([a, c - 1]\)、\([c, b]\)、\([b + 1, d]\) 都是连续段,而且它们必然处在一个合点的有序孩子中。
那么我们对于每个合点的孩子进行一个双指针扫描就可以确定让中间这个 \([c, b]\) 的长度大于等于 \(X\) 的那些方案,左右两侧的方案数只要做一点简单的数学变换即可。
因为只会口胡,不会写析合树,理解也不透彻,所以代码咕了。
感兴趣的读者可以自行前往 析合树 - OI Wiki 查阅。
ADANTS
挑战题,不做分析。