数据结构

数据结构

线性基

线性相关与线性无关

线性基相当于向量在模 \(2\) 意义下进行运算,就是 \(xor\)

构造

给定 \(n\) 个向量,考虑找到一个集合满足集合大小最小,每个向量都能表示为集合内几个元素的异或和,集合内任取元素相互异或均不为 \(0\)

考虑最开始集合为这 \(n\) 个向量,下面逐步减少集合中元素。

若集合中存在两个元素 \(a, b\) ,显然可以将这两个元素替换为 \(a, a\ xor\ b\)

因此考虑任取集合中一个元素 \(a\) ,找到该元素最高位 \(1\) 的位置,对于其他元素,如果该位为 \(1\) ,那么异或上 \(a\) ,显然经过这样的操作后,该位全部为 \(0\) ,我们可以去除该位继续构造。

构造过程中取出所有 \(0\) 向量。

那么所有的 \(a\) 构成的集合就是一组线性基。

实现过程参考

for ( int i = log_max;i >= 0;i -- )
{
    if ( x & ( 1ll << i ) )
    {
        if ( !d[i] ){d[i] = x;break;}
        else if ( d[i] ) x ^= d[i];
    }
}

应用

albus就是要第一个出场

已知一个长度为 \(n\) 的正整数序列 \(A\)(下标从 \(1\) 开始),令 \(S = \{ x | 1 \le x \le n \}\)\(S\) 的幂集 \(2^S\) 定义为 \(S\) 所有子集构成的集合。定义映射 \(f : 2^S \to Z,f(\emptyset) = 0,f(T) = \mathrm{XOR}\{A_t\}, (t \in T)\)

现在 albus 把 \(2^S\) 中每个集合的f值计算出来,从小到大排成一行,记为序列 \(B\)(下标从 \(1\) 开始)。

给定一个数,那么这个数在序列 \(B\) 中第 \(1\) 次出现时的下标是多少呢?

题解

求解 \(A\) 的一组线性基 \(T\) ,显然 \(B\) 的大小为 \(2^{|T|}\)

对于 \(B\) 中的每一个数,其被表示的方案数为 \(2^{n-|T|}\)

简单证明,设目前异或和为 \(x\) ,显然除线性基内包含的数外,剩余 \(n-|T|\) 个数,这几个数随意选,都能够在线性基内找到唯一的方案s使得整体的异或和为 \(x\)

玛里苟斯

\(S\) 是一个可重集合,有 \(S = \{a_1, a_2, a_3, ..., a_n\}\) ,设 \(A\)\(S\) 的一个子集,设 \(A\) 的异或和为 \(x\) ,求 \(x^k\) 的期望。

\(1\le n\le 1e5, 1\le k\le 5, a_i\ge 0\) ,最终答案小于 \(2^{63}\)

题解

第一种情况:\(k==1\),此时我们把每个数拆成二进制,对于每一位进行处理,如果有一位至少有一次为\(1\),那么这一位对答案所产生的贡献为\(2^{i-1}\),因为我们有\(\tfrac{1}{2}\)的概率选择奇数次,有\(\tfrac{1}{2}\)的概率选择偶数次。

第二种情况:\(k==2\),我们将答案\(x\)拆成二进制的形式\(b_{n}b_{n-1}b_{n-2}…b_1b_0\),那么我们有\(x^2=\sum_{i=0}^n\sum_{j=0}^nb[i]*b[j]*2^{i+j}\),于是我们顺着第一种情况的思路进行讨论,首先如果\(i\)\(j\)中有一位一定为\(0\),那么我们可以直接跳过,因为它的贡献一定为\(0\),如果每个数中两位相同且并非完全是\(0\),那么我们知道当我们选择时,有\(\tfrac{1}{2}\)的概率会选择奇数次,有\(\tfrac{1}{2}\)的概率会选择偶数次,因此这时对答案产生的贡献为\(2^{i+j-1}\),如果存在一个数两位不同,将所有数分为两位相同和两位不同的两组,分类讨论发现最终产生贡献的概率为 \(\tfrac{1}{4}\)

第三种情况:\(k>=3\),此时由于题目保证最终的\(ans<=2^{64}\),那么显然\(x<2^{22}\),因此这时我们暴力枚举线性基求解答案。

