题解:[ICPC 2022 Nanjing R] Proposition Composition
称初始链上的边 \((i,i+1)\) 为链边 \(i\),称一条新加入的边 \((u,v)(u\le v)\) 覆盖了链边 \(u\sim v-1\)。这 \(n-1\) 条链边是最重要的已知条件,所以考虑对删去的两条边中的链边分类讨论(设当总共有 \(tot\) 条边):
- 若删去的均不是链边,这些链边就会把 \(n\) 个点连通,所以这种情况不存在;
- 若删去的边中有一条链边(设为链边 \(i\)),而且这条链边没有覆盖过,那么只要删掉这条边就会得到 \([1,i],[i+1,n]\) 两个连通块,所以另外一条边可以任选。设没有被覆盖过的链边有 \(c_0\) 条,简单容斥去重可以得到这部分的方案数为 \(c_0(tot-1)-\binom{c_0}{2}\)。
- 若删去的边中只有一条链边且被覆盖了至少一次(设为链边 \(i\)),那么容易发现如果删去这条边和另一条非链边 \(e\) 后图不连通,一定也是分成 \([1,i],[i+1,n]\) 两个连通块。这说明 \(e\) 一定是唯一一条两端点分别在 \([1,i],[i+1,n]\) 的非链边,也即唯一一条覆盖链边 \(i\) 的非链边。这要求链边 \(i\) 恰好被覆盖一次,同时 \(e\) 被 \(i\) 唯一确定。所以设恰好被覆盖一次的链边有 \(c_1\) 条,这部分的方案数为 \(c_1\)。
- 若删去的两条边均为链边,设为链边 \(i,j(i<j)\),且它们都被覆盖至少一次,在只考虑剩下的链边的情况下图会分为三个连通快 \(C_1=[1,i],C_2=[i+1,j],C_3=[j+1,n]\)。由于链边 \(i\) 被覆盖过,所以 \(C_1\) 一定会向 \(C_2\) 或 \(C_3\) 连非链边,同理 \(C_3\) 一定会向 \(C_2\) 或 \(C_1\) 连非链边。可以看出,如果希望 \(C_1,C_2,C_3\) 不完全连通,只有可能是 \(C_1\) 与 \(C_3\) 连通,且都不与 \(C_2\) 连通。进一步分析,可以发现这等价于覆盖链边 \(i\) 的非链边集合与覆盖链边 \(j\) 的非链边集合相同。设有恰好被集合 \(S\) 内的非链边覆盖的链边集合为 \(T_S\),则这部分方案数即为 \(\sum_{S\neq \varnothing}\binom{|T_S|}{2}\)。由 \(|T_{\varnothing}|=c_0\),所以这部分方案数也可以改写为 \(\sum_S\binom{cnt_S}{2}-\binom{c_0}{2}\)。
\(c_0,c_1\) 可以用并查集维护,每个位置只会被覆盖两次之后就不会再有贡献,可以用并查集跳过。这部分复杂度可以做到 \(\mathcal O((n+m)\log n)\)(序列并查集实际上可以做到线性)。
对于删两条链边的情况,考虑维护所有 \(T_S\)。在加入一条边 \(e=(u,v)(u\le v)\) 时,原来的 \(T_S\) 会被分成 \(T^{\prime}_S\) 和 \(T^{\prime}_{S\cup\{e\}}\) 两个集合。\(e\) 恰好覆盖链边 \([u,v-1]\),所以有 \(T^{\prime}_{S\cup\{e\}}=T_S\cap[u,v-1],T^{\prime}_S=T_S\setminus T^{\prime}_{S\cup\{e\}}\)。
考虑将每个 \(T_S\) 中的链边从小到大用链表维护,对于每次加入 \(e\),第一步需要找到所有需要分裂的集合,也就是满足 \(T^{\prime}_S\) 和 \(T^{\prime}_{S\cup\{e\}}\) 都非空的 \(T_S\)。设链边 \(i\) 在它所在的链表中的前驱为 \(pre_i\),后继为 \(suf_i\),那么需要分裂的位置一定会满足 \(i\in [u,v-1],pre_i\notin [u,v-1]\) 或 \(i\notin [u,v-1],suf_i\in [u,v-1]\)。这部分可以用线段树维护 \(pre_i\) 的最小值和 \(suf_i\) 的最大值,然后线段树上二分实现。
找出所有这样 \(i\) 后,如果 \(T_S\) 中恰有一个这样的 \(i\),\(T_S\) 就会被分成左右两部分;如果 \(T_S\) 恰有两个这样的 \(i,j\),\(T_S\) 就会被分成 \(i,j\) 之间和 \(i,j\) 两侧这两部分;一个 \(T_S\) 不可能有三个及以上这样的位置。总分裂次数是 \(\mathcal O(n)\) 次(因为每次分裂会使链表中中边数减小),所以这样的 \(i\) 总共只有 \(\mathcal O(n)\) 个,线段树二分总复杂度为 \(\mathcal O((n+m)\log n)\)。而 \(pre\) 或 \(suf\) 会改变的位置也只有这些 \(i\) 和 \(pre_i,suf_i\),总量也是 \(\mathcal O(n)\),线段树修改总复杂度是 \(\mathcal O(n\log n)\)
为了维护答案,分裂时需要知道这两部分的大小,同时还需要维护每条链边所属的集合。一个可能的做法是平衡树,但考虑到这个题实际上维护的是将一个集合不断分裂,更具性价比的做法是启发式分裂。在将一个链表 \(T\) 分裂成两部分 \(U,V\) 时,同时遍历 \(U,V\),直到其中一个遍历完。这样就可以通过 \(O(\min(|U|,|V|))\) 的复杂度找到 \(U,V\) 中大小较小的那个集合,也就可以维护上面提到的信息。由启发式分裂的结论,这部分总复杂度为 \(\mathcal O(n\log n)\)。
总复杂度 \(\mathcal O((n+m)\log n)\)。
参考实现:
#include <bits/stdc++.h>
typedef long long LL;
typedef __int128 LLL;
typedef unsigned long long ULL;
typedef std::pair<int, int> pii;
typedef std::pair<int, LL> pil;
typedef std::pair<LL, LL> pll;
typedef std::pair<LL, int> pli;
typedef long double RN;
#define fi first
#define se second
#define MP std::make_pair
#define EB emplace_back
char buf[1 << 20], *p1, *p2;
#define getchar() ((p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 20, stdin), (p1 == p2))) ? 0 : (*p1++))
LL read()
{
LL s = 0; int f = 1, c = getchar();
for (; !isdigit(c); c = getchar()) f ^= (c == '-');
for (; isdigit(c); c = getchar()) s = s * 10 + (c ^ 48);
return f ? s : -s;
}
template<typename T>
void write(T x, char end = '\n')
{
if (x < 0) x = -x, putchar('-');
static int d[100], cur = 0;
do { d[++cur] = x % 10; } while (x /= 10);
while (cur) putchar(48 ^ d[cur--]);
putchar(end);
}
const int inf = 0x3f3f3f3f;
const LL INF = 0x3f3f3f3f3f3f3f3fll;
template<typename T> void Fmin(T &x, T y){ if (y < x) x = y; }
template<typename T> void Fmax(T &x, T y){ if (x < y) x = y; }
const int MAXN = 250005;
int n, m, cnt[MAXN];
int c0, c1;
LL tot;
struct UFDS
{
int fa[MAXN];
void init(int n){ std::iota(fa + 1, fa + n + 1, 1); }
int find(int x){ while (x != fa[x]) x = fa[x] = fa[fa[x]]; return x; }
void merge(int x, int y){ fa[find(x)] = find(y); }
} vis;
void cover(int l, int r)
{
for (int x = vis.find(l); x < r; x = vis.find(x + 1))
if (++cnt[x] == 1) c0--, c1++;
else c1--, vis.merge(x, x + 1);
}
struct Info
{
int pre, suf;
Info(){}
Info(int _p, int _s) : pre(_p), suf(_s){}
friend Info operator+(Info a, Info b)
{ return Info(std::min(a.pre, b.pre), std::max(a.suf, b.suf)); }
friend bool operator&&(Info a, Info b)
{ return a.pre <= b.pre && b.suf <= a.suf; }
} ;
pii pos[MAXN];
int cur;
namespace SGT
{
#define ls (x << 1)
#define rs (x << 1 | 1)
Info sgt[MAXN << 2];
int N;
void maintain(int x){ sgt[x] = sgt[ls] + sgt[rs]; }
void build(int n, Info a[])
{
for (N = 1; N <= n; N <<= 1);
for (int i = 1; i <= n; i++) sgt[i + N] = a[i];
for (int i = N - 1; i; i--) maintain(i);
}
void modify(int x, Info v)
{
sgt[x += N] = v;
for (x >>= 1; x; x >>= 1) maintain(x);
}
void putin(int x, Info lim)
{
if (sgt[x].pre < lim.pre) pos[++cur] = MP(x - N, 0);
if (sgt[x].suf > lim.suf) pos[++cur] = MP(x - N, 1);
}
void query(int x, Info lim)
{
if (x >= N) return putin(x, lim);
if (!(lim && sgt[ls])) query(ls, lim);
if (!(lim && sgt[rs])) query(rs, lim);
}
void query(int l, int r)
{
Info lim(l, r);
l += N, r += N;
if (!(lim && sgt[l])) putin(l, lim);
if (l == r) return ;
if (!(lim && sgt[r])) putin(r, lim);
for (; l ^ r ^ 1; l >>= 1, r >>= 1)
{
if (!(l & 1) && !(lim && sgt[l ^ 1])) query(l ^ 1, lim);
if ((r & 1) && !(lim && sgt[r ^ 1])) query(r ^ 1, lim);
}
}
#undef ls
#undef rs
}
int pre[MAXN], suf[MAXN], bel[MAXN], K, siz[MAXN];
void update(int x){ SGT::modify(x, Info(pre[x], suf[x])); }
LL comb(int x){ return (LL)x * (x - 1) / 2; }
void cut(int x)
{
int id = bel[x]; tot -= comb(siz[id]);
int y = suf[x];
suf[x] = -inf, update(x);
pre[y] = inf, update(y);
int tx = x, ty = y;
while (tx != inf && ty != -inf) tx = pre[tx], ty = suf[ty];
if (tx == inf) for (++K; x != inf; x = pre[x]) bel[x] = K, siz[K]++;
else for (++K; y != -inf; y = suf[y]) bel[y] = K, siz[K]++;
siz[id] -= siz[K], tot += comb(siz[id]) + comb(siz[K]);
}
void cut(int l, int r)
{
int id = bel[l]; tot -= comb(siz[id]);
int x = pre[l], y = suf[r];
pre[l] = inf, suf[r] = -inf;
update(l);
if (l != r) update(r);
suf[x] = y, pre[y] = x;
update(x), update(y);
int t1 = l, t2 = x;
while (t1 != -inf && t2 != inf) t1 = suf[t1], t2 = pre[t2];
for (t2 = y; t1 != -inf && t2 != -inf; t1 = suf[t1], t2 = suf[t2]);
if (t1 == -inf) for (++K; l != -inf; l = suf[l]) bel[l] = K, siz[K]++;
else
{
for (++K; x != inf; x = pre[x]) bel[x] = K, siz[K]++;
for (; y != -inf; y = suf[y]) bel[y] = K, siz[K]++;
}
siz[id] -= siz[K], tot += comb(siz[id]) + comb(siz[K]);
}
void split(int l, int r)
{
cur = 0, SGT::query(l, r - 1);
std::sort(pos + 1, pos + cur + 1, [](pii i, pii j){ return bel[i.fi] != bel[j.fi] ? bel[i.fi] < bel[j.fi] : i.se < j.se; });
for (int i = 1; i <= cur; i++)
if (i < cur && bel[pos[i].fi] == bel[pos[i + 1].fi])
cut(pos[i].fi, pos[i + 1].fi), i++;
else if (pos[i].se == 0) cut(pre[pos[i].fi]);
else cut(pos[i].fi);
}
void init()
{
n = read(), m = read();
c0 = n - 1, c1 = 0;
vis.init(n);
static Info tmp[MAXN];
K = 1, siz[1] = n - 1, tot = comb(n - 1);
for (int i = 1; i < n; i++)
{
pre[i] = (i == 1 ? inf : i - 1);
suf[i] = (i == n - 1 ? -inf : i + 1);
bel[i] = 1, cnt[i] = 0;
tmp[i] = Info(pre[i], suf[i]);
}
if (n > 1) SGT::build(n - 1, tmp);
}
int main()
{
for (int Tcnt = read(); Tcnt--; )
{
init();
for (int i = 1; i <= m; i++)
{
int l = read(), r = read();
if (l > r) std::swap(l, r);
if (l != r) cover(l, r), split(l, r);
write((LL)c0 * (n - 2 + i) + c1 + tot - (LL)c0 * (c0 - 1));
}
memset(siz + 1, 0, K << 2);
}
return 0;
}

浙公网安备 33010602011771号