开坑难填之A层邀请赛2
你问为什么赛时排行榜上找不到我?因为我知道自己什么都不会,交就是爆零(10==0),所以就没交……
但是我真的有认真地思考了好久……caorong为证!
判词有云:霁月难逢,彩云易散,心比天高,身在B层
A. inversions
10分的话输出0就好了,因为它什么都没算……但是我很认真的写了个暴力也是10分……算了抄题解去吧还是**
从这个神奇的区间长度里应该是能发现一点东西的,比如每次翻转的范围都是恰好包含的关系,断点永远都是特定的位置,就是,某个区间长度内的逆序对个数只会是正/反数列中的逆序对个数,用归并排序预处理每个长度对应的逆序对个数,这个归并排序真令人兴奋,不仅是长度分的非常恰好,还有solve函数的写法让我立刻想到了cdq,至于天使玩偶那个卡sort的题,那个排序方式,原来就是归并排序!!!感觉我到现在才终于明白了归并排序是什么东西,所以每一层只有区间对区间的影响。
翻转能影响的当前长度和长度更小的区间(把它们变成反向数组),bool变量记录一下翻转的位置就可以O(n)累加了。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <bits/stdc++.h> using namespace std; typedef long long ll; typedef unsigned long long ull; const int maxn = (1<<20) + 5; int a[maxn], q[maxn], n, m, threshold, cnt, ls[maxn], t[maxn]; ll rm[23], r1[23], las; bool now[23]; ull k1, k2; ull xorShift128Plus() { ull k3 = k1, k4 = k2; k1 = k4; k3 ^= (k3 << 23); k2 = k3 ^ k4 ^ (k3 >> 17) ^ (k4 >> 26); return k2 + k4; } void gen(int n, int m, int threshold, ull _k1, ull _k2) { k1 = _k1, k2 =_k2; for (int i=1; i<=(1<<n); i++) a[i] = xorShift128Plus() % threshold + 1; for (int i=1; i<=m; i++) q[i] = xorShift128Plus() % (n + 1); } //用归并排序预处理每个长度对应的逆序对数,区间长度是2^n,分治一下那不就正好是……!!! void solve(int l, int r, int dep, bool op) { if(l >= r) return; int mid = (l + r) >> 1; solve(l, mid, dep-1, op); solve(mid+1, r, dep-1, op); int p1 = l, pos = l-1; for(int p2=mid+1; p2<=r; p2++) { while(p1<=mid && ls[p1]<=ls[p2]) { t[++pos] = ls[p1]; p1++; } if(op) r1[dep] += mid - p1 + 1; else rm[dep] += mid - p1 + 1; t[++pos] = ls[p2]; } while(p1 <= mid) t[++pos] = ls[p1], p1++; for(int i=l; i<=r; i++) ls[i] = t[i]; } int main() { scanf("%d%d%d%lld%lld", &n, &m, &threshold, &k1, &k2); gen(n, m, threshold, k1, k2); if(n == 0) { printf("0\n"); exit(0); } int mx = (1<<n); for(int i=1; i<=mx; i++) ls[i] = a[i]; sort(ls+1, ls+1+mx); cnt = unique(ls+1, ls+1+mx)-ls-1; for(int i=1; i<=mx; i++) { a[i] = lower_bound(ls+1, ls+cnt+1, a[i])-ls; } for(int i=1; i<=mx; i++) ls[i] = a[i]; solve(1, mx, n, 1); for(int i=1; i<=mx; i++) ls[i] = a[mx-i+1]; solve(1, mx, n, 0); ll ans = 0; for(int i=1; i<=m; i++) { ll las = 0; now[q[i]] = 1 - now[q[i]]; int fl = 1; for(int j=n; j>=1; j--) { if(now[j]) fl = 1-fl; if(!fl) las += rm[j]; else las += r1[j]; } ans = ans ^ (las * i); } printf("%lld\n", ans); return 0; }
B. 选择
当时的想法是是跑个tarjan,判断两个点缩点之后有没有在同一组,至于删边操作,就把每个边都存一下,预处理出每次询问时边的状态,对每一个询问把边清空重建再重新tarjan一下……到考试结束连样例都没调出来……
考试结束后跑到网上搜题解结果全是LCT,于是就咕了……啊如果我现在有时间的话好想去学个LCT啊
看到您的题解时感觉眼前的世界都变得辽阔%%%Chen_jr
//两点之间路径全被标记过可以用并查集维护,而并到一个集合上之后树的具体形态已经不重要了
//每次把路径上遍历的点的父亲都改成lca,在并查集中合并即可
//——我不明白的是为什么要更改树上的父亲,而不是直接在并查集中合并
//Re:大概是为了给以后求LCA节省时间,因为合并的目标是相同的
//全都清空,所以前面的操作不影响答案
//比如提前把问题里的树边加上什么的
//如果是非树边,就把u到v路径上的边(点)染色
//S1...==0代表两个点在树上连通,其实S2应该也可以做到这个?
//S2连接时用到的树是全体时间的树,所以其中lca的出现时间可能不合法
//那为什么还能往上合并啊AAA———把删边看成加边的话,时间最晚的时候边最少
//u或v和lca之间的树边可能不存在
//如果这两个点在树上的路径本来就不存在……
//不对——和lca之间有断边的话,因为这是一棵树,所以u和v在S1上就不可能连通
//lca是从u到v的必经点嘛
//也不用担心重新循环一遍树的形态会变,顺序相同,u和v都是排好序的,
//就是重新建树应该也是一样的
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn = 1e5 + 5; const int N = 1e7 + 2; const int mod = 1e9 + 7; const ll INF = 0x3f3f3f3f3f3f3f3f; int n, m, q; char c[3]; inline int read() { int x = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') { f = -1; } ch = getchar(); } while(ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch^48); ch = getchar(); } return x * f; } struct edge { int u, v; edge(){} edge(int x, int y) { //u = min(u, v); //v = max(u, v); u = min(x, y); v = max(x, y); } friend bool operator < (const edge x, const edge y) { return x.u == y.u ? x.v < y.v : x.u < y.u; } }; set<edge> s; struct Edge { int next, to; }a[maxn<<1]; int head[maxn], len = 1; void add(int x, int y) { a[++len].to = y; a[len].next = head[x]; head[x] = len; } struct opt { int op, u, v, ans; }o[maxn]; struct SET { int f[maxn]; void pre(int x) { for(int i=1; i<=x; i++) f[i] = i; } int fa(int x) { return f[x] = f[x] == x ? x : fa(f[x]); } bool hb(int x, int y) { x = fa(x), y = fa(y); if(x != y) f[x] = y; else return false; return true; } }S1, S2; void link(int u, int v) { if(S1.hb(u, v) == 0) return; add(u, v); add(v, u); } int dep[maxn], fa[maxn]; void dfs(int x)//搞得还真是个树了 { for(int i=head[x]; i; i=a[i].next) { int v = a[i].to; if(dep[v]) continue; dep[v] = dep[x] + 1; fa[v] = x; dfs(v); } } int LCA(int u, int v)//所以这是个朴素算法!? { while(fa[u] != fa[v]) { if(dep[fa[u]] > dep[fa[v]]) u = fa[u]; else v = fa[v]; } return u == v ? u : fa[u]; } //两点之间路径全被标记过可以用并查集维护,而并到一个集合上之后树的具体形态已经不重要了 //每次把路径上遍历的点的父亲都改成lca,在并查集中合并即可 //——我不明白的是为什么要更改树上的父亲,而不是直接在并查集中合并 //Re:大概是为了给以后求LCA节省时间,因为合并的目标是相同的 void modify(int u, int v) { int lca = LCA(u, v); if(lca == 0) return; while(u != lca && u) { S2.hb(u, lca); int lx = u; u = fa[u]; fa[lx] = lca; } u = v; while(u != lca && u) { S2.hb(u, lca); int lx = u; u = fa[u]; fa[lx] = lca; } } int main() { n = read(); m = read(); q = read(); for(int i=1; i<=m; i++) { int u = read(), v = read(); s.insert(edge(u, v)); } for(int i=1; i<=q; i++) { scanf("%s", c); o[i].u = read(); o[i].v = read(); if(c[0] == 'Z') o[i].op = 1, s.erase(edge(o[i].u, o[i].v)); else o[i].op = 0; } S1.pre(n); for(auto x : s) link(x.u, x.v);//先建一个生成树,有可能是森林 for(int i=q; i>=1; i--) { if(o[i].op) link(o[i].u, o[i].v);//生成树继续完善但还是一棵树(森林) } for(int i=1; i<=n; i++) { if(!dep[i]) dep[i] = 1, dfs(i); } S1.pre(n); S2.pre(n); //全都清空,所以前面的操作不影响答案 //比如提前把问题里的树边加上什么的 //如果是非树边,就把u到v路径上的边(点)染色 //S1...==0代表两个点在树上连通,其实S2应该也可以做到这个? //S2连接时用到的树是全体时间的树,所以其中lca的出现时间可能不合法 //那为什么还能往上合并啊AAA———把删边看成加边的话,时间最晚的时候边最少 //u或v和lca之间的树边可能不存在 //如果这两个点在树上的路径本来就不存在…… //不对——和lca之间有断边的话,因为这是一棵树,所以u和v在S1上就不可能连通 //lca是从u到v的必经点嘛 //也不用担心重新循环一遍树的形态会变,顺序相同,u和v都是排好序的, //就是重新建树应该也是一样的 for(auto x : s) { if(S1.hb(x.u, x.v) == 0) modify(x.u, x.v); } for(int i=q; i>=1; i--) { if(o[i].op) { if(S1.hb(o[i].u, o[i].v)) continue; modify(o[i].u, o[i].v); } else o[i].ans = S2.fa(o[i].u) == S2.fa(o[i].v) ? 1 : 0; } for(int i=1; i<=q; i++) { if(!o[i].op) { if(o[i].ans) printf("Yes\n"); else printf("No\n"); } } return 0; }
时光花火,水月星辰