代码细节较多,注意中间过程可能超出 \(unsigned\) \(long\) \(long\)

DZY Loves Chinese II

今Dzy有一图,其上有N座祭坛,又有M条边。
时而Dzy狂WA而怒发冲冠,神力外溢,遂有K条边灰飞烟灭。
而后俟其日A50题则又令其复原。(可视为立即复原)
然若有祭坛无法相互到达,Dzy之神力便会大减,于是欲知其是否连通。

题解

经典套路题

我们可以对原图跑一棵树,找出图中的树边和返祖边,那么对于一条树边,如果所有覆盖它的返祖边同时被断开,那么整张图一定会分成两部分,于是,我们不妨对于每一个返祖边随机一个值,然后让树边等于所有覆盖它的返祖边的异或和,这样我们可以快速检查图的连通性。

梦想封印

渐渐地,Magic Land上的人们对那座岛屿上的各种现象有了深入的了解。

为了分析一种奇特的称为梦想封印(Fantasy Seal)的特技,需要引入如下的概念:

每一位魔法的使用者都有一个“魔法脉络”,它决定了可以使用的魔法的种类。

一般地,一个“魔法脉络”可以看作一个无向图,有N个结点及M条边,将结点编号为1~N,其中有一个结点是特殊的,称为核心(Kernel),记作1号结点。每一条边有一个固有(即生成之后再也不会发生变化的)权值,是一个不超过U的自然数。

每一次魔法驱动,可看作是由核心(Kernel)出发的一条有限长的道路(Walk),可以经过一条边多次,所驱动的魔法类型由以下方式给出:

将经过的每一条边的权值异或(xor)起来,得到s。

如果s是0,则驱动失败,否则将驱动编号为s的魔法(每一个正整数编号对应了唯一一个魔法)。

需要注意的是,如果经过了一条边多次,则每一次都要计入s中。

这样,魔法脉络决定了可使用魔法的类型,当然,由于魔法与其编号之间的关系尚未得到很好的认知,此时人们仅仅关注可使用魔法的种类数。

梦想封印可以看作是对“魔法脉络”的破坏:

该特技作用的结果是,“魔法脉络”中的一些边逐次地消失。

我们记总共消失了Q条边,按顺序依次为Dis1、Dis2、……、DisQ。

给定了以上信息,你要计算的是梦想封印作用过程中的效果,这可以用Q+1个自然数来描述:

Ans0为初始时可以使用魔法的数量。

Ans1为Dis1被破坏(即边被删去)后可以使用魔法的数量。

Ans2为Dis1及Dis2均被破坏后可使用魔法的数量。

……

AnsQ为Dis1、Dis2、……、DisQ全部被破坏后可以使用魔法的数量。

题解

对于一张图,我们仍然可以按树进行处理,此时树上的返祖边将于一些树边构成环,那么我们可以发现一个点\(i\)到节点\(1\)的任意路径的异或和都可以用\(1\)节点到\(i\)节点的所有树边的异或和\(f[i]\) \(xor\) 一些环的异或和。

那么我们可以用线性基维护选择环时有多少种可能的值,也就是,我们需要得到所有本质不同的环,但是我们却不能将\(f[i]\)同时插入线性基中,此时线性基将表示任意两点的路径的异或和,因此对于\(f[i]\),我们单独维护,我们将\(f[i]\)\(f[j]\)扔进线性基中进行消元,如果它们最后得到的值相同,那么它们为本质相同的,因为\(f[i]\) \(xor\) 一些环的异或和 与 \(f[j]\) \(xor\) 一些环的异或和 的可能的结果完全相同,我们需要找出所有本质不同的\(f[i]\),由于数据范围并不大,因此我们用\(set\)进行维护,如果我们需要插入一个新的\(f[i]\),那么我们现将它与线性基进行消元再插入\(set\)中,如果我们需要插入一个新的环,那么我们将\(set\)中的值全部取出,与新的环的消元后的值进行消元,再全部插回\(set\)中,那么我们的设\(set\)大小为\(size\),线性基大小为\(cnt\),那么我们要求的答案为\(size*2^{cnt}-1\)

