「解题报告」The 1st Universal Cup. Stage 0: Nanjing

挑了些有意义的题来写的,可以自己尝试下。

大概按难度排序,过于简单和没啥意义的题(指计算几何)没写。

QOJ(有中文题面) Codeforces Gym

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\) 的儿子全部染为黑色的最小代价。

容易得出转移式子:

\[f_{u, i} = \min\{a_i, \sum_{v \in son(u)} f_{v, i - 1}\} \]

与深度有关的 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\)

  1. 交换两个相邻元素
  2. 顺时针旋转四个相邻元素
  3. 将某个 \(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\) 子树内的权值和。那么转移有:

\[f_{u, i} = \max_{j = 0}^i f'_{u, i - j} + f_{v, j} + w_{u, v} j (k-j) \]

后者是一个凸函数,众所周知两个凸函数的 \(\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\)

我们将额外边看做对一个区间进行覆盖。

考虑选择的边有以下几种情况:

  1. 选择了一条链边:
    • 如果这条链边没有被任何额外边覆盖过,那么删去它一定会使图不联通,此时第二条边可以任意选择。
    • 如果这条链边被一条额外边覆盖过,那么删除这条边与覆盖它的额外边能够使图不连通。
  2. 选择了两条链边:
    • 容易发现,如果删除的两条链边被覆盖的情况相同(被同样的额外边覆盖过),那么删去这两条边能够使图不连通。

前两种情况用线段树很容易维护,考虑后者。

我们考虑将覆盖情况相同的边用链表链到一起,那么答案就是每个链表中选选两条边的方案数之和。

新加入一条额外边后,发现实际上就是将若干个链表分裂开了。而容易发现,分裂的总次数最多 \(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;
}
posted @ 2023-02-19 11:48  APJifengc  阅读(390)  评论(0编辑  收藏  举报