T1
给定一张 n n n 个点 m m m 条边的有向图,求至少增加多少条有向边,使得对于图中任意 3 3 3 个不同的点 u , v , w u,v,w u,v,w,若 u u u 到 v v v 有连边、 v v v 到 w w w 有连边,那么 u u u 到 w w w 有连边。
3 ≤ n ≤ 2000 3\le n\le 2000 3≤n≤2000, 0 ≤ m ≤ 2000 0\le m\le 2000 0≤m≤2000。
不妨考虑图中的一条链,可以发现这条链的起始点要向链上所有点连边。那么可以推出对于点 u u u,需要向 u u u 能走到的所有点进行连边。 n n n 次 dfs 即可。
// Problem: [ABC292E] Transitivity
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/AT_abc292_e
// Memory Limit: 1 MB
// Time Limit: 2000 ms
// Good luck to the code >w<
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define open(x) freopen(#x".in", "r", stdin), freopen(#x".out", "w", stdout)
#define abs(x) (((x) > (0)) ? (x) : (-(x)))
#define print(x) printf("%d %d\n", x.first, x.second)
const int maxn = 2005;
bool used[maxn][maxn]; vector<int> mp[maxn];
void addEdge(int u, int v) { mp[u].push_back(v); }
int n, m, ans; bool vis[maxn];
void dfs(int u, int st) {
if (vis[u]) return ; vis[u] = 1;
if (st != u && !used[st][u]) used[st][u] = 1, ans ++;
for (auto v : mp[u])
dfs(v, st);
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1, u, v; i <= m; i ++)
scanf("%d %d", &u, &v), addEdge(u, v), used[u][v] = 1;
for (int i = 1; i <= n; i ++) {
memset(vis, 0, sizeof(vis));
dfs(i, i);
} printf("%d\n", ans);
return 0;
}
T2
有 n n n 个点,初始没有连边, Q Q Q 次操作每次操作为以下操作之一:
- 给定 u , v u,v u,v,连一条 ( u , v ) (u,v) (u,v) 的有向边。
- 给定 u u u,删除所有连着 u u u 的边。
每次操作后输出度为 0 0 0 的点的数量。
2 ≤ n ≤ 3 × 1 0 5 2\le n\le 3\times 10^5 2≤n≤3×105, 1 ≤ Q ≤ 3 × 1 0 5 1\le Q\le 3\times 10^5 1≤Q≤3×105。
考虑使用 set
来维护每个点连出去的边,然后就做完了。删边时均摊
O
(
Q
)
O(Q)
O(Q)。
// Problem: E - Isolation
// Contest: AtCoder - TOYOTA MOTOR CORPORATION Programming Contest 2023#2 (AtCoder Beginner Contest 302)
// URL: https://atcoder.jp/contests/abc302/tasks/abc302_e?lang=en
// Memory Limit: 1024 MB
// Time Limit: 2000 ms
// Good luck to the code >w<
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define open(x) freopen(#x".in", "r", stdin), freopen(#x".out", "w", stdout)
#define abs(x) (((x) > (0)) ? (x) : (-(x)))
#define print(x) printf("%d %d\n", x.first, x.second)
const int maxn = 3e5 + 5;
set<int> s[maxn];
int n, Q;
int main() {
scanf("%d %d", &n, &Q);
for (int i = 1, ans = n, op, u, v; i <= Q; i ++) {
scanf("%d", &op);
if (op == 1) {
scanf("%d %d", &u, &v);
ans -= s[u].empty() + s[v].empty();
s[u].insert(v), s[v].insert(u);
} else {
scanf("%d", &u), ans += !s[u].empty();
for (auto x : s[u]) {
s[x].erase(s[x].find(u));
if (s[x].empty()) ans ++;
}
s[u].clear();
} printf("%d\n", ans);
}
return 0;
}
T3
给定 n n n 个集合,每个集合包含 [ 1 , m ] [1,m] [1,m] 中的某些数。每次操作可以找到两个集合 S , T S,T S,T 满足 S S S 与 T T T 的交不为空集,然后将 S , T S,T S,T 删去,获得一个 S S S 与 T T T 的并的集合。求获得一个至少包含 1 1 1 和 m m m 两个原色的集合,需要的最少操作次数。
1 ≤ n ≤ 2 × 1 0 5 1\le n\le 2\times 10^5 1≤n≤2×105, 2 ≤ m ≤ 2 × 1 0 5 2\le m\le 2\times 10^5 2≤m≤2×105,集合总大小 ≤ 5 × 1 0 5 \le 5\times 10^5 ≤5×105。
考虑分别建 n n n 个点表示 n n n 个集合,建 m m m 个点表示数字,每个集合向集合中的数连边权为 1 1 1 的有向边、数向包含它的集合连边权为 0 0 0 的边,从所有包含 1 1 1 的集合开始跑最短路,最后的答案即为 m m m 值对应的点的最短路 − 1 -1 −1,原因是最后一步不需要操作。
// Problem: F - Merge Set
// Contest: AtCoder - TOYOTA MOTOR CORPORATION Programming Contest 2023#2 (AtCoder Beginner Contest 302)
// URL: https://atcoder.jp/contests/abc302/tasks/abc302_f?lang=en
// Memory Limit: 1024 MB
// Time Limit: 3000 ms
// Good luck to the code >w<
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define open(x) freopen(#x".in", "r", stdin), freopen(#x".out", "w", stdout)
#define abs(x) (((x) > (0)) ? (x) : (-(x)))
#define print(x) printf("%d %d\n", x.first, x.second)
const int maxn = 2e5 + 5, maxm = 5e5 + 5;
namespace Graph {
struct Edge { int to, nxt, val; } e[maxm << 1];
int head[maxn << 1], ecnt = 0;
void addEdge(int u, int v, int w) {
e[++ ecnt] = Edge { v, head[u], w };
head[u] = ecnt;
}
} using namespace Graph;
int n, m, dis[maxn << 1];
int main() {
scanf("%d %d", &n, &m); deque<int> q;
for (int i = 1; i <= n + m; i ++) dis[i] = -1;
for (int i = 1, siz; i <= n; i ++) {
scanf("%d", &siz);
for (int x; siz --; ) {
scanf("%d", &x), addEdge(i, x + n, 1), addEdge(x + n, i, 0);
if (x == 1) q.push_back(i), dis[i] = 0;
}
} while (!q.empty()) {
int u = q.front(); q.pop_front();
// cout << u << '\n';
if (u == n + m) break;
for (int i = head[u]; i; i = e[i].nxt)
if (dis[e[i].to] == -1)
dis[e[i].to] = dis[u] + e[i].val, e[i].val ? q.push_back(e[i].to) : q.push_front(e[i].to);
} printf("%d\n", dis[n + m] == -1 ? -1 : dis[n + m] - 1);
return 0;
}
T4
给定一个长度为 n n n 的数列 a a a,其中 a i ∈ [ 1 , 4 ] a_i\in [1,4] ai∈[1,4],每次操作可以选择两个数交换,求最少操作次数使得 a a a 单调不降。
2 ≤ n ≤ 2 × 1 0 5 2\le n\le 2\times 10^5 2≤n≤2×105, 1 ≤ a i ≤ 4 1\le a_i\le 4 1≤ai≤4。
先将 a a a 从小到大排序得到目标状态,记为 b b b;那么对于 a i ≠ b i a_i\ne b_i ai=bi 的 i i i,显然要把 a i a_i ai 进行交换到 b b b 中值为 a i a_i ai 的段里。我们令 f ( i , j ) f(i,j) f(i,j) 表示所有 x x x,满足 a x = i a_x=i ax=i 且 b x = j b_x=j bx=j,即它需要从 b b b 中值为 j j j 的一个段移到值为 i i i 的段。显然若能找到一对点 ( u , v ) (u,v) (u,v) 使得 u u u 和 v v v 的起点是对方的终点,那么直接交换是最优的。其次是 3 3 3 个点顺次交换,最后是 4 4 4 个点。然后就做完了。
// Problem: G - Sort from 1 to 4
// Contest: AtCoder - TOYOTA MOTOR CORPORATION Programming Contest 2023#2 (AtCoder Beginner Contest 302)
// URL: https://atcoder.jp/contests/abc302/tasks/abc302_g?lang=en
// Memory Limit: 1024 MB
// Time Limit: 2000 ms
// Good luck to the code >w<
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define open(x) freopen(#x".in", "r", stdin), freopen(#x".out", "w", stdout)
#define abs(x) (((x) > (0)) ? (x) : (-(x)))
#define print(x) printf("%d %d\n", x.first, x.second)
const int maxn = 2e5 + 5;
int a[maxn], b[maxn];
int cnt[5][5], n;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i ++)
scanf("%d", &a[i]), b[i] = a[i];
sort(b + 1, b + n + 1);
int chg = 0;
for (int i = 1; i <= n; i ++)
if (a[i] != b[i])
chg ++, cnt[a[i]][b[i]] ++;
int ans = 0;
for (int i = 1; i <= 4; i ++)
for (int j = 1; j <= 4; j ++) if (i != j)
for (; cnt[i][j] && cnt[j][i]; ans ++, chg -= 2, cnt[i][j] --, cnt[j][i] --);
for (int i = 1; i <= 4; i ++)
for (int j = 1; j <= 4; j ++) if (i != j)
for (int k = 1; k <= 4; k ++) if (i != k && k != j)
for (; cnt[i][j] && cnt[j][k] && cnt[k][i]; ans += 2, chg -= 3, cnt[i][j] --, cnt[j][k] --, cnt[k][i] --);
printf("%d\n", ans + (chg >> 2) * 3);
return 0;
}
T5
给定 n n n 个长度均为 m m m 的字符串,每个字符串中仅包含 0 , 1 , ⋯ , 9 , ? \texttt{0},\texttt{1},\cdots,\texttt{9},\texttt{?} 0,1,⋯,9,? 这些字符,其中 ? \texttt{?} ? 可以替换为 0 \texttt{0} 0 到 9 \texttt{9} 9 中的任意字符。求使这 m m m 个字符串字典序严格递增的方案数。
2 ≤ n ≤ 40 2\le n\le 40 2≤n≤40, 1 ≤ m ≤ 40 1\le m\le 40 1≤m≤40。
赛时想了一个数位 dp,但是非常难写而且好像是错的。考虑区间 dp,令 f ( l , r , i , v ) f(l,r,i,v) f(l,r,i,v) 表示当前考虑让 [ l , r ] [l,r] [l,r] 中的字符串合法,当前填到了第 i i i 位,当前位的下界是 v v v。边界条件即为 i > m i>m i>m 时返回 [ l = r ] [l=r] [l=r],即填完了检查一下是否还有相等的元素。
显然 f ( l , r , i , v ) f(l,r,i,v) f(l,r,i,v) 可以从 f ( l , r , i , v + 1 ) f(l,r,i,v+1) f(l,r,i,v+1) 处转移,然后我们考虑枚举一个分界点 k ∈ [ l , r − 1 ] k\in [l,r-1] k∈[l,r−1],将区间分为 [ l , k ] [l,k] [l,k] 和 [ k + 1 , r ] [k+1,r] [k+1,r] 两部分,前部分全部在 i i i 位上填 v v v(填 > v >v >v 的情况在 f ( l , r , i , v + 1 ) f(l,r,i,v+1) f(l,r,i,v+1) 中已经统计),答案是 f ( l , k , i + 1 , 0 ) f(l,k,i+1,0) f(l,k,i+1,0);后部分的下界提到了 v + 1 v+1 v+1,答案是 f ( k + 1 , r , i , v + 1 ) f(k+1,r,i,v+1) f(k+1,r,i,v+1);如果前部分有能力全部填成 v v v 那么对答案的贡献即为 f ( l , k , i + 1 , 0 ) × f ( k + 1 , r , i , v + 1 ) f(l,k,i+1,0)\times f(k+1,r,i,v+1) f(l,k,i+1,0)×f(k+1,r,i,v+1),如果 [ l , r ] [l,r] [l,r] 可以全部填成 v v v 那么还可以从 f ( l , r , i + 1 , 0 ) f(l,r,i+1,0) f(l,r,i+1,0) 处转移。记忆化搜索即可解决转移顺序问题。
// Problem: [ABC292G] Count Strictly Increasing Sequences
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/AT_abc292_g
// Memory Limit: 1 MB
// Time Limit: 2000 ms
// Good luck to the code >w<
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define open(x) freopen(#x".in", "r", stdin), freopen(#x".out", "w", stdout)
#define abs(x) (((x) > (0)) ? (x) : (-(x)))
#define print(x) printf("%d %d\n", x.first, x.second)
const int maxn = 45, P = 998244353;
int f[maxn][maxn][maxn][10], n, m;
char s[maxn][maxn];
void addto(int &x, int y) { (x += y) %= P; }
int add(int x, int y) { return (x + y) % P; }
int mul(int x, int y) { return 1ll * x * y % P; }
int calc(int L, int R, int i, int val) {
if (i > m) return L == R;
if (L > R) return 1; if (val > 9) return 0;
if (f[L][R][i][val] != -1) return f[L][R][i][val];
int res = calc(L, R, i, val + 1);
for (int k = L; k <= R; k ++) {
if (s[k][i] != '?' && s[k][i] != val + '0') break;
addto(res, mul(calc(L, k, i + 1, 0), calc(k + 1, R, i, val + 1)));
}
return f[L][R][i][val] = res;
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i ++)
scanf("%s", s[i] + 1);
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
for (int k = 1; k <= m; k ++)
for (int t = 0; t < 10; t ++)
f[i][j][k][t] = -1;
return printf("%d\n", calc(1, n, 1, 0)), 0;
}
T6
有 n n n 场比赛,初始第 i i i 场比赛得分为 p i p_i pi,前 i i i 场比赛的 rating 为 ∑ j = 1 i p j i \cfrac{\sum_{j=1}^i p_j}{i} i∑j=1ipj;特别地,若存在一个最小的 i i i 使得 [ 1 , i ] [1,i] [1,i] 的 rating ≥ B \ge B ≥B,那么 i i i 及以后的比赛 rating 全部为 B B B。 Q Q Q 次操作每次修改一场比赛的得分,每次操作后求 [ 1 , n ] [1,n] [1,n] 的 rating。
1 ≤ n ≤ 5 × 1 0 5 1\le n\le 5\times 10^5 1≤n≤5×105, 1 ≤ Q ≤ 1 0 5 1\le Q\le 10^5 1≤Q≤105。
我们令
p
i
′
=
p
i
−
B
p'_i= p_i-B
pi′=pi−B,然后求
p
′
p'
p′ 的前缀和,记为
s
s
s;因为
1
i
∑
j
=
1
i
p
j
≥
B
1
i
∑
j
=
1
i
p
j
−
B
≥
0
1
i
[
(
∑
j
=
1
i
p
j
)
−
B
i
]
≥
0
1
i
∑
j
=
1
i
(
p
j
−
B
)
≥
0
1
i
s
i
≥
0
s
i
≥
0
\begin{aligned}\cfrac{1}{i}\sum_{j=1}^i p_j &\ge B\\\cfrac{1}{i}\sum_{j=1}^i p_j-B&\ge0\\\cfrac{1}{i}\left[\left(\sum_{j=1}^i p_j\right)-Bi\right]&\ge0\\\cfrac{1}{i}\sum_{j=1}^i (p_j-B)&\ge 0\\ \cfrac 1i s_i&\ge0\\s_i&\ge0\end{aligned}
i1j=1∑ipji1j=1∑ipj−Bi1[(j=1∑ipj)−Bi]i1j=1∑i(pj−B)i1sisi≥B≥0≥0≥0≥0≥0
所以我们只需要找到第一个
s
i
≥
0
s_i\ge 0
si≥0 的
i
i
i 即可。用线段树轻松维护,每次操作影响一段后缀。
// Problem: [ABC292Ex] Rating Estimator
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/AT_abc292_h
// Memory Limit: 1 MB
// Time Limit: 3000 ms
// Good luck to the code >w<
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define lson l, mid, rt << 1
#define rson mid + 1, r, rt << 1 | 1
using pr = pair<int, int>;
const int maxn = 5e5 + 5;
int a[maxn], n, B, Q;
ll mx[maxn << 2], col[maxn << 2], sum[maxn];
void update(int rt) { mx[rt] = max(mx[rt << 1], mx[rt << 1 | 1]); }
void build(int l, int r, int rt) {
if (l == r) return mx[rt] = sum[l], void(0);
int mid = (l + r) >> 1;
build(lson), build(rson), update(rt);
} void color(int rt, ll k) { col[rt] += k, mx[rt] += k; }
void pushcol(int rt) {
if (!col[rt]) return ;
color(rt << 1, col[rt]), color(rt << 1 | 1, col[rt]), col[rt] = 0;
} double query(int l, int r, int rt) {
if (l == r) return 1.0 * (mx[rt] + 1ll * B * l) / l;
int mid = (l + r) >> 1; pushcol(rt);
if (mx[rt << 1] >= 0) return query(lson);
return query(rson);
} void modify(int l, int r, int rt, int nowl, int nowr, ll k) {
if (nowl <= l && r <= nowr) return color(rt, k);
int mid = (l + r) >> 1; pushcol(rt);
if (nowl <= mid) modify(lson, nowl, nowr, k);
if (mid < nowr) modify(rson, nowl, nowr, k);
update(rt);
} ll all;
int main() {
scanf("%d %d %d", &n, &B, &Q);
for (int i = 1; i <= n; i ++)
scanf("%d", &a[i]), sum[i] = sum[i - 1] + a[i] - B, all += a[i];
build(1, n, 1);
for (int i = 1, x, k; i <= Q; i ++) {
scanf("%d %d", &x, &k), all += k - a[x];
modify(1, n, 1, x, n, k - a[x]), a[x] = k;
if (mx[1] < 0) printf("%.15lf\n", 1.0 * all / n);
else printf("%.15lf\n", query(1, n, 1));
}
return 0;
}
T7
给定一棵 n n n 个点的树,第 i i i 个点上有 ( a i , b i ) (a_i,b_i) (ai,bi) 两个数。每经过一个点可以选择 a , b a,b a,b 中的一个数加入集合 S S S(重复加入仅计算一次),求所有从 1 1 1 出发的路径能得到的最大的 ∣ S ∣ |S| ∣S∣。
2 ≤ n ≤ 2 × 1 0 5 2\le n\le 2\times 10^5 2≤n≤2×105, 1 ≤ a i , b i ≤ n 1\le a_i,b_i\le n 1≤ai,bi≤n。
对于一条路径,我们考虑将路径上的点 i i i 的 a i , b i a_i,b_i ai,bi 进行连边,这样会得到一张无向图,且有若干连通块。对于一个连通块,设它的点数为 p p p 边数为 q q q,那么它对答案的贡献就是 min ( p , q ) \min(p,q) min(p,q)。若这个连通块是一棵树,那么不选根节点后其他点都能入选,答案为边数 q q q;否则考虑图中的环和链,显然环上点都可以入选,而链总是连着一个环,第一个元素已经入选,于是剩下的也都可以选上,答案为点数 p p p。前者 q < p q<p q<p 后者 q ≥ p q\ge p q≥p,于是取 min ( p , q ) \min(p,q) min(p,q) 即为答案。
我们考虑在树上 dfs,每遍历一个点就将 ( a i , b i ) (a_i,b_i) (ai,bi) 加入,回溯时撤销加入这条边,显然可以使用可撤销并查集,用一个栈维护进行过的操作,合并时分类讨论一下即可。代码还没写。