那么我们可以倒着进行处理,把删边操作改成加边操作,如果我们加入了一条\(u\)\(v\)均被\(Dfs\)过的边,那么它将形成一个环,我们直接插入这个环即可,如果我们加入了一条\(u\)\(v\)均没有被\(Dfs\)过的边,那么我们可以不进行任何操作,如果我们加入了\(u\)\(v\)一个被\(Dfs\)过,一个没有被\(Dfs\)过的边,那么我们计算出没有被\(Dfs\)过的节点的\(f[i]\),同时我们对没有被\(DFs\)的节点的子树进行\(Dfs\)

主席树

例题

给定长度为 \(n\) 的序列,给定 \(q\) 次询问,每次询问 \(L, R, l, r\) ,查询序列在 \([L, R]\) 区间内,值域在 \([l, r]\) 区间内的值的个数。

题解

使用前缀和思想,分别求解 \([1, R]\)\([1, L-1]\) 的答案。

普通权值线段树操作可以实现在插入到 \(a_i\) 时维护 \([1, i]\) 所有数的值域信息。

主席树只需要在插入时新建节点即可保留历史版本信息。

应用

middle

一个长度为n的序列a,设其排过序之后为b,其中位数定义为b[n/2],其中a,b从0开始标号,除法取下整。

给你一个长度为n的序列s。

回答Q个这样的询问:s的左端点在[a,b]之间,右端点在[c,d]之间的子序列中,最大的中位数。

其中a<b<c<d。

位置也从0开始标号。

我会使用一些方式强制你在线。

题解

我们有一个求解中位数的经典套路为首先二分中位数,我们大于等于中位数的数设为\(1\),把小于中位数的数设为\(-1\),那么我们统计整个序列的和\(sum\),如果\(sum>=0\),那么中位数大于等于这个数,如果\(sum<0\),那么中位数小于这个数。

对于这道题,我们也可以采用二分的方法,我们首先二分中位数,将大于等于这个数的设为\(1\),小于等于这个数的设为\(-1\),我们发现对于每一次询问\(a,b,c,d\),其中\([b+1,c-1]\)这个区间是一定选择的,由于我们要使得中位数尽可能大,因此我们要让\(sum\)尽可能大,我们要求出\([a,b]\)的最大后缀和和\([c,d]\)的最大前缀和,显然我们可以用线段树维护,但是我们不能对于每一次二分的值都重构一次线段树,我们发现如果我们已经对于\(x\)建出线段树,如果我们想要建出\(x+1\)的,我们发现这个过程中,只有\(x\)的值变成了\(-1\),其他的值均没有发生变化,因此我们直接用主席树维护即可。

神秘数

一个可重复数字集合 \(S\) 的神秘数定义为最小的不能被 \(S\) 的子集的和表示的正整数。例如 \(S=\{1,1,1,4,13\}\),有:\(1 = 1\)\(2 = 1+1\)\(3 = 1+1+1\)\(4 = 4\)\(5 = 4+1\)\(6 = 4+1+1\)\(7 = 4+1+1+1\)

\(8\) 无法表示为集合 \(S\) 的子集的和,故集合 \(S\) 的神秘数为 \(8\)

现给定长度为 \(n\)正整数序列 \(a\)\(m\) 次询问,每次询问包含两个参数 \(l,r\),你需要求出由 \(a_l,a_{l+1},\cdots,a_r\) 所组成的可重集合的神秘数。

对于 \(100\%\) 的数据点,\(1\le n,m\le {10}^5\)\(\sum a\le {10}^9\)

题解

我们发现对于一个序列,我们想知道它的神秘数,我们需要先将其排序,然后我们设现在可以表示的区间为\([1,pos]\),如果我们下一个加入的数小于等于\(pos+1\),那么我们可以将区间扩展为\([1,pos+x]\),如果我们下一个加入的数大于\(pos+1\),那么我们的\(ans=pos+1\),因为后边的数都大于等于当前的数。

那么我们考虑如何求出任意区间的神秘数,我们设当前最小的不能被表示的数为\(ans\),那么我们可以表示的值域为\([1,ans-1]\),我们可以查询这段区间小于等于\(ans\)的所有数之和\(res\),如果\(res>=ans\),那么我们可以将\(ans\)扩展到\(res+1\),如果\(res\)小于\(ans\),那么我们的最终答案就是\(ans\)

