2024.8.23 模拟赛总结
A.dist
Statement:
给定一棵 \(n(n \le 10^6)\) 个节点带边权的树,定义 \(\mathrm{Min}(x, y)\) 是 \((x, y)\) 路径上的边权最小值。求 \(\max_{r = 1}^n {\sum_{v \ne i} \mathrm{Min}(r, v)}\)。
Solution:
经典套路题。
首先注意到一条路径上的只有最小值才会产生贡献,于是对于一个连通块,我们找到最小的那个边,并分成两个连通块 \(\mathrm{A, B}\),假如把根放在 \(\mathrm A\) 中,那么对于连通块 \(\mathrm B\) 到根的所有路径贡献都是这个最小边的边权,然后递归计算 \(\mathrm A\) 的贡献,对于放在 \(\mathrm B\) 中的情况一样做即可。但是这样的时间复杂度是 \(O(n^2)\) 的。
于是将删边改成加边,具体的,我们将边按边权从大到小排序,每次合并两个集合,用个并查集维护即可。时间复杂度 \(O(n \log n)\)。
qwq
#include<bits/stdc++.h>
#pragma GCC optimize(3, "Ofast", "inline")
#define int long long
using namespace std;
const int N = 1e6 + 10;
int n, fa[N], siz[N], val[N];
struct Edge{
int u, v, w;
}E[N];
bool cmp(struct Edge E1, struct Edge E2){
return E1.w > E2.w;
}
int findfa(int x){return fa[x] = (fa[x] == x) ? x : findfa(fa[x]);}
void merge(int x, int y, int w){
int fx = findfa(x), fy = findfa(y);
if(fx == fy) return; if(siz[fx] < siz[fy]) swap(fx, fy);
val[fx] = max(val[fx] + siz[fy] * w, val[fy] + siz[fx] * w);
fa[fy] = fx; siz[fx] += siz[fy];
}
signed main(){
// freopen("dist3.in", "r", stdin);
freopen("a.in", "r", stdin);
freopen("a.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n; for(int i = 1; i <= n; i++) siz[i] = 1, fa[i] = i;
for(int i = 1; i < n; i++) cin >> E[i].u >> E[i].v >> E[i].w;
sort(E + 1, E + n, cmp);
for(int i = 1; i < n; i++) merge(E[i].u, E[i].v, E[i].w);
cout << val[findfa(1)];
return 0;
}
B.wolf
Statement:
有 \(n(1 \le n \le 3000)\) 间屋子,由 \(n − 1\) 条双向道路连接成了一个树形结构。其中,第 \(i\) 间屋子里居住了 \(1\) 个种族为 \(c_i\) 的狼人。现在你要召集一个连通块内的狼人。设 \(a_j\) 表示被召集的种族为 \(j\) 的狼人个数,若存在 \(2 a_k > \sum a_j\) ,则种族为 \(k\) 的狼人会造反。你想知道有多少个连通块会使某种狼人造反,答案对 \(998244353\) 取模。
Solution:
首先对判定进行转化:\(2a_k > \sum_{j = 1}^n a_j \Leftrightarrow a_k - \sum_{j \ne k} a_j > 0\)。
所以可以枚举作为众数的种类然后做一遍做一遍树上背包,这样时间复杂度是 \(O(n^3)\)。然后注意到对于一个总共有 \(cnt_i\) 个狼人的种类 \(i\),背包上下限是 \(cnt_i\),由树上背包的时间复杂度做一次是 \(O(ncnt_i)\) 的,于是总复杂度是 \(O(n^2)\) 的了。
关于容量上限为 $k$ 的树上背包的复杂度证明
考虑合并两颗子树时交叉点的贡献,将节点顺序排列成一个有序序列,我们对一个点认为它只可以与距离小于等于 $k$ 的点配对。显然只会有 $nk$ 个对,于是时间复杂度为 $O(nk)$。qwq
#include<bits/stdc++.h>
#pragma GCC optimize(3, "Ofast", "inline")
#define int long long
using namespace std;
const int N = 3000 + 10, mod = 998244353;
int n, m, col[N], _col[N], f[N][2 * N], cnt0[N], cnt1[N], ans, tmp[2 * N];
struct edge{
int v, next;
}edges[N << 1];
int head[N], idx;
int id(int val){if(val < 0) val += 2 * m + 1; return val;}
void ADD(int& x, int y){x = (x + y) % mod;}
void add_edge(int u, int v){
edges[++idx] = {v, head[u]};
head[u] = idx;
}
int mymax(int x, int y){return (x > y) ? x : y;}
void dfs(int u, int fa){
cnt0[u] = (!col[u]); cnt1[u] = col[u]; //f[u][0] = 1;
for(int i = -m; i <= m; i++) f[u][id(i)] = 0;
f[u][(cnt0[u] ? id(-1) : 1)] = 1;
// cout << u << " " << f[u][id(-1)] << " " << f[u][1] << "\n";
for(int i = head[u]; i; i = edges[i].next){
int v = edges[i].v; if(v == fa) continue;
dfs(v, u);
for(int i = mymax(-m, -cnt0[u]); i <= cnt1[u]; i++) tmp[id(i)] = f[u][id(i)];
for(int i = mymax(-m, -cnt0[u]); i <= cnt1[u]; i++){
for(int j = mymax(-m, -cnt0[v]); j <= cnt1[v]; j++){
if(i + j >= -m && i + j <= m) ADD(f[u][id(i + j)], tmp[id(i)] * f[v][id(j)] % mod);
}
}
cnt1[u] += cnt1[v]; cnt0[u] += cnt0[v];
}
// for(int i = mymax(-m, -cnt0[u]); i <= cnt1[u]; i++) cout << u << " " << i << " " << f[u][id(i)] << "\n";
for(int i = 1; i <= cnt1[u]; i++) ADD(ans, f[u][i]);
}
signed main(){
// freopen("wolf6.in", "r", stdin);
freopen("b.in", "r", stdin);
freopen("b.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n; for(int i = 1; i <= n; i++) cin >> _col[i];
for(int i = 1; i < n; i++){
int x, y; cin >> x >> y;
add_edge(x, y); add_edge(y, x);
}
for(int tcol = 1; tcol <= n; tcol++){
m = 0;
for(int i = 1; i <= n; i++) col[i] = (_col[i] == tcol), m += col[i];
if(m) dfs(1, 0);
}
cout << ans;
return 0;
}
C.ancestor
Statement:
给定一棵有 \(n\) 个节点的树和 \(q\) 次查询。每次查询时有三个整数 \(k, m\) 和 \(r\),接着是 \(k\) 个树的节点 \(a_1, a_2 ,...,a_k\) 。假设树以 \(r\) 为根。
我们希望将这 \(k\) 个给定的节点分成至多 \(m\) 组,使得满足以下条件:
- 每个节点必须在恰好一个组中,每个组必须至少有一个节点。
- 在任何组内,不应存在两个不同的节点,使得一个节点是另一个节点的祖先(直接或间接)。
你需要输出每次查询的方案数,结果对 \(10^9 + 7\) 取模。
其中 \(n, q \le 2 \times 10^5, \sum k \le 2 \times 10^5, m \le 300\)。
Solution:
注意到影响到一个点分组的节点只有他的父亲,定义影响点 \(x\) 分组,即点集中的它的父亲个数为 \(ex_x\)。于是我们可以设 \(f_{i, j}\) 为考虑前 \(i\) 个点,分 \(j\) 组的方案数。注意到这个影响条件有传递性,于是考虑经典做法,我们将影响到它的点放到它的前面,即按 \(ex\) 值对序列排序。于是可以有 \(f_{i, j} = f_{i - 1, j - 1} + f_{i - 1, j} \times (j - ex_i)\)。而 \(ex\) 值直接树剖即可。时间复杂度 \(O(n \log n)\)。
qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10, mod = 1e9 + 7, M = 300 + 10;
struct edge{
int v, next;
}edges[N << 1];
int head[N], idx;
int n, fa[N], dep[N], dfn[N], top[N], siz[N], son[N], dfc, a[N], ex[N];
int f[N][M];
void add_edge(int u, int v){
edges[++idx] = {v, head[u]};
head[u] = idx;
}
void dfs1(int u, int father){
fa[u] = father; siz[u] = 1; dep[u] = dep[fa[u]] + 1;
for(int i = head[u]; i; i = edges[i].next){
int v = edges[i].v; if(v == fa[u]) continue;
dfs1(v, u); siz[u] += siz[v];
if(siz[son[u]] < siz[v]) son[u] = v;
}
}
void dfs2(int u, int tp){
dfn[u] = ++dfc; top[u] = tp; //cout << u << " " << top[u] << " " << son[u] << "\n";
if(son[u]) dfs2(son[u], tp);
else return;
for(int i = head[u]; i; i = edges[i].next){
int v = edges[i].v;
if(v != fa[u] && v != son[u]) dfs2(v, v);
}
}
struct BIT{
int t[N];
int lowbit(int x){return x & -x;}
void add(int x, int k){for(; x <= n; x += lowbit(x)) t[x] += k;}
int sum(int x){
int ret = 0;
for(; x; x -= lowbit(x)) ret += t[x];
return ret;
}
int qry(int l, int r){return sum(r) - sum(l - 1);}
}tr;
bool cmp(int x, int y){return ex[x] < ex[y];}
int listqry(int x, int y){
int ret = 0; //cout << x << " " << y << "\n";
while(top[x] != top[y]){
if(dep[top[x]] < dep[top[y]]) swap(x, y);
// cout << x << " " << top[x] << " " << dfn[x] << "\n";
ret += tr.qry(dfn[top[x]], dfn[x]);
x = fa[top[x]];
}
if(dfn[x] > dfn[y]) swap(x, y);
ret += tr.qry(dfn[x], dfn[y]);
return ret;
}
void solve(){
int k, m, r; cin >> k >> m >> r;
for(int i = 1; i <= k; i++) cin >> a[i], tr.add(dfn[a[i]], 1);
for(int i = 1; i <= k; i++) ex[a[i]] = listqry(r, a[i]);
sort(a + 1, a + k + 1, cmp); f[0][0] = 1;
for(int i = 1; i <= k; i++) ex[a[i]]--;//, cout << a[i] << " " << ex[a[i]] << "\n";
for(int i = 1; i <= k; i++){
// cout << i << " " << 0 << " " << f[i][0] << "\n";
for(int j = 1; j <= m; j++){
f[i][j] = f[i - 1][j - 1];
if(j - ex[a[i]] > 0) f[i][j] = (f[i][j] + f[i - 1][j] * (j - ex[a[i]]) % mod) % mod;
// cout << i << " " << j << " " << f[i][j] << "\n";
}
// cout << dfn[a[i]] << "\n";
}
int ans = 0;
for(int i = 1; i <= m; i++) ans = (ans + f[k][i]) % mod;
for(int i = 1; i <= k; i++) tr.add(dfn[a[i]], -1);
cout << ans << "\n";
}
signed main(){
freopen("c.in", "r", stdin);
freopen("c.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int Q; cin >> n >> Q;
for(int i = 1; i < n; i++){
int x, y; cin >> x >> y;
add_edge(x, y); add_edge(y, x);
}
dfs1(1, 0); dfs2(1, 1);
// for(int i = 1; i <= n; i++) cout << dep[i] << " " << fa[i] << " " << dfn[i] << " " << son[i] << " " << top[i] << " " << siz[i] << "\n";
while(Q--) solve();
return 0;
}
D.chess
Statement:
\(\rm A\) 和 \(\rm B\) 正在进行一个游戏。这个游戏在 \(N\) 个顶点的图上进行。顶点编号为 \(1,2,...,N\)。图中有 \(N − 1\) 条红边和 \(N − 1\) 条蓝边,二者都构成一棵树。红边用 \((a_i, b_i)\) 表示,蓝边用 \((c_i, d_i)\) 表示。两人各有一个棋子,\(\rm A\) 的初始位置在顶点 \(\rm X\),\(\rm B\) 的初始位置在顶点 \(\rm Y\) 。
游戏采取轮流制,依次进行 \(1,2,3,...\) 的轮次。在奇数轮 \((1,3,5,...)\) 时,\(\rm A\) 可以选择移动到与其当前所在树 \(\rm a\) 中点相邻的节点,或者选择不动;在偶数轮 \((2,4,6,...)\) 时,\(\rm B\) 进行选择与其当前所在树 \(\rm b\) 中点相邻的节点,或者选择不动。当两人到达编号相同的点时,游戏结束。\(\rm A\) 希望尽可能地最大化游戏轮数,而 \(\rm B\) 则希望尽可能地最小化游戏轮数。在假定两者都按照最优策略行动的情况下,判断游戏能否结束,并在能结束的情况下求出总轮数。
其中 \(N \le 200000\)。
Solution:
这道题实际上就是 \(\rm B\) 抓 \(\rm A\) 的过程,而 \(\rm A\) 可以不断的瞬移。首先预处理出每个点从 \(\rm A,B\) 的到达的步数 \(distA, distB\)。显然 \(\forall x, distA_x \le distB_x\) 的 \(x\),\(\rm A\) 和他在 \(\rm A\) 子树中的点显然是不能去的。接着考虑其他点,观察到假如有一条 \(\rm A\) 中的边 \((x_i, y_i)\),且在 \(\rm B\) 中 \((x_i, y_i)\) 的距离大于 \(2\),\(\rm A\) 就可以遛狗了,这样就无法结束了。
对于接下来的情况,显然 \(\rm A\) 不可能跳出 \(\rm B\) 的子树,于是 \(\rm A\) 就只能跑到 \(A\) 能到达中最深的点然后等死了。距离可以直接分类讨论,不用求 \(\rm LCA\),时间复杂度 \(O(n)\)。
qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 10;
vector<int> B[N], R[N];
int n, dep[N], _fa[N], _upd3[N], ans, X, Y;
struct Edge{
int x, y;
}E[N];
void dfsB(int u, int fa){
_fa[u] = fa;
for(int i = 0; i < B[u].size(); i++){
int v = B[u][i]; if(v == fa) continue;
dep[v] = dep[u] + 1; dfsB(v, u);
}
}
bool upd3(int x, int y){
if(dep[x] < dep[y]) swap(x, y);
if(dep[x] == dep[y] && _fa[x] != _fa[y]) return 1;
if(dep[x] == dep[y] + 1 && _fa[x] != y) return 1;
if(dep[x] == dep[y] + 2 && _fa[_fa[x]] != y) return 1;
return (dep[x] - dep[y] > 2);
}
bool dfsR(int u, int fa, int _dep){
if(_dep >= dep[u]) return false;
if(_upd3[u]){cout << -1; return true;} ans = max(ans, dep[u] * 2);
for(int i = 0; i < R[u].size(); i++){
int v = R[u][i];
if(v != fa){
if(dfsR(v, u, _dep + 1)) return true;
}
}
return false;
}
void solve(){
cin >> n >> X >> Y; ans = 0;
for(int i = 1; i <= n; i++) R[i].clear(), B[i].clear(), _upd3[i] = dep[i] = _fa[i] = 0;
for(int i = 1; i < n; i++){
int x, y; cin >> x >> y; E[i] = {x, y};
R[x].push_back(y); R[y].push_back(x);
}
for(int i = 1; i < n; i++){
int x, y; cin >> x >> y;
B[x].push_back(y); B[y].push_back(x);
}
dfsB(Y, 0);
for(int i = 1; i < n; i++) if(upd3(E[i].x, E[i].y)) _upd3[E[i].x] = _upd3[E[i].y] = 1;
if(!dfsR(X, 0, 0)) cout << ans; cout << "\n";
}
signed main(){
freopen("d.in", "r", stdin);
freopen("d.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int T; cin >> T; while(T--) solve();
return 0;
}