题意:
有一些人,其中p1个人说真话,剩下p2个人说假话,现在有n句话,每句话是一个人说某一个人是否是说真话的人,请你判断是否能唯一判断哪些人说真话、哪些人说假话,升序输出说真话人的编号。
n <= 1000, p1, p2 <= 300。
题解:
一个说真话的人a,如果说b是说真话的,那么b是说真话的,否则b是说假话的;一个说假话的人a,如果说b是说真话的,那么b是说假话的,否则b是说真话的。
说真话的人只会说说真话的人会说真话,说假话的人只会说说假话的人说真话,根据这个可以确定一些人的真假关系,但是不能确定某个人是否是说真话的人。
用带权并查集表示的话,说真话的人和说假话的人是一个二元环,实际上可能会存在多个互不干扰的二元环。
如果能算出每个连通分量里两类人的数量,则可以通过dp来决策每个分量里人员的分配,可以计算出有p1个说真话的人的方案是否唯一,若唯一,按照状态转移的顺序依次标记说真话的人即可。dp的形式类似于分组背包,注意每一个分量都必须标人。
不用带权并查集也可使用二倍并查集,但是要注意dp所需要的size的计算,因为二倍并查集添加了虚结点n + i表示与i关系互异的人。
时间复杂度O(nα(p1+p2)+p1(p1+p2))。
代码:
#include <cstdio> #include <cstring> const int maxp = 601; int n, p1, p2, fa[maxp], dist[maxp], cnt[maxp][2], f[maxp][maxp], pre[maxp][maxp]; bool vis[maxp][2]; int find(int x) { if(x == fa[x]) return x; int tmp = fa[x]; fa[x] = find(fa[x]); dist[x] ^= dist[tmp]; return fa[x]; } int main() { while(scanf("%d%d%d", &n, &p1, &p2) == 3 && n + p1 + p2) { memset(f, 0, sizeof f); memset(vis, 0, sizeof vis); memset(pre, 0, sizeof pre); memset(cnt, 0, sizeof cnt); memset(dist, 0, sizeof dist); for(int i = 1; i <= p1 + p2; ++i) fa[i] = i; while(n--) { int x, y, u, v; char op[5]; scanf("%d%d%s", &x, &y, op); if((u = find(x)) != (v = find(y))) { fa[u] = v; dist[u] = dist[x] ^ dist[y] ^ (op[0] == 'n'); } } for(int i = 1; i <= p1 + p2; ++i) { int j = find(i); ++cnt[j][dist[i]]; } f[0][0] = 1; for(int i = 1; i <= p1 + p2; ++i) if(!cnt[i][0] && !cnt[i][1]) for(int j = 0; j <= p1; ++j) { f[i][j] = f[i - 1][j]; pre[i][j] = pre[i - 1][j]; } else for(int j = 0; j <= p1; ++j)//·ÇºÃ¼´»µ { if(j >= cnt[i][0] && f[i - 1][j - cnt[i][0]]) { f[i][j] += f[i - 1][j - cnt[i][0]]; pre[i][j] = i + i; } if(j >= cnt[i][1] && f[i - 1][j - cnt[i][1]]) { f[i][j] += f[i - 1][j - cnt[i][1]]; pre[i][j] = i + i + 1; } } if(f[p1 + p2][p1] != 1) { puts("no"); continue; } for(int i = p1 + p2, j = p1; i > 0 && j > 0; --i) { int ii = pre[i][j] >> 1, jj = pre[i][j] & 1; if(!ii && !jj) break; vis[ii][jj] = 1; i = ii; j -= cnt[ii][jj]; } for(int i = 1; i <= p1 + p2; ++i) if(vis[find(i)][dist[i]]) printf("%d\n", i); puts("end"); } return 0; }
题意:
有n个点,m条边,现在有t次操作,每次操作破坏编号为i的边或者询问x和y点的连通性。
n, m, t <= 10 ^ 5。
题解:
题目并没有要求在线解决,可以倒着处理操作,则破坏操作可以认为是添加操作,利用并查集维护连通性即可,注意边被多次破坏的情况应该转化为选择最早的一次操作添加。
时间复杂度O((m+t)α(n))。
代码:
#include <cstdio> const int maxn = 1e5 + 1, maxm = 1e5 + 1, maxt = 1e5 + 1; int n, m, t, fa[maxn], u[maxm], v[maxm], query[maxt][3], ans[maxt]; bool vis[maxm]; int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); } int main() { scanf("%d%d", &n, &m); for(int i = 1; i <= m; ++i) scanf("%d%d", u + i, v + i); scanf("%d", &t); for(int i = 1; i <= t; ++i) { int x, y; char op[2]; scanf("%s", op); if(op[0] == 'Q') { scanf("%d%d", &x, &y); query[i][0] = 1; query[i][1] = x; query[i][2] = y; } else { scanf("%d", &x); if(vis[x]) query[i][0] = 2; else { query[i][1] = x; vis[x] = 1; } } } for(int i = 1; i <= n; ++i) fa[i] = i; for(int i = 1; i <= m; ++i) if(!vis[i]) fa[find(u[i])] = find(v[i]); for(int i = t; i > 0; --i) if(query[i][0] == 1) ans[i] = find(query[i][1]) == find(query[i][2]); else if(!query[i][0]) fa[find(u[query[i][1]])] = find(v[query[i][1]]); for(int i = 1; i <= t; ++i) if(query[i][0] == 1) printf("%d\n", ans[i]); return 0; }
题意:
有一个元素未知的长度为n的01序列,现在有k句话,每句话说明序列的第l位到第r位元素之和的奇偶性,问从第几句话开始出现矛盾。
n <= 10 ^ 9, k <= 5000。
题解:
sum[l, r] = sum[1, r] - sum[1, l - 1],序列的第l位到第r位元素之和sum[l, r]为奇,表示前缀和[1, l - 1]和前缀和[1, r]的奇偶性不同,否则奇偶性相同。
则可以用带权并查集维护前缀区间的相对奇偶性,也可以开二倍并查集,不过此题n过大,但最终发生改变的前缀区间最多只有2k个,离散化一下即可。
时间复杂度O(kα(k))。
代码:
#include <map> #include <cstdio> #include <algorithm> using namespace std; const int maxn = 10001; map<int, int> Hash; int n, tot, fa[maxn << 1], ans; int idx(int x) { if(Hash.find(x) != Hash.end()) return Hash[x]; return Hash[x] = tot++; } int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); } int main() { while(scanf("%d", &n) != EOF && n != -1) { tot = ans = 0; Hash.clear(); for(int i = 0; i < maxn << 1; ++i) fa[i] = i; scanf("%d", &n); bool flag = 0; for(int i = 1; i <= n; ++i) { int l, r; char op[5]; scanf("%d%d%s", &l, &r, op); if(flag) continue; int x = idx(l - 1), y = idx(r); int fx1 = find(x), fx2 = find(x + n); int fy1 = find(y), fy2 = find(y + n); if(op[0] == 'e') { if(fx1 == fy2 || fx2 == fy1) { flag = 1; continue; } fa[find(fx1)] = find(fy1); fa[find(fx2)] = find(fy2); } else { if(fx1 == fy1 || fx2 == fy2) { flag = 1; continue; } fa[find(fx1)] = find(fy2); fa[find(fx2)] = find(fy1); } ans = i; } printf("%d\n", ans); } return 0; }
题意:
有n个未知的数字,编号1~n,有m个操作,要么告诉你第b个数字比第a个数字大w,要么问你能否知道第b个数字比第a个数字大多少。
n, m <= 10 ^ 5, w <= 10 ^ 6。
题解:
利用带权并查集可以很容易的表示第b个数字比第a个数字大w,即b到a的距离为w,而第b个数字比第a个数字大多少即b到根的距离与a到根的距离之差,不可计算当且仅当他们不在同一个连通分量里。时间复杂度O(mα(n))。
代码:
#include <cstdio> #include <cstring> const int maxn = 100001; int n, m, fa[maxn]; long long dist[maxn]; int find(int x) { if(x == fa[x]) return x; int tmp = fa[x]; fa[x] = find(fa[x]); dist[x] += dist[tmp]; return fa[x]; } int main() { while(scanf("%d%d", &n, &m) == 2 && n + m) { memset(dist, 0, sizeof dist); for(int i = 1; i <= n; ++i) fa[i] = i; while(m--) { int a, b, w, u, v; char op[2]; scanf("%s", op); if(op[0] == '!') { scanf("%d%d%d", &a, &b, &w); if((u = find(a)) != (v = find(b))) { fa[v] = u; dist[v] += w + dist[a] - dist[b]; } } else { scanf("%d%d", &a, &b); if(find(a) == find(b)) printf("%d\n", dist[b] - dist[a]); else puts("UNKNOWN"); } } } return 0; }
题意:
现在有一个n * m格的纸,颜色初始均为0,现在有q次操作,每次会将一块区域的颜色变成c,区域的形状可能是圆、菱形、矩形和等腰三角形,菱形的对角线和三角形的底边均与格子平行。
n <= 200, m, q <= 50000。
题解:
考虑每个格子最终的颜色一定是最后一个涂上的颜色,如果倒着做操作则操作可视为对没有涂过色的格子进行涂色,每一行维护一个并查集可以快速找到未涂色的格子。
时间复杂度O(nmα(m)+nq)。
代码:
#include <cmath> #include <cstdio> const int maxn = 201, maxm = 50010, maxq = 50010, maxc = 10; int n, m, q, fa[maxn][maxm], ans[maxc]; struct Query { char op; int x, y, r, c, l; } query[maxq]; int min(int x, int y) { return x < y ? x : y; } int max(int x, int y) { return x < y ? y : x; } int abs(int x) { return x < 0 ? -x : x; } int find(int x, int y) { return y == fa[x][y] ? y : fa[x][y] = find(x, fa[x][y]); } int main() { while(scanf("%d%d%d", &n, &m, &q) == 3) { for(int i = 1; i < maxc; ++i) ans[i] = 0; for(int i = 0; i < n; ++i) for(int j = 0; j <= m; ++j) fa[i][j] = j; for(int i = 0; i < q; ++i) { char op[10]; int x, y, c, r, l = 0; scanf("%s%d%d%d%d", op, &x, &y, &r, &c); if(op[0] == 'R') scanf("%d", &l); query[i] = (Query){op[0], x, y, r, c, l}; } for(int t = q - 1; t >= 0; --t) { char &op = query[t].op; int &x = query[t].x, &y = query[t].y, &r = query[t].r, &c = query[t].c, &l = query[t].l; switch(op) { case 'C' : for(int i = max(x - r, 0), ii = min(x + r, n - 1); i <= ii; ++i) for(int j = find(i, max(y - (int)sqrt(r * r - (i - x) * (i - x)), 0)), jj = min(y + (int)sqrt(r * r - (i - x) * (i - x)), m - 1); j <= jj; j = find(i, j)) { ++ans[c]; fa[i][j] = j + 1; } break; case 'D' : for(int i = max(x - r, 0), ii = min(x + r, n - 1); i <= ii; ++i) for(int j = find(i, max(y - (r - abs(x - i)), 0)), jj = min(y + (r - abs(x - i)), m - 1); j <= jj; j = find(i, j)) { ++ans[c]; fa[i][j] = j + 1; } break; case 'R' : for(int i = x, ii = min(x + r - 1, n - 1); i <= ii; ++i) for(int j = find(i, y), jj = min(y + c - 1, m - 1); j <= jj; j = find(i, j)) { ++ans[l]; fa[i][j] = j + 1; } break; case 'T' : for(int i = x, ii = min(x + (r - 1 >> 1), n - 1); i <= ii; ++i) for(int j = find(i, max(y - ((r - 1 >> 1) - (i - x)), 0)), jj = min(y + ((r - 1 >> 1) - (i - x)), m - 1); j <= jj; j = find(i, j)) { ++ans[c]; fa[i][j] = j + 1; } } } for(int i = 1; i < maxc; ++i) printf("%d%c", ans[i], " \n"[i + 1 == maxc]); } return 0; }
题意:
有n个未知的数字,编号1~n,有m个等式,表示sum[l, r]的值为w,问每个数字应该是什么,或是无法知道。
n, m <= 10 ^ 5, w <= 10 ^ 9。
题解:
sum[l, r] = sum[1, r] - sum[1, l - 1],利用带权并查集维护即可。
第i个数字的值即为sum[i, i],若前缀区间[1, i - 1]和[1, i]在同一个连通分量里则第i个数字的值可解。
时间复杂度O((n + m)α(n))。
代码:
#include <cstdio> const int maxn = 1e5 + 1; int n, m, fa[maxn]; long long dist[maxn]; int find(int x) { if(x == fa[x]) return x; int tmp = fa[x]; fa[x] = find(fa[x]); dist[x] += dist[tmp]; return fa[x]; } int main() { while(scanf("%d%d", &n, &m) == 2) { for(int i = 0; i <= n; ++i) { fa[i] = i; dist[i] = 0; } while(m--) { int s, t, c, u, v; scanf("%d%d%d", &s, &t, &c); if((u = find(s - 1)) != (v = find(t))) { fa[v] = u; dist[v] = c + dist[s - 1] - dist[t]; puts("Accepted!"); } else puts(dist[t] - dist[s - 1] == c ? "Accepted!" : "Error!"); } for(int i = 1; i <= n; ++i) if(find(i - 1) != find(i)) puts("Unknown!"); else printf("%lld\n", dist[i] - dist[i - 1]); } return 0; }
题意:
有一台笔记本电脑,它的性能有f个等级,最开始在最低的1等级,从一个等级换到另外一个等级需要每秒e焦耳能量,共需a秒。
现在要依次处理p个程序,给出每个程序在每个等级下工作的功率(焦耳/秒)和时间,问完成所有程序所需的最少能量是多少。
f <= 20, p <= 5000, 功率, 时间 <= 1000。
题解:
这道题和并查集没有关系,直接考虑f[i][j]表示当前在j等级下处理第i个程序后使用的最少能量,坑点只在于最开始的等级为1。
直接dp时间复杂度O(f^2p),记录每层最大值次大值可以做到时间复杂度O(fp)。
代码:
#include <cstdio> #include <cstring> const int maxn = 5001, maxm = 21; int n, m, x; long long f[maxn][maxm], ans; int main() { int e, a; while(scanf("%d%d%d%d", &m, &n, &e, &a) == 4 && m + n + e + a) { memset(f, 0x3f, sizeof f); x = e * a; f[0][0] = 0; for(int i = 1; i <= n; ++i) for(int j = 0; j < m; ++j) { scanf("%d%d", &e, &a); for(int k = 0; k < m; ++k) { int tmp = e * a; if(j != k) tmp += x; if(f[i][j] > f[i - 1][k] + tmp) f[i][j] = f[i - 1][k] + tmp; } } ans = f[n][0]; for(int i = 1; i < m; ++i) if(ans > f[n][i]) ans = f[n][i]; printf("%lld\n", ans); } return 0; }
题意:
有一个环形的r * c的海域,第1行之上和第r行之下是两片陆地,每一行的第1格和第c格也是相邻的,这里的相邻是指四连通。
现在要尝试把海域里的n个位置改成陆地,如果变成陆地之后会导致上下两片陆地无法通航则不执行填海造陆的操作,问最终海中有几块陆地。
题解:
上下无法通航当且仅当海上出现了一串成环的陆地阻隔了两边,这里的陆地应该是八连通的。
考虑并查集可以维护陆地的连通性,但很难维护是否出现一个环,因为这里是八连通的,无法确定串上的一块土地的前继与后继。
考虑将每个点i复制成两个,i和c+i,则只需要考虑i是否会和n+i连通即可判断是否出现环。
按点建并查集,时间复杂度O(nα(rc))。
代码:
#include <cstdio> const int maxn = 3001, dx[] = {-1, 0, 1, -1, 1, -1, 0, 1}, dy[] = {-1, -1, -1, 0, 0, 1, 1, 1}; int r, c, n, fa[maxn * maxn * 2], ans; bool vis[maxn][maxn * 2]; int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); } int main() { scanf("%d%d%d", &r, &c, &n); if(c == 1) { puts("0"); return 0; } for(int i = 0; i < r * c * 2; ++i) fa[i] = i; while(n--) { int x, y; bool flag = 0; scanf("%d%d", &x, &y); --x, --y; for(int i = 0; i < 8; ++i) { int x1 = x + dx[i], y1 = y + dy[i]; if(x1 < 0 || x1 >= r) continue; if(y1 < 0) y1 = c * 2 - 1; if(!vis[x1][y1]) continue; for(int j = 0; j < 8; ++j) { int x2 = x + dx[j], y2 = y + c + dy[j]; if(x2 < 0 || x2 >= r) continue; if(y2 >= c * 2) y2 = 0; if(!vis[x2][y2]) continue; if(find(x1 * c * 2 + y1) == find(x2 * c * 2 + y2)) { flag = 1; break; } } if(flag) break; } if(!flag) { ++ans; for(int i = 0; i < 8; ++i) { int x1 = x + dx[i], y1 = y + dy[i]; if(x1 < 0 || x1 >= r) continue; if(y1 < 0) y1 = c * 2 - 1; if(vis[x1][y1]) fa[find(x * c * 2 + y)] = find(x1 * c * 2 + y1); } for(int i = 0; i < 8; ++i) { int x1 = x + dx[i], y1 = y + c + dy[i]; if(x1 < 0 || x1 >= r) continue; if(y1 >= c * 2) y1 = 0; if(vis[x1][y1]) fa[find(x * c * 2 + y + c)] = find(x1 * c * 2 + y1); } vis[x][y] = vis[x][y + c] = 1; } } printf("%d\n", ans); return 0; }
并查集的知识点里路径压缩最为基础,带权并查集的权值设计也很巧妙,但本场专题没能完全体现。带权的一个经典的模型就是k元环的关系,不带权的经典模型是快速跳跃的模型。较为经典的技巧就是区间转前缀区间,比较巧妙的还是P题的判环技巧。