K-D Tree

例题

在一个二维平面上,给定 \(q\) 次操作,对于插入操作,给定 \((x, y, v)\) ,给 \((x, y)\) 坐标上的权值 \(val_{x, y}\) 增加 \(v\) ,对于查询操作,给定 \((x_1, x_2, y_1, y_2)\) ,查询 \(\sum_{x_1\le i\le x_2, y_1\le j\le y_2} val_{i,j}\) ,初始所有点权值为 \(0\)

题解

K-D Tree 的原型是替罪羊树,主要思想和平衡树类似,不同之处在于,对于深度为奇数的节点,以 \(x\) 为第一关键字, \(y\) 为第二关键字排序,对于深度为偶数的节点,以 \(y\) 为第一关键字, \(x\) 为第二关键字排序。

K-D Tree 很多情况下复杂度不正确,主要可以解决偏序问题( cdq 分治)和平面最近点对(分治法)。

(https://oi-wiki.org/geometry/nearest-points/)

应用

简单题

你有一个\(N \times N\)的棋盘,每个格子内有一个整数,初始时的时候全部为 \(0\),现在需要维护两种操作:

  • 1 x y A \(1\le x,y\le N\)\(A\) 是正整数。将格子x,y里的数字加上 \(A\)
  • 2 x1 y1 x2 y2 \(1 \le x_1 \le x_2 \le N\)\(1 \le y_1\le y_2 \le N\)。输出 \(x_1, y_1, x_2, y_2\) 这个矩形内的数字和
  • 3 无 终止程序

\(1\leq N\leq 5\times 10^5\),操作数不超过 \(2\times 10^5\) 个,内存限制 \(20\texttt{MB}\),保证答案在 int 范围内并且解码之后数据仍合法。

题解

板子题

#include <cstdio>
#include <algorithm>
#include <cassert>
#include <iostream>
using namespace std;
const int max1 = 2e5;
const int inf = 0x3f3f3f3f;
const double alpha = 0.7;
struct Point
{
    int x, y;
    Point () {}
    Point ( int __x, int __y )
        { x = __x, y = __y; }
};
bool Lower ( const Point &A, const Point &B, const int &kind )
{
    if ( kind )
        return A.x != B.x ? A.x < B.x : A.y < B.y;
    return A.y != B.y ? A.y < B.y : A.x < B.x;
}
bool Upper ( const Point &A, const Point &B, const int &kind )
{
    if ( kind )
        return A.x != B.x ? A.x > B.x : A.y > B.y;
    return A.y != B.y ? A.y > B.y : A.x > B.x;
}
struct KD_Tree
{
    #define lson(now) tree[now].son[0]
    #define rson(now) tree[now].son[1]
    struct Struct_KD_Tree
    {
        int son[2], siz, minx, maxx, miny, maxy, val, sum;
        Point p;
    }tree[max1 + 5];
    int root, total;
    Point up;
    int s[max1 + 5], len;
    void Clear ()
    {
        root = total = lson(0) = rson(0) = tree[0].siz = tree[0].sum = 0;
        tree[0].minx = tree[0].miny = inf;
        tree[0].maxx = tree[0].maxy = -inf;
        return;
    }
    void Push_Up ( int now )
    {
        tree[now].siz = tree[lson(now)].siz + tree[rson(now)].siz + 1;
        tree[now].minx = min(tree[now].p.x, min(tree[lson(now)].minx, tree[rson(now)].minx));
        tree[now].maxx = max(tree[now].p.x, max(tree[lson(now)].maxx, tree[rson(now)].maxx));
        tree[now].miny = min(tree[now].p.y, min(tree[lson(now)].miny, tree[rson(now)].miny));
        tree[now].maxy = max(tree[now].p.y, max(tree[lson(now)].maxy, tree[rson(now)].maxy));
        tree[now].sum = tree[lson(now)].sum + tree[rson(now)].sum + tree[now].val;
        return;
    }
    bool Judge ( int now )
        { return tree[now].siz * alpha + 5 < max(tree[lson(now)].siz, tree[rson(now)].siz); }
    void Dfs ( int now )
    {
        if ( !now )
            return;
        Dfs(lson(now));
        s[++len] = now;
        Dfs(rson(now));
        return;
    }
    int Build ( int l, int r, int depth )
    {
        if ( l > r )
            return 0;
        int mid = l + r >> 1;
        nth_element(s + l, s + mid, s + r + 1, [&]( const int &A, const int &B ) -> bool { return Lower(tree[A].p, tree[B].p, depth & 1); });
        lson(s[mid]) = Build(l, mid - 1, depth + 1);
        rson(s[mid]) = Build(mid + 1, r, depth + 1);
        Push_Up(s[mid]);
        return s[mid];
    }
    int Rebuild ( int now, int depth, Point p )
    {
        assert(now);
        if ( Lower(p, tree[now].p, depth & 1) )
            lson(now) = Rebuild(lson(now), depth + 1, p);
        else if ( Upper(p, tree[now].p, depth & 1) )
            rson(now) = Rebuild(rson(now), depth + 1, p);
        else
            { len = 0; Dfs(now); now = Build(1, len, depth); }
        Push_Up(now);
        return now;
    }
    int Insert ( int now, int depth, Point p, int val )
    {
        if ( !now )
        {
            now = ++total;
            lson(now) = rson(now) = 0;
            tree[now].p = p;
            tree[now].val = 0;
        }
        if ( Lower(p, tree[now].p, depth & 1) )
            lson(now) = Insert(lson(now), depth + 1, p, val);
        else if ( Upper(p, tree[now].p, depth & 1) )
            rson(now) = Insert(rson(now), depth + 1, p, val);
        else
            tree[now].val += val;
        Push_Up(now);
        if ( Judge(now) )
            up = tree[now].p;
        return now;
    }
    void Insert ( Point p, int val )
    {
        up.x = -1;
        root = Insert(root, 1, p, val);
        if ( up.x != -1 )
            root = Rebuild(root, 1, up);
        return;
    }
    int Query ( int now, int x1, int x2, int y1, int y2 )
    {
        if ( !now )
            return 0;
        if ( tree[now].minx >= x1 && tree[now].maxx <= x2 && tree[now].miny >= y1 && tree[now].maxy <= y2 )
            return tree[now].sum;
        else if ( tree[now].minx > x2 || tree[now].maxx < x1 || tree[now].miny > y2 || tree[now].maxy < y1 )
            return 0;
        return Query(lson(now), x1, x2, y1, y2) + Query(rson(now), x1, x2, y1, y2) + ( tree[now].p.x >= x1 && tree[now].p.x <= x2 && tree[now].p.y >= y1 && tree[now].p.y <= y2 ? tree[now].val : 0 );
    }
    int Query ( int x1, int x2, int y1, int y2 )
        { return Query(root, x1, x2, y1, y2); }
}Tree;
int main ()
{
    int n, opt, x1, y1, x2, y2, val, last;
    scanf("%d", &n);
    Tree.Clear();
    last = 0;
    while ( true )
    {
        scanf("%d", &opt);
        if ( opt == 1 )
        {
            scanf("%d%d%d", &x1, &y1, &val);
            x1 ^= last, y1 ^= last, val ^= last;
            Tree.Insert(Point(x1, y1), val);
        }
        else if ( opt == 2 )
        {
            scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
            x1 ^= last, x2 ^= last, y1 ^= last, y2 ^= last;
            printf("%d\n", last = Tree.Query(x1, x2, y1, y2));
        }
        else
            break;
    }
    return 0;
}

李超线段树

例题

HEOI2013

要求在平面直角坐标系下维护两个操作:

  1. 在平面上加入一条线段。记第 \(i\) 条被插入的线段的标号为 \(i\)
  2. 给定一个数 \(k\),询问与直线 \(x = k\) 相交的线段中,交点纵坐标最大的线段的编号。

题解

李超树基本思路:

1.维护一棵线段树,每个区间 \(l,r\) 维护完全覆盖该区间所有线段中中点处 \(y\) 值最大的线段。

2.标记永久化。

考虑每次插入操作,首先将线段递归到线段树上的整区间,对于每个区间 \(l,r\) ,设当前插入线段编号为 \(f\) ,区间内维护线段编号为 \(g\)

如果中点处值 \(f\)\(g\) 优,则交换 \(f,g\)

考虑中点处值 \(f\)\(g\) 劣的情况。

如果左端点 \(f\)\(g\) 优,那么左区间可能被更新,向左区间递归。

如果右端点 \(f\)\(g\) 优,那么右区间可能被更新,向右区间递归。

注意:

我们每个节点并非维护了真正的中点处 \(y\) 值最大的线段,而是用于维护该种信息的 \(lazy\) 标记,由于是单点查询,我们无需将标记下放,因此标记永久化即可。

插入复杂度为 \(O(\log^2n)\) ,查询复杂度为 \(O(\log n)\)

#include <cstdio>
#include <algorithm>

using namespace std;

const int maX1 = 1e5, max2 = 39989;
int n, num, X0[maX1 + 5], X1[maX1 + 5], Y0[maX1 + 5], Y1[maX1 + 5];
int lastans;

int Transx ( int x )
{
    return (x + lastans - 1) % 39989 + 1;
}

int Transy ( int y )
{
    return (y + lastans - 1) % 1000000000 + 1;
}

double Get ( int id, int mid )
{
    if ( X0[id] == X1[id] )
        return 1.0 * max(Y0[id], Y1[id]);

    double k = 1.0 * (Y1[id] - Y0[id]) / (X1[id] - X0[id]);
    return k * (mid - X0[id]) + Y0[id];
}

bool Cmp ( int x, int y, int mid )
{
    double v1 = Get(x, mid), v2 = Get(y, mid);

    if ( abs(v1 - v2) < 1e-9 )
        return x < y;
    
    return v1 > v2;
}

struct Segment_Tree
{
    #define lson(now) (now << 1)
    #define rson(now) (now << 1 | 1)
    int tree[max2 * 4 + 5];

    void Build ( int now, int L, int R )
    {
        tree[now] = 0;

        if ( L == R )
            return;
        
        int mid = (L + R) >> 1;
        Build(lson(now), L, mid), Build(rson(now), mid + 1, R);
        return;
    }

    void Insert ( int now, int L, int R, int id )
    {
        int mid = (L + R) >> 1;

        if ( L >= X0[id] && R <= X1[id] )
        {
            if ( Cmp(id, tree[now], mid) )
                swap(id, tree[now]);

            if ( L == R )
                return;
            
            if ( Cmp(id, tree[now], L) )
                Insert(lson(now), L, mid, id);
            
            if ( Cmp(id, tree[now], R) )
                Insert(rson(now), mid + 1, R, id);
            
            return;
        }

        if ( X0[id] <= mid )
            Insert(lson(now), L, mid, id);
        if ( X1[id] > mid )
            Insert(rson(now), mid + 1, R, id);
        
        return;
    }

    int Query ( int now, int L, int R, int x )
    {
        if ( L == R )
            return tree[now];
        
        int mid = (L + R) >> 1, up;
        if ( x <= mid )
            up = Query(lson(now), L, mid, x);
        else
            up = Query(rson(now), mid + 1, R, x);
        
        if ( Cmp(tree[now], up, x) )
            up = tree[now];
        
        return up;
    }
}Tree;

int main ()
{
    int op, x;
    X0[0] = 1, X1[0] = 39989;
    Y0[0] = 0, Y1[0] = 0;
    Tree.Build(1, 1, max2);
    scanf("%d", &n);
    for ( int i = 1; i <= n; i ++ )
    {
        scanf("%d", &op);
        if ( op == 0 )
        {
            scanf("%d", &x);
            x = Transx(x);
            printf("%d\n", lastans = Tree.Query(1, 1, max2, x));
        }
        else
        {
            ++num;
            scanf("%d%d%d%d", &X0[num], &Y0[num], &X1[num], &Y1[num]);
            X0[num] = Transx(X0[num]);
            X1[num] = Transx(X1[num]);
            Y0[num] = Transy(Y0[num]);
            Y1[num] = Transy(Y1[num]);

            if ( X0[num] > X1[num] )
            {
                swap(X0[num], X1[num]);
                swap(Y0[num], Y1[num]);
            }

            Tree.Insert(1, 1, max2, num);
        }
    }

    return 0;
}

左偏树

posted @ 2024-07-18 16:15  KafuuChinocpp  阅读(166)  评论(0)    收藏  举报