2024.11.11日校内模拟赛总结 +题解(kmp + 分类讨论 + 最大生成树 + 扫描线)
省流:
ddp写了1个半小时没调出来,赛后5min后调出来了,但是T了。
T3暴力没交上,200pts滚蛋了。
时间安排
- \(7:40 - 7:50\) 开题,看完 T1 就有思路,没往下看,写了一发。假了。
- \(7:50 - 8:05\) 改了改,过了大样例,就跳了。
- \(8:10 - 9:50\) 写 T2,没啥好说的,就是纯找规律加推一点式子。但是我感觉细节很多就写了好久。所幸写完没咋调就过了。然后发现时间不到两个小时了。
- \(9:50 - 12:00\) 先花 \(10min\) 看了T3和T4,然后决定先写T3。首先 \(n^2\) 的dp很显然。然后感觉这个东西能 \(ddp\),复杂度是 \(O(n \times log_2^2n)\)。当时感觉没啥别的做法了,虽然有点疑惑 \(n\) 开到 \(5 \times 10^5\) 能不能过,但心里还是坚信能过的。并且T3放 ddp 感觉也正常,就觉得这题标算就是 ddp。然后推了一会儿细节,纠结了1min 决定挑战一下自己看能不能冲出来。写的时候挺顺利的,并没有卡壳。于是在还有 \(30min\) 的时候写完了。测样例发现过不去??开始调试。最开始发现了一些数组没更新等错误,然后程序不会re了。本来以为能过了,但小样例的后两个没过!!!有点急,然后脑子不是很清醒,最后感觉差一点但是没调出来。然后匆忙交了个暴力,最后比赛结束了发现暴力注释没删。然后T3喜提0pts。
总结
赛后T3调了 5min 过了,但是还是TLE了,有70pts。呜呜呜~
- 比赛中如果时间并不足以支持写一个代码量很大的正解,就尽量先写部分分在写正解。
- T3正解并不是ddp。要记住 5e5一般都是单log做法!!!!
- 心态还是要再稳一些。
题解
T3 在异世界迷宫开音趴
题意:
给你一棵 \(n\) 个节点的树,每条边断掉的代价为 \(w_i\)。
然后给你一个排列 \(a_i\),表示第 \(i\) 个时刻会有一个人出现在 \(a_i\) 节点上。
你需要对每个 \(i \in [1, n]\),求出前 \(i\) 个人各自待在自己的位置上时,断掉若干条边使得它们不联通的最小花费。
\(1 \leq n \leq 5 \times 10^5\), \(1 \leq w_i \leq 10^9\),保证 \(a_i\) 为一个 \(1 \sim n\) 的排列。
分析:
对于当前有人的点,称他们为 关键点。
如果考虑 \(dp\),那么可以想到设 \(dp_{x, 0/1}\) 表示 \(x\) 的子树中是否有关键点能够到达 \(x\) 的最小代价。转移不再赘述。暴力做 \(n\) 次复杂度是 \(O(n^2)\),但是注意到状态数只有 \(0/1\) 是很少的,可以用 \(ddp\) 优化到 \(O(n \times log_2^2n)\)。期望得分70pts。
CODE:
namespace Sol2 { // 动态dp
const int N = 5e5 + 10;
const LL INF = 7e16;
struct matrix {
LL mt[3][3];
friend matrix operator * (matrix a, matrix b) {
matrix c; memset(c.mt, 0x3f, sizeof c.mt);
for(int i = 0; i < 3; i ++ )
for(int j = 0; j < 3; j ++ )
for(int k = 0; k < 3; k ++ )
c.mt[i][j] = min(c.mt[i][j], a.mt[i][k] + b.mt[k][j]);
return c;
}
};
struct SegmentTree {
int l, r; matrix mat;
#define l(x) t[x].l
#define r(x) t[x].r
#define mat(x) t[x].mat
}t[N * 4];
struct edge {
int v, last; LL w;
}E[N * 2];
int a[N];
int tot, head[N];
void add(int u, int v, LL w) {
E[++ tot].v = v;
E[tot].w = w;
E[tot].last = head[u];
head[u] = tot;
}
bool is[N];
int dep[N], fat[N], bel[N], big[N], sz[N];
int ID[N], dfn[N], dfc, bot[N]; // 每条链的链尾
int deg[N];
LL S0[N], mn[N], v0[N]; // 全都是0的代价和 挂在链头的mn, 与v0
LL fae[N]; // 每个点父边的边权
multiset< LL > s[N]; // 求轻儿子mn用
void dfs0(int x, int fa) {
sz[x] = 1; fat[x] = fa; dep[x] = dep[fa] + 1;
for(int i = head[x]; i; i = E[i].last) {
int v = E[i].v; LL w = E[i].w;
if(v == fa) continue;
fae[v] = w;
dfs0(v, x);
sz[x] += sz[v];
if(sz[v] > sz[big[x]]) big[x] = v;
}
}
void dfs1(int x, int b) {
if(x == b) mn[x] = INF, v0[x] = 0;
bel[x] = b; dfn[x] = ++ dfc;
bot[b] = x; ID[dfc] = x;
if(big[x]) dfs1(big[x], b);
for(int i = head[x]; i; i = E[i].last) {
int v = E[i].v;
if(dep[v] == dep[x] - 1 || v == big[x]) continue;
dfs1(v, v);
s[x].insert(INF);
}
}
matrix get(int x) { // 获得 x 的矩阵
matrix c;
if(is[x]) { // x 上有人
for(int i = 0; i < 2; i ++ ) c.mt[i][0] = INF;
for(int i = 0; i < 2; i ++ ) {
if(i == 0) c.mt[i][1] = c.mt[i][2] = S0[x];
else c.mt[i][1] = c.mt[i][2] = S0[x] + (big[x] != 0 ? fae[big[x]] : 0);
}
}
else { // 没人
LL mn = S0[x] + (s[x].empty() ? INF : *s[x].begin());
LL w = (big[x] != 0 ? fae[big[x]] : 0);
c.mt[0][0] = 0; c.mt[1][0] = c.mt[2][0] = w;
c.mt[0][1] = mn; c.mt[1][1] = w + mn; c.mt[2][1] = S0[x];
c.mt[0][2] = mn; c.mt[1][2] = w + mn; c.mt[2][2] = S0[x];
}
return c;
}
void update(int p) {
mat(p) = mat(p << 1 | 1) * mat(p << 1);
}
void build(int p, int l, int r) {
l(p) = l, r(p) = r;
if(l == r) {
mat(p) = get(ID[l]);
return ;
}
int mid = (l + r >> 1);
build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
update(p);
}
void ins(int p, int pos, matrix c) {
if(l(p) == r(p)) {
mat(p) = c;
return ;
}
int mid = (l(p) + r(p) >> 1);
if(pos <= mid) ins(p << 1, pos, c);
else ins(p << 1 | 1, pos, c);
update(p);
}
matrix ask(int p, int l, int r) {
if(l <= l(p) && r >= r(p)) return mat(p);
int mid = (l(p) + r(p) >> 1);
if(r <= mid) return ask(p << 1, l, r);
else if(l > mid) return ask(p << 1 | 1, l, r);
else return ask(p << 1 | 1, l, r) * ask(p << 1, l, r);
}
void Add(int x) { // 将一个点加入
is[x] = 1;
ins(1, dfn[x], get(x)); // 更改矩阵加入
int now = x;
while(bel[now] != bel[1]) {
matrix f; f.mt[0][0] = 0, f.mt[0][1] = INF, f.mt[0][2] = INF;
f = f * ask(1, dfn[bel[now]], dfn[bot[bel[now]]]);
int u = fat[bel[now]];
s[u].erase(s[u].find(mn[bel[now]]));
S0[u] -= v0[bel[now]];
LL dp0 = f.mt[0][0], dp1 = f.mt[0][1];
s[u].insert(dp1 - min(dp0, dp1 + fae[bel[now]]));
S0[u] += min(dp0, dp1 + fae[bel[now]]);
v0[bel[now]] = min(dp0, dp1 + fae[bel[now]]);
mn[bel[now]] = dp1 - min(dp0, dp1 + fae[bel[now]]);
ins(1, dfn[u], get(u));
now = u;
}
}
void solve() {
for(int i = 1; i < n; i ++ ) {
int u, v; LL w; scanf("%d%d%lld", &u, &v, &w);
add(u, v, w); add(v, u, w);
deg[u] ++; deg[v] ++;
}
for(int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
dfs0(1, 0);
dfs1(1, 1); // 求出重链
for(int i = 1; i <= n; i ++ ) { // 将所有轻儿子的mn插入s[i] 中
int sz = deg[i] - (i != 1) - (big[i] != 0);
S0[i] = 0;
}
build(1, 1, n);
for(int i = 1; i <= n; i ++ ) {
Add(a[i]);
matrix f; f.mt[0][0] = 0, f.mt[0][1] = INF, f.mt[0][2] = INF;
f = f * ask(1, dfn[1], dfn[bot[1]]);
printf("%lld\n", min(f.mt[0][0], f.mt[0][1]));
}
}
}
对于正解:
好像有一些奇怪的做法,比如说 有答案可以继承等性质,但是我不是很懂。
下面是一种比较容易理解的做法:
考虑给你一些关键点,该怎么求出删边的最小代价:
首先 删掉最小代价的边使得关键点不联通 等价于 保留一些边权和最大的边使得关键点不联通。
然后可以建一个虚点,虚点向所有关键点连一条边权为 INF 的边,然后对于所有的边跑一遍 最大生成树,那么所有保留下来树边边权之和就是答案。
这样做正确性在于如果说保留下来的树边会使得两个关键点联通,那么它们在生成树上一定会和虚点连出去的边构成环,不符合生成树的定义。因此保留下来的树边一定不会使关键点连通。
那么每次暴力做就是 \(n^2 \times \log_2n\) 的。考虑按照边权从大到小排序后建出一棵 \(kruskal\) 生成树,那么连边顺序就是自下向上的。然后如果一条边联通的左右儿子子树中都含有关键点,这时候这条边是不能保留的。那么我们如果我们对于每棵子树都求出子树中关键点出现的最早时间 \(tim\),那么 一条边的最早删除时刻就是左右儿子子树 \(tim\) 取 \(max\)。那么它删边的贡献就会算到时间的一段后缀上,直接差分就可以维护。
这样总复杂度是 \(O(m \times \log_2m)\) 的。代码很小清新。
CODE:
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
typedef long long LL;
int n, tim[N];
LL ans[N];
int bin[N];
int Find(int x) {return x == bin[x] ? x : bin[x] = Find(bin[x]);}
struct edge {
int u, v; LL w;
}E[N];
bool cmp(edge x, edge y) {
return x.w > y.w;
}
int main() {
scanf("%d", &n);
for(int i = 1; i < n; i ++ ) {
int u, v; LL w; scanf("%d%d%lld", &u, &v, &w);
E[i] = (edge) {u, v, w};
}
sort(E + 1, E + n, cmp);
for(int i = 1; i <= n; i ++ ) {
int x; scanf("%d", &x);
tim[x] = i; bin[i] = i;
}
for(int i = 1; i < n; i ++ ) {
int u = E[i].u, v = E[i].v; LL w = E[i].w;
int f1 = Find(u), f2 = Find(v);
int o = max(tim[f1], tim[f2]);
ans[o] += w;
bin[f1] = f2; tim[f2] = min(tim[f2], tim[f1]);
}
for(int i = 1; i <= n; i ++ ) ans[i] += ans[i - 1];
for(int i = 1; i <= n; i ++ ) printf("%lld\n", ans[i]);
return 0;
}
T4 超超超超喜欢你的一百个数据结构题 第二季
题意:
给你一个长度为 \(n\) 的序列 \(a\),一开始 \(a_i = i\),称此时为初状态。
有 \(m\) 个操作,第 \(i\) 次操作可以用一个二元组 \((x, y)\) 表示把序列中等于 \(x\) 的数都改成 \(y\)。
接着有 \(q\) 次询问,每次给你 \(l, r\),你需要从初状态开始,依次执行第 \(l, l + 1,..., r\) 次操作,之后输出操作结束后序列里有多少个极长的的同色连续段。
\(1 \leq n,m, q \leq 5 \times 10^5\) 。
每个操作保证 \(1 \leq x_i, y_i \leq n\), 每次询问保证 \(1 \leq l \leq r \leq m\)。
分析:
感觉正解并不是很容易想到。
可以想到答案等于 \(n\) 减去 \(col_i = col_{i + 1}\) 的数量
首先一次操作相当于将两种颜色合并成一种。
那么考虑每次合并 \((x, y)\),就建出一个新点 \(z\),让 \(x\) 和 \(y\) 的父亲是 \(z\), \(z\) 的颜色是 \(y\) 的颜色。
下次合并如果再次用到 \(y\) 就让 \(z\) 的父亲是新建的点。
那么如果两种颜色在同一棵树里,那么这两种颜色就被合并成了一种。
然后考虑这个操作能不能倒过来:倒着扫描操作,那么对于当前的操作 \((x, y)\),相当于在开始就把 \(x\) 的颜色变成 \(y\)。这个可以直接在原来合并树上稍作修改:新建一个虚点 \(z\),让 \(x\) 的父亲为 \(z\), \(y\) 的父亲变成 \(z\),\(z\) 的父亲是 \(y\) 原来的父亲。其余点的父亲不变。
我们发现这样操作实际上连虚点的颜色都不用记录。
令虚点的编号为 操作编号 \(+n\)。那么两个点 \(x, y\) 在树上的 \(lca\) 就是它们 以初始状态,从当前操作开始变成同色的最早时刻。
然后有一个技巧:考虑对 \(l\) 从右往左扫描线,同时维护每一个 \(r\) 的答案。
那么每次 \(l\) 变成 \(l - 1\),对答案有影响的只有 \(x_{l - 1}\) 对答案的贡献发生了变化。首先将 \(x_{l - 1}\) 对答案的贡献撤销:在原来的合并树上找到 \(x_{l - 1}\) 和 \(x_{l - 1} -1\) 的 \(lca\),那么可以求出这两种颜色变成同一种的最早时刻,在这个时刻往后答案都会减一,而我们要撤销这个贡献,因此需要让一个后缀加一。 撤销 \(x_{l - 1}\) 和 \(x_{l - 1} + 1\) 的贡献同理。
然后求出现在 \(x_{l - 1}\) 对答案的贡献:将合并树进行修改,然后修改 祖先倍增数组。接着找到此时 \(x_{l - 1}\) 和 \(x_{l - 1} -1\) 的最早同色时刻,做一个后缀减一。 \(x_{l - 1}\) 和 \(x_{l - 1} + 1\) 的贡献同理。
对于倍增数组的修改:由于我们只改变了叶子,所以只需要对改变的三个点做一遍倍增进行修改: \(x_{l - 1}, y_{l - 1}\) 和新建虚点 \(z\)。
求 \(lca\) 可以直接倍增求。
时间复杂度:每次修改 \(\log_2n\),查询 \(\log_2n\)。总共会做 \(m\) 次。总复杂度 \(O(m \times \log_2n)\)
CODE:
// 有一个技巧是对l扫描线,然后用数据结构维护所有r的答案
#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
const int N = 5e5 + 10;
int n, m, q;
int x[N], y[N];
int l[N], r[N];
int ans[N];
vector< int > qry[N];
struct BIT {
int c[N];
int lowbit(int x) {return x & -x;}
void add(int x, int y) {for(; x < N; x += lowbit(x)) c[x] += y;}
int ask(int x) {int res = 0; for(; x; x -= lowbit(x)) res += c[x]; return res;}
}T;
int fat[N * 2][21], dep[N * 2];
int Lca(int x, int y) {
if(dep[x] < dep[y]) swap(x, y);
for(int i = 20; i >= 0; i -- ) {
if(dep[fat[x][i]] >= dep[y]) x = fat[x][i];
}
if(x == y) return x;
for(int i = 20; i >= 0; i -- ) {
if(fat[x][i] != fat[y][i]) x = fat[x][i], y = fat[y][i];
}
return fat[x][0] == fat[y][0] ? fat[x][0] : -1;
}
void ins(int p) { // 将 x[p] 变成了 y[p]
if(x[p] != 1) {
int lc = Lca(x[p], x[p] - 1);
if(lc > 0) {
int tim = lc - n;
T.add(tim, 1); // 后缀加 撤销贡献
}
}
if(x[p] != n) {
int lc = Lca(x[p], x[p] + 1);
if(lc > 0) {
int tim = lc - n;
T.add(tim, 1);
}
}
if(x[p] != y[p]) { // x[p] 不等于 y[p] 才操作 新开一个节点 n + p 然后让 x 的父亲变成 n + p, y 的父亲变成 n + p
for(int i = 0; i <= 20; i ++ ) fat[n + p][i] = fat[y[p]][i];
dep[n + p] = dep[y[p]];
dep[y[p]] = dep[x[p]] = dep[n + p] + 1;
fat[x[p]][0] = fat[y[p]][0] = n + p;
for(int i = 1; i <= 20; i ++ ) fat[x[p]][i] = fat[fat[x[p]][i - 1]][i - 1], fat[y[p]][i] = fat[fat[y[p]][i - 1]][i - 1];
}
if(x[p] != 1) {
int lc = Lca(x[p], x[p] - 1);
if(lc > 0) {
int tim = lc - n;
T.add(tim, -1); // 后缀加 撤销贡献
}
}
if(x[p] != n) {
int lc = Lca(x[p], x[p] + 1);
if(lc > 0) {
int tim = lc - n;
T.add(tim, -1);
}
}
}
int main() {
scanf("%d%d%d", &n, &m, &q);
for(int i = 1; i <= m; i ++ ) scanf("%d%d", &x[i], &y[i]);
for(int i = 1; i <= q; i ++ ) {
scanf("%d%d", &l[i], &r[i]);
qry[l[i]].pb(i);
}
for(int i = m; i >= 1; i -- ) {
ins(i); // 插入i次操作
for(auto v : qry[i]) ans[v] = n + T.ask(r[v]);
}
for(int i = 1; i <= n + m; i ++ ) dep[i] = 1;
for(int i = 1; i <= q; i ++ ) printf("%d\n", ans[i]);
return 0;
}

浙公网安备 33010602011771号