訴訟(壹)
\(\Large\text{A. Digits Sum}\)
暴力枚举即可。
官解的做法更有趣些:若 \(n = 10^k\),则 \(ans = 10\);否则 \(ans = \text{digits-sum(n)}\).
首先有 \(\text{d-s}(a) + \text{d-s}(b) \geqslant \text{d-s}(n)\),每进位一次数字和少 9。当 \(\text{d-s}(n) \geqslant 2\) 时可以取等,否则要至少进位一次,即 \(10\)。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
void solve()
{
int n;
cin >> n;
auto calc = [](int x)
{
int res = 0;
while (x)
{
res += x % 10;
x /= 10;
}
return res;
};
int ans = 0x3f3f3f3f;
for (int a = 1, b = n - 1; a <= b; a++, b--)
ans = min(ans, calc(a) + calc(b));
cout << ans << endl;
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{B. RGB Coloring}\)
还是水题。显然红蓝各自独立,因此枚举合法的组 \((a, b)\),\(ans = \sum\limits_{(a, b)} \binom{n}{a} \cdot \binom{n}{b}\).
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 3e5;
constexpr long long P = 998244353;
long long fac[N + 5], inv[N + 5];
void solve()
{
long long n, A, B, k;
cin >> n >> A >> B >> k;
fac[0] = 1;
for (long long i = 1; i <= n; i++)
fac[i] = fac[i - 1] * i % P;
auto ksm = [&](long long x)
{
long long res = 1, u = P - 2;
while (u)
{
if (u & 1)
res = res * x % P;
u >>= 1;
x = x * x % P;
}
return res;
};
inv[n] = ksm(fac[n]);
for (int i = n - 1; i >= 0; i--)
inv[i] = inv[i + 1] * (i + 1) % P;
auto calc = [&](long long x)
{
return fac[n] * inv[x] % P * inv[n - x] % P;
};
long long ans = 0;
for (long long a = 0; a <= n; a++)
{
if (A * a > k)
break;
long long rest = k - A * a;
if (rest % B != 0)
continue;
long long b = rest / B;
if (b > n)
continue;
ans = (ans + calc(a) * calc(b) % P) % P;
}
cout << ans << endl;
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{C. Interval Game}\)
开始非平凡了。
容易有一个贪心做法:一左一右交替横跳,每次取最远的线段。如果初始就在最左或最右,凭感觉是对的。因此只需枚举第一次往哪边走,重复做两遍取 \(\max\) 即可。
然而不会严谨证明,题解区看到一篇相对靠谱的。对每个最小单位,考虑最大贡献。对每一条单位线段,其贡献不超过 \(2 \min\{\text{左侧右端点数量},\text{右侧左端点数量}\}\),在反复横跳时取等。实现上,另加入线段 \((0, 0)\),前缀和统计。
官解又是不同做法。
首先,连续两次向同一方向移动一定不优,横跳一下更好。此外,随线段数增加,答案不减。不妨先忽略一些线段,并假设每条线段都能让玩家移动。累计长度,易知 \(ans = 2(\sum L - \sum R)\)。
所以问题变为选择一些左右端点。根据横跳知左右端点数量相等。因此枚举端点数量,取最大 / 小的左 / 右端点按上式计算。不用担心左右取到同一条线段的情况,因为该线段贡献为负。此后的方案均劣于先前方案。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <set>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 1e5;
typedef pair<int, int> pir;
int n;
pir seg[N + 5];
set<pir> L, R;//{key, id}
void solve()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> seg[i].first >> seg[i].second;
}
auto findL = [&]()
{
auto it = L.end();
it--;
return it -> second;
};
auto findR = [&]()
{
return R.begin() -> second;
};
auto clear = [&](int id)
{
auto it = L.find(pir{seg[id].first, id});
L.erase(it);
it = R.find(pir{seg[id].second, id});
R.erase(it);
};
auto update = [&](int &x, long long &sum, int id)
{
int l = seg[id].first, r = seg[id].second;
if (x < l)
{
sum += l - x;
x = l;
}
else if (x > r)
{
sum += x - r;
x = r;
}
else
{;}
};
auto solve = [&](int p)
{
for (int i = 1; i <= n; i++)
{
L.insert(pir{seg[i].first, i});
R.insert(pir{seg[i].second, i});
}
int x = 0;
long long sum = 0;
for (int t = 1; t <= n; t++)
{
int id = (p ? findR() : findL());
update(x, sum, id);
clear(id);
p ^= 1;
}
sum += (x > 0 ? x : -x);
return sum;
};
long long ans = solve(0);
ans = max(ans, solve(1));
cout << ans << endl;
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{D. Chooing Points}\)
困难的图论,看题解感觉也并非不可及……?
从 \(4n^2\) 个点中选出 \(n^2\) 个,\(4 = 2 \times 2\) ……?
或者,从简单情况入手,比如只有一个 \(D\)?给冲突的点对连边,此时需要发现图是二分图。图论的思考点不多,所以这是自然的……?证明:
F1:若 \(D\) 为奇数,\(s^2 + t^2 = D\) 说明 \(s, t\) 一奇一偶。按 \(x + y\) 对 \((x, y)\) 染色,则边的两端 \(x + y\) 必然不同奇偶。
若 \(D\) 为偶数,说明 \(s, t\) 同奇偶。对格点黑白染色,则黑白之间无连边。对每种颜色单独考虑,相当于一个扩大 \(\sqrt{2}\) 倍的斜 \(45^\circ\) 的新格点图。因此递归为 \(D / 2\) 的问题。最终仍为二分图。
F2:由上可知,直接设 \(D = 4^k \cdot p \; (p \mod 4 \neq 0)\),则 \(s, t\) 必形如 \(2^k \cdot u, 2^k \cdot v \; (u^2 + v^2 = p)\)。按模 \(4\) 讨论,\(p \equiv 3\) 不存在 \(s, t\),其余情况都可讨论出具体的染色方案,略。
每个 \(D\) 都将图染成两种颜色,则 \(2 \times 2 = 4\) 种染色组合中,最大的色块大小必然至少占 \(1 / 4\)。
此外,\((s, t)\) 对数很少,总复杂度仍 \(O(n^2)\)。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 300, M = N << 1;
int n, d[2];
vector<int> dx[2], dy[2];
int col[M + 5][M + 5][2];//2 3 4 5
void dfs(int x, int y, int t, int c)
{
col[x][y][t] = c;
for (auto i = 0; i < dx[t].size(); i++)
{
int xx = x + dx[t][i], yy = y + dy[t][i];
if (xx < 0 || xx >= n + n || yy < 0 || yy >= n + n)
continue;
if (col[xx][yy][t])
continue;
dfs(xx, yy, t, c ^ 1);
}
return ;
}
void solve()
{
cin >> n >> d[0] >> d[1];
for (int x = 1 - n - n; x <= n + n - 1; x++)
for (int y = 1 - n - n; y <= n + n - 1; y++)
{
for (int t = 0; t < 2; t++)
{
if (x * x + y * y == d[t])
dx[t].push_back(x), dy[t].push_back(y);
}
}
for (int t = 0; t < 2; t++)
{
for (int x = 0; x < n + n; x++)
for (int y = 0; y < n + n; y++)
{
if (!col[x][y][t])
dfs(x, y, t, (t == 0 ? 2 : 4));
}
}
int cnt[8][8] = {};
for (int x = 0; x < n + n; x++)
for (int y = 0; y < n + n; y++)
cnt[ col[x][y][0] ][ col[x][y][1] ]++;
for (int p = 2; p <= 3; p++)
for (int q = 4; q <= 5; q++)
{
if (cnt[p][q] < n * n)
continue;
int tot = n * n;
for (int x = 0; x < n + n && tot; x++)
for (int y = 0; y < n + n && tot; y++)
if (col[x][y][0] == p && col[x][y][1] == q)
cout << x << ' ' << y << '\n', tot--;
return ;
}
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{E. Walking on a Tree}\)
困难的。
不过答案很好猜!猜测能取到最大值,即覆盖一次的有 \(1\) 的贡献,覆盖两次及以上的都有 \(2\) 的贡献。看一眼样例发现是对的。后面不会。
题解区想到了欧拉路径。不过路径还是不方便,考虑连成欧拉回路。它强化了条件:对于每条边,强制要求两个方向的路径至多差一条。对每条路径两端连边,再对覆盖奇数次的边额外填一条。调整后每条边覆盖偶数次,满足欧拉回路。
这个做法的瓶颈在 \(\text{LCA}\),其余部分线性。然而不会写无向图欧拉路了。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 2000, Lg = 10;
constexpr int E = (N + N) << 1;
int n, m;
vector<int> tree[N + 5];
int path[N + 5][2];
int fa[N + 5][Lg + 5], dep[N + 5];
void predfs(int u)
{
for (int k = 1; k <= Lg; k++)
fa[u][k] = fa[ fa[u][k - 1] ][k - 1];
for (auto v : tree[u])
{
if (v == fa[u][0])
continue;
fa[v][0] = u, dep[v] = dep[u] + 1;
predfs(v);
}
return ;
}
int LCA(int u, int v)
{
if (dep[u] < dep[v])
swap(u, v);
for (int k = Lg; k >= 0; k--)
{
if (dep[fa[u][k]] >= dep[v])
u = fa[u][k];
}
if (u == v)
return u;
for (int k = Lg; k >= 0; k--)
{
if (fa[u][k] != fa[v][k])
u = fa[u][k], v = fa[v][k];
}
return fa[u][0];
}
int tag[N + 5];
int head[N + 5], to[E + 5], nxt[E + 5], tot = 1;
int id[E + 5], used[E + 5];
void add_edge(int u, int v, int _id = 0)
{
tot++;
nxt[tot] = head[u];
to[tot] = v;
id[tot] = _id;
head[u] = tot;
return ;
}
void add(int u, int v, int _id = 0)
{
add_edge(u, v, _id);
add_edge(v, u, _id);
return ;
}
int dir[N + 5];
int ans;
void dfs(int u)
{
for (auto v : tree[u])
{
if (v == fa[u][0])
continue;
dfs(v);
tag[u] += tag[v];
}
if (u != 1)
ans += (tag[u] > 0) + (tag[u] > 1);
if (u != 1 && (tag[u] & 1))
add(u, fa[u][0]);
return ;
}
void find_path(int u)
{
for (int i = head[u]; i; i = nxt[i])
{
int v = to[i];
if (used[i])
continue;
used[i] = used[i ^ 1] = 1;
if (id[i])
dir[id[i]] = (u != path[id[i]][0]);
find_path(v);
}
return ;
}
void solve()
{
cin >> n >> m;
for (int i = 1, u, v; i < n; i++)
{
cin >> u >> v;
tree[u].push_back(v);
tree[v].push_back(u);
}
predfs(1);
for (int i = 1, u, v; i <= m; i++)
{
cin >> u >> v;
path[i][0] = u, path[i][1] = v;
int lca = LCA(u, v);
tag[u]++, tag[v]++;
tag[lca] -= 2;
add(u, v, i);
}
dfs(1);
for (int i = 1; i <= n; i++)
find_path(i);
cout << ans << '\n';
for (int i = 1; i <= m; i++)
{
if (dir[i])
swap(path[i][0], path[i][1]);
cout << path[i][0] << ' ' << path[i][1] << '\n';
}
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
官解是归纳的思路。每次删一个叶子 \(u\)。若 \(<u, fa>\) 覆盖一次,不妨把对应路径 \((u, v)\) 改为 \((fa, v)\),最终延申一截即可。若 \(<u, fa>\) 覆盖多次,任取两条路径 \((x, u), (u, y)\),以路径 \((x, y)\) 取代之。如果最终定向 \(x \to y\),则对应 \(x \to u, u \to y\),反之类似。这样两条路径重合部分总被正反覆盖。
按 dfs 序倒序取点,每次均为叶子。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 2000, SIZE = N * 3;
int n, m;
vector<int> tree[N + 5];
int fa[N + 5], dep[N + 5];
int seq[N + 5], dfn;
void dfs(int u)
{
seq[++dfn] = u;
for (auto v : tree[u])
{
if (v == fa[u])
continue;
fa[v] = u, dep[v] = dep[u] + 1;
dfs(v);
}
return ;
}
struct Path
{
int id, u, v;
int x, y;//x + y -> this
int state;//0: undef; 1: delete; 2: root
void flip()
{
swap(u, v);
return ;
}
};
Path path[SIZE + 5];
int cur;
int tag[N + 5];
int ans;
void cover(int u, int v)
{
while (u != v)
{
if (dep[u] < dep[v])
swap(u, v);
tag[u]++;
u = fa[u];
}
return ;
}
int tmp[SIZE + 5], len;
void modify(int w)
{
len = 0;
for (int i = 1; i <= cur; i++)
{
if (path[i].state > 0)
continue;
if (path[i].u == w || path[i].v == w)
tmp[++len] = i;
}
for (int i = 1; i + 1 <= len; i += 2)
{
int x = tmp[i], y = tmp[i + 1];//x: u -> w; y: w -> v
if (path[x].u == w)
path[x].flip();
if (path[y].v == w)
path[y].flip();
if (path[x].u == path[y].v)//overlap
{
path[x].state = path[y].state = 2;
continue;
}
cur++;
path[cur] = Path{cur, path[x].u, path[y].v, x, y, 0};
path[x].state = path[y].state = 1;
}
if (len & 1)
{
int x = tmp[len];//u -> w
if (path[x].u == w)
path[x].flip();
if (path[x].u == fa[w])
path[x].state = 2;
else
{
cur++;
path[cur] = Path{cur, path[x].u, fa[w], x, -1, 0};
path[x].state = 1;
}
}
return ;
}
void pushdown(int w)
{
int x = path[w].x, y = path[w].y;
if (x == -1)
return ;
if (y == -1)
{
if (path[x].u == path[w].v)
path[x].flip();
path[x].state = 2;
return ;
}
if (path[x].u == path[w].v)
{
path[x].flip();
path[y].flip();
}
path[x].state = path[y].state = 2;
return ;
}
void solve()
{
cin >> n >> m;
for (int i = 1, u, v; i < n; i++)
{
cin >> u >> v;
tree[u].push_back(v);
tree[v].push_back(u);
}
dep[1] = 1;
dfs(1);
for (int i = 1, u, v; i <= m; i++)
{
cin >> u >> v;
path[i] = Path{i, u, v, -1, -1, 0};
cover(u, v);
}
cur = m;
for (int t = n; t > 1; t--)
{
int u = seq[t];
modify(u);
}
for (int t = cur; t > 0; t--)
{
if (path[t].state == 2)
pushdown(t);
}
for (int i = 2; i <= n; i++)
ans += (tag[i] > 0) + (tag[i] > 1);
cout << ans << '\n';
for (int i = 1; i <= m; i++)
cout << path[i].u << ' ' << path[i].v << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{F. Addition and Andition}\)
相对好想的题。
逐位考虑,只有同时为 \(1\) 非平凡。取与再相加,必然进位,相当于两个 \(1\) 同时前移一步。
所有的 \((1, 1)\) 同时移动,\((1, 0), (0, 1)\) 对它们构成阻碍。靠前的 \((1, 1)\) 产生的影响必然先于后来者,因此可以从高到低逐位确定。
用栈维护 \(1\) 的位置,每次跳到最近的 \(1\) 并暴力进位,检查是否产生了新的 \((1, 1)\)。均摊线性,但是细节很多。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 2e6, Inf = N << 1;
int n, m, k;
int s[N + 5], t[N + 5];
int S[N + 5], szS;
int T[N + 5], szT;
int tmpS[N + 5], sztS;
int tmpT[N + 5], sztT;
void Mov(int p)//k = 0 is undef
{
int cur = 0;
while (cur < k)
{
int x = (szS ? S[szS] : Inf);
int y = (szT ? T[szT] : Inf);
int z = min(x, y);
if (p + k - cur < z)
{
p += k - cur;
S[++szS] = p;
T[++szT] = p;
break;
}
cur += z - p;
int pS = z;
while (szS && S[szS] == pS)
pS++, szS--;
S[++szS] = pS;
int pT = z;
while (szT && T[szT] == pT)
pT++, szT--;
T[++szT] = pT;
if (pS < pT)
{
while (pS < pT)
{
tmpS[++sztS] = pS;
pS = S[--szS];
if (!szS)
{
pS = Inf;
break;
}
}
if (pS != pT)
break;
}
else if (pS > pT)
{
while (pS > pT)
{
tmpT[++sztT] = pT;
pT = T[--szT];
if (!szT)
{
pT = Inf;
break;
}
}
if (pS != pT)
break;
}
else
{;}
p = pS;
if (cur < k)//otherwise cur = k, will lost two 1 (for each)
szS--, szT--;
}
while (sztS)
S[++szS] = tmpS[sztS--];
while (sztT)
T[++szT] = tmpT[sztT--];
return ;
}
void solve()
{
cin >> n >> m >> k;
for (int i = n; i > 0; i--)
{
char c;
cin >> c;
s[i] = c - '0';
}
for (int i = m; i > 0; i--)
{
char c;
cin >> c;
t[i] = c - '0';
}
for (int i = max(n, m); i > 0; i--)
{
int x = s[i], y = t[i];
if ((!x) & (!y))
continue;
if (!(x & y))
{
if (x)
S[++szS] = i;
if (y)
T[++szT] = i;
continue;
}
Mov(i);
}
cout << 1;
for (int i = 2; i <= szS; i++)
{
for (int j = S[i - 1] - 1; j > S[i]; j--)
cout << 0;
cout << 1;
}
for (int j = S[szS] - 1; j > 0; j--)
cout << 0;
cout << '\n';
cout << 1;
for (int i = 2; i <= szT; i++)
{
for (int j = T[i - 1] - 1; j > T[i]; j--)
cout << 0;
cout << 1;
}
for (int j = T[szT] - 1; j > 0; j--)
cout << 0;
cout << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{A. Colorful Slimes 2}\)
颜色数 \(10^4 \gg n\),每次都可染成新颜色。对每个连续段考虑,至少要改 \(\lfloor len / 2 \rfloor\) 次。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 100;
int n;
int a[N + 5];
void solve()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
int ans = 0;
for (int l = 1, r = 1; l <= n; l = r + 1)
{
r = l;
while (r + 1 <= n && a[r + 1] == a[l])
r++;
ans += (r - l + 1) >> 1;
}
cout << ans << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large{\text{B. rng} \_ \text{10s}}\)
不会 sad
先判一些容易的。\(a < b\),第一天就寄了;\(d < b\),入不敷出。下面 \(a, d \geqslant b\)。
显然不停机的条件更严苛。不妨每天补货前为分界。经过充分长时间,(以任意 \(a\) 开头)库存 \(x\) 应当在 \(l \sim r\) 之间波动,且有 \(l \leqslant c < r\)。具体的,
\(l, r\) 必须要取到,不难有 \(l = c + 1 - b, \; r = c + d - b\)。即 \(x \in [c + 1 - b, \; c + d - b]\)。
另一方面,从同余考虑,\(x = a + \lambda d - \mu b\),由裴蜀定理知 \(x \equiv a \pmod{\gcd(b, d)}\)。只需求出在范围内的最小 \(x\) 且满足 \(x > 0\) 即不停机。(感性理解,范围内的所有 \(x \equiv a\) 均可取到。)
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
#define TestCases
long long a, b, c, d;
#define Y return cout << "Yes\n", void()
#define N return cout << "No\n", void()
void solve()
{
cin >> a >> b >> c >> d;
if (a < b || d < b)
N;
if (c >= b - 1)
Y;
long long gcd = __gcd(b, d);
if ((a - b) / gcd < (a - c - 1) / gcd)
N;
else
Y;
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{C. String Coloring}\)
数据范围看着像指数级。折半搜索。把后一半倒过来,这样前后部分的字符串对应相同,只是交换颜色。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <map>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 18;
constexpr long long P[] = {998244353, 994233853}, M = 1e9 + 7;
int n;
char a[N + 5], b[N + 5];
long long Hash(int s, char c[])
{
long long v[2][2] = {};
for (int i = 1; i <= n; i++)
{
int flag = (s >> (i - 1)) & 1;
for (int t = 0; t < 2; t++)
v[flag][t] = (v[flag][t] * P[t] % M + c[i] - 'a' + 1) % M;
}
long long high = (v[0][0] * P[1] % M + v[0][1] * P[0] % M) % M;
long long low = (v[1][0] * P[1] % M + v[1][1] * P[0] % M) % M;
return (high << 30) | low;
}
map<long long, long long> cnt;
void solve()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= n; i++)
cin >> b[i];
reverse(b + 1, b + n + 1);
int m = 1 << n;
for (int s = 0; s < m; s++)
cnt[Hash(s, a)]++;
long long ans = 0;
for (int s = 0; s < m; s++)
ans += cnt[Hash(s, b)];
cout << ans << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{D. Histogram Coloring}\)
先考虑如何涂色。每行划分为若干连续段,比如(下文按红与蓝):
| \(\color{blue}\text{b}\) | \(\color{red}\text{r}\) | \(\color{red}\text{r}\) | \(\color{red}\text{r}\) | \(\color{blue}\text{b}\) | \(\color{red}\text{r}\) | \(\color{blue}\text{b}\) | \(\color{red}\text{r}\) |
|---|---|---|---|---|---|---|---|
| ? | ? | ? | ? | ? | ? | ? | ? |
考虑下面一行怎么填。注意到对于连续的 \({\color{red}\text{rr}} / {\color{blue}\text{bb}}\),其下面只能填涂相反的颜色,继而每个长度至少为 2 的连续段下方均唯一。
| \(\color{blue}\text{b}\) | \(\color{red}\text{r}\) | \(\color{red}\text{r}\) | \(\color{red}\text{r}\) | \(\color{blue}\text{b}\) | \(\color{red}\text{r}\) | \(\color{blue}\text{b}\) | \(\color{red}\text{r}\) |
|---|---|---|---|---|---|---|---|
| ? | \(\color{blue}\text{b}\) | \(\color{blue}\text{b}\) | \(\color{blue}\text{b}\) | ? | ? | ? | ? |
再考虑连续段交界处,比如 \(\begin{matrix} \color{red}\text{r} & \color{blue}\text{b} \\ \color{blue}\text{b} & ? \end{matrix}\),知三求一,因此旁边的连续段下方也要反色。综上,如果一行中有长度大于 1 的连续段,则下方涂色方案唯一:全部反色。
如果全部是 \(\color{red}\text{r} \color{blue}\text{b}\) 交替呢?反色当然可以,由样例知同色也合法。这是唯二的方案。
| \(\color{blue}\text{b}\) | \(\color{red}\text{r}\) | \(\color{blue}\text{b}\) | \(\color{red}\text{r}\) | \(\color{blue}\text{b}\) | \(\color{red}\text{r}\) | \(\color{blue}\text{b}\) | \(\color{red}\text{r}\) | |
|---|---|---|---|---|---|---|---|---|
| \(\color{red}\text{r}\) | \(\color{blue}\text{b}\) | \(\color{red}\text{r}\) | \(\color{blue}\text{b}\) | \(\color{blue}\text{b}\) | \(\color{red}\text{r}\) | \(\color{blue}\text{b}\) | \(\color{red}\text{r}\) |
因此,只有连续段的划分是关键。上述关于行的涂法对于列自然也对,不过按行自底向上填可能更方便(?)。
从下往上,区间逐渐分解,预处理每个 \([l, r]\) 对应多少层(显然,要求极大)。对于顶端的单点区间,涂法不受下层影响,最后另行统计。设 \(dp_{i, j}\) 表示确定第一层前 \(i\) 个的划分,最近的段是 \((j, j + 1)\),仅考虑包含于 \([1, i]\) 的所有区间(第二层及以上)的涂色方案数。\(j = 0\) 即全部红蓝交替。转移显然。初值 \(dp_{1, 0} = 1\),验证 \(dp_{2, 0} = 2^{cnt_{1, 2}}, dp_{2, 1} = 1\) 正确。
注意,我们没有指定颜色,同一种划分对应两种方案,\(ans \gets ans \times 2\)。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 100;
constexpr long long P = 1e9 + 7;
int n;
int h[N + 5];
int cnt[N + 5][N + 5];
long long dp[N + 5][N + 5];
void solve()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> h[i];
auto ksm = [](long long d, int u)
{
long long res = 1;
while (u)
{
if (u & 1)
res = res * d % P;
u >>= 1;
d = d * d % P;
}
return res;
};
for (int l = 1; l <= n; l++)
{
int Min = h[l];
for (int r = l; r <= n; r++)
{
Min = min(Min, h[r]);
if (h[l - 1] < Min && h[r + 1] < Min)
cnt[l][r] = Min - max(h[l - 1], h[r + 1]);
}
}
cnt[1][n]--;//not include 1st floor !
dp[1][0] = 1;
for (int i = 2; i <= n; i++)
{
for (int j = 0; j < i - 1; j++)
dp[i][i - 1] = (dp[i][i - 1] + dp[i - 1][j]) % P;
for (int j = 0; j < i - 1; j++)//last same: j, j + 1
{
dp[i][j] = dp[i - 1][j];
for (int k = j + 1; k < i; k++)
dp[i][j] = dp[i][j] * ksm(2, cnt[k][i]) % P;
}
}
long long ans = 0;
for (int i = 0; i < n; i++)
ans = (ans + dp[n][i]) % P;
ans = ans * 2 % P;//2 col for 1st
for (int i = 1; i <= n; i++)
ans = ans * ksm(2, cnt[i][i]) % P;
cout << ans << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{E. Synchronized Subsequence}\)
支持 \(O(n^2)\) 复杂度。考虑枚举第一个字母所属字母对。由于按序排列,只有该对之后的字母对可以考虑,显然倒序枚举。
-
\(\texttt{a, ..., b}\)
二者之间至多只有 \(\texttt{a}\),否则能匹配到更靠前的 \(\texttt{b}\),矛盾。因此只能以 \(\texttt{ab}\) 开头,再从 \(\texttt{b}\) 之后的那些完整字母对考虑。这可以维护一个后缀 \(\max\)。
因此,预处理 \(behind_i\):第 \(i\) 个字符之后,最近的字母对(均在 \(i\) 之后)是哪对。
-
\(\texttt{b, ..., a}\)
同理,二者之间至多只有 \(\texttt{b}\)。显然这些 \(\texttt{b}\) 必须全选,它们与对应的 \(\texttt{a}\) 之间又会包含新的 \(\texttt{b}\),连锁反应。而所有 \((\texttt{b}, \texttt{a})\) 之间必然没有属于更靠后的字母对的 \(\texttt{a}\),原因相同。此后归为子问题。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 3000, M = N << 1;
int n;
char s[M + 5];
int a[N + 5], b[N + 5];
int behind[M + 5];
string str[N + 5];//suffix max
int q[N + 5], l, r;
void solve()
{
cin >> n;
for (int i = 1, cnt[2] = {}; i <= n + n; i++)
{
cin >> s[i];
cnt[s[i] - 'a']++;
if (s[i] == 'a')
a[cnt[0]] = i;
else
b[cnt[1]] = i;
}
behind[n + n] = n + 1;
for (int t = n + n - 1; t > 0; t--)
{
int p = behind[t + 1];
while (min(a[p - 1], b[p - 1]) > t)
p--;
behind[t] = p;
}
for (int t = n; t > 0; t--)
{
if (a[t] < b[t])
{
str[t] = "ab" + str[behind[b[t]]];
str[t] = max(str[t], str[t + 1]);
continue;
}
str[t] = "b";
l = r = 1, q[1] = a[t];
int p = t + 1, lst = t;
for (; l <= r; l++)
{
while (p <= n && min(a[p], b[p]) < q[l])
{
if (b[p] < q[l] && q[l] < a[p])
{
str[t] += "b";
q[++r] = a[p];
lst = p;
}
p++;
}
str[t] += "a";
}
for (; l <= r; l++)
str[t] += "a";
str[t] += str[behind[q[r]]];
str[t] = max(str[t], str[t + 1]);
}
cout << str[1] << endl;
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{F. Manju Game}\)
没有独立切的妙妙题!感觉是普及组,但是它的位置不太普及(符合 AGC 刻板印象(?
下文中,先 / 后手指人,先 / 后行指具体的先后次序。我的 \(\LaTeX\) 水平显著提高(
自然地,把奇数位和偶数位上的数分开,分别记作 \(o, e\)。
-
\(n\) 是偶数:\(o, e, o, e, \dots, o, e, o, e\)
先手取最左(右)边的数,得分 \(\sum o\)(\(\sum e\)),因此得分 \(\geqslant \max\{ \sum o, \sum e \}\).
如果先手从中间取,不妨取到 \(o\)。此时序列分成两段,左侧有偶数个,右侧有奇数个。后手先取 \(o\) 左边的数,这样把左侧取完,并且在右侧变为先行。再取最右端,这样后手取走所有的 \(e\)。记先手红色,后手蓝色。
\[\begin{matrix} &\underbrace{\colorbox{white}{o} \quad \colorbox{white}{e}}_{\text{even}} \quad &\mathop{\colorbox{red}{o}}\limits^{\downarrow} \quad &\underbrace{\colorbox{white}{e} \quad \colorbox{white}{o} \quad \colorbox{white}{e} \quad \colorbox{white}{o} \quad \colorbox{white}{e}}_{\text{odd}} \\ &\underbrace{\colorbox{white}{o} \quad \mathop{\colorbox{blue}{e}}\limits^{\downarrow}}_{\text{even}} \quad &\colorbox{red}{o} \quad &\underbrace{\colorbox{white}{e} \quad \colorbox{white}{o} \quad \colorbox{white}{e} \quad \colorbox{white}{o} \quad \colorbox{white}{e}}_{\text{odd}} \\ &\underbrace{\colorbox{red}{o} \quad \colorbox{blue}{e}}_{\text{even}} \quad &\colorbox{red}{o} \quad &\underbrace{\colorbox{white}{e} \quad \colorbox{white}{o} \quad \colorbox{white}{e} \quad \colorbox{white}{o} \quad \mathop{\colorbox{blue}{e}}\limits^{\downarrow}}_{\text{odd}} \\ &\underbrace{\colorbox{red}{o} \quad \colorbox{blue}{e}}_{\text{even}} \quad &\colorbox{red}{o} \quad &\underbrace{\colorbox{blue}{e} \quad \colorbox{red}{o} \quad \colorbox{blue}{e} \quad \colorbox{red}{o} \quad \colorbox{blue}{e}}_{\text{odd}} \end{matrix} \]同理,若先手取到中间的 \(e\),后手可以取走所有 \(o\)。
因此,若先手在中间取,后手得分总 \(\geqslant \min\{ \sum o, \sum e \}\),故先手得分 \(\leqslant sum - \min\{ \sum o, \sum e \} = \max\{ \sum o, \sum e \}\)。
综上,最终必然先手取 \(\max\),后手取 \(\min\)。
-
\(n\) 是奇数:\(o, e, o, e, \dots, o, e, o, e, o\)
先手取两端,得分都是 \(\sum o\)。如果取中间?
-
取 \(o\)
\[\underbrace{o \quad e \quad o \quad e}_{\text{even}} \quad \mathop{\colorbox{red}{o}}\limits^{\downarrow} \quad \underbrace{e \quad o \quad e \quad o}_{\text{even}} \]左右两段均为偶数,因此左右均为后手先行。偶数的子问题上面已经讨论过,每一段先手所得为 \(\min\{ \sum o, \sum e \} \leqslant \sum o\),因此先手总得分 \(\leqslant \sum o\)。
不优于先手直接取端点,故必不会在中间取 \(o\)。
-
取 \(e\)
\[\underbrace{o \quad e \quad o}_{\text{odd}} \quad \mathop{\colorbox{red}{e}}\limits^{\downarrow} \quad \underbrace{o \quad e \quad o \quad e \quad o}_{\text{odd}} \]左右两端均为奇数。后手选一段取走其中的 \(\sum o\),留下 \(\sum e\) 给先手。另一段仍为先手先行,递归为子问题,直至先手通过取端点拿走 \(\sum o\) 结束游戏。整个过程形成一棵树:
\[\begin{matrix} \mathop{\boxed{\quad \quad \quad \quad \quad}}\limits_{\downarrow} &\color{red}{e} &\mathop{\boxed{\quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad}}\limits_{\downarrow} \\ {\color{blue}\sum o} \quad {\color{red}\sum e} &\quad &{\begin{matrix} \mathop{\boxed{\quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad}}\limits_{\downarrow} &\color{red}{e} &\mathop{\boxed{\quad \quad \quad \quad}}\limits_{\downarrow} \\ {\begin{matrix} \mathop{\boxed{\quad \quad \quad \quad}}\limits_{\downarrow} &\color{red}e &\mathop{\boxed{\quad \quad \quad \quad}}\limits_{\downarrow} \\ {\color{blue}\sum o} \quad {\color{red}\sum e} &\quad &\underbrace{{\color{red}\sum o} \quad {\color{blue}\sum e}}_{\text{终止节点}} \end{matrix}} &\quad &{{\color{blue}\sum o} \quad {\color{red}\sum e}} \end{matrix}} \end{matrix} \]不妨认为先手初始得分 \(\sum e\)。先手将序列切成若干段,最终取一段得到另外的 \(\sum o - \sum e\),最大化得分。记 \(\delta = \sum o - \sum e\)。如果先手要获得 \(\delta\) 的加分,必须保证每个叶子节点 \(\sum o - \sum e \geqslant \delta\),因为后手必然会尽力将终止节点向 \(< \delta\) 的方向移动。若所有叶子均满足条件,则后手是否遵循划分都无影响,因为先手可以将未选择的那一侧作为终止,易知最终答案不变。
不难看出 \(\delta\) 可二分。设 \(dp_i\) 表示 \(1 \sim i\) 能否划分,显然 \(i\) 为奇数。其为真当且仅当有 \(dp_j = \text{true} \; \land \; w(j + 2, i) \geqslant \delta\)。因此,对所有 \(dp_j = \text{true}\) 的位置,维护最小前缀和 \(s_{j + 1}\)。这样甚至不用显式存储 \(dp\)。
取两端也是一种取中间,不过把全局当叶子。
-
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 3e5;
int n;
int a[N + 5];
int Odd, Even;
int s[N + 5];
bool check(int delta)
{
int Min = 0;
for (int i = 1; i <= n; i += 2)
{
if (s[i] - Min >= delta)//dp[i] = true
Min = min(Min, s[i + 1]);//dp[i] <- dp[j], sum(i, j + 2) >= delta -> s[i] - s[j + 1] >= delta
}
return s[n] - Min >= delta;//dp[n]
}
void solve()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
if (i & 1)
Odd += a[i], s[i] = s[i - 1] + a[i];
else
Even += a[i], s[i] = s[i - 1] - a[i];
}
if (!(n & 1))//even
{
cout << max(Odd, Even) << " " << min(Odd, Even) << '\n';
return ;
}
int l = 0, r = Odd + Even, delta = 0;
while (l <= r)
{
int mid = (l + r) >> 1;
if (check(mid))
{
delta = mid;
l = mid + 1;
}
else
r = mid - 1;
}
cout << Even + delta << " " << Odd - delta << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
AGC 027 比 AGC 026 更 AGC。
\(\Large\text{A. Candy Distribution Again}\)
从小到大考虑即可,注意最后一个是否恰好。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 100;
int n, x;
int a[N + 5];
void solve()
{
cin >> n >> x;
for (int i = 1; i <= n; i++)
cin >> a[i];
sort(a + 1, a + n + 1);
for (int i = 1; i <= n; i++)
{
if (x < a[i])
return cout << i - 1 << '\n', void();
x -= a[i];
}
cout << (x ? n - 1 : n) << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{B. Garbage Collector}\)
场上卡了很多人的题。
根据一些模糊的印象(025 c?),尝试先枚举一些东西。自然地,枚举一共跑几趟。
同样自然地,把每个点的贡献拆开。对每一趟,第一个垃圾花费 \(1^2 x + 2^2x = 5x\),第 \(i > 1\) 个垃圾花费 \(((1 + i)^2 - i^2)x = (2i + 1)x\)。把系数列出来:\(5, 5, 7, \dots\)
做法便呼之欲出了!系数不降,显然按距离远至近分配,前缀和维护。注意爆 long long。
总复杂度调和数。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 2e5;
int n;
long long w;
long long x[N + 5];
void solve()
{
cin >> n >> w;
for (int i = 1; i <= n; i++)
{
cin >> x[i];
x[i] += x[i - 1];
}
long long ans = 4e18;
for (int t = 1; t <= n; t++)
{
long long cur = w * (n + t) + (x[n] - x[n - t]) * 2;//begin
if (cur > ans)
continue;
for (int base = 1, l = n - t, r = n; r > 0; base++, l -= t, r -= t)
{
cur += (x[r] - x[max(l, 0)]) * (base + base + 1);
if (cur > ans)
break;
}
ans = min(ans, cur);
}
cout << ans << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{C. ABland Yard}\)
自此开始三道题不会做。
初始想法:
边可以来回走,生成奇数或偶数长的序列。考虑迭代,记当前轮次为 \(r\),记录以每个点出发,能否生成所有含 \(r\) 个连续段的串。又能走回头路,\(r\) 应该不太大。跑 10 轮看看。
讲不出任何严谨的东西。WA 了也懒得改。
事实上,只需考虑能否生成 \((\text{AABB})^{\infty}\),这里不限定简单路径。对任意串 \(s\),每一个奇数长度的连续段都代表折返(譬如,从左到右扫描 \(s\) 变为从右到左)。只需靠右选择起点,左侧预留足够的空间即可。
如果一个节点的领域为空或只有一种颜色,则该节点没有价值,删去。如此反复,如果图不空则有解。这也侧面说明,符合要求的子图满足每个点都能到 \(\text{A \& B}\)。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 2e5;
int n, m;
int col[N + 5];
vector<int> e[N + 5];
int cnt[N + 5][2];
int del[N + 5];//attention: self-loop & multi-edge -> v occur > 1 times
int q[N + 5], l = 1, r = 0;
void solve()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
char c;
cin >> c;
col[i] = c - 'A';
}
for (int i = 1, u, v; i <= m; i++)
{
cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
cnt[u][col[v]]++;
cnt[v][col[u]]++;
}
for (int i = 1; i <= n; i++)
{
if (!cnt[i][0] || !cnt[i][1])
del[i] = 1, q[++r] = i;
}
while (l <= r)
{
int u = q[l++];
for (auto v : e[u])
{
cnt[v][col[u]]--;
if (!del[v] && (!cnt[v][0] || !cnt[v][1]))
del[v] = 1, q[++r] = v;
}
}
cout << (r < n ? "Yes" : "No") << '\n';//total: delete r nodes
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{D. Modulo Matrix}\)
显然与 \(\operatorname{lcm}\) 有关,问题是如何限制其大小。
官解直接黑白染色,取 \(m = 1\),令黑格为极大值。筛出前 \(1000\) 个质数。以主对角线或副对角线方向划分,白格均可构成 \(n\) 条斜线。对这 \(2n\) 条斜线,每条线分配一个小质数,白格取值为经过它的两条线对应的质数之积。
\(n = 2\) 要特判,此时会出现相同值。其余情况,白格互不同,黑格也互不同。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 500, M = 1000, Pool = 1e4;
constexpr int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
int n;
long long v[N + 5][N + 5];
int prime[M + 5], cnt;
bool ban[Pool + 5];
void sieve()
{
ban[1] = 1;
for (int i = 2; cnt < M; i++)
{
if (!ban[i])
prime[++cnt] = i;
for (int j = i + i; j <= Pool; j += i)
ban[j] = 1;
}
return ;
}
void solve()
{
cin >> n;
if (n == 2)//must !
return cout << "4 7\n23 10\n", void();
fill(v[0], v[0] + sizeof(v) / sizeof(long long), 1);
sieve();
int p = 0;
for (int s = 2; s <= n + n; s += 2)
{
p++;
for (int x = 1, y = s - 1; x < s; x++, y--)
{
if (x <= n && y <= n)
v[x][y] *= prime[p];
}
}
for (int s = 0; s <= n - 1; s += 2)
{
p++;
for (int x = 1, y = x + s; y <= n; x++, y++)
v[x][y] *= prime[p];
if (s)
{
p++;
for (int y = 1, x = y + s; x <= n; x++, y++)
v[x][y] *= prime[p];
}
}
for (int x = 1; x <= n; x++)
for (int y = 1; y <= n; y++)
{
if (v[x][y] > 1)
continue;
for (int t = 0; t < 4; t++)
{
int i = x + dx[t], j = y + dy[t];
if (i < 1 || i > n || j < 1 || j > n)
continue;
v[x][y] = v[i][j] / __gcd(v[i][j], v[x][y]) * v[x][y];
}
v[x][y]++;
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
cout << v[i][j] << ' ';
cout << '\n';
}
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{E. ABBreviate}\)
操作题先找不变量。后来尝试 vp ucupf 2025,一个签也是这个思路。证明大量参考了 \(\text{PinkRabbit}\) 老师的题解。
取映射 \(p(c) = \begin{cases} 1 \quad c = \texttt{a} \\ 2 \quad c = \texttt{b} \end{cases}\),记 \(v(s) = \sum\limits_{c \in s} p(c) \bmod 3\)。发现操作不改变 \(v(s)\)。
最终每个字符由 \(s\) 的一段字串合并而成。自然地,考虑能合并的条件。显然有 \(v(s) = p(c)\),下文默认满足。\(s = c\) 的情况是平凡的,不妨假设 \(|s| > 1\)。如果 \(s\) 由 \(\texttt{ab}\) 交错构成,则无法经行任何操作。反之,只要有一对相邻相同字符,便能缩合:只需每次对连续段交界处的相同字符操作,这样问题规模减少 \(1\) 而条件不变。因此,有 \(\texttt{aa / bb}\) 即为充要条件。
不过问题是对结果计数,而非对操作序列计数。套路地,考虑钦定某种原则。贪心匹配是个好想法。逐一对 \(t\) 的每个字母,匹配合法的最短前缀。记 \(|t| = k\),设划分结果 \(s = s_1 + \cdots + s_k + s_{k + 1}\)。显然 \(v(s_{k + 1}) = 0\)。问题归为证明此策略构成双射。
-
合法的 \(t\) 必然能贪心匹配
取 \(t\) 的划分 \(s'_1 + \cdots + s'_k\)。不妨设 \(s'_1\) 为第一个不满足最短前缀的位置,则 \(s_1\) 为其前缀,记 \(s'_1 = s_1 + w\),有 \(v(w) = 0\)。
如果 \(w\) 含有相邻相同字符,则可以移给 \(s'_2\),仍能合并出正确结果。
如果没有呢?则 \(w\) 一定形如 \(\texttt{ab...ab}\),字母交替且数量相等,以满足总和同余零。仍然考虑移到后面,除非 \(s'_2\) 没有相邻相同字符,且拼接处也没有产生。这也意味着 \(s'_2\) 为单个字符。不妨 \(w = \texttt{ab...ab}, s'_2 = \texttt{a}\)。此时可令 \(s'_2\) 为 \(w\) 的第一个 \(\texttt{a}\),\(w\) 错了一位到它后面。以此类推,所有串都可修改为最短前缀。
-
一组贪心匹配对应一个合法的 \(t\)
和上文类似,无非把过程倒过来。移给一个有相邻相同字符的前缀。
考虑 dp。设 \(dp_i\) 表示 \(i\sim n\) 能变成几种串。枚举第一个字符,维护前缀和上次出现位置即可。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 1e5, P = 1e9 + 7;
string str;
int n;
int s[N + 5];
int dp[N + 5];
void solve()
{
cin >> str;
n = str.size();
for (int i = 1; i <= n; i++)
s[i] = str[i - 1] - 'a' + 1;
int dif = 0;
for (int i = 1; i < n; i++)
dif |= (s[i] == s[i + 1]);
if (!dif)
return cout << 1 << endl, void();
int lst[3] = {n + 1, n + 1, n + 1};
for (int i = n; i > 0; i--)
{
s[i] = (s[i] + s[i + 1]) % 3;
dp[i] = (s[i] > 0);//i ~ n -> one char
for (int c = 1; c <= 2; c++)//2 or more char
{
int v = (s[i] + 3 - c) % 3;
int pos = lst[v];
if (pos <= n)
dp[i] += dp[pos];
}
dp[i] %= P;
lst[s[i]] = i;
}
cout << dp[1] << endl;
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{F. Grafting}\)
独立切了好欸。
将叶子 \(u\) 连到 \(x\) 上,操作后 \(u\) 不能再动。那么 \(x\) 会动吗?这意味着让 \(x\) 变成叶子,而它那一侧是全部剩下的 \(n - 1\) 个点。因此其它所有点都要移走,也说明这样的 \(x\) 只能是第一轮的 \(x\)。进一步地,大多数时候 \(u\) 会直接连到树 \(B\) 上的邻域。
不过,真的有可能动所有点吗?开始毛估估感觉不会。此时的做法便明晰了。枚举那个不动点 \(r\) 并以其为根,求出相同的连通块,那么所有不在块中的点都要移动。移动顺序同时受到两棵树的父子关系限制,建图拓扑排序判环。无环即合法。
交一发,wa 了。下载数据,发现真的有 \(ans = n\) 的数据点。具体例子可以看官方题解,是一个 \(n = 8\) 的 hack。
问题无非出在第一步,而这样小的数据范围随便暴力。枚举叶子 \(u\) 和 \(x\),连接后 \(u\) 成为不变的根,再跑上述判定。总复杂度意外地低,是 \(O(n^3)\)。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std;
// #define Debug
// #define LOCAL
#define TestCases
constexpr int N = 50;
int n;
int treeA[N + 5][2], deg[N + 5], belong[N + 5];
vector<int> a[N + 5], b[N + 5];
int faA[N + 5], faB[N + 5];
int keep[N + 5];
vector<int> g[N + 5];
int in[N + 5];
int q[N + 5], l, r;
void build(int ban)
{
for (int i = 1; i <= n; i++)
a[i].clear();
for (int i = 1, u, v; i <= n - (!ban); i++)//when ban = 0, treeA[n][] isn't cleared
{
if (i == ban)
continue;
u = treeA[i][0], v = treeA[i][1];
a[u].push_back(v), a[v].push_back(u);
}
for (int i = 1; i <= n; i++)
sort(a[i].begin(), a[i].end());
return ;
}
void init(int root)
{
auto dfs = [](int u, vector<int> edge[], int father[], auto self) -> void
{
for (auto v : edge[u])
{
if (father[u] == v)
continue;
father[v] = u;
self(v, edge, father, self);
}
return ;
};
memset(faA, 0, sizeof(int) * (n + 1));
memset(faB, 0, sizeof(int) * (n + 1));
dfs(root, a, faA, dfs);
dfs(root, b, faB, dfs);
return ;
}
int calc(int root)
{
int res = n;
auto border = [&](int u, auto self) -> void
{
keep[u] = 1;
res--;
int tot = b[u].size(), cur = 0;
for (auto v : a[u])
{
if (v == faA[u])
continue;
while (cur < tot && b[u][cur] < v)
cur++;
if (cur == tot)
break;
if (v == b[u][cur])
self(v, self);
}
return ;
};
memset(keep, 0, sizeof(int) * (n + 1));
border(root, border);
for (int i = 1; i <= n; i++)
g[i].clear();
memset(in, 0, sizeof(int) * (n + 1));
for (int u = 1; u <= n; u++)
{
if (keep[u])
continue;
int fa = faB[u];
if (!keep[fa])
g[fa].push_back(u), in[u]++;
for (auto v : a[u])
{
if (!keep[v] && faA[v] == u)
g[v].push_back(u), in[u]++;
}
}
l = 1, r = 0;
for (int i = 1; i <= n; i++)
{
if (!in[i])
q[++r] = i;
}
while (l <= r)
{
int u = q[l++];
for (auto v : g[u])
{
in[v]--;
if (!in[v])
q[++r] = v;
}
}
for (int i = 1; i <= n; i++)
{
if (in[i])
res = n + 1;//failed
}
return res;
}
void solve()
{
cin >> n;
memset(deg, 0, sizeof(int) * (n + 1));
for (int i = 1, u, v; i < n; i++)
{
cin >> u >> v;
treeA[i][0] = u, treeA[i][1] = v;
deg[u]++, deg[v]++;
belong[u] = belong[v] = i;
}
for (int i = 1; i <= n; i++)
b[i].clear();
for (int i = 1, u, v; i < n; i++)
{
cin >> u >> v;
b[u].push_back(v), b[v].push_back(u);
}
for (int i = 1; i <= n; i++)
sort(b[i].begin(), b[i].end());
build(0);
bool same = 1;
for (int i = 1; i <= n; i++)
{
if (a[i].size() != b[i].size())
{
same = 0;
break;
}
int s = a[i].size();
for (int t = 0; t < s; t++)
same &= (a[i][t] == b[i][t]);
}
if (same)
return cout << 0 << '\n', void();
int ans = n + 1;
for (int u = 1; u <= n; u++)//first step
{
if (deg[u] != 1)//not leaf
continue;
for (int v = 1; v <= n; v++)
{
if (u == v)
continue;
treeA[n][0] = u, treeA[n][1] = v;//u is fixed
build(belong[u]);
init(u);
ans = min(ans, calc(u) + 1);
}
}
cout << (ans == n + 1 ? -1 : ans) << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
Any solvable problems?
\(\Large\text{A. Two Abbreviations}\)
翻译一下,等于取好串的所有 \(n\) 等分点和 \(m\) 等分点,要求拼起来刚好分别是 \(S, T\)。显然那些 \(n\) 等分点与 \(m\) 等分点重合的位置需要关注。记 \(\gcd(n, m) = g\),则 \(n = n_0 \times g,\ m = m_0 \times g,\ n_0 \perp m_0\)。
故重合与 \(L\) 无关。易知 \(n_0 | x\),则 \(x = k n_0,\ y = k m_0,\ k \in \mathbb{N}\)。有限制 \(x < n,\ y < m\),枚举 \(k\) 检查是否对应位均相同即可。若有解,取 \(ans = \operatorname{lcm}(n, m)\) 即可。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 1e5;
int n, m;
char s[N + 5], t[N + 5];
void solve()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> s[i];
for (int i = 1; i <= m; i++)
cin >> t[i];
int g = __gcd(n, m);
int dn = n / g, dm = m / g;
for (int i = 1, j = 1; i <= n && j <= m; i += dn, j += dm)
{
if (s[i] != t[j])
return cout << "-1\n", void();
}
long long ans = 1ll * n / g * m;
cout << ans << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{B. Removing Blocks}\)
自此开始再无可做题……
开始考虑拆贡献推式子,然而陷入了一滩烂泥……遂看题解。
方案总和与方案期望可以相互转换。期望具有线性性,可以简单地拆分为若干独立的问题,这是比求总和优越的地方。对于此题,在所有 \(n!\) 种序列中等概率抽一个,算每个位置的贡献。
每次删数会将一个连续段一分为二,整个操作序列自然形成一个树形结构,换言之是按删除时间建立小根笛卡尔树。在笛卡尔树上考虑,发现每个点的贡献次数恰为其深度。而深度同样可以拆贡献,考虑每个点是否为祖先,有 \(\mathrm{E}(\text{dep}_x) = \sum_y \mathrm{P}(\text{y is ancestor})\)。
不妨设 \(y < x\),另一侧是对称的。稍加思索发现充要条件是 \(y\) 是 \(y \sim x\) 中最早删除的。仅考虑这 \(x - y + 1\) 个数的相对删除时间早晚,在 \(n!\) 种排列中,直觉上最小值在每个位置出现次数相等,故 \(\mathrm{P}(\text{y is ancestor}) = \frac{1}{x - y + 1}\)。对所有 \(y \leqslant x\) 的概率求和,是一个调和数(\(y = x\) 计算了删 \(x\) 时贡献)。\(y \geqslant x\) 的情况同样是一个调和数。注意 \(y = x\) 重复计算了,减一。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 1e5, P = 1e9 + 7;
int n;
int a[N + 5];
int H[N + 5];
int ksm(int d, int u)
{
int res = 1;
while (u)
{
if (u & 1)
res = 1ll * res * d % P;
u >>= 1;
d = 1ll * d * d % P;
}
return res;
}
void solve()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
int fac = 1;
for (int i = 1; i <= n; i++)
{
H[i] = (H[i - 1] + ksm(i, P - 2)) % P;
fac = 1ll * fac * i % P;
}
int ans = 0;
for (int i = 1; i <= n; i++)
{
int l = i, r = n - i + 1;
int w = (H[l] + H[r] - 1) % P;//self is counted twice
ans = (ans + 1ll * w * a[i] % P) % P;
}
ans = 1ll * ans * fac % P;
cout << ans << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{C. Min Cost Cycle}\)
想了一些贪心,全部倒闭……
通过纳入非法情况,简化限制。钦定每个 \(A_i,\ B_i\) 是否纳入边权和。对于每条边 \(<u, v>\),\(A_u\) 和 \(B_v\) 中恰好选择一个,这里将取 \(\min\) 泛化为二选一。所有点按 \(A/B\) 是否被选可分为 \(00, \; 01, \; 10, \; 11\) 四类,考虑能拼成一个环的条件。对于一个环,一共选了 \(n\) 个数,故所有数对中应恰有 \(n\) 个 0 和 \(n\) 个 1。进一步地,\(00\) 和 \(11\) 的点数必须相等,以保证平均每个数零一均有。当存在 \(00, \; 11\)时,以上为必要条件,而构造使其成为充要条件:先将 \(00, \; 01\) 交替排布,再将 \(01, \; 10\) 任意插入交界处即可。而不存在 \(00, \; 01\) 时,必须全为 \(01\) 或全为 \(10\),自然也是充要的。
现在问题变为,在所有能成环的钦定方案中,求最小和。全为 \(01 / 10\) 的情况是平凡的。简化的限制使得存在简单的贪心。不妨先取 \(\{ A_1, B_1, \cdots, A_n, B_n \}\) 中的前 \(n\) 小,显然这是答案的下界,并且 \(00\) 和 \(11\) 数量必然相等,从平均一个 1 知。唯一的问题是这两类均不存在,即 \(01, \; 10\) 混杂。仍然贪心,对大数做一些微调,制造 \(00, \; 11\)。一种是把第 \(n\) 小换为第 \(n + 1\) 小,若二者所属不同,则制造成功。否则,有两种做法:\(n + 2\) 换 \(n\),或 \(n + 1\) 换 \(n - 1\)。想一想,其他的调整方案均可以确定更劣。对上述几种可能取最小值。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <utility>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 1e5;
typedef pair<int, int> pir;
int n;
pir node[N + N + 5];
int cnt[N + 5];
void solve()
{
cin >> n;
long long sumA = 0, sumB = 0;
for (int i = 1, a, b; i <= n; i++)
{
cin >> a >> b;
sumA += a, sumB += b;
node[i + i - 1] = pir{a, i};
node[i + i] = pir{b, i};
}
long long ans = min(sumA, sumB);
sort(node + 1, node + n + n + 1);
long long sum = 0;
int dif = 1;
for (int i = 1; i <= n; i++)
{
sum += node[i].first;
cnt[node[i].second]++;
if (cnt[node[i].second] == 2)
dif = 0;
}
if (dif)
{
if (node[n].second == node[n + 1].second)
sum += min(-node[n].first + node[n + 2].first,
-node[n - 1].first + node[n + 1].first);
else
sum += -node[n].first + node[n + 1].first;
}
ans = min(ans, sum);
cout << ans << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{D. Chords}\)
神秘。看起来并不像困难的区间 dp,但还是不会……
首先,把点对看成区间,则弦无交等于区间无交或包含。这里区间为 \([\min(u, v), \max(u, v)]\),不用倍长序列以断环,每条弦都有跨过 \((n, 1)\) 与不跨过的两种看法,而事实上没有影响。
然后怎么做?一种考虑是容斥,而其基础是存在可以快速计算的简单情况。对于本题,点任意相连的方案数是容易的。对于 \(x = 2k\) 个孤立点,每次取最左侧的单点连边,方案数依次为 \(x - 1, \; x - 3, \; \cdots, \; 1\),是 \((x - 1)!!\)。另一方向,考虑刻画“连通块”,可以用块内最小、最大点编号记录,进而可考虑每个连通块的出现次数。
记 \(f(S)\) 为 \(S\) 内部的点之间任意连边的方案数,\(f(S)\) 合法当且仅当有偶数个单点,并且已有线段中没有跨越 \(S\) 内外的。设 \(dp_{l, r}\) 表示,仅考虑 \(l \sim r\) 内所有连边情况,满足 \(l, r\) 同属一个连通块的方案数。\(l, r\) 将图分成独立的内外两部分,则其贡献为 \(dp_{l, r} \times ff(U \setminus [l, r])\)。由容斥,先令 \(dp_{l, r} \gets f([l, r])\)。枚举 \(l\) 所属连通块的右界,非法情况为 \(\sum_{r'} dp_{l, r'} \times g([r' + 1, r])\)。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 600, P = 1e9 + 7;
int n, k;
int bel[N + 5];//no need to copy the chain !
long long g[N + 5];
long long f[N + 5][N + 5];
int cnt[N + 5];
int ety[N + 5][N + 5];//empty; -1 for illegal
void solve()
{
cin >> n >> k;
n <<= 1;
for (int i = 1, u, v; i <= k; i++)
{
cin >> u >> v;
bel[u] = bel[v] = i;
}
g[0] = 1;
for (int i = 2; i <= n; i += 2)
g[i] = g[i - 2] * (i - 1) % P;
for (int l = 1; l <= n; l++)
{
int one = 0, tot = 0;
for (int r = l; r <= n; r++)
{
if (bel[r])
{
one -= cnt[bel[r]] & 1;
cnt[bel[r]]++;
one += cnt[bel[r]] & 1;
}
else
tot++;
if (((r & 1) ^ (l & 1)) && !one)
ety[l][r] = tot;
else
ety[l][r] = -1;
}
for (int r = l; r <= n; r++)
{
if (bel[r])
cnt[bel[r]]--;
}
}
long long ans = 0;
for (int len = 2; len <= n; len += 2)
{
for (int l = 1, r = len; r <= n; l++, r++)
{
if (ety[l][r] == -1)
continue;
f[l][r] = g[ety[l][r]];
for (int k = l + 1; k < r; k += 2)
{
if (ety[l][k] != -1 && ety[k + 1][r] != -1)
f[l][r] = (f[l][r] + P - f[l][k] * g[ety[k + 1][r]] % P) % P;
}
int other = ety[1][n] - ety[l][r];
ans = (ans + f[l][r] * g[other] % P) % P;
}
}
cout << ans << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{E. High Elements}\)
字典序最小,显然的贪心。
刻画当前状态。\(X, Y\) 分别已有 \(x, y\) 个前缀最大值。依贪心,总可以枚举下一个数放的位置,变为判定是否有合法方案。看似无从下手,正需要一个结论来简化问题。
注意到,原序列的前缀最大值,无论放在哪里必然还是前缀最大值,不妨称它们为“旧的”。而那些不是旧的前缀最大值,不妨称为“新的”。在继承当前状态下,对于一种合法方案,设两个序列后来增加的前缀最大值中分别有 \(u < v\) 个新的。如果各选 \(u\) 个新的,交换所在的序列,那么此时依然合法!因为在原序列中,新的一定被某些旧的阻挡。如果在 \(X\) 中是新的,则阻挡它的必然在 \(Y\)。如此操作后,则一定有一个序列全部由旧的组成(指后续划分的部分)。(感觉可以直接猜出来……?)
这时可以再枚举哪个序列全部由旧的组成,只是乘了一些 \(O(1)\) 上去。\(x, y\) 为已经决定了 \(p_i\) 去向后的结果。记 \(p_{i + 1} \sim p_n\) 中有 \(k\) 个旧的,不妨令 \(X\) 全是旧的,而 \(Y\) 有 \(\Delta\) 个旧的和 \(\sigma\) 个新的(这样设变量后续有好处)。有:
注意,等号右侧为定值。给左式一个组合意义:对于一个上升子序列,每个旧的积两分,每个新的积一分。不过这仍不足以求解,现在是一个存在性问题。不过,可以给所有非原序列前缀最大值的数分组,每个数任意划给一个阻挡它的数,则 \(Y\) 可以由 \(\Delta\) 个组和抽出来的 \(\sigma\) 个新的数组成,而其他数通通划给 \(X\),却不影响 \(X\) 仅由旧的组成。奇偶性出现了:如果将一整个组从 \(Y\) 划给 \(X\),除了 \(Y\) 的分数减少二以外没有改变。因此存在性问题化为了最优化问题:对每种奇偶性,求最大分数。倒序扫描序列,类似求 \(\text{LIS}\) 一样用两棵线段树维护,预处理出以每种值开头的最大分数。此外,\(x + k - y < 0\) 时必然无解。实现上,可以 \(p_i\) 给 \(X\) 无解就划到 \(Y\),最后看前缀最大值数量是否相等。
两次通过结论简化问题,共性是基于简单粗暴的想法?
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 2e5, SZ = N << 2;
int n;
int a[N + 5];
struct Tree
{
int v[SZ + 5];
#define ls(p) (p << 1)
#define rs(p) (p << 1 | 1)
void pushup(int p)
{
return v[p] = max(v[ls(p)], v[rs(p)]), void();
}
void build(int p, int l, int r)
{
v[p] = 0;
if (l == r)
return ;
int mid = (l + r) >> 1;
build(ls(p), l, mid), build(rs(p), mid + 1, r);
return ;
}
void modify(int p, int l, int r, int x, int val)
{
if (l == r)
return v[p] = val, void();
int mid = (l + r) >> 1;
if (x <= mid)
modify(ls(p), l, mid, x, val);
else
modify(rs(p), mid + 1, r, x, val);
return pushup(p), void();
}
int query(int p, int l, int r, int L, int R)
{
if (L <= l && r <= R)
return v[p];
int mid = (l + r) >> 1, res = 0;
if (L <= mid)
res = query(ls(p), l, mid, L, R);
if (R > mid)
res = max(res, query(rs(p), mid + 1, r, L, R));
return res;
}
};
Tree even, odd;
int premax[N + 5];
int f[N + 5][2];
int ans[N + 5];
void solve()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
int tot = 0;
for (int i = 1, cur = 0; i <= n; i++)
{
if (a[i] > cur)
{
premax[i] = 1;
cur = a[i];
}
tot += premax[i];
}
for (int i = n; i > 0; i--)
{
int e = even.query(1, 1, n, a[i], n);
int o = odd.query(1, 1, n, a[i], n);
if (premax[i])
f[i][0] = e + 2, f[i][1] = o + 2;
else
f[i][0] = o + 1, f[i][1] = e + 1;
f[i][0] = (!(f[i][0] & 1)) * f[i][0];
f[i][1] = (f[i][1] & 1) * f[i][1];
even.modify(1, 1, n, a[i], f[i][0]);
odd.modify(1, 1, n, a[i], f[i][1]);
}
int P = 0, Q = 0;
int lstP = 1, lstQ = 1;
for (int i = 1; i <= n; i++)
{
tot -= premax[i];
even.modify(1, 1, n, a[i], 0);
odd.modify(1, 1, n, a[i], 0);
/*to P*/
int curP = max(lstP, a[i]), curQ = lstQ;
P += (curP == a[i]);
int typ = (((P - Q + tot) & 1) + 2) & 1;//equiv Q - P + t
if (!typ)
ans[i] = !((even.query(1, 1, n, curQ, n) >= P - Q + tot && P - Q + tot >= 0) || //Q: other
(even.query(1, 1, n, curP, n) >= Q - P + tot && Q - P + tot >= 0));//P: other
else
ans[i] = !((odd.query(1, 1, n, curQ, n) >= P - Q + tot && P - Q + tot >= 0) || //Q: other
(odd.query(1, 1, n, curP, n) >= Q - P + tot && Q - P + tot >= 0));//P: other
P -= (curP == a[i]);
if (!ans[i])
{
lstP = max(lstP, a[i]);
P += (lstP == a[i]);
}
else
{
lstQ = max(lstQ, a[i]);
Q += (lstQ == a[i]);
}
}
if (P != Q)
return cout << -1 << '\n', void();
for (int i = 1; i <= n; i++)
cout << ans[i];
cout << endl;
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{F. Reachable Cells}\)
不弱于 \(\text{DAG}\) 可达性,\(\text{bitset}\ O(n^4 / w)\),此题终……?
假的。本题的图是网格图,不知为何但是性质更好。不过有卡常大手子的 \(O(n^4 / w)\) 真能过。
有哪些是好求的?对于 \((x, y)\),可以自底向上、自右向左递推求出它到每一行 \(k\) 的最左端 \(L(x, y, k)\) 和最右端 \(R(x, y, k)\)。这是因为,如果固定行 \(k \geqslant x\),则 \((x, 1 \sim n)\) 到第 \(k\) 行的 \(L(x, \ast, k), \; R(x, \ast, k)\) 分别单调不减。如果 \((x, y_1), \; (x, y_2)\) 分别能到 \((k, w_1), \; (k, w_2)\),但 \(y_1 < y_2, \; w_1 > w_2\),则两条路线必然交叉,进而两个点分别可达两个点。
如果之间所有点均可达,那么做前缀和即可,枚举点,算贡献枚举行,轻松 \(O(n^3)\)。问题是有些点被阻挡。但是如果现在已被阻挡,对于更浅层的点,它同样不可达(感性理解一下)。所以可以把这些点视为障碍。具体地,在处理完第 \(x\) 行后,如果有 \((x, y)\) 满足左侧和上方均不可达,则标为障碍,并递归更新,这部分总复杂度显然 \(O(n^2)\)。之后更新前缀和,总 \(O(n^3)\)。
本题有加强版,欲知后事如何,且听下题分解。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 500, SZ = N * N;
int n;
int a[N + 5][N + 5];
int L[N + 5][N + 5][N + 5], R[N + 5][N + 5][N + 5];
int sum[N + 5][N + 5];
void init(int t)
{
sum[t][0] = 0;
for (int i = 1; i <= n; i++)
sum[t][i] = sum[t][i - 1] + max(a[t][i], 0);
return ;
}
int query(int l, int r, int t)
{
if (l > r)
return 0;
return sum[t][r] - sum[t][l - 1];
}
void dfs(int x, int y)
{
if (a[x][y] == -1)
return ;
if (a[x - 1][y] != -1 || a[x][y - 1] != -1)
return ;
a[x][y] = -1;
init(x);
dfs(x + 1, y);
dfs(x, y + 1);
return ;
}
void solve()
{
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
{
char c;
cin >> c;
if (c == '#')
a[i][j] = -1;
else
a[i][j] = c - '0';
}
for (int t = 1; t <= n; t++)
a[t][0] = a[t][n + 1] = a[0][t] = a[n + 1][t] = -1;
long long ans = 0;
for (int i = n; i > 0; i--)
{
init(i);
for (int j = n; j > 0; j--)
{
for (int k = n; k > i; k--)
{
L[i][j][k] = n + 1, R[i][j][k] = -1;
if (a[i][j] == -1)
continue;
if (a[i + 1][j] != -1)
L[i][j][k] = min(L[i][j][k], L[i + 1][j][k]),
R[i][j][k] = max(R[i][j][k], R[i + 1][j][k]);
if (a[i][j + 1] != -1)
L[i][j][k] = min(L[i][j][k], L[i][j + 1][k]),
R[i][j][k] = max(R[i][j][k], R[i][j + 1][k]);
ans += a[i][j] * query(L[i][j][k], R[i][j][k], k);
}
if (a[i][j] != -1)
{
L[i][j][i] = j;
R[i][j][i] = (a[i][j + 1] == -1 ? j : R[i][j + 1][i]);
ans += a[i][j] * query(L[i][j][i] + 1, R[i][j][i], i);//exclude self
}
dfs(i, j);
}
}
cout << ans << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{A. Irreversible operation}\)
在序列上,一次操作等于把一对相邻的 \(\texttt{BW} \to \texttt{WB}\)。结束时序列必然形如 \(\texttt{W...WB...B}\),统计逆序对即可。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 2e5;
int n;
string s;
void solve()
{
cin >> s;
n = s.size();
long long ans = 0;
int cnt = 0;
for (auto c : s)
{
cnt += (c == 'B');
ans += (c == 'W') * cnt;
}
cout << ans << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
\(\Large\text{B. Powers of two}\)
因为前几天刚刚被 \(\text{THUPC 2026 Pre J.}\) 序列击杀,所以这题很顺利地切了(??
对于 \(x\),满足 \(x + y = 2^t\) 的 \(y\) 很多(不难得出,若将 \(y\) 从小到大排序,则相邻数之间恰好差 \(2^k\))。而如果限定 \(x \geqslant y\),则 \(y\) 是唯一的,即刚好补足 \(x\) 的低位的 \(y\)。
从值域上考虑,每种 \(x\) 指向对应的 \(y\),构成了一片森林。显然总可以从 \((x, y)\) 中的较大值考虑,则匹配限定为树边。对此,经典的贪心是自底向上匹配父亲。
回到原题,每个值其实对应多个数,不过直觉上贪心仍然成立。从大到小排序后贪心即可。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <set>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 2e5;
int n;
int a[N + 5];
multiset<int> s;
void solve()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
sort(a + 1, a + n + 1);
for (int i = 1; i <= n; i++)
s.insert(a[i]);
int ans = 0;
for (int i = n; i > 0; i--)
{
auto it = s.find(a[i]);
if (it == s.end())//have been used
continue;
s.erase(it);
int k = __lg(a[i]);
if (a[i] == (1 << k))//power of 2
{
it = s.find(a[i]);
if (it != s.end())
ans++, s.erase(it);
continue;
}
int u = (1 << (k + 1)) - a[i];
it = s.find(u);
if (it != s.end())
ans++, s.erase(it);
}
cout << ans << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
$ \Large\text{C. Lexicographic constraints} $
直接贪心是困难的,而如果固定字符集 \(\Sigma\) 大小,判定有自然的贪心。
\(s_i < s_{i + 1}\):用 \(\texttt{a}\) 补齐。
\(s_i \geqslant s_{i + 1}\):截断,取最低位 \(+1\),后面的部分仍用 \(\texttt{a}\) 补足。
因此考虑二分 \(\Sigma\)。维护连续段,复杂度类似多进制加法,均摊 \(O(n)\)。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 2e5;
int n;
int a[N + 5];
struct Node
{
int l, r, x;
};
Node node[N + 5];
bool check(int sigma)
{
int top = 1;
node[1] = Node{1, a[1], 1};
for (int i = 2; i <= n; i++)
{
if (a[i] > a[i - 1])
{
node[++top] = Node{a[i - 1] + 1, a[i], 1};
continue;
}
while (node[top].l > a[i])//cut off
top--;
node[top].r = a[i];
while (top > 0 && node[top].x == sigma)//overflow
top--;
if (!top)
return false;
if (node[top].l < node[top].r)
{
int r = node[top].r, x = node[top].x;
node[top].r--;
node[++top] = Node{r, r, x + 1};
}
else
node[top].x++;
if (node[top].r < a[i])
{
int r = node[top].r;
node[++top] = Node{r + 1, a[i], 1};
}
}
return true;
}
void solve()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
int l = 1, r = n, ans = 0;
while (l <= r)
{
int mid = (l + r) >> 1;
if (check(mid))
{
ans = mid;
r = mid - 1;
}
else
l = mid + 1;
}
cout << ans << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
$ \Large\text{D. Grid game} $
一开始读错题了……以为双方都能向右和向下走,不会做,看题解……读对应该能独立切?
先手只能向下走,后手只能向右走,双方也可选择保持静止。对于先手,其必须每步都走,否则后手选择静止而终止游戏。
最终必定是走到边界或某个障碍的上方。仍然考虑贪心,后手向右能走就走,这样能最快抵达每一列。对每一列找当前位置下面第一个障碍,求最小值即可。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <set>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 2e5;
int n, m, q;
set<int> s[N + 5];//column
void solve()
{
cin >> n >> m >> q;
for (int i = 1, x, y; i <= q; i++)
{
cin >> x >> y;
s[y].insert(x);
}
for (int i = 1; i <= n; i++)
s[m + 1].insert(i);
for (int j = 1; j <= m; j++)
s[j].insert(n + 1);
int x = 1, y = 1;
int ans = n;//including two consecutive round that doing nothing
while (1)
{
ans = min(ans, *s[y].lower_bound(x) - 1);//already included
if (s[y].find(x + 1) != s[y].end())
break;
x++;
y += (s[y + 1].find(x) == s[y + 1].end());
}
cout << ans << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
$ \Large\text{E. Wandering TKHS} $
独立切的喵喵题。复盘发现中途有一步想少了,如果当时立刻意识到可能反而否决了这个想法……?塞翁失马。
从一些自然的地方探索。比如,记 \(\mathrm{Vis}_u\) 为 \(r = u\) 时访问的节点集合,那么必有 \(\mathrm{Vis}_u \supseteq \mathrm{Vis}_{fa}\)。因为在任意时刻,\(u\) 访问的节点总不少于 \(fa\),从而领域最小值亦不大于 \(fa\)(或者已经访问了)。
考虑 \(u\) 对其他点的贡献?然而满足 \(u \in \mathrm{Vis}_v\) 的 \(v\) 未必是 \(u\) 的祖先或后代。或者考虑 \(u\) 去往 \(fa\) 之前,访问了哪些点,记该集合为 \(\mathrm{Bef}_u \subseteq \mathrm{Vis}_u\)。不妨称 \(\mathrm{Bef}_u\) 中的节点是 \(u\) “自发”访问的。如果维护每个点去往父亲前的访问序列(强于集合 \(\mathrm{Bef}_u\),记为 \(\mathrm{Bef-Seq}_u\)),则 \(u\) 的序列即为所有儿子的序列做归并的结果,并在大于 \(fa\) 处截断。但是序列并非有序,\(u\) 总是第一个访问的。此外,走到祖先后仍有可能回访 \(\text{subtree-}(u)\),譬如祖先的领域都是大数。这些点不是 \(u\) 自发访问的。
贡献的思路没有全错。考虑 \(u\) 对祖先的贡献,一定是 \(u\) 和某个祖先 \(p\) 之间所有点均需访问 \(u\),而 \(q = fa_p\) 及往上均不访问 \(u\)。从 \(\mathrm{Vis}_u \supseteq \cdots \supseteq \mathrm{Vis}_p \supseteq \mathrm{Vis}_q \supseteq \cdots \mathrm{Vis}_1\) 易知出现的连续性。
记 \(occ_u = p\) 为 \(u\) 的祖先中,最后一次访问 \(u\) 的点。要想访问 \(u\),必须访问 \(fa\),故 \(occ_{fa}\) 是 \(occ_u\) 的祖先。这指示了自顶向下递推的想法。进一步地,发现有 \(x \in \mathrm{Bef}_{occ_x}\)。反证,如果 \(occ_xx\) 没有自发地访问 \(x\),而它的祖先都不需要 \(x\),由 \(\mathrm{Vis}_x\) 包含所有祖先集合知 \(x \not\in \mathrm{Vis}_x\) 而矛盾。因此,对于 \(fa \sim occ_{fa}\) 上的所有点 \(x\),最迟扩展到 \(occ_{fa}\) 时 \(u\) 进入邻域。又由归并,\(x\) 走到 \(fa\) 之前,须取走所有 \(\mathrm{Bef-Seq}_{son}\) 的“小于 \(fa\) 的”前缀。不难得出,必须有一个祖先 \(anc > u\) 才能取走 \(u\)(作为 \(\mathrm{Bef}_{anc}\))。如果维护 \(x\) 到根路径上所有点编号最大值,则容易倍增求出满足 \(anc > u\) 的最浅祖先。而 \(occ_u\) 必为 \(occ_{fa}\) 与 \(anc\) 的较深者,因二者共同限制且独立。
(这时一个错误的思路已逐渐成型:\(u\) 只对 \(u \sim occ_{u}\) 有贡献,做树上差分。显然假了,但求出的值依然有用。)
正解一步之遥。考虑从 \(fa\) 更新 \(u\),那么哪些 \(x \in \text{subtree-}(u)\) 是 \(u\) 所独有的呢?正是满足 \(occ_x = u\) 的点,于是开桶维护即可。
更短小的线性做法:
可以直接写出条件:如果 \((anc \sim 1]\) 的最大值大于 \([u \sim anc)\) 的最大值,则 \(u \in \mathrm{Vis}_{anc}\)。这里括号的含义同开闭区间。
记 \(\max\) 为 \(u \sim 1\) 上的最大值。如果 \(\max \not= anc\),那么上述两个最大值中必有 \(\max\),进而 \(u \in \mathrm{Vis}_{anc} \iff \max \in (anc, 1]\)。而 \(\max = anc\) 时,用路径次大值重复上述判断即可。
在该结论下,讨论 \(u\) 所独有的点的条件,即可线性。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
#include <set>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 2e5, Lg = 17;
int n;
vector<int> e[N + 5];
int fa[N + 5][Lg + 5], dep[N + 5];
int imax[N + 5];
int occ[N + 5];//last occur
int cnt[N + 5];
int ans[N + 5];
void dfs(int u, int fat, int depth)
{
fa[u][0] = fat, dep[u] = depth;
imax[u] = max(u, imax[fat]);
for (int k = 1; k <= Lg; k++)
fa[u][k] = fa[fa[u][k - 1]][k - 1];
int p = u;
for (int k = Lg; k >= 0; k--)
{
if (imax[fa[fa[p][k]][0]] >= u)
p = fa[p][k];
}
occ[u] = (dep[p] > dep[occ[fat]] ? p : occ[fat]);
cnt[occ[u]]++;
for (auto v : e[u])
{
if (v ^ fat)
dfs(v, u, depth + 1);
}
return ;
}
void calc(int u, int fat)
{
if (u != 1)
ans[u] = ans[fat] + cnt[u];
for (auto v : e[u])
{
if (v ^ fat)
calc(v, u);
}
return ;
}
void solve()
{
cin >> n;
for (int i = 1, u, v; i < n; i++)
{
cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs(1, 0, 1);
calc(1, 0);
for (int i = 2; i <= n; i++)
cout << ans[i] << " \n"[i == n];
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}
$ \Large\text{F. Construction of a tree} $
真正厉害的喵喵题!参考了 \(\text{PinkRabbit}\) 的题解。
过于发散以致难以入手。是树不是环,点数比边多。具体地,任取一些边 \(\forall S \subset \{1, \cdots, n - 1\}\),考虑它们所包含的点 \(V(S) = \bigcup_{i \in S} E_i\),应有 \(|V(S)| > |S|\)。这很像 \(\text{Hall's Theorem}\)!虽然严格大于是比定理更强的限制,但抽象题适合网络流亦不失为一条经验。
构建一张二分图,左边为点 \(1 \sim n\),右边为边集 \(1 \sim n - 1\),有边 \(<u, v> \iff u \in E_v\)。如果有解,把根拿走,剩下的所有 \(u\) 和 \(<u, fa>\) 构成自然的完美匹配。反过来,有大小为 \(n - 1\) 的匹配是有解的必要条件。
那么有合法匹配呢?失配的点作为根,\(<u, E_i>\) 决定了 \(u\) 的父亲从哪里找。直接 \(\text{dfs}\) 或 \(\text{bfs}\),若当前在节点 \(u\),对所有 \(u \in E_i\),如果 \(v = \text{match}(E_i)\) 尚未加入树,则指定 \(fa_v \gets u\)。如此构造看似随意。如果所有点都纳入了树,即得一解。如果没有呢?记当前树的点集为 \(V\),邻域为 \(N(V) = \{ E_i: \; \exists <u, E_i>, u \in V \}\)。此时构造停止,则 \(V \setminus \{root\}\) 与 \(N(V)\) 应恰好构成完美匹配,有 \(|V| = |N(V)| + 1\),其中 \(+1\) 因为根。同时取补集,完美匹配的差仍为完美匹配,则 \(|U_{\text{node}} \setminus V| = |U_{\text{edge}} \setminus N(V)|\)。点边相等,必然成环,所以构造停止等于无解。
瓶颈是 \(\text{Dinic}\) 求二分图匹配,复杂度 \(O(V \sqrt{V})\)。
点击查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
// #define Debug
// #define LOCAL
// #define TestCases
constexpr int N = 1e5;
int n;
vector<int> bel[N + 5];//belong
constexpr int V = (N << 1) + 2, S = 2e5, E = S << 2;
constexpr int Inf = 0x3f3f3f3f;
int s, t;
int head[E + 5], nxt[E + 5], to[E + 5], fl[E + 5], tot = 1;
void add_edge(int u, int v, int f)
{
tot++;
to[tot] = v;
nxt[tot] = head[u];
head[u] = tot;
fl[tot] = f;
return ;
}
void add(int u, int v, int f)
{
add_edge(u, v, f);
add_edge(v, u, 0);
return ;
}
int dep[V + 5], cur[V + 5];
bool bfs()
{
for (int i = 0; i <= n + n; i++)
{
dep[i] = 0;
cur[i] = head[i];
}
dep[s] = 1;
queue<int> q;
q.push(s);
while (!q.empty())
{
int u = q.front();
q.pop();
for (int i = head[u]; i; i = nxt[i])
{
int v = to[i];
if (!fl[i] || dep[v])
continue;
dep[v] = dep[u] + 1;
q.push(v);
}
}
return dep[t];
}
int dfs(int u, int flow)
{
if (u == t)
return flow;
int used = 0;
for (int i = cur[u]; i; i = nxt[i])
{
cur[u] = i;
int v = to[i];
if (!fl[i] || dep[v] != dep[u] + 1)
continue;
int delta = dfs(v, min(fl[i], flow - used));
fl[i] -= delta, fl[i ^ 1] += delta;
used += delta;
if (used == flow)
break;
}
return used;
}
int Dinic()
{
int flow = 0;
while (bfs())
flow += dfs(s, Inf);
return flow;
}
int match[V + 5];
int ans[N + 5][2];
int vis[V + 5];
void build(int u)//ensure u \in {1, ..., n}
{
vis[u] = 1;
for (auto e : bel[u])
{
int v = match[e + n];
if (vis[v])
continue;
ans[e][0] = u, ans[e][1] = v;
build(v);
}
return ;
}
void solve()
{
cin >> n;
for (int i = 1, sz, x; i < n; i++)
{
cin >> sz;
while (sz--)
cin >> x, bel[x].push_back(i);
}
s = 0, t = n << 1;
for (int u = 1; u <= n; u++)
{
for (auto v : bel[u])
add(u, v + n, 1);
add(s, u, 1);
}
for (int v = 1; v < n; v++)
add(v + n, t, 1);
int flow = Dinic();
if (flow != n - 1)
return cout << -1 << '\n', void();
int root = 0;
for (int u = 1; u <= n; u++)
{
for (int i = head[u]; i; i = nxt[i])
{
int v = to[i];
if (!fl[i])
match[u] = v, match[v] = u;
}
if (!match[u])
root = u;
}
build(root);
for (int u = 1; u <= n; u++)
if (!vis[u])
return cout << -1 << '\n', void();
for (int i = 1; i < n; i++)
cout << ans[i][0] << " " << ans[i][1] << '\n';
return ;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("mycode.out", "w", stdout);
#endif
ios :: sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
#ifdef TestCases
cin >> T;
#endif
while (T--)
solve();
return 0;
}

浙公网安备 33010602011771号