开坑难填之A层邀请赛2

你问为什么赛时排行榜上找不到我?因为我知道自己什么都不会,交就是爆零(10==0),所以就没交……

但是我真的有认真地思考了好久……caorong为证!

判词有云:霁月难逢,彩云易散,心比天高,身在B层

A. inversions

10分的话输出0就好了,因为它什么都没算……但是我很认真的写了个暴力也是10分……算了抄题解去吧还是**

 从这个神奇的区间长度里应该是能发现一点东西的,比如每次翻转的范围都是恰好包含的关系,断点永远都是特定的位置,就是,某个区间长度内的逆序对个数只会是正/反数列中的逆序对个数,用归并排序预处理每个长度对应的逆序对个数,这个归并排序真令人兴奋,不仅是长度分的非常恰好,还有solve函数的写法让我立刻想到了cdq,至于天使玩偶那个卡sort的题,那个排序方式,原来就是归并排序!!!感觉我到现在才终于明白了归并排序是什么东西,所以每一层只有区间对区间的影响

翻转能影响的当前长度和长度更小的区间(把它们变成反向数组),bool变量记录一下翻转的位置就可以O(n)累加了。

#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;
}
View Code

 

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都是排好序的,
    //就是重新建树应该也是一样的
#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;
}
View Code

 

posted @ 2022-08-14 17:09  Catherine_leah  阅读(21)  评论(0编辑  收藏  举报
/* */