P4396 [AHOI2013] 作业 莫队+值域分块
题意:给出一个静态区间和多个询问,询问分为下面两种:
在区间 $[l,r]$ 中有多少个 $i$,使得 $a_i∈[a,b]$;
在区间 $[l,r]$ 中有多少种 $a_i(i∈[l,r])$,使得 $ai∈[a,b]$
第二问其实就是第一问去重后的结果,统计时可以直接去重,只需要考虑第一问,给定区间数颜色,想到莫队,但是还有个值域的限制,我们可以想到前缀和相减,直接上个树状数组带 $\log$ ,复杂度是 $O(n \sqrt n\log n)$,考虑值域分块,即可做到 $O(1)$ 插入,$O(\sqrt n)$ 查询。块长取到 $\dfrac {n}{\sqrt m}$ 为最优,最终复杂度 $O(n\sqrt m+m\sqrt n)$
#include <bits/stdc++.h> using namespace std;const int maxn = 1e5 + 5 ;int n, m, block, lal = 1 , lar = 0 ;int a[maxn], belong[maxn], cnt[maxn];int ans1[maxn], ans2[maxn], val1[maxn], val2[maxn];struct node { int l, r, a, b, id; bool operator < (const node &b)const { return belong[l] != belong[b.l] ? l < b.l : r < b.r; } } q[maxn];inline void add (int x) { if (!cnt[x])val2[belong[x]]++; cnt[x]++; val1[belong[x]]++; }inline void del (int x) { cnt[x]--; if (!cnt[x])val2[belong[x]]--; val1[belong[x]]--; }inline int query1 (int a, int b) { int res = 0 ; if (belong[a] == belong[b]) { for (int i = a; i <= b; i++)res += cnt[i]; return res; } for (int i = a; i <= block * belong[a]; i++)res += cnt[i]; for (int i = belong[a] + 1 ; i < belong[b]; i++)res += val1[i]; for (int i = block * (belong[b] - 1 ) + 1 ; i <= b; i++)res += cnt[i]; return res; }inline int query2 (int a, int b) { int res = 0 ; if (belong[a] == belong[b]) { for (int i = a; i <= b; i++)res += cnt[i]>0 ; return res; } for (int i = a; i <= block * belong[a]; i++)res += cnt[i]>0 ; for (int i = belong[a] + 1 ; i < belong[b]; i++)res += val2[i]; for (int i = block * (belong[b] - 1 ) + 1 ; i <= b; i++)res += cnt[i]>0 ; return res; }inline void read (int &res) { int f = 1 ; res = 0 ; char ch = getchar (); while (!isdigit (ch)) {if (ch == '-' )f = -1 ; ch = getchar ();} while (isdigit (ch)) {res = res * 10 + (ch ^ 48 ); ch = getchar ();} res *= f; }int main () { read (n), read (m); block = 1.0 * n / sqrt (m); for (int i = 1 ; i <= n; i++) { read (a[i]); belong[i] = (i - 1 ) / block + 1 ; } for (int i = 1 ; i <= m; i++) { read (q[i].l), read (q[i].r), read (q[i].a), read (q[i].b); q[i].id = i; } sort (q + 1 , q + 1 + m); for (int i = 1 ; i <= m; i++) { int l = q[i].l, r = q[i].r; while (l < lal)lal--, add (a[lal]); while (l > lal)del (a[lal]), lal++; while (r < lar)del (a[lar]), lar--; while (r > lar)lar++, add (a[lar]); ans1[q[i].id] = query1 (q[i].a, q[i].b); ans2[q[i].id] = query2 (q[i].a, q[i].b); } for (int i = 1 ; i <= m; i++) { printf ("%d %d\n" , ans1[i], ans2[i]); } return 0 ; }
CF1620E Replace the Numbers 维护一个数列,这个数列初始为空。
对于这个数列,总共有 $q$ 次操作,每次操作分为如下两个种类:
1 x
,意为在数列末尾加一个数字
2 x y
,意为将数列中所有值为 $x$ 的数的值替换成 $y$
请在 $q$ 次操作后,输出这个数列。$x,y,z\le5\times 10^5$
同样的数字批量修改,考虑并查集。记录每个位置 $i$ 的数字是多少 $val[i]$,值为 $x$ 的位置有哪些(通过并查集,事实上只记录路径压缩后的根节点即可),碰到相同的数字直接拉在一起,否则初始化根节点为自身,修改的时候如果 $y$ 已经存在,只需要把值为 $x$ 的节点全都拉到值为 $y$ 的根节点下,如果 $y$ 不存在,把 $x$ 的所有信息继承给 $y$ 即可。
#include <bits/stdc++.h> using namespace std;const int maxn = 5e5 + 5 ;int n, q, a[maxn];int f[maxn], val[maxn], pos[maxn];int find (int x) { return x == f[x] ? x : f[x] = find (f[x]); }inline void read (int &res) { int f = 1 ; res = 0 ; char ch = getchar (); while (!isdigit (ch)) {if (ch == '-' )f = -1 ; ch = getchar ();} while (isdigit (ch)) {res = res * 10 + (ch ^ 48 ); ch = getchar ();} res *= f; }int main () { read (q); for (int i = 1 , op, x, y; i <= q; i++) { read (op), read (x); if (op == 1 ) { a[++n] = i, f[i] = i, val[i] = x; if (pos[x])f[i] = pos[x]; else pos[x] = i; } else { read (y); if (x != y && pos[x]) { if (pos[y]) { f[pos[x]] = pos[y]; pos[x] = 0 ; } else { pos[y] = pos[x]; val[pos[x]] = y; pos[x] = 0 ; } } } } for (int i = 1 ; i <= n; i++)printf ("%d " , val[find (a[i])]); return 0 ; }
P3302 [SDOI2013] 森林
查询 $x$ 到 $y$ 路径第 $K$ 小
在 $x$ 与 $y$ 中间连一条边
树上主席树+启发式合并
#include <bits/stdc++.h> using namespace std;const int maxn = 8e4 + 5 ;int testcase;int n, m, T, lastans, nn, tot;int lg[maxn], w[maxn], a[maxn], core[maxn], root[maxn];int head[maxn], to[maxn << 1 ], nxt[maxn << 1 ], cnt;inline void add (int u, int v) { nxt[++cnt] = head[u]; to[cnt] = v; head[u] = cnt; }int f[maxn][20 ], dep[maxn], size[maxn];struct node { int lc, rc, v;#define ls(x) st[(x)].lc #define rs(x) st[(x)].rc #define val(x) st[(x)].v } st[maxn * 400 ];void modify (int pre, int &rt, int l, int r, int wal) { rt = ++tot; st[rt] = st[pre], val (rt)++; if (l == r)return ; int mid = (l + r) >> 1 ; if (wal <= mid)modify (ls (pre), ls (rt), l, mid, wal); else modify (rs (pre), rs (rt), mid + 1 , r, wal); }void dfs (int u, int fa, int rt) { f[u][0 ] = fa, dep[u] = dep[fa] + 1 ; size[rt]++, core[u] = rt; modify (root[fa], root[u], 1 , nn, w[u]); for (int i = 1 ; i < 20 ; i++) { f[u][i] = f[f[u][i - 1 ]][i - 1 ]; } for (int i = head[u]; i; i = nxt[i]) { int v = to[i]; if (v == fa)continue ; dfs (v, u, rt); } }inline int LCA (int x, int y) { if (dep[x] < dep[y])swap (x, y); while (dep[x] > dep[y])x = f[x][lg[dep[x] - dep[y]]]; if (x == y)return x; for (int i = 19 ; ~i; i--) { if (f[x][i] != f[y][i])x = f[x][i], y = f[y][i]; } return f[x][0 ]; }int query (int x, int y, int z, int fz, int l, int r, int wal) { if (l == r)return l; int sum = val (ls (x)) + val (ls (y)) - val (ls (z)) - val (ls (fz)); int mid = (l + r) >> 1 ; if (sum >= wal)return query (ls (x), ls (y), ls (z), ls (fz), l, mid, wal); else return query (rs (x), rs (y), rs (z), rs (fz), mid + 1 , r, wal - sum); }inline void read (int &res) { int f = 1 ; res = 0 ; char ch = getchar (); while (!isdigit (ch)) {if (ch == '-' )f = -1 ; ch = getchar ();} while (isdigit (ch)) {res = res * 10 + (ch ^ 48 ); ch = getchar ();} res *= f; }int main () { read (testcase); read (n), read (m), read (T); for (int i = 2 ; i < maxn; i++)lg[i] = lg[i >> 1 ] + 1 ; for (int i = 1 ; i <= n; i++)read (w[i]), a[i] = w[i], core[i] = i; sort (a + 1 , a + 1 + n); nn = unique (a + 1 , a + 1 + n) - a - 1 ; for (int i = 1 ; i <= n; i++) { w[i] = lower_bound (a + 1 , a + 1 + nn, w[i]) - a; } for (int i = 1 , u, v; i <= m; i++) { read (u), read (v); add (u, v); add (v, u); } for (int i = 1 ; i <= n; i++) { if (core[i] == i)dfs (i, 0 , i); } for (int i = 1 , x, y, k; i <= T; i++) { string op; cin >> op; read (x), read (y), x ^= lastans, y ^= lastans; if (op[0 ] == 'Q' ) { read (k), k ^= lastans; int z = LCA (x, y); lastans = a[query (root[x], root[y], root[z], root[f[z][0 ]], 1 , nn, k)]; printf ("%d\n" , lastans); } else { add (x, y), add (y, x); int cox = core[x], coy = core[y]; if (size[cox] < size[coy])swap (x, y); dfs (y, x, cox); } } return 0 ; }
P5356 [Ynoi2017] 由乃打扑克 给你一个长为 $n$ 的序列 $a$,需要支持 $m$ 次操作,操作有两种:
查询区间 $[l,r]$ 的第 $k$ 小值。
区间 $[l,r]$ 加上 $k$。
动态区间 $k$ 小值,发现要支持区间修改操作。可惜树套树打不了标记。
考虑分块,对于每个块块内排序,区间加直接打个标记,散块暴力加,查询的时候二分一个值,全部小于等于二分值的块直接加进去统计数量(块内单调,比较端点),否则在块内 upper_bound
,散块依然暴力统计,最后都加起来和 $k$ 比较即可。
块长 $\sqrt n$ ,复杂度 $O(n\sqrt {n} \log n\log V)$
据说被卡掉了?交了一下发现没有。
在卡了一年之后发现 hack #1 一直过不去。怎么回事呢?打开评论区一看原来需要开 long long
,感觉有点脑溢血了。
看着还行,实则调了很久。
#include <bits/stdc++.h> using namespace std;const int maxn = 1e5 + 5 ;#define int long long int n, m, block, times = 1 ;int a[maxn], belong[maxn], tag[maxn];struct node { int v, id; bool operator < (const node &a)const { return v < a.v; } } b[maxn];inline int bl (int B) { return (B - 1 ) * block + 1 ; }inline int br (int B) { return B * block; }inline int Upper_bound (int l, int r, int val) { int L = l, R = r; while (L < R) { int mid = (L + R) >> 1 ; if (b[mid].v <= val)L = mid + 1 ; else R = mid; } return L; }inline int query (int l, int r, int k) { if (r - l + 1 < k)return -1 ; if (belong[l] == belong[r]) { int c[r - l + 2 ]; for (int i = l; i <= r; i++) { c[i - l + 1 ] = a[i]; } nth_element (c + 1 , c + k, c + 1 + r - l + 1 ); return c[k] + tag[belong[l]]; } int L = -2e4 * times, R = 2e4 * times; while (L < R) { int mid = (L + R) >> 1 , cnt = 0 ; for (int i = belong[l] + 1 ; i < belong[r]; i++) { if (b[br (i)].v + tag[i] <= mid)cnt += block; else if (b[bl (i)].v + tag[i] <= mid) { cnt += Upper_bound (bl (i), br (i), mid - tag[i]) - bl (i); } } for (int i = l; i <= br (belong[l]); i++) { if (a[i] + tag[belong[l]] <= mid)cnt++; } for (int i = bl (belong[r]); i <= r; i++) { if (a[i] + tag[belong[r]] <= mid)cnt++; } if (cnt >= k)R = mid; else L = mid + 1 ; } return L; }inline void update (int l, int r, int k) { for (int i = bl (belong[l]); i <= min (n, br (belong[l])); i++) { if (b[i].id >= l && b[i].id <= r) { b[i].v += k, a[b[i].id] += k; } } sort (b + bl (belong[l]), b + 1 + min (br (belong[l]), n)); }inline void modify (int l, int r, int k) { if (belong[l] == belong[r]) { update (l, r, k); return ; } for (int i = belong[l] + 1 ; i < belong[r]; i++)tag[i] += k; update (l, br (belong[l]), k), update (bl (belong[r]), r, k); }inline void read (int &res) { int f = 1 ; res = 0 ; char ch = getchar (); while (!isdigit (ch)) {if (ch == '-' )f = -1 ; ch = getchar ();} while (isdigit (ch)) {res = res * 10 + (ch ^ 48 ); ch = getchar ();} res *= f; }signed main () { read (n), read (m); block = sqrt (n); for (int i = 1 ; i <= n; i++) { read (a[i]), b[i].v = a[i], b[i].id = i; belong[i] = (i - 1 ) / block + 1 ; } for (int i = 1 ; i <= n; i += block) { sort (b + bl (belong[i]) , b + min (br (belong[i]), n) + 1 ); } sort (b + br (n / block) + 1 , b + 1 + n); for (int i = 1 , op, l, r, k; i <= m; i++) { read (op), read (l), read (r), read (k); if (op == 1 ) { printf ("%d\n" , query (l, r, k)); } else { times++; modify (l, r, k); } } return 0 ; }
然后发现题解有 $O(n\sqrt{n}\log V)$ 的,块长取 $S=\sqrt n\log n$,每次重构的时候,考虑到加的部分是有序的,不加的部分也是有序的,实际上不需要排序,直接归并即可把重构复杂度从 $O(S\log S)$ 变成 $O(S)$,这样单次修改是 $O(\sqrt n\log n)$,查询的时候块数是 $O(n/S)$,也就是 $O(\dfrac{\sqrt n}{\log n})$,查询复杂度 $O(\sqrt n\log V)$
P7962 [NOIP2021] 方差 补补当年的遗憾。
给定长度为 $n$ 的非严格递增正整数数列 $$1 \le a_1 \le a_2 \le \cdots \le a_n $$,每次可以任意选择一个 $a_i$ 变为 $$a_{i - 1} + a_{i + 1} - a_i$$。求在若干次操作之后,该数列的方差最小值是多少。请输出最小值乘以 $n^2$ 的结果。
首先拆一拆方差式子
$$ D = \frac{ \sum_\limits{i = 1}^{n} {(a_i - \bar a)}^2}{n}=\dfrac{\sum_\limits{i = 1}^{n}(a_i^2-2a_i\bar a+\bar a^2)}{n}=\dfrac{\sum_\limits{i = 1}^{n}a_i^2-2\bar a\sum\limits _{i=1}^{n}a_i+n\bar a^2}{n} $$
$$ =\dfrac{\sum_\limits{i = 1}^{n}a_i^2-2n\bar a^2+n\bar a^2}{n}=\dfrac{\sum_\limits{i = 1}^{n}a_i^2-n\bar a^2}{n} $$
也就是均值的平方减去平方的均值,最后只需要求
$$ n\sum_\limits{i = 1}^{n}a_i^2-\sum\limits_{i=1}^{n}a_i $$ 观察题目中的操作,可以发现对于 $a_i$ 的操作,事实上等价于对差分数组 $d_i$ 和 $d_{i+1}$ 进行交换。多轮操作后其实就相当于差分数组除了第一位剩下的可以随便重排。
之后有一个性质,需要尽量保证差分数组呈现单谷。证明考虑微扰。假设现在满足单谷,对于谷的右侧或者左侧,交换相邻两个,会使得当前位置 $a_i$ 变大,而前面和后面都不变,也就是从 $a_i$ 开始,后面的数字统一上升了一层,导致差距变大,方差变大。
然后做到这可以随机化,可以暴搜枚举每一个数字放在什么位置,似乎大几十分就到手了。(好像真有人直接搜满了,有点那啥了)
考虑 DP,先对差分数组排序,然后一个一个插入。
回到答案的式子
$$ n\sum_\limits{i = 1}^{n}a_i^2-\sum\limits_{i=1}^{n}a_i $$ 设 $f[i][j]$ 表示考虑了前 $i$ 个位置的 $d$ ,$\sum a_i=j$ 时,$\sum a_i^2$ 的最小值。
当插入右边的时候:记 $t=\sum d_i$,则 $a[i]=t$
$f[i][j]=min(f[i-1][j],f[i-1][j-a[i]]+a[i]^2)$
当插入左边的时候:后面所有位置都会加上 $d_i$ ,也就是总和加上了 $i\times d_i$ ,平方和加上了
$$ \sum(x_k+d_i)^2-\sum x_k^2=\sum2d_ix_k+\sum d_i^2\=2d_i\sum x_k+i\times d_i^2=2d_i(j-i\times d_i)+i\times d_i^2 $$
$f[i][j]=min(f[i-1][j],f[i-1][j-i\times d_i]+2d_i(j-i\times d_i)+i\times d_i^2)$
显然可以滚动数组,复杂度 $O(n^2V)$
然后发现 #23-#25 $n$ 大的离谱,究竟是怎么一回事呢?然后发现 $a_i$ 小的奇怪,这也就意味着 $a$ 数组有大量重复,差分是 0,那我们直接无脑往中间放就好了,因为题目保证数组单调不减。
#include <bits/stdc++.h> using namespace std;const int maxn = 1e4 + 5 ;int n, m, now, t, sum, a[maxn], d[maxn];using i64 = long long ;constexpr i64 INF = 0x3f3f3f3f3f3f3f3f ; i64 f[2 ][maxn * 600 ], ans = INF;inline void read (int &res) { int f = 1 ; res = 0 ; char ch = getchar (); while (!isdigit (ch)) {if (ch == '-' )f = -1 ; ch = getchar ();} while (isdigit (ch)) {res = res * 10 + (ch ^ 48 ); ch = getchar ();} res *= f; }int main () { read (n), m = n - 1 ; for (int i = 1 ; i <= n; i++) { read (a[i]); } sum = n * a[n]; for (int i = 1 ; i <= m; i++)d[i] = a[i + 1 ] - a[i]; for (int i = 1 ; i <= n; i++)sum += i * d[i]; sort (d + 1 , d + 1 + m); memset (f, 0x3f , sizeof (f)); f[0 ][0 ] = 0 ; for (int i = 1 ; i <= m; i++) { if (d[i] == 0 )continue ; now ^= 1 , t += d[i]; for (int j = 0 ; j <= sum; j++) { f[now][j] = INF; if (j - t >= 0 ) f[now][j] = min (f[now][j], f[now ^ 1 ][j - t] + (i64)t * t); if (j - i * d[i] >= 0 ) f[now][j] = min (f[now][j], f[now ^ 1 ][j - i * d[i]] + (i64)2 * d[i] * (j - i * d[i]) + (i64)i * d[i] * d[i]); } } for (int i = 0 ; i <= sum; i++) { if (f[now][i] != INF) { ans = min (ans, n * f[now][i] - (i64)i * i); } } printf ("%lld\n" , ans); return 0 ; }
P7961 [NOIP2021] 数列 给定整数 $n, m, k$,和一个长度为 $m + 1$ 的正整数数组 $v_0, v_1, \ldots, v_m$。
对于一个长度为 $n$,下标从 $1$ 开始且每个元素均不超过 $m$ 的非负整数序列 $${a_i}$$ ,我们定义它的权值为 $v_{a_1} \times v_{a_2} \times \cdots \times v_{a_n}$。
当这样的序列 ${a_i}$ 满足整数 $S = 2^{a_1} + 2^{a_2} + \cdots + 2^{a_n}$ 的二进制表示中 $1$ 的个数不超过 $k$ 时,我们认为 ${a_i}$ 是一个合法序列。
计算所有合法序列 ${a_i}$ 的权值和对 $998244353$ 取模的结果。
$1 \leq k \leq n \leq 30$,$0 \leq m \leq 100$,$1 \leq v_i < 998244353$
注意到合法序列的要求是 $S$ 的二进制表示中有多少个 $1$ ,而这个东西是涉及到进位的。从低位向高位考虑进位,设状态 $f(i,j,k,l)$ 表示当前考虑到前 $i$ 位,前 $i$ 位有 $j$ 个 $1$ ,填了 $k$ 个 $a$ ,向下一位进位 $p$,的合法序列的权值和
考虑这个状态怎么向下转移。对于下一位,如果有 $t$ 个 $a$ 可以对这位有 $1$ 的贡献,那么 再加上上一位的进位 这一位一共有 $t+l$ 个 $1$ ,同时这一位与此同时就确认了 $t$ 个 $a$ ,最后当前位置就是 $(t+p)\bmod 2$,向下一位的进位就是 $\lfloor \dfrac{t+p}{2}\rfloor$ .
因此,$f(i,j,k,l)$ 可以转移到 $f(i+1,j+(t+p)\bmod2,k+t,\lfloor \dfrac{t+p}{2}\rfloor)$
接下来考虑贡献。不难得出就是 $v_i^t\times \binom{n-k}{t}$
转移方程:
$$ f(i+1,j+(t+l)\bmod2,k+t,\lfloor \dfrac{t+l}{2}\rfloor)= \sum\limits_{t=0}^{n-k}f(i,j,k,l)\times v_i^t\times \binom{n-k}{t} $$
预处理组合数,还有 $v$ 的次幂。
最后直到,到了第 $m$ 位依然是可以发生进位的,所以需要快速统计出进完位后第 $m$ 位往后的 $1$ 的个数,再加上 $j$ 这一维的数量,如果小于等于输入的 $k$,就可以统计进答案,统计答案只需要看 $m+1$。
#include <bits/stdc++.h> using namespace std;const int maxn = 105 ;const int mod = 998244353 ;using i64 = long long ;int n, m, K, ans;int C[35 ][35 ], vp[maxn][35 ];int f[maxn][35 ][35 ][16 ];inline void read (int &res) { int f = 1 ; res = 0 ; char ch = getchar (); while (!isdigit (ch)) {if (ch == '-' )f = -1 ; ch = getchar ();} while (isdigit (ch)) {res = res * 10 + (ch ^ 48 ); ch = getchar ();} res *= f; }int main () { read (n), read (m), read (K); for (int i = 0 ; i <= m; i++) { read (vp[i][1 ]), vp[i][0 ] = 1 ; for (int j = 2 ; j <= n; j++) vp[i][j] = ((i64)vp[i][j - 1 ] * vp[i][1 ]) % mod; } for (int i = 0 ; i <= n; i++)C[i][0 ] = 1 ; for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= i; j++) { C[i][j] = (C[i - 1 ][j] + C[i - 1 ][j - 1 ]) % mod; } } f[0 ][0 ][0 ][0 ] = 1 ; for (int i = 0 ; i <= m; i++) { for (int j = 0 ; j <= K; j++) { for (int k = 0 ; k <= n; k++) { for (int l = 0 ; l <= (n >> 1 ); l++) { for (int t = 0 ; t <= n - k; t++) { (f[i + 1 ][j + ((t + l) & 1 )][k + t][(t + l) >> 1 ] += (i64)f[i][j][k][l] * vp[i][t] % mod * C[ n - k][t] % mod) %= mod; } } } } } for (int j = 0 ; j <= K; j++) { for (int l = 0 ; l <= (n >> 1 ); l++) { if (j + __builtin_popcount(l) <= K) { ans = (ans + f[m + 1 ][j][n][l]) % mod; } } } printf ("%d\n" , ans); return 0 ; }
CF1242B 0-1 MST 给定一张完全图, 其中有 $m$ 条边权值为 $1$, 求这张图的最小生成树
$n,m\le10^5$
转化一下就是完全图,有 $m$ 对点之间没有连边,求出连通块个数 $-1$.
完全图共 $\dfrac{n\times (n-1)}{2}$ 条边,可以发现不连边的情况其实相较很少。也就是说,点之间连接的可能其实很大。我们考虑可能性最大的一个,也就是断边最少的那一个点,如果我们直接从这个点出发,把所有和他相连的点都和他合并,可以想象到这应该是一个很大的连通块。
我们理性考虑一下,相当于 $2m$ 个边的端点分给 $n$ 个点,连接 $1$ 边最少的点肯定连接了小于等于 $\dfrac{2m}{n}$ 条边,也就是这一个很大的连通块大小大概在 $n-\dfrac{2m}{n}$,剩下的点大概在 $\dfrac{2m}{n}$ ,之后剩下的这些单点和那个巨大的连通块暴力合并就可以了,这部分是 $O(n\times \dfrac{2m}{n})=O(m)$
神奇吧,这个东西居然是线性的!
#include <bits/stdc++.h> using namespace std;const int maxn = 1e5 + 5 ;const int mod = 998244353 ;using i64 = long long ;int n, m;int f[maxn];int find (int x) { return x == f[x] ? x : f[x] = find (f[x]); }int deg[maxn], block[maxn], vis[maxn];inline void merge (int x, int y) { x = find (x), y = find (y); f[x] = y; }int head[maxn], to[maxn << 1 ], nxt[maxn << 1 ], cnt;inline void add (int u, int v) { nxt[++cnt] = head[u]; to[cnt] = v; head[u] = cnt; } vector <int > point;inline void read (int &res) { int f = 1 ; res = 0 ; char ch = getchar (); while (!isdigit (ch)) {if (ch == '-' )f = -1 ; ch = getchar ();} while (isdigit (ch)) {res = res * 10 + (ch ^ 48 ); ch = getchar ();} res *= f; }int main () { read (n), read (m); for (int i = 1 ; i < maxn; i++)f[i] = i; for (int i = 1 , u, v; i <= m; i++) { read (u), read (v); add (u, v), add (v, u); deg[u]++, deg[v]++; } int Min = deg[1 ], pos = 1 ; for (int i = 1 ; i <= n; i++) { if (deg[i] < Min)Min = deg[i], pos = i; } for (int i = head[pos]; i; i = nxt[i]) { int v = to[i]; vis[v] = 1 ; } for (int i = 1 ; i <= n; i++) { if (!vis[i]) { merge (i, pos); block[i] = pos; } } memset (vis, 0 , sizeof (vis)); for (int i = 1 ; i <= n; i++) { if (!block[i])point.emplace_back (i); } for (int u = 1 ; u <= n; u++) { int tot = 0 ; for (int i = head[u]; i; i = nxt[i]) { int v = to[i]; vis[v] = 1 ; tot += (block[v] == pos); } for (auto v : point) { if (vis[v])continue ; merge (u, v); } if ((tot + point.size ()) < n)merge (u, pos); for (int i = head[u]; i; i = nxt[i]) { int v = to[i]; vis[v] = 0 ; } } int ans = 0 ; for (int i = 1 ; i <= n; i++) { ans += (i == f[i]); } printf ("%d\n" , ans - 1 ); return 0 ; }
P9869 [NOIP2023] 三值逻辑 一开始给定序列 $x$ ,每个位置都有一个初值(共有三种,T,F,U),接下来 $m$ 个操作,每个操作三种可能:
把 $x_i$ 置成 T,F,U 三者其一。
$x_i \leftarrow x_j$
$x_i \leftarrow \neg x_j $
一眼扩展域并查集。一个点拆成两个,一个代表正,一个代表反,修改操作直接考虑连边。判断时只需考虑是否某个点的正点和反点在同一个集合。然后兴冲冲写了一发 wa 了,仔细一考虑发现每次更改只是单点更改,并不是一条链,好像有点愚蠢了。
#include <bits/stdc++.h> using namespace std;const int maxn = 1e5 + 5 ;const int mod = 998244353 ;using i64 = long long ;int testcase, T, n, m, cnt;char c[3 ];int f[maxn << 1 ], val[maxn];inline void init () { for (int i = 1 ; i < (maxn << 1 ); i++)f[i] = i; for (int i = 1 ; i <= n; i++)val[i] = i; cnt = 0 ; }int find (int x) { return x == f[x] ? x : f[x] = find (f[x]); }inline void merge (int x, int y) { f[find (x)] = find (y); }inline void read (int &res) { int f = 1 ; res = 0 ; char ch = getchar (); while (!isdigit (ch)) {if (ch == '-' )f = -1 ; ch = getchar ();} while (isdigit (ch)) {res = res * 10 + (ch ^ 48 ); ch = getchar ();} res *= f; }int main () { read (testcase), read (T); while (T--) { read (n), read (m); init (); for (int i = 1 , u, v; i <= m; i++) { scanf ("%s" , c); if (c[0 ] == '+' ) { read (u), read (v); val[u] = val[v]; } else if (c[0 ] == '-' ) { read (u), read (v); val[u] = -val[v]; } else { read (u); if (c[0 ] == 'T' )val[u] = 2 * n + 1 ; if (c[0 ] == 'F' )val[u] = 2 * n + 2 ; if (c[0 ] == 'U' )val[u] = 2 * n + 3 ; } } for (int i = 1 ; i <= n; i++) { if (abs (val[i]) == 2 * n + 1 || abs (val[i]) == 2 * n + 2 )continue ; if (abs (val[i]) == 2 * n + 3 )merge (i, n + i); else if (val[i] > 0 ) { merge (i, val[i]); merge (i + n, val[i] + n); } else if (val[i] < 0 ) { merge (i, -val[i] + n); merge (i + n, -val[i]); } } for (int i = 1 ; i <= n; i++) { if (find (i) == find (i + n))cnt++; } printf ("%d\n" , cnt); } return 0 ; }
另一种做法是考虑拆点,某个位置被修改了 $k$ 次就拆成 $k+1$ 个点,操作一直接新建 $x$ 版本然后暴力赋值,操作二和操作三就是先新建版本,然后 $x$ 的新版本向 $y$ 的新版本连一条边,如果是取反边权就是 $1$,否则是 $0$ 。之后我们在每一个位置的最初版本和最后版本连一条边权为 $0$ 的边,代表二者相等。都处理完后就变成了一个基环树森林。之后基环树找环,判断基环树边权和是否是奇数,如果是,那么这一整颗树都是 $U$,或者是这一个树中有 $U$ ,那么直接整个树也是 $U$.
代码可能找个闲的时候再写。