并查集
1250. 格子游戏
- 并查集解决的是连通性(无向图联通分量)和传递性(家谱关系)问题,并且可以动态的维护。抛开格子不看,任意一个图中,增加一条边形成环当且仅当这条边连接的两点已经联通,于是可以将点分为若干个集合,每个集合对应图中的一个连通块。
- 并查集(典型并查集判断是否存在环的问题)
- 并查集中一般用一维的坐标会方便一些,所以这个题要把二维坐标转化成一维坐标,有个很常用的方式,(x,y) -> x*n+y,前提是x和y都从0开始
- 将每个坐标看成一个点值,为了方便计算,将所有的坐标横纵坐标都减1,第一个位置即(1,1)看成是0,(1,2)看成是1,依次类推,将所有的坐标横纵坐标都减1后,假设当前点是(x,y),则该点的映射值是a = (x * n + y),若向下画,则b = [(x + 1) * n + y],若向右画,则b = [x * n + y - 1]
- 并查集操作复杂度接近O(1),本来应该这题是O(n+m),但由于实现时只用了路径压缩,没有用什么什么合并,所以本质这道题复杂度是mlogn的,但这个logn非常小,因为并查集常数很小,
#include <iostream>
using namespace std;
const int N = 40010; // 200 * 200
int n, m;
int fa[N];
int get(int x, int y)
{
return x * n + y;
}
int find(int x)
{
if (fa[x] != x) fa[x] = find(fa[x]);
return fa[x];
}
int main()
{
cin >> n >> m;
// for (int i = 1; i <= n; i ++ ) fa[i] = i;
for (int i = 0; i < n * n; i ++ ) fa[i] = i; // wa!!!! [0, n * n - 1]
int res = 0;
for (int i = 1; i <= m; i ++ )
{
int x, y;
char d;
cin >> x >> y >> d;
x -- , y -- ;
int a = get(x, y);
int b;
if (d == 'D') b = get(x + 1, y);
else b = get(x, y + 1);
int pa = find(a), pb = find(b);
if (pa == pb)
{
res = i;
break;
}
fa[pa] = pb;
}
if (!res) cout << "draw" << endl;
else cout << res << endl;
return 0;
}
树状数组
- 树状数组相较于数组和前缀和数组的作用
- 采取的是二进制思想,例如求1~x的和,就可以用下图的方案在log(x)中算出;即可以在log(n)时间内算出前n项的前缀和
- 那如何求每个区间的和呢?先看看区间的性质,第一个区间包含 2 i 1 2^{i_1} 2i1个数,第二个区间包含 2 i 2 2^{i_2} 2i2个数,以此类推,然后发现 2 i 1 2^{i_1} 2i1是x的二进制表示的最后一位1, 2 i 2 2^{i_2} 2i2是x - 2 i 1 2^{i_1} 2i1的最后一位1。因此,在这样的(L, R]区间中,这个区间长度一定是R的二进制表示的最后一位1所对的次幂。(lowbit(x)是O(1)的),那么这个区间形式就可以改写为[R-lowbit( R)+1,R],那么就可以用数组,C[R]来表示这个区间的总和了,这样的区间最多有n个。
- C[x] = a[x - lowbit( x) + 1, x],对于每个x而言,1~x的所有数的和,最多可以拆成log(x)个C[x]的和,所以是log(n)
- 这样就挖掘出了所有不同C之间的关系
241. 楼兰图腾
#include <iostream>
#include <cstring>
using namespace std;
typedef long long ll;
const int N = 2e5 + 10;
int n;
int a[N], tr[N];
int Greater[N], lower[N];
int lowbit(int x)
{
return x & (-x);
}
void add(int x, int c)
{
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
int sum(int x)
{
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
// 从左到右
for (int i = 1; i <= n; i ++ )
{
int y = a[i];
Greater[i] = sum(n) - sum(y); // [y + 1, n] // y + 1 ~ n之间已出现的数的个数
lower[i] = sum(y - 1); // [1, y - 1] // 1 ~ y - 1之间已出现的数的个数
add(y, 1); // 记录y这个数已经出现了
}
memset(tr, 0, sizeof tr); // 清空,重新建树,之间是从左向右遍历数组建树,后面要从右向左了
ll res1 = 0, res2 = 0;
for (int i = n; i; i -- )
{
int y = a[i];
res1 += Greater[i] * (ll)(sum(n) - sum(y));
res2 += lower[i] * (ll)(sum(y - 1));
add(y, 1);
}
printf("%lld %lld", res1, res2);
return 0;
}
线段树
// 简单线段树的操作 :
//传入节点编号,用子节点信息来算父节点信息
push up(int u)
//将一段区间初始化为一颗线段树
build()
//修改操作,修改某一个点或者某一个区间(懒标记)
modify()
//查询某一段区间的信息
query()
- 定义 :线段树是一颗满二叉树,以一颗长度为10的序列为例 :
- N个叶结点的满二叉树有N + N/2 + N/4 + … = 2N-1个结点,因为在上述存储方式下,最后还有一层产生了空余,最后一层最坏情况下是上一层的两倍,也就是还有2N个,所以保存线段树的数组长度要不小于4N
- 线段树的查询操作,例如查询【5, 9】,有8个区间要查 。且可以发现为log(n),不过最多有4logn,而树状数组只有1logn,所以线段树复杂度更大
- 线段树的结构 :
//一般使用结构体来存储线段树,空间大小开四倍
struct Node{
int l,r; //维护的区间
int v; //维护的信息...
} tree[N*4];
- 线段树的建树 :
//build
void build(int u,int l,int r){ //构建节点u,其维护的是区间[l,r]
tr[u]={l,r};
if(l==r) return ; //已经是叶子节点
int mid=l+r>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
}
- push_up操作 :
//push_up操作,用子节点信息来更新父节点信息,以维护最大值为例
void push_up(int u){
tree[u].v=max(tree[u<<1].v,tree[u<<1|1].v);
}
- 查询操作 :
//query操作,用来查询某一段区间内的信息,以最大值为例
int query(int u,int l,int r){ //从u节点开始查询[l,r]区间内的某一信息
if(tree[u].l>=l&&tree[u].r<=r) return tree[u].v; //说明这一段的信息已经被完全包含,因此不需要继续向下递归,直接返回即可
int res=0;
//否则需要判断该递归那一边
int mid=tree[u].l+tree[u].r >> 1;
if(l<=mid) res=max(res,query(u<<1,l,r)); //递归左边并更新信息
if(mid<r) res=max(res,query(u<<1|1,l,r)); //递归右边并更新信息,切记是mid<r,无等号
return res;
}
- 修改操作
//query操作,用来查询某一段区间内的信息,以最大值为例
int query(int u,int l,int r){ //从u节点开始查询[l,r]区间内的某一信息
if(tree[u].l>=l&&tree[u].r<=r) return tree[u].v; //说明这一段的信息已经被完全包含,因此不需要继续向下递归,直接返回即可
int res=0;
//否则需要判断该递归那一边
int mid=tree[u].l+tree[u].r >> 1;
if(l<=mid) res=max(res,query(u<<1,l,r)); //递归左边并更新信息
if(mid<r) res=max(res,query(u<<1|1,l,r)); //递归右边并更新信息,切记是mid<r,无等号
return res;
}
1275. 最大数
- 这道题的两个操作可以被转化成上述两个操作,而这两个操作就是线段树的经典操作,动态修改某个位置上的数,动态查询某个区间内的最大值
- 不过,这个问题比较特殊,每个位置只会被修改一次,因此这题可以用RMQ算法做,可以看作静态问题,不过局限性比较大,只能处理静态数据。但其实不是如此,这题由于每次修改的时候依赖上一次查询的,是动态的问题,不能把它全部读进来然后预处理去做的,所以不能用RMQ
- 注意!!凡是只要修改某一个点,单点,的,都不需要用到懒标记,本身都可以做到Logn的复杂度;凡是涉及到区间的,修改整一个区间的,这样的操作,一般都需要加上懒标记,否则复杂度会退化
- pushup由儿子算父节点的信息,pushdown是父节点的修改更新到儿子上面
#include <iostream>
using namespace std;
typedef long long ll;
const int N = 2e5 + 10;
int m, p;
struct Node
{
int l, r;
int v; // 区间[l, r]中的最大值
}tr[N * 4];
void pushup(int u) // 由子结点的信息,来计算父节点的信息
{
tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);
}
void build(int u, int l, int r)
{
tr[u] = {l, r};
if (l == r) return ; // 叶子结点
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}
int query(int u, int l, int r)
{
if (tr[u].l >= l && tr[u].r <= r) return tr[u].v; // 树中结点,已经被完全包含在[l, r]中了
int mid = tr[u].l + tr[u].r >> 1;
int v = 0;
if (l <= mid) v = query(u << 1, l, r);
if (r > mid) v = max(v, query(u << 1 | 1, l, r));
return v;
}
void modify(int u, int x, int v)
{
if (tr[u].l == x && tr[u].r == x) tr[u].v = v; // 叶节点,递归出口
else
{
int mid = tr[u].l + tr[u].r >> 1;
if (x <= mid) modify(u << 1, x, v);
else modify(u << 1 | 1, x, v);
pushup(u); // 回溯,用子结点的信息更新父节点信息
}
}
int main()
{
int n = 0, last = 0; // n表示树中结点个数,last保存上一次查询的结果
scanf("%d%d", &m, &p);
// 初始化线段树,结点的区间最多为[1, m]
build(1, 1, m);
int x;
char op[2]; // 可以读空字符
while (m -- )
{
scanf("%s%d", op, &x);
if (*op == 'Q')
{
last = query(1, n - x + 1, n); // 查询[n - x + 1, n]内的最大值,u = 1,即从根结点开始查询
printf("%d\n", last);
}
else
{
modify(1, n + 1, ((ll)last + x) % p); // 在n + 1处插入 // 需要将last + x的结果强制转换成ll,否则可能会溢出
n ++ ; // 结点个数 ++
}
}
return 0;
}
可持久化数据结构
- 可持久化的前提 :这个数据结构本身的拓扑结构在操作过程中保持不变。
- 可持久化数据结构 :1.trie的可持久化。2.线段树的可持久化-主席树。比如线段树,只会变线段树里面的信息,线段树本身是不会发生变化的。树状数组每个结点的儿子也是固定不变的,变的只是里面存的信息。堆也是,结构不会发生变化,完全二叉树;而有些数据结构会发生拓扑序的变化,比如平衡树,左旋和右旋,操作后结点之间的拓扑序会发生变化。
- 可持久化解决的问题是什么 :可以存下来数据结构的所有历史版本
- 核心思想 :只会记录当前版本和前一个版本不一样的地方。比如线段树,每次修改,如果n个结点,操作涉及最多4logn个结点,每次操作最多改变的节点数只有o(logn)个。即,最多只会记录mlogn个结点
- 可持久化trie :
- 凡是有变化的点就裂开成一个新的点,比如根结点就每次必然有变化
256. 最大异或和
- 原始数据最多有3e5,再加上3e5个操作,因此整个序列长度最大是6e5
- 2^24正好大于10 ^7,所以trie长度最多为24,再加上根结点,所以每次最多建立25个新结点
#include <iostream>
using namespace std;
// 30w初始数据和30w新增, 而10的7次方小于2的24次方, 再加上根节点, 就是说每个数最多需要25位;
const int N = 6e5, M = N * 25;
int n, m;
int s[N]; // 前缀和序列
int tr[M][2];
int max_id[M]; // 用于记录当前根结点版本的最大id范围
int root[N], idx;
// i是第i个插入的数的i,k是现在取到第k位,p是上一个插入的数的结点号,q是当前节点号
void insert(int i, int k, int p, int q)
{
// 如果记录结束了
if (k < 0)
{
max_id[q] = i; // 记录当前节点(可能会被后面公用)所能到达的最大范围i
return ;
}
int v = s[i] >> k & 1;
// 如果前一个节点存在当前节点没有的分支, 那就把当前节点的这个空的路径指过去, 这就相当于复制! v ^ 1指的是与v相反的一侧
if (p) tr[q][v ^ 1] = tr[p][v ^ 1];
tr[q][v] = ++ idx; // 现在才是正常trie树插入,给v开辟一个新结点
insert(i, k - 1, tr[p][v], tr[q][v]); // 以v这个结点为父节点,继续往下处理
max_id[q] = max(max_id[tr[q][0]], max_id[tr[q][1]]);
}
int query(int root, int C, int L) // L为限制 即L ~ root之间的版本是符合要求的
{
int p = root;
for (int i = 23; i >= 0; i -- )
{
int v = C >> i & 1;
if (max_id[tr[p][v ^ 1]] >= L) p = tr[p][v ^ 1];
else p = tr[p][v];
}
return C ^ s[max_id[p]];
}
int main()
{
scanf("%d%d", &n, &m);
s[0] = 0;
max_id[0] = -1;
root[0] = ++ idx;
insert(0, 23, 0, root[0]);
for (int i = 1; i <= n; i ++ )
{
int x;
scanf("%d", &x);
s[i] = s[i - 1] ^ x;
root[i] = ++ idx;
insert(i, 23, root[i - 1], root[i]);
}
char op[2];
int l, r, x;
while (m -- )
{
scanf("%s", op);
if (*op == 'A')
{
scanf("%d", &x);
n ++ ;
s[n] = s[n - 1] ^ x;
root[n] = ++ idx;
insert(n, 23, root[n - 1], root[n]);
}
else
{
scanf("%d%d%d", &l, &r, &x);
// 至少要包住第r个点, 所以用r-1, 否则会因为异或把root[r]抵消掉
// l也同理
printf("%d\n", query(root[r - 1], s[n] ^ x, l - 1));
}
}
return 0;
}