数据结构
数据结构
线性基
线性相关与线性无关

线性基相当于向量在模 \(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
要求在平面直角坐标系下维护两个操作:
- 在平面上加入一条线段。记第 \(i\) 条被插入的线段的标号为 \(i\)。
- 给定一个数 \(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;
}

浙公网安备 33010602011771号