「解题报告」The 1st Universal Cup. Stage 0: Nanjing
挑了些有意义的题来写的,可以自己尝试下。
大概按难度排序,过于简单和没啥意义的题(指计算几何)没写。
J. Perfect Matching
给定一个长为 \(n\) 的序列 \(\{a_i\}\),构造一个有 \(n\) 个点的无向图,满足 \(i, j\) 之间有连边当且仅当 \(|a_i - a_j| = |i - j|\),求该图是否存在完美匹配。若存在,构造任意一组。
\(n \le 10^5, |a_i| \le 10^9\)
考虑将 \(|a_i - a_j| = |i - j|\) 的绝对值拆开,那么连边条件变为了 \(a_i - i = a_j = j\) 或 \(a_i + i = a_j + j\)。
我们建一张二分图,然后对于每个 \(i\),从左边的 \(a_i - i\) 向右边的 \(a_i + i\) 连边,那么两个点 \(i,j\) 有连边当且仅当它们代表的两条边有公共顶点。
那么题目转化成了将一个二分图划分成若干个长为 \(2\) 的链。
显然连通块边数为奇数时无解。
考虑先整出一颗 DFS 树(森林),然后先让儿子贪心地去匹配,这样每个子树内只会剩下 \(0/1\) 条边。如果剩下 \(1\) 条边,就让这条边与这个节点向父亲连的边进行匹配。注意非树边也要当做儿子来匹配。
容易证明如果最后剩下边,那么说明总边数一定是奇数。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
int a[MAXN], n;
vector<int> val1, val2;
vector<pair<int, int>> e[MAXN];
void add(int a, int b, int i) {
e[a].push_back({b, i});
e[b].push_back({a, i});
}
int u[MAXN], v[MAXN];
bool vis[MAXN];
bool used[MAXN];
vector<pair<int, int>> ans;
void dfs(int u, int pre) {
vis[u] = 1;
int lst = 0;
for (auto p : e[u]) if (p.second != pre) {
int v = p.first;
int w = p.second;
if (!vis[v]) dfs(v, w);
if (!used[w]) {
if (lst) {
ans.push_back({lst, w});
used[lst] = used[w] = 1;
lst = 0;
} else {
lst = w;
}
}
}
if (lst && pre) {
ans.push_back({lst, pre});
used[lst] = used[pre] = 1;
}
}
int T;
int main() {
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
if (n & 1) {
printf("No\n");
continue;
}
val1.clear(), val2.clear();
ans.clear();
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
val1.push_back({a[i] - i});
val2.push_back({a[i] + i});
}
sort(val1.begin(), val1.end());
sort(val2.begin(), val2.end());
val1.erase(unique(val1.begin(), val1.end()), val1.end());
val2.erase(unique(val2.begin(), val2.end()), val2.end());
int m = val1.size() + val2.size();
for (int i = 1; i <= n; i++) {
used[i] = 0;
}
for (int i = 1; i <= m; i++) {
vis[i] = 0;
e[i].clear();
}
for (int i = 1; i <= n; i++) {
int u = lower_bound(val1.begin(), val1.end(), a[i] - i) - val1.begin() + 1;
int v = lower_bound(val2.begin(), val2.end(), a[i] + i) - val2.begin() + 1;
v += val1.size();
add(u, v, i);
}
for (int i = 1; i <= m; i++) if (!vis[i]) {
dfs(i, 0);
}
if (ans.size() == n / 2) {
printf("Yes\n");
for (auto p : ans) {
printf("%d %d\n", p.first, p.second);
}
} else {
printf("No\n");
}
}
return 0;
}
/*
6
14 22 33 11 25 36
*/
E. Color the Tree
给定一颗树与一个序列 \(\{a_0, a_1, \cdots, a_{n - 1}\}\),以 \(1\) 为根,初始全部为白色,每次可以将某个节点 \(u\) 的 \(i\) 级儿子全部染成黑色,代价为 \(a_i\),求将整棵树染成黑色的最小代价。
\(n \le 10^5, 0 \le a_i \le 10^9\)
考虑 DP。
设 \(f_{u, i}\) 表示将 \(u\) 子树内深度为 \(i\) 的儿子全部染为黑色的最小代价。
容易得出转移式子:
与深度有关的 DP 考虑长链剖分。那么考虑直接往上继承一个,然后合并。
对于被合并到的深度,可以直接取 \(\min\)。对于没有更新到的深度,我们发现它被取到的 \(\min\) 是 \(a_i\) 的一个区间,我们可以用另一个数组 \(g_{u, i}\) 维护区间的左端点,当被合并的时候再取一个区间 \(\min\) 即可。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
int T, n, a[MAXN][20];
vector<int> e[MAXN];
void init() {
for (int j = 1; j <= 18; j++) {
for (int i = 0; i + (1 << j) - 1 < n; i++) {
a[i][j] = min(a[i][j - 1], a[i + (1 << (j - 1))][j - 1]);
}
}
}
long long query(int l, int r) {
if (l > r) return LLONG_MAX;
int len = __lg(r - l + 1);
return min(a[l][len], a[r - (1 << len) + 1][len]);
}
int dep[MAXN], son[MAXN];
long long buc[MAXN << 1], *top = &buc[0];
long long *f[MAXN], *g[MAXN];
void dfs1(int u, int pre) {
dep[u] = 0;
son[u] = 0;
for (int v : e[u]) if (v != pre) {
dfs1(v, u);
dep[u] = max(dep[u], dep[v]);
if (dep[v] > dep[son[u]]) son[u] = v;
}
dep[u]++;
}
void dfs2(int u, int pre, bool t) {
if (t)
f[u] = top, top += dep[u], g[u] = top, top += dep[u],
memset(f[u], 0, dep[u] * sizeof(long long)),
memset(g[u], 0, dep[u] * sizeof(long long));
else
f[u] = f[pre] + 1, g[u] = g[pre] + 1;
if (son[u]) dfs2(son[u], u, false);
for (int v : e[u]) if (v != pre && v != son[u]) {
dfs2(v, u, true);
}
}
void dfs3(int u, int pre) {
if (son[u]) dfs3(son[u], u);
int odep = 0;
for (int v : e[u]) if (v != pre && v != son[u]) {
dfs3(v, u);
for (int i = 0; i < dep[v]; i++) {
if (g[u][i + 1] != i + 1) {
f[u][i + 1] = min(f[u][i + 1], query(g[u][i + 1], i + 1));
g[u][i + 1] = i + 1;
}
f[u][i + 1] += min(f[v][i], query(g[v][i], i));
}
}
f[u][0] = a[0][0];
}
int main() {
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &a[i][0]);
e[i + 1].clear();
}
init();
top = buc;
for (int i = 1; i < n; i++) {
int u, v; scanf("%d%d", &u, &v);
e[u].push_back(v);
e[v].push_back(u);
}
dfs1(1, 0);
dfs2(1, 0, 1);
dfs3(1, 0);
long long ans = 0;
for (int i = 0; i < dep[1]; i++) {
ans += min(f[1][i], query(g[1][i], i));
}
printf("%lld\n", ans);
}
return 0;
}
/*
3
4
10 15 40 1
1 2
2 3
2 4
*/
C. Fabulous Fungus Frenzy
给你两个 \(n \times m\) 的矩阵 \(A, B\) 与 \(k\) 个任意大小矩阵 \(C_i\),你需要进行以下操作若干次,使得 \(A\) 变成 \(B\):
- 交换两个相邻元素
- 顺时针旋转四个相邻元素
- 将某个 \(C_i\) 矩阵覆盖到任意位置上
构造一种方案,或输出无解。要求总操作次数 \(\le 4 \times 10^5\),操作三不超过 \(400\) 次。
\(n, m, k \le 20\)
考虑将这个操作反过来做。前两个操作都是容易反过来做的,而第三个操作可以看做将一个 \(C_i\) 矩阵抠出来,这时候原来的字符就可以是任意的,我们可以用通配符来表示。
而这样我们只需要一直替换出通配符,直到替换不了为止。这样我们剩下了若干剩下的字符和若干通配符,看看能不能交换得到原来的矩阵即可。
代码感觉很难写,咕了。
H. Factories Once More
给你一颗树,树有边权,树上放 \(k\) 个棋子,每个点上只能放一个棋子,最大化每两个棋子的距离之和。
\(k \le n \le 10^5\)
设 \(f_{u, i}\) 表示 \(u\) 子树内的权值和。那么转移有:
后者是一个凸函数,众所周知两个凸函数的 \(\max\) 卷积还是凸函数,做闵可夫斯基和即可。
可以拿平衡树维护差分数组,支持区间加等差数列(\(w_{u, v} j (k-j)\) 的差分为等差数列),插入一个数,然后树上启发式合并,复杂度 \(O(n \log^2 n)\)。
不知道为啥题解说是 \(O(n \log n)\),splay 在树上启发式合并的问题有特殊复杂度吗?
我超还真有,牛逼。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
int n, k;
vector<pair<int, int>> e[MAXN];
mt19937 Rand(chrono::system_clock::now().time_since_epoch().count());
struct Treap {
int lc[MAXN], rc[MAXN], rnd[MAXN];
long long val[MAXN], k[MAXN], b[MAXN];
int siz[MAXN];
int tot;
stack<int> s;
int newNode(long long v) {
int p = s.empty() ? ++tot : s.top();
if (!s.empty()) s.pop();
k[p] = b[p] = lc[p] = rc[p] = 0;
val[p] = v, siz[p] = 1, rnd[p] = Rand();
return p;
}
void pushUp(int p) {
siz[p] = siz[lc[p]] + siz[rc[p]] + 1;
}
void tag(int p, long long K, long long B) {
val[p] += K * (siz[lc[p]] + 1) + B;
k[p] += K, b[p] += B;
}
void pushDown(int p) {
if (k[p] || b[p]) {
if (lc[p]) tag(lc[p], k[p], b[p]);
if (rc[p]) tag(rc[p], k[p], k[p] * (siz[lc[p]] + 1) + b[p]);
k[p] = b[p] = 0;
}
}
void split(long long v, int p, int &x, int &y) {
if (!p) x = y = 0;
else {
pushDown(p);
if (v > val[p]) {
y = p;
split(v, lc[p], x, lc[p]);
} else {
x = p;
split(v, rc[p], rc[p], y);
}
pushUp(p);
}
}
int merge(int x, int y) {
if (!x || !y) return x + y;
pushDown(x), pushDown(y);
if (rnd[x] < rnd[y]) {
rc[x] = merge(rc[x], y);
pushUp(x);
return x;
} else {
lc[y] = merge(x, lc[y]);
pushUp(y);
return y;
}
}
void flatten(int p, vector<long long> &v) {
s.push(p);
pushDown(p);
if (lc[p]) flatten(lc[p], v);
v.push_back(val[p]);
if (rc[p]) flatten(rc[p], v);
}
void insert(int &p, long long v) {
int x, y; split(v, p, x, y);
p = merge(merge(x, newNode(v)), y);
}
} t;
int root[MAXN];
int siz[MAXN];
void dfs(int u, int pre) {
siz[u] = 1;
int s = 0;
for (auto p : e[u]) if (p.first != pre) {
int v = p.first, w = p.second;
dfs(v, u);
t.tag(root[v], -2ll * w, 1ll * w * (k + 1));
siz[u] += siz[v];
if (siz[v] > siz[s]) s = v;
}
if (s) root[u] = root[s];
t.insert(root[u], 0);
for (auto p : e[u]) if (p.first != pre && p.first != s) {
int v = p.first;
vector<long long> val;
t.flatten(root[v], val);
for (long long w : val) {
t.insert(root[u], w);
}
}
assert(siz[u] == t.siz[root[u]]);
}
int main() {
// freopen("H.in", "r", stdin);
scanf("%d%d", &n, &k);
for (int i = 1; i < n; i++) {
int u, v, w; scanf("%d%d%d", &u, &v, &w);
e[u].push_back({v, w});
e[v].push_back({u, w});
}
int rt = 1;
dfs(rt, 0);
long long ans = 0;
vector<long long> val;
t.flatten(root[rt], val);
for (int i = 1; i <= k; i++) {
ans += val[i - 1];
}
printf("%lld\n", ans);
// printf("tot = %d\n", t.tot);
return 0;
}
/*
6 3
1 2 3
2 3 2
2 4 1
1 5 2
5 6 3
*/
K. NaN in a Heap
定义
NaN
与任意一个数的比较结果均为false
(见下方代码)。求有多少种 \(\{1, 2, 3, \cdots, n - 1, \text{NaN}\}\) 的排列,能够由以下构建小根堆的代码得到:for (int i = 1; i <= n; i++) { for (int j = i; j != 1; j /= 2) { if (a[j / 2] > a[j]) { // 若 a[j / 2] 或 a[j] 为 NaN,该语句为 false swap(a[j / 2], a[j]); } else { break; } } }
多测,\(T \le 1000, n \le 10^9\)。
先考虑没有 NaN 怎么做。很容易想到令 \(f_u\) 为 \(u\) 子树内的答案,那么有 \(f_u = \binom{siz_u - 1}{siz_{lc}} f_{lc} f_{rc}\)。
考虑把组合数拆成阶乘,然后全部展开,发现很多阶乘都能消除掉。可以得出,最后的答案为 \(\frac{n!}{\prod siz_i}\)。
考虑现在的 NaN。发现 NaN 的实质就是它不会动,然后把原树拆成了三棵树。那么假如三棵树的大小分别为 \(a, b, c\),那么答案大概长成 \(\binom{n - 1}{a, b, c} \frac{a!}{\prod siz_i} \frac{b!}{\prod siz_i} \frac{c!}{\prod siz_i}=\frac{(n - 1)!}{\prod siz_i \prod siz_i \prod siz_i}\) 的形式。考虑到最后求概率,要除以 \(n!\),也就是我们只需要考虑 \(\frac{1}{{\prod siz_i \prod siz_i \prod siz_i}}\) 的和即可。
朴素想法为直接枚举 NaN 的位置然后进行计算,显然复杂度很劣。我们观察堆的性质。发现大部分子树都是满二叉树。
考虑第 \(n\) 个节点的位置。发现除了从根到第 \(n\) 个点的这条链上的点,其它的子树都是满二叉树。那么我们可以把这条链拉出来,然后计算出链上每个点的节点数。
我们可以先提前预处理出来原树的 \(\prod \frac{1}{siz_i}\)。发现,对于 NaN 所在的节点,它的 \(siz_i\) 要删掉,其子树内的 \(siz_i\) 不变,而只有这个节点到根的 \(siz_i\) 全部减少了当前节点的大小。
同时,在一颗满二叉树中,同一层的节点的答案是相等的,我们可以合在一起来算。这样,我们就只有 \(\log^2 n\) 个节点的答案需要计算,直接计算即可。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 35, P = 1000000007;
int qpow(int a, int b) {
int ans = 1;
while (b) {
if (b & 1) ans = 1ll * ans * a % P;
a = 1ll * a * a % P;
b >>= 1;
}
return ans;
}
int T, n;
int a[MAXN], siz[MAXN];
int main() {
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
int m = __lg(n);
for (int i = 0; i < m; i++) {
a[i + 1] = i + ((n >> i) & 1);
}
siz[0] = 1;
for (int i = 1; i <= m; i++) {
siz[i] = siz[i - 1] + (1 << a[i]);
}
int prod = 1;
for (int i = 1; i <= m; i++) {
prod = 1ll * prod * qpow(siz[i], P - 2) % P;
for (int j = 1; j <= a[i]; j++) {
prod = 1ll * prod * qpow(qpow((1 << j) - 1, P - 2), 1 << (a[i] - j)) % P;
}
}
int ans = 0;
for (int i = 0; i <= m; i++) {
{ // select root
int tmp = prod;
tmp = 1ll * tmp * siz[i] % P;
for (int j = m; j > i; j--) {
tmp = 1ll * tmp * siz[j] % P;
tmp = 1ll * tmp * qpow(siz[j] - siz[i], P - 2) % P;
}
ans = (ans + tmp) % P;
}
for (int j = 1; j <= a[i]; j++) {
int tmp = prod;
tmp = 1ll * tmp * ((1 << j) - 1) % P;
for (int k = a[i]; k > j; k--) {
tmp = 1ll * tmp * ((1 << k) - 1) % P;
tmp = 1ll * tmp * qpow((1 << k) - (1 << j), P - 2) % P;
}
for (int k = m; k >= i; k--) {
tmp = 1ll * tmp * siz[k] % P;
tmp = 1ll * tmp * qpow(siz[k] - ((1 << j) - 1), P - 2) % P;
}
ans = (ans + 1ll * tmp * (1 << (a[i] - j))) % P;
}
}
ans = 1ll * ans * qpow(n, P - 2) % P;
// for (int i = 1; i <= n; i++) {
// ans = 1ll * ans * i % P;
// }
printf("%d\n", ans);
}
return 0;
}
L. Proposition Composition
给你一个 \(n\) 个点的无向图, \(i\) 与 \(i + 1\) 有连边。有 \(m\) 条额外边,每次加入一条边,问有多少种选择 \(e, f\) 两条边的方案,使得将图中的 \(e, f\) 两条边删去后,图不联通。选择的两条边之间没有顺序。加入的边不会被删除,也就是说会对之后的询问造成影响。
\(n, m \le 2.5 \times 10^5\)
我们将额外边看做对一个区间进行覆盖。
考虑选择的边有以下几种情况:
- 选择了一条链边:
- 如果这条链边没有被任何额外边覆盖过,那么删去它一定会使图不联通,此时第二条边可以任意选择。
- 如果这条链边被一条额外边覆盖过,那么删除这条边与覆盖它的额外边能够使图不连通。
- 选择了两条链边:
- 容易发现,如果删除的两条链边被覆盖的情况相同(被同样的额外边覆盖过),那么删去这两条边能够使图不连通。
前两种情况用线段树很容易维护,考虑后者。
我们考虑将覆盖情况相同的边用链表链到一起,那么答案就是每个链表中选选两条边的方案数之和。
新加入一条额外边后,发现实际上就是将若干个链表分裂开了。而容易发现,分裂的总次数最多 \(O(n)\) 次,所以我们每加入一条边时,可以直接找出需要分裂的所有的链表进行分裂。
查找需要分裂的链表实际上就是找 \(pre_i < L\) 和 \(nxt_i > R\) 的点 \(i\),于是我们可以用线段树维护 \(pre, nxt\) 的最大值最小值,然后每次在线段树区间上把所有的点全找出来即可。
分裂链表需要计算新的两个链边的大小,可以使用启发式分裂,然后维护出链表的大小即可。具体做法可以直接看代码实现。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 800005;
const int the_answer_to_life_the_universe_and_everything = 42;
int T, n, m;
int pre[MAXN], nxt[MAXN], siz[MAXN], root[MAXN], bl[MAXN], tot;
long long cnt;
struct SegmentTree {
struct Node {
int mn, mx;
int cnt[2], tag;
} t[MAXN << 2];
#define lc (i << 1)
#define rc (i << 1 | 1)
void tag(int i, int v) {
if (v == 1) {
t[i].cnt[1] = t[i].cnt[0], t[i].cnt[0] = 0;
} else {
t[i].cnt[0] = t[i].cnt[1] = 0;
}
t[i].tag += v;
}
void pushDown(int i) {
if (t[i].tag) {
tag(lc, t[i].tag);
tag(rc, t[i].tag);
t[i].tag = 0;
}
}
void pushUp(int i) {
t[i].mn = min(t[lc].mn, t[rc].mn);
t[i].mx = max(t[lc].mx, t[rc].mx);
t[i].cnt[0] = t[lc].cnt[0] + t[rc].cnt[0];
t[i].cnt[1] = t[lc].cnt[1] + t[rc].cnt[1];
}
void build(int i = 1, int l = 1, int r = n - 1) {
t[i].tag = 0;
if (l == r) {
t[i].cnt[0] = 1, t[i].cnt[1] = 0;
t[i].mn = pre[l], t[i].mx = nxt[l];
return;
}
int mid = (l + r) >> 1;
build(lc, l, mid);
build(rc, mid + 1, r);
pushUp(i);
}
void add(int a, int b, int i = 1, int l = 1, int r = n - 1) {
if (a <= l && r <= b) {
tag(i, 1);
return;
}
pushDown(i);
int mid = (l + r) >> 1;
if (a <= mid) add(a, b, lc, l, mid);
if (b > mid) add(a, b, rc, mid + 1, r);
pushUp(i);
}
void findL(int a, int b, int v, vector<pair<int, int>> &p, int i = 1, int l = 1, int r = n - 1) {
if (t[i].mn >= v) return;
if (l == r) {
p.push_back({ bl[l], l });
return;
}
int mid = (l + r) >> 1;
if (a <= mid) findL(a, b, v, p, lc, l, mid);
if (b > mid) findL(a, b, v, p, rc, mid + 1, r);
}
void findR(int a, int b, int v, vector<pair<int, int>> &p, int i = 1, int l = 1, int r = n - 1) {
if (t[i].mx <= v) return;
if (l == r) {
p.push_back({ bl[nxt[l]], nxt[l] });
return;
}
int mid = (l + r) >> 1;
if (a <= mid) findR(a, b, v, p, lc, l, mid);
if (b > mid) findR(a, b, v, p, rc, mid + 1, r);
}
void setPre(int d, int v, int i = 1, int l = 1, int r = n - 1) {
if (l == r) {
pre[l] = t[i].mn = v;
return;
}
pushDown(i);
int mid = (l + r) >> 1;
if (d <= mid) setPre(d, v, lc, l, mid);
else setPre(d, v, rc, mid + 1, r);
pushUp(i);
}
void setNxt(int d, int v, int i = 1, int l = 1, int r = n - 1) {
if (l == r) {
nxt[l] = t[i].mx = v;
return;
}
pushDown(i);
int mid = (l + r) >> 1;
if (d <= mid) setNxt(d, v, lc, l, mid);
else setNxt(d, v, rc, mid + 1, r);
pushUp(i);
}
} st;
long long calcAns(int m) {
long long ans = 0;
int zero = st.t[1].cnt[0], one = st.t[1].cnt[1];
// case 1: zero - m and zero - nonzero
ans += 1ll * zero * (n - 1 + m - zero);
// case 2: one - m
ans += one;
// case 3: nonzero - nonzero
ans += cnt;
return ans;
}
void connect(int x, int y) {
st.setPre(y, x);
st.setNxt(x, y);
}
void disconnect(int x) {
int y = pre[x];
st.setPre(x, x);
st.setNxt(y, y);
}
void newList(vector<int> &a) {
++tot;
root[tot] = a[0];
siz[tot] = a.size();
for (int i : a) bl[i] = tot;
}
long long C(int x) {
return 1ll * x * (x - 1) / 2;
}
// 启发式分裂
void cut(int p, int l) {
cnt -= C(siz[p]);
vector<int> v1, v2;
int p1 = root[p], p2 = l;
while (the_answer_to_life_the_universe_and_everything == 42) {
v1.push_back(p1), v2.push_back(p2);
int np1 = nxt[p1] == l ? p1 : nxt[p1];
int np2 = nxt[p2];
if (np1 == p1) {
newList(v1);
root[p] = l;
siz[p] -= v1.size();
break;
} else if (np2 == p2) {
newList(v2);
siz[p] -= v2.size();
break;
} else {
p1 = np1;
p2 = np2;
}
}
disconnect(l);
cnt += C(siz[tot]) + C(siz[p]);
}
// 启发式分裂
void cut(int p, int l, int r) {
cnt -= C(siz[p]);
vector<int> v1, v2;
int p1 = root[p], p2 = l;
while (the_answer_to_life_the_universe_and_everything == 42) {
v1.push_back(p1), v2.push_back(p2);
int np1 = nxt[p1] == l ? r : nxt[p1];
int np2 = nxt[p2] == r ? p2 : nxt[p2];
if (np1 == p1) {
newList(v1);
root[p] = l;
siz[p] -= v1.size();
break;
} else if (np2 == p2) {
newList(v2);
siz[p] -= v2.size();
break;
} else {
p1 = np1;
p2 = np2;
}
}
int tmp = pre[l];
disconnect(l), disconnect(r), connect(tmp, r);
cnt += C(siz[tot]) + C(siz[p]);
}
int main() {
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
if (n == 1) {
for (int i = 1; i <= m; i++) {
scanf("%*d%*d");
printf("0\n");
}
continue;
}
tot = 1;
cnt = C(n - 1);
root[1] = 1;
nxt[n - 1] = n - 1, pre[1] = 1;
siz[1] = n - 1;
for (int i = 1; i <= n - 1; i++) {
bl[i] = 1;
}
for (int i = 1; i < n - 1; i++) {
nxt[i] = i + 1, pre[i + 1] = i;
}
st.build();
for (int i = 1; i <= m; i++) {
int u, v; scanf("%d%d", &u, &v);
if (u == v) {
printf("%lld\n", calcAns(i));
continue;
}
if (u > v) swap(u, v);
v--;
st.add(u, v);
vector<pair<int, int>> pt;
st.findL(u, v, u, pt);
st.findR(u, v, v, pt);
sort(pt.begin(), pt.end());
for (int i = 0; i < pt.size(); i++) {
if (i + 1 < pt.size() && pt[i].first == pt[i + 1].first) {
// 将链表中的 [l, r] 断开
cut(pt[i].first, pt[i].second, pt[i + 1].second);
i++;
} else {
// 将链表从 d 断开
cut(pt[i].first, pt[i].second);
}
}
printf("%lld\n", calcAns(i));
}
}
return 0;
}