第四单元
一. 并查集
1.普通并查集
const int N=1e5;
int s[N];
int height[N];
void init_set(){
for(int i=1;i<=N;i++){
s[i]=i;
height[i]=0;
}
}
int find_set(int x){
if(x!=s[x])
s[x]=find_set(s[x]);
return s[x];
}
//防爆栈板find_set函数
/*
int find_set(int x){
int r=x;
while(s[r]!=r)
r=s[r];
int i=x,j;
while(i!=r){
j=s[i];
s[i]=r;
i=j;
}
return r;
}
*/
void merge_set(int x,int y){
x=find_set(x);
y=find_set(y);
if(height[x]==height[y]){
height[x]=height[x]+1;
s[y]=x;
}
else{
if(height[x]<height[y])
s[x]=y;
else
s[y]=x;
}
}
2.带权并查集
const int N = 200010;
int s[N];
int d[N];
int ans;
void init_set(){
for(int i = 0; i <= N; i++) { s[i] = i; d[i] = 0; }
}
int find_set(int x){
if(x != s[x]) {
int t = s[x];
s[x] = find_set(s[x]);
d[x] += d[t];
}
return s[x];
}
void merge_set(int a, int b,int v){
int roota = find_set(a), rootb = find_set(b);
s[roota] = rootb; //前缀和
d[roota] = d[b]- d[a] + v;
}
二. 树状数组
前言:树状数组可以解决的,线段树也可以解决,树状数组只是在一些问题上有奇效,优先建议练习时还是优先使用线段树
本质上,解决的问题实际是 差分数组 − > 原始数组 − > 前缀和数组 差分数组->原始数组->前缀和数组 差分数组−>原始数组−>前缀和数组 这其中要使用其二或其三的一种使用树优化效率的一种方法
1. 单点修改+区间查询
const int N = 1000;
#define lowbit(x) ((x) & - (x))
int tree[N]={0};
void update(int x, int d) { //单点修改:修改元素a[x], a[x] = a[x] + d
while(x <= N) {
tree[x] += d;
x += lowbit(x);
}
}
int sum(int x) { //查询前缀和:返回前缀和sum = a[1] + a[2] +... + a[x]
int ans = 0;
while(x > 0){
ans += tree[x];
x -= lowbit(x);
}
return ans;
}
2. 区间修改+单点查询
在上面的基础上,每个节点由差分数组组成
对于区间,左端L节点 + d +d +d,右端R+1节点 − d -d −d
用sum求得单点值
3. 区间修改+区间查询
在上面的基础上,每个节点由差分数组组成
s u m ( L , R ) = s u m ( 1 , R ) − s u m ( 1 , L − 1 ) sum(L,R)=sum(1,R)-sum(1,L-1) sum(L,R)=sum(1,R)−sum(1,L−1)
s u m ( 1 , R ) = ∑ i = 1 k D i − ∑ i = 1 k ( i − 1 ) D i sum(1,R)=\sum_{i=1}^{k}{D_i}-\sum_{i=1}^k{(i-1)D_i} sum(1,R)=i=1∑kDi−i=1∑k(i−1)Di
从而实现从差分数组到前缀和的转变
4. 单点修改+区间最值
const int N = 2e5+10;
int n,m,a[N],tree[N];
int lowbit(int x){return x&(-x);}
void update(int x,int value){ //更新tree[x]的最大值
while(x<=n){
tree[x]=value;
for(int i=1;i<lowbit(x);i<<=1)
tree[x]=max(tree[x],tree[x-i]);
x+=lowbit(x);
}
}
int query(int L,int R){ //关于区间[L,R]的最值
int ans=;
while(L<R){
ans=max(ans,a[R]);
R--;
while(R-L>=lowbit(R)){
ans=max(ans,tree[R]);
R-=lowbit(R);
}
}
return ans;
}
三. 线段树
1. 区间查询
int tree[N*4];
int ls(int p){
return p<<1;
}
int rs(int p){
return p<<1|1;
}
void push_up(int p){
tree[p]=tree[ls(p)]+tree[rs(p)];
//tree[p]=min(tree[ls(p)],tree[rs(p)]);
/*
注意这个地方决定了子区间与父区间之间的关系
*/
}
void build(int p,int pl,int pr){ //默认(1,1,n)建树,基本这个保持不变
if(pl==pr){
tree[p]=a[pl];
return;
}
int mid=(pl+pr)>>1;
build(ls(p),pl.mid);
build(rs(p),mid+1,pr);
push_up(p);
}
int quary(int L, int R, int p, int pl, int pr) { //找[L,R]区间内的值,后面三位默认1,1,n
if(L <= pl && pr <= R)
return tree[P];
ll mid = (pl + pr) >> 1;
ll res = 0;
if(L <= mid)
res += query(L, R, ls(p), pl, mid);
if(R > mid)
res += query(L, R, rs(p), mid + 1, pr);
return res;
}
2. 区间修改+区间查询
ll a[N];
ll tree[N << 2];
ll tag[N << 2];
ll ls(ll p) {
return p << 1;
}
ll rs(ll p) {
return p << 1 | 1;
}
void push_up(ll p) {
tree[p] = tree[ls(p)] + tree[rs(p)];
}
void build(ll p, ll pl, ll pr) {
tag[p] = 0;
if(pl == pr) {
tree[p] = a[pl];
return;
}
ll mid = (pl + pr) >> 1;
build(ls(p), pl, mid);
build(rs(p), mid + 1, pr);
push_up(p);
}
void addtag(ll p, ll pl, ll pr, ll d) {//注意:如果想使用区间修改来优化,重点就是修改这一部分
tag[p] += d;
tree[p] += d * (pr - pl + 1);
}
void push_down(ll p, ll pl, ll pr) {
if(tag[p]) {
ll mid = (pl + pr) >> 1;
addtag(ls(p), pl, mid, tag[p]);
addtag(rs(p), mid + 1, pr, tag[p]);
tag[p] = 0;
}
}
void update(ll L, ll R, ll p, ll pl, ll pr, ll d) {
if(L <= pl && pr <= R) {
addtag(p, pl, pr, d);
return;
}
push_down(p, pl, pr);
ll mid = (pl + pr) >> 1;
if(L <= mid)
update(L, R, ls(p), pl, mid, d);
if(R > mid)
update(L, R, rs(p), mid + 1, pr, d);
push_up(p);
}
ll query(ll L, ll R, ll p, ll pl, ll pr) {
if(pl >= L && R >= pr)
return tree[p];
push_down(p, pl, pr);
ll res = 0;
ll mid = (pl + pr) >> 1;
if(L <= mid)
res += query(L, R, ls(p), pl, mid);
if(R > mid)
res += query(L, R, rs(p), mid + 1, pr);
return res;
}
3.区间最值
主要是通过tag标签来简化区间运算的,重点和难点在于tag的定义
4. 区间合并
主要是通过tag标签来简化区间运算的,重点和难点在于tree数组的定义
5.扫描线
1. 面积
int ls(int p){ return p<<1; }
int rs(int p){ return p<<1|1;}
const int N = 20005;
int Tag[N]; //标志:线段是否有效,能否用于计算宽度
double length[N]; //存放区间i的总宽度
double xx[N]; //存放矩形右边x坐标值,下标用lower_bound查找
struct ScanLine{ //定义扫描线
double y; //边的y坐标
double right_x,left_x; //边的x坐标:右、左
int inout; //入边为1,出边为-1
ScanLine(){}
ScanLine(double y,double x2,double x1,int io):
y(y),right_x(x2),left_x(x1),inout(io){}
}line[N];
bool cmp(ScanLine &a,ScanLine &b) { return a.y<b.y; } //y坐标排序
void pushup(int p,int pl,int pr){ //从下往上传递区间值
if(Tag[p]) length[p] = xx[pr]-xx[pl];
//结点的Tag为正,这个线段对计算宽度有效。计算宽度
else if(pl+1 == pr) length[p] = 0; //叶子结点没有宽度
else length[p] = length[ls(p)] + length[rs(p)];
}
void update(int L,int R,int io,int p,int pl,int pr){
if(L<=pl && pr<=R){ //完全覆盖
Tag[p] += io; //结点的标志,用来判断能否用来计算宽度
pushup(p,pl,pr);
return;
}
if(pl+1 == pr) return; //叶子结点
int mid = (pl+pr) >> 1;
if(L<=mid) update(L,R,io,ls(p),pl,mid);
if(R>mid) update(L,R,io,rs(p),mid,pr); //注意不是mid+1
pushup(p,pl,pr);
}
int main(){
int n, t = 0;
while(scanf("%d",&n),n){
int cnt = 0; //边的数量,包括入边和出边
while(n--){
double x1,x2,y1,y2; scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2);//输入一个矩形
line[++cnt] = ScanLine(y1,x2,x1,1); //给入边赋值
xx[cnt] = x1; //记录x坐标
line[++cnt] = ScanLine(y2,x2,x1,-1); //给出边赋值
xx[cnt] = x2; //记录x坐标
}
sort(xx+1,xx+cnt+1); //对所有边的x坐标排序
sort(line+1,line+cnt+1,cmp); //对扫描线按y轴方向从低到高排序
int num = unique(xx+1,xx+cnt+1)-(xx+1); //离散化:用unique去重,返回个数
memset(Tag,0,sizeof(Tag));
memset(length,0,sizeof(length));
double ans = 0;
for(int i=1;i<=cnt;++i) { //扫描所有入边和出边
int L,R;
ans += length[1]*(line[i].y-line[i-1].y);//累加当前扫描线的面积=宽*高
L = lower_bound(xx+1,xx+num+1,line[i].left_x)-xx;
//x坐标离散化:用相对位置代替坐标值
R = lower_bound(xx+1,xx+num+1,line[i].right_x)-xx;
update(L,R,line[i].inout,1,1,num);
}
printf("Test case #%d\nTotal explored area: %.2f\n\n",++t,ans);
}
return 0;
}
2. 周长
int ls(int p){ return p<<1; }
int rs(int p){ return p<<1|1;}
const int N = 200005;
struct ScanLine {
int l, r, h, inout; //inout=1 下边, inout=-1 上边
ScanLine() {}
ScanLine(int a, int b, int c, int d) :l(a), r(b), h(c), inout(d) {}
}line[N];
bool cmp(ScanLine &a, ScanLine &b) { return a.h<b.h; } //y坐标排序
bool lbd[N<<2], rbd[N<<2]; //标记这个结点的左右两个端点是否被覆盖(0表示没有,1表示有)
int num[N << 2]; //这个区间有多少条独立的边
int Tag[N << 2]; //标记这个结点是否有效
int length[N << 2]; //这个区间的有效宽度
void pushup(int p, int pl, int pr) {
if (Tag[p]) { //结点的Tag为正,这个线段对计算宽度有效
lbd[p] = rbd[p] = 1;
length[p] = pr - pl + 1;
num[p] = 1; //每条边有两个端点
}
else if (pl == pr) length[p]=num[p]=lbd[p]=rbd[p]=0;//叶子结点
else {
lbd[p] = lbd[ls(p)]; //和左儿子共左端点
rbd[p] = rbd[rs(p)]; //和右儿子共右端点
length[p] = length[ls(p)] + length[rs(p)];
num[p] = num[ls(p)] + num[rs(p)];
if (lbd[rs(p)] && rbd[ls(p)]) num[p] -= 1; //合并边
}
}
void update(int L, int R, int io, int p, int pl, int pr) {
if(L<=pl && pr<=R){ //完全覆盖
Tag[p] += io;
pushup(p, pl, pr);
return;
}
int mid = (pl + pr) >> 1;
if (L<= mid) update(L, R, io, ls(p), pl, mid);
if (mid < R) update(L, R, io, rs(p), mid+1, pr);
pushup(p, pl, pr);
}
int main() {
int n;
while (~scanf("%d", &n)) {
int cnt = 0;
int lbd = 10000, rbd = -10000;
for (int i = 0; i < n; i++) {
int x1,y1,x2,y2; scanf("%d%d%d%d", &x1,&y1,&x2,&y2); //输入矩形
lbd = min(lbd, x1); //横线最小x坐标
rbd = max(rbd, x2); //横线最大x坐标
line[++cnt] = ScanLine(x1, x2, y1, 1); //给入边赋值
line[++cnt] = ScanLine(x1, x2, y2, -1); //给出边赋值
}
sort(line+1, line + cnt+1, cmp); //排序。数据小,不用离散化
int ans = 0, last = 0; //last:上一次总区间被覆盖长度
for (int i = 1; i <= cnt ; i++){ //扫描所有入边和出边
if (line[i].l < line[i].r)
update(line[i].l, line[i].r-1, line[i].inout, 1, lbd, rbd-1);
ans += num[1]*2 * (line[i + 1].h - line[i].h); //竖线
ans += abs(length[1] - last); //横线
last = length[1];
}
printf("%d\n", ans);
}
return 0;
}
四. 可持久化线段树(主席树)
一种记录多个历史时期的线段树的算法,一般用于求第k大/小问题,虽然在空间复杂度上已经有很好的优化,但是时间复杂度相较于分块还是太大,建议使用莫队算法解决这类问题
第k小/大问题
const int N = 200010;
int cnt = 0; //用cnt标记可以使用的新结点
int a[N], b[N], root[N]; //a[]是原数组,b[]是排序后数组,root[i]记录第i棵线段树的根结点编号
struct{ //定义结点
int L, R, sum; //L左儿子, R右儿子,sum[i]是结点i的权值(即图中圆圈内的数字)
}tree[N<<5]; // <<4是乘16倍,不够用;<<5差不多够用
int build(int pl, int pr){ //初始化一棵空树,实际上无必要
int rt = ++ cnt; //cnt为当前结点编号
tree[rt].sum = 0;
int mid=(pl+pr)>>1;
if (pl < pr){
tree[rt].L = build(pl, mid);
tree[rt].R = build(mid+1, pr);
}
return rt; //返回当前结点的编号
}
int update(int pre, int pl, int pr, int x){ //建一棵只有logn个结点的新线段树
int rt = ++cnt; //新的结点,下面动态开点
tree[rt].L = tree[pre].L; //该结点的左右儿子初始化为前一棵树相同位置结点的左右儿子
tree[rt].R = tree[pre].R;
tree[rt].sum = tree[pre].sum + 1; //插了1个数,在前一棵树的相同结点加1
int mid = (pl+pr)>>1;
if (pl < pr){ //从根结点往下建logn个结点
if (x <= mid) //x出现在左子树,修改左子树
tree[rt].L = update(tree[pre].L, pl, mid, x);
else //x出现在右子树,修改右子树
tree[rt].R = update(tree[pre].R, mid+1, pr, x);
}
return rt; //返回当前分配使用的新结点的编号
}
int query(int u, int v, int pl, int pr, int k){ //查询区间[u,v]第k小
if (pl == pr) return pl; //到达叶子结点,找到第k小,pl是结点编号,答案是b[pl]
int x = tree[tree[v].L].sum - tree[tree[u].L].sum; //线段树相减
int mid = (pl+pr)>>1;
if (x >= k) //左儿子数字大于等于k时,说明第k小的数字在左子树
return query(tree[u].L, tree[v].L, pl, mid, k);
else //否则在右子树找第k-x小的数字
return query(tree[u].R, tree[v].R, mid+1, pr, k-x);
}
int main(){
int n, m; scanf("%d%d", &n, &m);
for (int i=1; i<=n; i++){ scanf("%d", &a[i]); b[i]=a[i]; }
sort(b+1, b+1+n); //对b排序
int size = unique(b+1, b+1+n)-b-1; //size等于b数组中不重复的数字的个数
//root[0] = build(1, size); //初始化一棵包含size个元素的空树,实际上无必要
for (int i = 1; i <= n; i ++){ //建n棵线段树
int x = lower_bound(b+1, b+1+size, a[i]) - b;
//找等于a[i]的b[x]。x是离散化后a[i]对应的值
root[i] = update(root[i-1], 1, size, x);
//建第i棵线段树,root[i]是第i棵线段树的根结点
}
while (m--){
int x, y, k; scanf("%d%d%d", &x, &y, &k);
int t = query(root[x-1], root[y], 1, size, k);
//第y棵线段树减第x-1棵线段树,就是区间[x,y]的线段树
printf("%d\n", b[t]);
}
return 0;
}
五. 分块与莫队算法
莫队算法=离线+暴力+分块
typedef long long ll;
const ll maxn = 5e5 + 5;
ll a[maxn];
ll partLen; //块长
ll partID[maxn]; //块号
ll partSum[maxn]; //块和
ll tag[maxn]; //块标记
void add(ll l, ll r, ll c)
{
ll startID = partID[l], endID = partID[r];
if (startID == endID) //l和r在同一个块,直接暴力
{
for (ll i = l; i <= r; i++)
{
a[i] += c;
partSum[startID] += c;
}
return;
}
//不在同一个块,分成三段处理
for (ll i = l; partID[i] == startID; i++)//起始段
{
a[i] += c;
partSum[startID] += c;
}
for (ll i = startID + 1; i < endID; i++)//中间若干个整块
{
tag[i] += c;
partSum[i] += c * partLen;
}
for (ll i = r; partID[i] == endID; i--)//末尾段
{
a[i] += c;
partSum[endID] += c;
}
}
//查询的思想和修改基本相同
ll query(ll l, ll r, ll c)
{
ll ans = 0;
ll startID = partID[l], endID = partID[r];
if (startID == endID)
{
for (ll i = l; i <= r; i++)
{
ans += a[i] + tag[endID];
ans %= c;
}
return ans;
}
for (ll i = l; partID[i] == startID; i++)
{
ans += a[i] + tag[startID];
ans %= c;
}
for (ll i = startID + 1; i < endID; i++)
{
ans += partSum[i];
ans %= c;
}
for (ll i = r; partID[i] == endID; i--)
{
ans += a[i] + tag[endID];
ans %= c;
}
return ans;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
ll n;
cin >> n;
partLen = sqrt(n);
memset(partID, -1, sizeof(partID));
for (ll i = 0; i < n; i++)
{
cin >> a[i];
partID[i] = i / partLen + 1;// 第i个应该在那个位置
partSum[partID[i]] += a[i];
}
for (ll i = 0; i < n; i++)
{
ll opt, l, r, c;
cin >> opt >> l >> r >> c;
l--, r--;
if (opt)
cout << query(l, r, c + 1) << '\n';
else
add(l, r, c);
}
return 0;
}
六. 块状链表
块状链表=分块+链表
注意:莫队算法跟这个都是分块算法,莫队算法不修改数据的规模,块状链表修改数据的规模
int block = 2500; //一个块的标准大小 = sqrt(n)
list<vector<char> > List; //整体是链表,链表的每个元素是一个块
typedef list<vector<char> >::iterator it;
it Find(int &pos) { //返回块,并更新x为这个块中的位置
for (it i = List.begin(); ;i++) { //逐个找链表上的每个块
if(i == List.end() || pos <= i->size()) return i;
pos -= i->size(); //每经过一个块,就更新x
}
}
void Output(int L, int R) { // [L, R)
it L_block = Find(L), R_block = Find(R);
for (it it1 = L_block; ; it1++){ //打印每个块
int a; it1 == L_block ? a=L : a=0; //一个块的起点
int b; it1 == R_block ? b=R : b=it1->size(); //块的终点
for (int i = a; i < b; i++) putchar(it1->at(i));
if(it1 == R_block) break; //迭代器it不能用 <= ,只有 == 和 !=
}
putchar('\n');
}
it Next(it x){return ++x; } //返回下一个块
void Merge(it x) { //合并块x和块x+1
x->insert(x->end(), Next(x)->begin(), Next(x)->end());
List.erase(Next(x));
}
void Split(it x, int pos){ //把第x个块在这个块的pos处分成2块
if (pos == x->size()) return; //pos在这个块的末尾
List.insert(Next(x), vector<char>(x->begin() + pos, x->end()));
//把pos后面的部分划给下一个块
x->erase(x->begin() + pos, x->end()); //删除划出的部分
}
void Update(){ //把每个块重新划成等长的块
for (it i = List.begin(); i != List.end(); i++){
while (i->size() >= (block << 1)) //如果块大于2个block,分开
Split(i, i->size() - block);
while (Next(i) != List.end() && i->size() + Next(i)->size() <= block)
Merge(i); //如果块+下一个块小于block,合并
while (Next(i) != List.end() && Next(i)->empty()) //删除最后的空块
List.erase(Next(i));
}
}
void Insert(int pos, const vector<char>& ch){
it curr = Find(pos);
if (!List.empty()) Split(curr, pos); //把一个块拆为两个
List.insert(Next(curr), ch); //把字符串插到两个块中间
Update();
}
void Delete(int L, int R) { // [L, R)区间的元素
it L_block, R_block;
L_block = Find(L); Split(L_block, L);
R_block = Find(R); Split(R_block, R);
R_block++;
while(Next(L_block) != R_block) List.erase(Next(L_block));
Update();
}
int main(){
vector<char> ch; int len, pos, n;
cin >> n;
while (n--) {
char opt[7]; cin >> opt;
if(opt[0]=='M') cin >> pos;
if(opt[0]=='I'){
ch.clear(); cin >> len; ch.resize(len);
for (int i = 0; i < len; i++){
ch[i] = getchar();
while(ch[i]<32||ch[i]>126) ch[i]=getchar();
} //读一个合法字符
Insert(pos, ch); //把字符串插入到链表中
}
if(opt[0]=='D'){ cin >> len; Delete(pos, pos + len); }
if(opt[0]=='G'){ cin >> len; Output(pos, pos + len); }
if(opt[0]=='P') pos--;
if(opt[0]=='N') pos++;
}
return 0;
}
七. 简单树上问题
1. 树的重心
重心的定义:以某一节点为根,如果该节点的最大子树的节点最小,则u为树的重心(也就是子树最均衡)
const int N = 50005; //最大结点数
struct Edge{ int to, next;} edge[N<<1]; //两倍:u-v, v-u
int head[N], cnt = 0;
void init(){ //链式前向星:初始化
for(int i=0; i<N; ++i){
edge[i].next = -1;
head[i] = -1;
}
cnt = 0;
}
void addedge(int u,int v){ //链式前向星:加边u-v
edge[cnt].to = v;
edge[cnt].next = head[u];
head[u] = cnt++;
}
int n;
int d[N], ans[N], num=0, maxnum=1e9; //d[u]: 以u为根的子树的结点数量
void dfs(int u,int fa){
d[u] = 1; //递归到最底层时,结点数加1
int tmp = 0;
for(int i=head[u]; ~i; i=edge[i].next){ //遍历u的子结点。~i也可以写成i!=-1
int v = edge[i].to; //v是一个子结点
if(v == fa) continue; //不递归父亲
dfs(v,u); //递归子结点,计算v这个子树的结点数量
d[u] += d[v]; //计算以u为根的结点数量
tmp = max(tmp,d[v]); //记录u的最大子树的结点数量
}
tmp = max(tmp, n-d[u]); //tmp = u的最大连通块的结点数
//以上计算出了u的最大连通块
//下面统计疑似教父。如果一个结点的最大连通块比其他结点的都小,它是疑似教父
if(tmp < maxnum){ //一个疑似教父
maxnum = tmp; //更新“最小的”最大连通块
num = 0;
ans[++num] = u; //把教父记录在第1个位置
}
else if(tmp == maxnum) ans[++num] = u; //疑似教父有多个,记录在后面
}
int main(){
scanf("%d",&n);
init();
for(int i=1; i<n; i++){
int u, v; scanf("%d %d", &u, &v);
addedge(u,v); addedge(v,u);
}
dfs(1,0);
sort(ans+1, ans+1+num);
for(int i=1;i<=num;i++) printf("%d ",ans[i]);
}
2. 树的直径
树的直径是树上最远的两点间的距离,又被称为树的最远点对。
1. 两次DFS
- 需要完整的路径
- 不可以有负权边
const int N=1e5+10;
struct edge{ int to,w;}; //to: 边的终点 w:权值
vector<edge> e[N]; //用邻接表存边
int dist[N]; //记录距离
void dfs(int u,int father,int d){ //用dfs计算从u到每个子结点的距离
dist[u]=d;
for(int i=0;i<e[u].size();i++)
if(e[u][i].to != father) //很关键,这一句保证不回头搜父结点
dfs(e[u][i].to, u, d + e[u][i].w);
}
int main(void){
int n; cin>>n;
for(int i=0;i<n-1;i++){
int a,b,w; cin>>a>>b>>w;
e[a].push_back({b,w}); //a的邻居是b,边长w
e[b].push_back({a,w}); //b的邻居是a
}
dfs(1,-1,0); //计算从任意点(这里用1号点)到树上每个结点的距离
int s = 1;
for(int i=1;i<=n;i++) //找最远的结点s, s是直径的一个端点
if(dist[i]>dist[s]) s = i;
dfs(s,-1,0); //从s出发,计算以s为起点,到树上每个结点的距离
int t = 1;
for(int i=1;i<=n;i++) //找直径的另一个端点t
if(dist[i]>dist[t]) t = i;
cout << dist[t]<<endl; //打印树的直径的长度
return 0;
}
2. 树形DP
只可以求长度
可以求负权边
const int N=1e5+10;
struct edge{int to,w; }; //to: 边的终点 w:权值
vector<edge> e[N];
int dp[N];
int maxlen = 0;
bool vis[N];
void dfs(int u){
vis[u] = true;
for(int i = 0; i < e[u].size(); ++ i){
int v = e[u][i].to, edge = e[u][i].w;
if(vis[v]) continue; //v已经算过
dfs(v);
maxlen = max(maxlen, dp[u]+ dp[v]+ edge);
//计算max{f[u]}。注意此时dp[u]不包括v这棵子树,下一行才包括
dp[u] = max(dp[u], dp[v] + edge); //计算dp[u],此时包括了v这棵子树
}
return ;
}
int main(){
int n; cin >> n;
for(int i = 0; i < n-1; i++){
int a, b, w; cin >> a >> b >> w;
e[a].push_back({b,w}); //a的邻居是b,路的长度w
e[b].push_back({a,w}); //b的邻居是a
}
dfs(1); //从点1开始DFS
cout << maxle
n << endl;
return 0;
}
八. LCA
即最近公共祖先,在x,y的公共祖先中,深度最大的被称为最近公共祖先
1. 基础算法
1. 倍增法
在线算法,可以单独处理每个查询
const int N=500005;
struct Edge{int to, next;}edge[2*N]; //链式前向星
int head[2*N], cnt;
void init(){ //链式前向星:初始化
for(int i=0;i<2*N;++i){ edge[i].next = -1; head[i] = -1; }
cnt = 0;
}
void addedge(int u,int v){ //链式前向星:加边
edge[cnt].to = v; edge[cnt].next = head[u]; head[u] = cnt++;
} //以上是链式前向星
int fa[N][20], deep[N];
void dfs(int x,int father){ //求x的深度deep[x]和fa[x][]。father是x的父结点。
deep[x] = deep[father]+1; //深度:比父结点深度多1
fa[x][0] = father; //记录父结点
for(int i=1; (1<<i) <= deep[x]; i++) //求fa[][]数组,它最多到根结点
fa[x][i] = fa[fa[x][i-1]][i-1];
for(int i=head[x]; ~i; i=edge[i].next) //遍历结点i的所有孩子。~i可写为i!=-1
if(edge[i].to != father) //邻居:除了父亲,都是孩子
dfs(edge[i].to, x);
}
int LCA(int x,int y){
if(deep[x]<deep[y]) swap(x,y); //让x位于更底层,即x的深度值更大
//(1)把x和y提到相同的深度
for(int i=19;i>=0;i--) //x最多跳19次:2^19 > 500005
if(deep[x]-(1<<i)>=deep[y]) //如果x跳过头了就换个小的i重跳
x = fa[x][i]; //如果x还没跳到y的层,就更新x继续跳
if(x==y) return x; //y就是x的祖先
//(2)x和y同步往上跳,找到LCA
for(int i=19;i>=0;i--) //如果祖先相等,说明跳过头了,换个小的i重跳
if(fa[x][i]!=fa[y][i]){ //如果祖先不等,就更新x、y继续跳
x = fa[x][i]; y = fa[y][i];
}
return fa[x][0]; //最后x位于LCA的下一层,父结点fa[x][0]就是LCA
}
int main(){
init(); //初始化链式前向星
int n,m,root; scanf("%d%d%d",&n,&m,&root);
for(int i=1;i<n;i++){ //读一棵树,用链式前向星存储
int u,v; scanf("%d%d",&u,&v);
addedge(u,v); addedge(v,u);
}
dfs(root,0); //计算每个结点的深度并预处理fa[][]数组
while(m--){
int a,b; scanf("%d%d",&a,&b);
printf("%d\n", LCA(a,b));
}
return 0;
}
2. Tarjan算法
离线算法,对查询进行排序后再计算,可以得到很好的效率
const int N=500005;
int fa[N], head[N], cnt, head_query[N], cnt_query, ans[N];
bool vis[N];
struct Edge{ int to, next, num;}edge[2*N], query[2*N]; //链式前向星
void init(){ //链式前向星:初始化
for(int i=0;i<2*N;++i){
edge[i].next = -1; head[i] = -1;
query[i].next = -1; head_query[i] = -1;
}
cnt = 0; cnt_query = 0;
}
void addedge(int u,int v){ //链式前向星:加边
edge[cnt].to = v;
edge[cnt].next = head[u];
head[u] = cnt++;
}
void add_query(int x, int y, int num) { //num 第几个查询
query[cnt_query].to = y;
query[cnt_query].num = num; //第几个查询
query[cnt_query].next = head_query[x];
head_query[x] = cnt_query++;
}
int find_set(int x) { //并查集查询
return fa[x] == x ? x : find_set(fa[x]);
}
void tarjan(int x){ //tarjan是一个DFS
vis[x] = true;
for(int i=head[x]; ~i; i=edge[i].next){ // ~i可以写为i!=-1
int y = edge[i].to;
if( !vis[y] ) { //遍历子结点
tarjan(y);
fa[y] = x; //合并并查集:把子结点y合并到父结点x上
}
}
for(int i = head_query[x]; ~i; i = query[i].next){ //查询所有和x有询问关系的y
int y = query[i].to;
if( vis[y]) //如果to被访问过
ans[query[i].num] = find_set(y); //LCA就是find(y)
}
}
int main () {
init();
memset(vis, 0, sizeof(vis));
int n,m,root; scanf("%d%d%d",&n,&m,&root);
for(int i=1;i<n;i++){ //读n个结点
fa[i] = i; //并查集初始化
int u,v; scanf("%d%d",&u,&v);
addedge(u,v); addedge(v,u); //存边
}
fa[n] = n; //并查集的结点n
for(int i = 1; i <= m; ++i) { //读m个询问
int a, b; scanf("%d%d",&a,&b);
add_query(a, b, i); add_query(b, a, i); //存查询
}
tarjan(root);
for(int i = 1; i <= m; ++i) printf("%d\n",ans[i]);
}
2. 树上两点之间的最短距离
两点深度之和减去两倍的LCA深度
d i s t ( x , y ) = d e e p [ x ] + d e e p [ y ] − 2 ∗ d e p p [ L C A ( x , y ) ] dist(x,y)=deep[x]+deep[y]-2*depp[LCA(x,y)] dist(x,y)=deep[x]+deep[y]−2∗depp[LCA(x,y)]
3. 树上差分
树上两点 u − > v u->v u−>v可看做 u − > L C A ( x , y ) u->LCA(x,y) u−>LCA(x,y)和 v − > L C A ( x , y ) v->LCA(x,y) v−>LCA(x,y),设x为L,LCA(x,y)的父节点为R+1,D[L]++,D[R+1]–,对另一条线路同理
//洛谷P3128,LCA + 树上差分
#include <bits/stdc++.h>
using namespace std;
#define N 50010
struct Edge{int to,next;}edge[2*N]; //链式前向星
int head[2*N],D[N],deep[N],fa[N][20],ans,cnt;
void init();
void addedge(int u,int v);
void dfs1(int x,int father);
int LCA(int x,int y); //以上4个函数和“树上的倍增”中洛谷P3379的倍增代码完全一样
void dfs2(int u,int fath){
for (int i=head[u];~i;i=edge[i].next){ //遍历结点i的所有孩子。~i可以写为i!=-1
int e=edge[i].to;
if (e==fath) continue;
dfs2(e,u);
D[u]+=D[e];
}
Ans = max(ans,D[u]);
}
int main(){
init(); //链式前向星初始化
int n,m; scanf("%d%d",&n,&m);
for (int i=1;i<n;++i){
int u,v; scanf("%d%d",&u,&v);
addedge(u,v); addedge(v,u);
}
dfs1(1,0); //计算每个结点的深度并预处理fa[][]数组
for (int i=1; i<=m; ++i){
int a,b; scanf("%d%d",&a,&b);
int lca = LCA(a,b);
D[a]++; D[b]++; D[lca]--; D[fa[lca][0]]--; //树上差分
}
dfs2(1,0); //用差分数组求每个结点的权值
printf("%d\n",ans);
return 0;
}
九. 树上的分治
点分治:核心是让子树的节点数接近,也就是使最大子树的子节点树最少
1. 静态分治点
只查询不修改,把树上内容分解为一个个独立的子树,进行求解
[例题](P3806 【模板】点分治 1 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))
2. 动态分支点
查改+修改+强制在线,即点分树
[例题](ZJOI 2007 Hide 捉迷藏 题解-优快云博客)
十. 树链剖分
- 修改点x到点y的路径上的个点权值
- 查询点x到点y的路径上节点权值之和
- 修改点x子树上各点的权值
- 查询点x子树上所有节点的权值之和
1. 点权值
#include<bits/stdc++.h>
using namespace std;
const int N=100000+10;
int n,m,r,mod;
//以下是链式前向星
struct Edge{int to, next;}edge[2*N];
int head[2*N], cnt;
void init(); //与前一小节“洛谷P3379树链剖分”的init()一样
void addedge(int u,int v); //与前一小节“洛谷P3379树链剖分”的addedge()一样
//以下是线段树
int ls(int x){ return x<<1; } //定位左儿子:x*2
int rs(int x){ return x<<1|1;} //定位右儿子:x*2 + 1
int w[N],w_new[N]; //w[]、w_new[]初始点权
int tree[N<<2], tag[N<<2]; //线段树数组、lazy-tag操作
void addtag(int p,int pl,int pr,int d){ //给结点p打tag标记,并更新tree
tag[p] += d; //打上tag标记
tree[p] += d*(pr-pl+1); tree[p] %= mod; //计算新的tree
}
void push_up(int p){ //从下往上传递区间值
tree[p] = tree[ls(p)] + tree[rs(p)]; tree[p] %= mod;
}
void push_down(int p,int pl, int pr){
if(tag[p]){
int mid = (pl+pr)>>1;
addtag(ls(p),pl,mid,tag[p]); //把tag标记传给左子树
addtag(rs(p),mid+1,pr,tag[p]); //把tag标记传给右子树
tag[p] = 0;
}
}
void build(int p,int pl,int pr){ //建线段树
tag[p] = 0;
if(pl==pr){
tree[p] = w_new[pl]; tree[p] %= mod;
return;
}
int mid = (pl+pr) >> 1;
build(ls(p),pl,mid);
build(rs(p),mid+1,pr);
push_up(p);
}
void update(int L,int R,int p,int pl,int pr,int d){
if(L<=pl && pr<=R){ addtag(p, pl, pr,d); return; }
push_down(p,pl,pr);
int mid = (pl+pr) >> 1;
if(L<=mid) update(L,R,ls(p),pl,mid,d);
if(R> mid) update(L,R,rs(p),mid+1,pr,d);
push_up(p);
}
int query(int L,int R,int p,int pl,int pr){
if(pl>=L && R >= pr) return tree[p] %= mod;
push_down(p,pl,pr);
int res =0;
int mid = (pl+pr) >> 1;
if(L<=mid) res += query(L,R,ls(p),pl,mid);
if(R> mid) res += query(L,R,rs(p),mid+1,pr);
return res;
}
//以下是树链剖分
int son[N],id[N],fa[N],deep[N],siz[N],top[N];
void dfs1(int x, int father); //与前一小节“洛谷P3379树链剖分”dfs1()一样
int num = 0;
void dfs2(int x,int topx){ //x当前结点,topx当前链的最顶端的结点
id[x] = ++num; //对每个结点新编号
w_new[num] = w[x]; //把每个点的初始值赋给新编号
top[x]=topx; //记录x的链头
if(!son[x]) return; //x是叶子,没有儿子,返回
dfs2(son[x],topx); //先dfs重儿子
for(int i=head[x];~i;i=edge[i].next){ //再dfs轻儿子
int y=edge[i].to;
if(y!=fa[x] && y!=son[x]) dfs2(y,y);//每个轻儿子都有一条从它自己开始的链
}
}
void update_range(int x,int y,int z){ //和求LCA(x, y)的过程差不多
while(top[x]!=top[y]){
if(deep[top[x]]<deep[top[y]]) swap(x,y);
update(id[top[x]],id[x],1,1,n,z); //修改一条重链的内部
x = fa[top[x]];
}
if(deep[x]>deep[y]) swap(x,y);
update(id[x],id[y],1,1,n,z); //修改一条重链的内部
}
int query_range(int x,int y){ //和求LCA(x,y)的过程差不多
int ans=0;
while(top[x]!=top[y]){ //持续往上跳,直到若x和y属于同一条重链
if(deep[top[x]]<deep[top[y]]) swap(x,y); //让x是链头更深的重链
ans += query(id[top[x]],id[x],1,1,n); //加上x到x的链头这一段区间
ans %= mod;
x = fa[top[x]]; //x穿过轻边,跳到上一条重链
}
if(deep[x]>deep[y]) swap(x,y);
//若LCA(x, y) = y,交换x,y,让x更浅,使得id[x] <= id[y]
ans += query(id[x],id[y],1,1,n); //再加上x, y的区间和
return ans % mod;
}
void update_tree(int x,int k){ update(id[x],id[x]+siz[x]-1,1,1,n,k); }
int query_tree(int x){ return query(id[x],id[x]+siz[x]-1,1,1,n) % mod; }
int main(){
init(); //链式前向星初始化
scanf("%d%d%d%d",&n,&m,&r,&mod);
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
for(int i=1;i<n;i++){
int u,v; scanf("%d%d",&u,&v);
addedge(u,v); addedge(v,u);
}
dfs1(r,0);
dfs2(r,r);
build(1,1,n); //建线段树
while(m--){
int k,x,y,z; scanf("%d",&k);
switch(k){
case 1:scanf("%d%d%d",&x,&y,&z);update_range(x,y,z); break; //修改点x到点y的路径上的个点权值
case 2:scanf("%d%d",&x,&y); printf("%d\n",query_range(x,y));break; //查询点x到点y的路径上节点权值之和
case 3: scanf("%d%d",&x,&y); update_tree(x,y); break; //修改点x子树上各点的权值
case 4: scanf("%d",&x); printf("%d\n",query_tree(x)); break; //查询点x子树上所有节点的权值之和
}
}
}
2. 边权值
将上述边权转化为点权,每条边上的权值赋给这条边下层的节点(根节点为0)
1. 区间求和
区间求和,多算一个LCA点的值
2. 区间查询
区间查询,多算一个LCA点的值
同上
#include<bits/stdc++.h>
using namespace std;
const int N=100000+10;
int n,m,r,mod;
struct Edge{int to, next;}edge[2*N];
int head[2*N], cnt;
void init();
void addedge(int u,int v);
int ls(int x){ return x<<1; }
int rs(int x){ return x<<1|1;}
int w[N],w_new[N];
int tree[N<<2], tag[N<<2];
void addtag(int p,int pl,int pr,int d){
tag[p] += d;
tree[p] += d*(pr-pl+1); tree[p] %= mod;
}
void push_up(int p){
tree[p] = (tree[ls(p)] + tree[rs(p)]) % mod;
}
void push_down(int p,int pl, int pr){
if(tag[p]){
int mid = (pl+pr)>>1;
addtag(ls(p),pl,mid,tag[p]);
addtag(rs(p),mid+1,pr,tag[p]);
tag[p] = 0;
}
}
void build(int p,int pl,int pr){
tag[p] = 0;
if(pl==pr){
tree[p] = w_new[pl]; tree[p] %= mod;
return;
}
int mid = (pl+pr) >> 1;
build(ls(p),pl,mid);
build(rs(p),mid+1,pr);
push_up(p);
}
void update(int L,int R,int p,int pl,int pr,int d){
if(L<=pl && pr<=R){ addtag(p, pl, pr,d); return; }
push_down(p,pl,pr);
int mid = (pl+pr) >> 1;
if(L<=mid) update(L,R,ls(p),pl,mid,d);
if(R> mid) update(L,R,rs(p),mid+1,pr,d);
push_up(p);
}
int query(int L,int R,int p,int pl,int pr){
if(pl>=L && R >= pr) return tree[p] %= mod;
push_down(p,pl,pr);
int res =0;
int mid = (pl+pr) >> 1;
if(L<=mid) res += query(L,R,ls(p),pl,mid);
if(R> mid) res += query(L,R,rs(p),mid+1,pr);
return res % mod;
}
int son[N],id[N],fa[N],deep[N],siz[N],top[N];
void dfs1(int x, int father);
int num = 0;
void dfs2(int x,int topx){
id[x] = ++num;
w_new[num] = w[x];
top[x]=topx;
if(!son[x]) return;
dfs2(son[x],topx);
for(int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
if(y!=fa[x] && y!=son[x]) dfs2(y,y);
}
}
int lca(int x, int y) {
while(top[x] != top[y]) {
if(deep[top[x]] < deep[top[y]]) swap(x, y);
x = fa[top[x]];
}
return deep[x] < deep[y] ? x : y;
}
void update_range(int x,int y,int z){
int LCA = lca(x, y);
while(top[x]!=top[y]){
if(deep[top[x]]<deep[top[y]]) swap(x,y);
if(top[x] != top[LCA])
update(id[top[x]],id[x],1,1,n,z);
else
update(id[LCA]+1,id[x],1,1,n,z); // avoid updating LCA
x = fa[top[x]];
}
if(deep[x]>deep[y]) swap(x,y);
if(x != LCA)
update(id[x],id[y],1,1,n,z); // update range except LCA
else
update(id[x]+1,id[y],1,1,n,z); // avoid updating LCA
}
int query_range(int x,int y){
int ans=0;
while(top[x]!=top[y]){
if(deep[top[x]]<deep[top[y]]) swap(x,y);
ans += query(id[top[x]],id[x],1,1,n);
ans %= mod;
x = fa[top[x]];
}
if(deep[x]>deep[y]) swap(x,y);
ans += query(id[x],id[y],1,1,n);
return ans % mod;
}
void update_tree(int x,int k){ update(id[x],id[x]+siz[x]-1,1,1,n,k); }
int query_tree(int x){ return query(id[x],id[x]+siz[x]-1,1,1,n) % mod; }
int main(){
init();
scanf("%d%d%d%d",&n,&m,&r,&mod);
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
for(int i=1;i<n;i++){
int u,v; scanf("%d%d",&u,&v);
addedge(u,v); addedge(v,u);
}
dfs1(r,0);
dfs2(r,r);
build(1,1,n);
while(m--){
int k,x,y,z; scanf("%d",&k);
switch(k){
case 1:scanf("%d%d%d",&x,&y,&z);update_range(x,y,z); break;
case 2:scanf("%d%d",&x,&y); printf("%d\n",query_range(x,y));break;
case 3: scanf("%d%d",&x,&y); update_tree(x,y); break;
case 4: scanf("%d",&x); printf("%d\n",query_tree(x)); break;
}
}
}
十一. 二叉查找树
感觉不如set
或者用lower_bound等当代餐更方便
以下11-16这部分要求对原理理解即可
十二. 替罪羊树
不平衡->中序遍历列出字数上所有元素,删除子树->中间元素为根,重建子树
//洛谷P3369 替罪羊树
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
const double alpha = 0.75; //不平衡率。一般用alpha来表示
struct Node{
int ls,rs; //左右儿子
int val; //结点存的数字
int tot; //当前子树占用的空间数量,包括实际存储的结点和被标记删去的点
int size; //子树上实际存储数字的数量
int del; //=1表示这个结点存有数字,=0表示这个点存的数字被删了
}t[N];
int order[N],cnt; //order[]记录拍扁后的结果,即那些存有数字的结点。cnt是数量
int tree_stack[N],top = 0; //用一个栈来回收和分配可用的结点
int root = 0; //根结点,注意重建过程中根结点会变化
void inorder(int u){ //中序遍历,“拍平”摧毁这棵子树
if(!u) return; //已经到达叶子,退出
inorder(t[u].ls); //先遍历左子树
if(t[u].del) order[++cnt] = u; //如果该结点存有数字,读取它
else tree_stack[++top] = u; //回收该结点,等待重新分配使用
inorder(t[u].rs); //再遍历右子树
}
void Initnode(int u){ //重置结点的参数
t[u].ls = t[u].rs = 0;
t[u].size = t[u].tot = t[u].del = 1;
}
void Update(int u){
t[u].size = t[t[u].ls].size + t[t[u].rs].size + 1;
t[u].tot = t[t[u].ls].tot + t[t[u].rs].tot + 1;
}
//int rebuild_num=0; //测试:统计重建次数
void build(int l,int r,int &u){ //把拍扁的子树拎起来,重建
// rebuild_num++; //测试:统计重建次数
int mid = (l + r) >> 1; //新的根设为中点,使重构出的树尽量平衡
u = order[mid];
if(l == r){Initnode(u); return;} //如果是叶子,重置后返回
if(l < mid) build(l,mid - 1,t[u].ls);//重构左子树
if(l == mid) t[u].ls = 0; //注意这里,不要漏了
build(mid + 1,r,t[u].rs); //重构右子树
Update(u); //更新
}
void rebuild(int &u){ //重建。注意是&u
cnt = 0;
inorder(u); //先拍平摧毁
if(cnt) build(1,cnt,u); //再拎起,重建树
else u = 0; //特判该子树为空的情况
}
bool notbalance(int u){ //判断子树u是否平衡
if((double)t[u].size*alpha <=(double)max(t[t[u].ls].size,t[t[u].rs].size))
return true; //不平衡了
return false; //还是平衡的
}
void Insert(int &u,int x){ //插入数字x。注意是&u,传回了新的u
if(!u){ //如果结点u为空,直接将x插到这里
u = tree_stack[top--]; //从栈顶拿出可用的空结点
t[u].val = x; //结点赋值
Initnode(u); //其他参数初始化
return;
}
t[u].size++;
t[u].tot++;
if(t[u].val >= x) Insert(t[u].ls,x); //插到右子树
else Insert(t[u].rs,x); //插到左子树
if(notbalance(u)) rebuild(u); //如果不平衡了,重建这棵子树
}
int Rank(int u,int x){ //排名,x是第几名
if(u==0) return 0;
if(x>t[u].val) return t[t[u].ls].size+ t[u].del + Rank(t[u].rs, x);
return Rank(t[u].ls,x);
}
int kth(int k){ //第k大数是几?
int u = root;
while(u){
if(t[u].del && t[t[u].ls].size + 1 == k) return t[u].val;
else if(t[t[u].ls].size >= k) u = t[u].ls;
else{
k -= t[t[u].ls].size + t[u].del;
u = t[u].rs;
}
}
return t[u].val;
}
void Del_k(int &u,int k){ //删除排名为k的数
t[u].size--; //要删除的数肯定在这棵子树中,size减1
if(t[u].del && t[t[u].ls].size + 1 == k){
t[u].del = 0; //del=0表示这个点u被删除了,但是还保留位置
return;
}
if(t[t[u].ls].size + t[u].del >= k) Del_k(t[u].ls,k); //在左子树上
else Del_k(t[u].rs,k - t[t[u].ls].size - t[u].del); //在右子树上
}
void Del(int x){ //删除值为k的数
Del_k(root,Rank(root,x)+1); //先找x的排名,然后用Del_k()删除
if(t[root].tot * alpha >= t[root].size)
rebuild(root); //如果子树上被删除的结点太多,就重构
}
/*
void print_tree(int u){ //测试:打印二叉树,观察
if(u){
cout<<"v="<<t[u].val<<",l="<<t[u].ls<<",r="<<t[u].rs<<endl;
print_tree(t[u].ls);
print_tree(t[u].rs);
}
}
int tree_deep[N]={0},deep_timer=0,max_deep=0; //测试
void cnt_deep(int u){ //测试:计算二叉树的深度
if(u){
tree_deep[u]=++deep_timer; //结点u的深度
max_deep = max(max_deep,tree_deep[u]); //记录曾经的最大深度
cnt_deep(t[u].ls);
cnt_deep(t[u].rs);
deep_timer--;
}
} */
int main(){
for(int i=N-1;i>=1;i--) tree_stack[++top] = i; //把所有可用的t[]记录在这个栈里面
int q; cin>>q;
// rebuild_num = 0;deep_timer=0;max_deep=0; //测试
while(q--){
int opt,x; cin >> opt >> x;
switch (opt){
case 1: Insert(root,x); break;
case 2: Del(x); break;
case 3: printf("%d\n",Rank(root,x)+1); break;
case 4: printf("%d\n",kth(x)); break;
case 5: printf("%d\n",kth(Rank(root,x))); break;
case 6: printf("%d\n",kth(Rank(root,x+1)+1)); break;
}
// cout<<">>"<<endl;print_tree(root);cout<<endl<<"<<"<<endl; //测试:打印二叉树
// cnt_deep(root); //测试:计算曾经的最大深度
}
// cout<<"rebuild num="<<rebuild_num<<endl; //测试:打印重建次数
// cout<<"deep="<<max_deep<<endl; //测试:打印替罪羊树的最大深度
return 0;
}
十三. Treap(树堆)
Treap树:
(1) 键值 满足 BST要求 l s . k e y < = f a . k e y < = r s . k e y ls.key <= fa.key <=rs.key ls.key<=fa.key<=rs.key
(2) 优先级 满足 堆的要求 f a . p r i > = m a x { l s . p r i , r e . p r i } fa.pri>=max {\{ls.pri,re.pri\}} fa.pri>=max{ls.pri,re.pri}
特性:
每个键值和优先级确定且不同,BST形式唯一
const int M=1e6+10;
int cnt = 0; //t[cnt]: 最新结点的存储位置
struct Node{
int ls,rs; //左右儿子
int key,pri; // key:键值;pri:随机的优先级
int size; //当前结点为根的子树的结点数量,用于求第k大和rank
}t[M]; //tree[],存树
void newNode(int x){ //初始化一个新结点
cnt++; //从t[1]开始存储结点,t[0]被放弃。若子结点是0,表示没有子结点
t[cnt].size = 1;
t[cnt].ls = t[cnt].rs = 0; //0表示没有子结点
t[cnt].key = x; //key: 键值
t[cnt].pri = rand(); //pri:随机产生,优先级
}
void Update(int u){ //更新以u为根的子树的size
t[u].size = t[t[u].ls].size + t[t[u].rs].size+1;
}
void rotate(int &o,int d){ //旋转,参考图示理解。 d=0右旋,d=1左旋
int k;
if(d==1) { //左旋,把o的右儿子k旋到根部
k=t[o].rs;
t[o].rs=t[k].ls;//图中的x
t[k].ls=o;
}
else { //右旋,把o的左儿子k旋到根部
k=t[o].ls;
t[o].ls=t[k].rs; //图中的x
t[k].rs=o;
}
t[k].size=t[o].size;
Update(o);
o=k; //新的根是k
}
void Insert(int &u,int x){
if(u==0){newNode(x);u=cnt;return;} //递归到了一个空叶子,新建结点
t[u].size++;
if(x>=t[u].key) Insert(t[u].rs,x); //递归右子树找空叶子,直到插入
else Insert(t[u].ls,x); //递归左子树找空叶子,直到插入
if(t[u].ls!=0 && t[u].pri>t[t[u].ls].pri) rotate(u,0);
if(t[u].rs!=0 && t[u].pri>t[t[u].rs].pri) rotate(u,1);
Update(u);
}
void Del(int &u,int x){
t[u].size--;
if(t[u].key==x){
if(t[u].ls==0&&t[u].rs==0){u=0; return;}
if(t[u].ls==0||t[u].rs==0){u=t[u].ls+t[u].rs; return;}
if(t[t[u].ls].pri < t[t[u].rs].pri)
{ rotate(u,0); Del(t[u].rs, x); return;}
else{ rotate(u,1); Del(t[u].ls, x); return;}
}
if(t[u].key>=x) Del(t[u].ls,x);
else Del(t[u].rs,x);
Update(u);
}
int Rank(int u,int x){ //排名,x是第几名
if(u==0) return 0;
if(x>t[u].key) return t[t[u].ls].size+1+Rank(t[u].rs, x);
return Rank(t[u].ls,x);
}
int kth(int u,int k){ //第k大数是几?
if(k==t[t[u].ls].size+1) return t[u].key; //这个数为根
if(k> t[t[u].ls].size+1) return kth(t[u].rs,k-t[t[u].ls].size-1);//右子树
if(k<=t[t[u].ls].size) kth(t[u].ls,k); //左子树
}
int Precursor(int u,int x){
if(u==0) return 0;
if(t[u].key>=x) return Precursor(t[u].ls,x);
int tmp = Precursor(t[u].rs,x);
if(tmp==0) return t[u].key;
return tmp;
}
int Successor(int u,int x){
if(u==0) return 0;
if(t[u].key<=x) return Successor(t[u].rs,x);
int tmp = Successor(t[u].ls,x);
if(tmp==0) return t[u].key;
return tmp;
}
int main(){
srand(time(NULL));
int root = 0; //root是整棵树的树根,0表示初始为空
int n; cin>>n;
while(n--){
int opt,x; cin >> opt >> x;
switch (opt){
case 1: Insert(root,x); break;
case 2: Del(root,x); break;
case 3: printf("%d\n",Rank(root,x)+1); break;
case 4: printf("%d\n",kth(root,x)); break;
case 5: printf("%d\n",Precursor(root,x)); break;
case 6: printf("%d\n",Successor(root,x)); break;
}
}
return 0;
}
十四. FHQ Treap
//洛谷P3369,FHQ Treap
#include<bits/stdc++.h>
using namespace std;
const int M=1e6+10;
int cnt=0, root=0; //t[cnt]: 最新结点的存储位置;root:整棵树的根,用于访问树
struct Node{
int ls,rs; //左儿子lson,右儿子rson
int key,pri; //key:键值,pri:随机的优先级
int size; //当前结点为根的子树的结点数量,用于求第k大和rank
}t[M]; //tree[],存树
void newNode(int x){ //建立只有一个点的树
cnt++; //从t[1]开始存储结点,t[0]被放弃
t[cnt].size = 1;
t[cnt].ls = t[cnt].rs = 0; //0表示没有子结点
t[cnt].key = x; //key: 键值
t[cnt].pri = rand(); //pri:随机产生,优先级
}
void Update(int u){ //更新以u为根的子树的size
t[u].size = t[t[u].ls].size + t[t[u].rs].size+1;
}
void Split(int u,int x,int &L,int &R) { //权值分裂。返回以L和R为根的2棵树
if(u == 0){L = R = 0; return;} //到达叶子,递归返回
if(t[u].key <= x){ //本结点比x小,那么到右子树上找x
L = u; //左树的根是本结点
Split(t[u].rs, x, t[u].rs, R); //通过rs传回新的子结点
}
else{ //本结点比x大,继续到左子树找x
R = u; //右数的根是本结点
Split(t[u].ls,x,L,t[u].ls);
}
Update(u);//更新当前结点的size
}
int Merge(int L,int R){ //合并以L和R为根的两棵树,返回一棵树的根
if(L==0 || R==0) return L+R; //到达叶子。若L==0,就是返回L+R=R
if(t[L].pri > t[R].pri){ //左树L优先级大于右树R,则L节点是父结点
t[L].rs = Merge(t[L].rs,R); //合并R和L的右儿子,并更新L的右儿子
Update(L);
return L; //合并后的根是L
}
else{ //合并后R是父结点
t[R].ls = Merge(L,t[R].ls); //合并L和R的左儿子,并更新R的左儿子
Update(R);
return R; //合并后的根是R
}
}
int Insert(int x){ //插入数字x
int L,R;
Split(root,x,L,R);
newNode(x); //新建一棵只有一个点的树t[cnt]
int aa = Merge(L,cnt);
root = Merge(aa,R);
}
int Del(int x){ //删除数字x。请对比后面例题洛谷P5055的排名分裂的删除操作
int L,R,p;
Split(root,x,L,R); //<=x的树和>x的树
Split(L,x-1,L,p); //<x的树和==x的树
p = Merge(t[p].ls,t[p].rs); //合并x=p的左右子树,也就是删除了x
root = Merge(Merge(L,p),R);
}
void Rank(int x){ //查询数x的排名
int L,R;
Split(root,x-1,L,R); // <x的树和>=x的树
printf("%d\n",t[L].size+1);
root = Merge(L,R); //恢复
}
int kth(int u,int k){ //求排名第k的数
if(k==t[t[u].ls].size+1) return u; //这个数为根
if(k<=t[t[u].ls].size) return kth(t[u].ls,k); //在左子树
if(k>t[t[u].ls].size) return kth(t[u].rs,k-t[t[u].ls].size-1); //在右子树
}
void Precursor(int x){ //求x的前驱
int L,R;
Split(root,x-1,L,R);
printf("%d\n",t[kth(L,t[L].size)].key);
root = Merge(L,R); //恢复
}
void Successor(int x){ //求x的后继
int L,R;
Split(root,x,L,R); //<=x的树和>x的树
printf("%d\n",t[kth(R,1)].key);
root = Merge(L,R); //恢复
}
int main(){
srand(time(NULL));
int n; cin >> n;
while(n--){
int opt,x; cin >> opt >> x;
switch (opt){
case 1: Insert(x); break;
case 2: Del(x); break;
case 3: Rank(x); break;
case 4: printf("%d\n",t[kth(root,x)].key); break; //排名x的结点
case 5: Precursor(x); break;
case 6: Successor(x); break;
}
}
return 0;
}
应用
这个分裂又整合的底层更新逻辑,和线段是相似,可以做一些线段树的操作
如:
快操作,区间和等等
1.luogu 4008
//洛谷P4008的FHQ treap代码
#include<bits/stdc++.h>
using namespace std;
const int M = 2e6+10;
int root = 0,cnt = 0;
struct Node{int ls,rs; char val; int pri; int size;}t[M]; //tree[]存树
void Update(int u){t[u].size = t[t[u].ls].size + t[t[u].rs].size+1;} //用于排名分裂
int newNode(char x){ //建立只有一个点的树
cnt++;
t[cnt].size = 1; t[cnt].pri = rand(); t[cnt].ls = t[cnt].rs = 0;
t[cnt].val = x; //一个字符
return cnt;
}
void Split(int u,int x,int &L,int &R){ //排名分裂,不是权值分裂
if(u == 0){L = R = 0; return ;}
if(t[t[u].ls].size+1 <= x){ //第x个数在u的右子树上
L = u; Split(t[u].rs, x-t[t[u].ls].size-1, t[u].rs, R);
}
else{R = u; Split(t[u].ls,x,L,t[u].ls); } //第x个数在左子树上
Update(u);
}
int Merge(int L,int R){ //合并
if(L==0 || R==0) return L+R;
if(t[L].pri > t[R].pri){ t[L].rs = Merge(t[L].rs,R); Update(L); return L; }
else{ t[R].ls = Merge(L,t[R].ls); Update(R); return R;}
}
void inorder(int u){ //中序遍历,打印结果
if(u == 0) return;
inorder(t[u].ls); cout << t[u].val; inorder(t[u].rs);
}
int main(){
srand(time(NULL));
int len, L, p, R, pos = 0; //pos是光标的当前位置
int n; cin>>n;
while(n--){
char opt[10]; cin>>opt;
if(opt[0]=='M') cin>>pos; //移动光标
if(opt[0]=='I'){ //插入len个字符
cin>>len;
Split(root,pos,L,R);
for(int i=1;i<=len;i++) { //逐个读入字符
char ch=getchar(); while(ch<32||ch>126) ch=getchar();
L = Merge(L,newNode(ch)); //把字符加到树中
}
root = Merge(L,R);
}
if(opt[0]=='D'){ //删除光标后len个字符
cin>>len; Split(root,pos+len,L,R); Split(L,pos,L,p);
root = Merge(L,R);
}
if(opt[0]=='G'){ //打印len个字符
cin>>len; Split(root,pos+len,L,R); Split(L,pos,L,p);
inorder(p); cout<<"\n"; //打印
root = Merge(Merge(L,p),R);
}
if(opt[0]=='P') pos--;
if(opt[0]=='N') pos++;
}
return 0;
}
2. 区间翻转
十五. 笛卡尔树
简化的Treap树,有键值和随机的优先级
主要目标是一个优先级和键值确定的数列
//改写自:https://blog.youkuaiyun.com/qq_40679299/article/details/80395824
#include<cstdio>
#include<algorithm>
#include<cstring>
#include <stack>
using namespace std;
const int N = 50005;
const int INF = 0x7fffffff;
struct Node{
char s[100]; int ls,rs,fa,pri;
friend bool operator<(const Node& a,const Node& b){
return strcmp(a.s,b.s)<0;}
}t[N];
void buildtree(int n){ //不用栈,直接查最右链
for(int i=1;i<=n;i++){
int pos = i-1; //从前一个结点开始比较,前一个结点在最右链的末端
while(t[pos].pri < t[i].pri)
pos = t[pos].fa; //沿着最右链一直找,直到pos的优先级比i大
t[i].ls = t[pos].rs; //图(4):i是19,pos是24, 15调整为19的左儿子
t[t[i].ls].fa = i; //图(4):15的父亲是19
t[pos].rs = i; //图(4):24的右儿子是19
t[i].fa = pos; //图(4):19的父亲是24
}
}
void buildtree2(int n){ //用栈来辅助建树
stack <int> ST; ST.push(0); //t[0]进栈,它一直在栈底
for(int i=1;i<=n;i++){
int pos = ST.top();
while(!ST.empty() && t[pos].pri < t[i].pri){
pos = t[ST.top()].fa;
ST.pop(); //把比i优先级小的弹出栈
}
t[i].ls = t[pos].rs;
t[t[i].ls].fa = i;
t[pos].rs = i;
t[i].fa = pos;
ST.push(i); //每个结点都一定要进栈
}
}
void inorder(int x){ //中序遍历打印
if(x==0) return;
printf("(");
inorder(t[x].ls); printf("%s/%d",t[x].s,t[x].pri); inorder(t[x].rs);
printf(")");
}
int main(){
int n;
while(scanf("%d",&n),n){
for(int i=1;i<=n;i++){
t[i].ls = t[i].rs = t[i].fa = 0; //有多组测试,每次要清零
scanf(" %[^/]/%d", t[i].s, &t[i].pri); //注意输入的写法
}
t[0].ls = t[0].rs = t[0].fa = 0; //t[0]不用,从t[1]开始插结点
t[0].pri = INF; //t[0]的优先级无穷大
sort(t+1,t+1+n); //对标签先排序,非常关键。这样建树时就只需要考虑优先级pri
buildtree(n); //buildtree2(n); //两种建树方法
inorder(t[0].rs); //t[0]在树的最左端,第一个点是t[0].rs
printf("\n");
}
return 0;
}
十六. Splay树
和FHQ Treap功能相似
//改写自 https://www.luogu.com.cn/blog/dedicatus545/solution-p4008
#include<bits/stdc++.h>
using namespace std;
const int M = 2e6+10;
int cnt, root;
struct Node{int fa,ls,rs,size; char val;}t[M]; //tree[]存树;
void Update(int u){t[u].size = t[t[u].ls].size + t[t[u].rs].size+1;} //用于排名
char str[M]={0}; //一次输入的字符串
int build(int L,int R,int f){ //把字符串str[]建成平衡树
if(L>R) return 0;
int mid = (L+R)>>1;
int cur = ++cnt;
t[cur].fa = f;
t[cur].val= str[mid];
t[cur].ls = build(L,mid-1,cur);
t[cur].rs = build(mid+1,R,cur);
Update(cur);
return cur; //返回新树的根
}
int get(int x){return t[t[x].fa].rs == x;} //如果u是右儿子,返回1;左儿子返回0
void rotate(int x){ //单旋一次
int f=t[x].fa, g=t[f].fa, son=get(x); //f:父亲;g:祖父
if(son==1) { //x是右儿子,左旋zag
t[f].rs = t[x].ls; //图“单旋”中的结点b
if(t[f].rs) t[t[f].rs].fa = f;
}
else { //x是左儿子,右旋zig
t[f].ls = t[x].rs;
if(t[f].ls) t[t[f].ls].fa = f;
}
t[f].fa=x; //x旋为f的父结点
if(son == 1) t[x].ls=f; //左旋,f变为x的左儿子
else t[x].rs=f; //右旋,f变为x的右儿子
t[x].fa = g; //x现在是祖父的儿子
if(g){ //更新祖父的儿子
if(t[g].rs==f) t[g].rs = x;
else t[g].ls = x;
}
Update(f); Update(x);
}
void splay(int x,int goal){ //goal=0,新的根是x;goal!=0,把x旋为goal的儿子
if(goal == 0) root=x;
while(1){
int f = t[x].fa, g=t[f].fa; //一次处理x,f,g三个点
if(f == goal) break;
if(g != goal) { //有祖父,分一字旋和之字旋两种情况
if(get(x)==get(f)) rotate(f); //一字旋,先旋f、g
else rotate(x);} //之字旋,直接旋x
rotate(x);
}
Update(x);
}
int kth(int k,int u){ //第k大数的位置
if( k == t[t[u].ls].size + 1) return u;
if( k <= t[t[u].ls].size ) return kth(k,t[u].ls);
if( k >= t[t[u].ls].size + 1) return kth(k-t[t[u].ls].size-1,t[u].rs);
}
void Insert(int L,int len){ //插入一段区间
int x = kth(L,root), y = kth(L+1,root); //x:第L个数的位置,y:第L+1个数的位置
splay(x,0); splay(y,x); //分裂
//先把x旋到根,然后把y旋到x的儿子,此时y是x的右儿子,且y的左儿子为空
t[y].ls = build(1,len,y); //合并:建一棵树,挂到y的左儿子上
Update(y); Update(x);
}
void del(int L,int R){ //删除区间[L+1,R]
int x=kth(L,root), y=kth(R+1,root);
splay(x,0); splay(y,x); //y是x的右儿子,y的左儿子是待删除的区间
t[y].ls=0; //剪断左子树,等于直接删除。这里为了简单,没有释放空间
Update(y); Update(x);
}
void inorder(int u){ //中序遍历
if(u==0) return;
inorder(t[u].ls);cout<<t[u].val;inorder(t[u].rs);
}
int main(){
t[1].size=2; t[1].ls=2; //小技巧:虚拟祖父,防止旋转时越界而出错
t[2].size=1; t[2].fa=1; //小技巧:虚拟父亲
root=1; cnt=2; //在操作过程中,root将指向字符串的根
int pos=1; //光标位置
int n; cin>>n;
while(n--){
int len; char opt[10]; cin>>opt;
if(opt[0]=='I'){
cin>>len;
for(int i=1;i<=len;i++){
char ch=getchar(); while(ch<32||ch>126) ch=getchar();
str[i] = ch;
}
Insert(pos,len);
}
if(opt[0]=='D'){ cin>>len; del(pos,pos+len);} //删除区间[pos+1,pos+len]
if(opt[0]=='G'){
cin>>len; int x=kth(pos,root), y=kth(pos+len+1,root);
splay(x,0); splay(y,x);
inorder(t[y].ls); cout<<"\n";
}
if(opt[0]=='M'){ cin>>len; pos=len+1;}
if(opt[0]=='P') pos--;
if(opt[0]=='N') pos++;
}
}
十七. K-D树
按x-y-…这个顺序交替轮流划分
1. 寻找最小值
#include<bits/stdc++.h>
using namespace std;
const int N = 100005;
const int K = 2;
#define ll long long
struct Point{int dim[K];}; //K维数据。本题是二维坐标x=dim[0],y=dim[1]
Point q[N]; //记录输入的n个坐标点
Point t[N]; //存二叉树,用最简单的方法:数组存二叉树
int now; //当前处于第几维,用于cmp()函数的比较
bool cmp(Point a,Point b){return a.dim[now] < b.dim[now];} //第now维数的比较
ll square(int x){return (ll)x*x;}
ll dis(Point a,Point b){ //点a、b距离的平方
return square(a.dim[0]-b.dim[0])+square(a.dim[1]-b.dim[1]);
}
void build(int L,int R,int dep){ //建树
if(L>=R) return;
int d = dep % K; //轮转法。dep是当前层的深度,d是当前层的维度
int mid = (L+R) >> 1;
now = d;
nth_element(t+L,t+mid,t+R,cmp); //找中值
build(L,mid,dep+1); //继续建二叉树的下一层
build(mid+1,R,dep+1);
}
ll ans; //答案:到最近点距离的平方值
void query(int L,int R,int dep,Point p){ //查找点p的最近点
if(L >= R)return;
int mid=(L+R)>>1;
int d = dep % K; //轮转法
ll mindis = dis(t[mid],p); //这棵子树的根到p的最小距离
if(ans == 0) ans = mindis; //赋初值
if(mindis!=0 && ans > mindis) ans = mindis; //需要特判t[mid]和p重合的情况
if(p.dim[d] > t[mid].dim[d]) { //在这个维度,p大于子树的根,接下来查右子树
query(mid+1,R,dep+1,p);
if(ans > square(t[mid].dim[d]-p.dim[d])) query(L,mid,dep+1,p);
//如果以ans为半径的圆与左子树相交,那么左子树也要查
}
else{
query(L,mid,dep+1,p); //在这个维度,p小于子树的根,接下来查左子树
if(ans > square(t[mid].dim[d]-p.dim[d])) //右子树也要查
query(mid+1,R,dep+1,p);
}
}
int main(){
int T; scanf("%d",&T);
while(T--){
int n; scanf("%d",&n);
for(int i=0;i<n;i++)
scanf("%d%d",&(q[i].dim[0]),&(q[i].dim[1])), t[i] = q[i];
build(0,n,0); //建树
for(int i=0;i<n;i++){
ans=0;
query(0,n,0,q[i]);
printf("%lld\n",ans);
}
}
return 0;
}
2. 区间查询
#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
const double alpha = 0.75; //替罪羊树的不平衡率
#define lc t[u].ls
#define rc t[u].rs
struct Point{
int dim[2],val; //dim[0]即x,dim[1]即y
Point(){};
Point(int x,int y,int vall){dim[0]=x,dim[1]=y,val=vall;}
};
Point order[N]; int cnt; //替罪羊树:用于拍平后存数据
struct kd_tree{
int ls,rs;
int mi[2],ma[2]; //mi[i]: 第i维上区间的下界; ma[i]:第i维上区间的上界
int sum; //以该点为根的子树权值之和
int size;
Point p;
}t[N];
int tot,root;
int top,tree_stack[N]; //替罪羊树:回收
int now;
bool cmp(Point a,Point b){return a.dim[now]<b.dim[now];}
void update(int u){
for(int i=0;i<2;i++){
t[u].mi[i] = t[u].ma[i] = t[u].p.dim[i];
if(lc){
t[u].mi[i] = min(t[u].mi[i],t[lc].mi[i]);
t[u].ma[i] = max(t[u].ma[i],t[lc].ma[i]);
}
if(rc){
t[u].mi[i] = min(t[u].mi[i],t[rc].mi[i]);
t[u].ma[i] = max(t[u].ma[i],t[rc].ma[i]);
}
}
t[u].sum = t[lc].sum + t[u].p.val+t[rc].sum;
t[u].size = t[lc].size + t[rc].size+1;
}
void slap(int u) { //替罪羊树:拍平
if(!u) return;
slap(lc); //这里用中序遍历。其实先序、后序也行
order[++cnt] = t[u].p;
tree_stack[++top] = u; //回收结点
slap(rc);
}
int build(int l,int r,int d) { //替罪羊树:建树
if(l>r) return 0;
int u;
if(top) u = tree_stack[top--];
else u = ++tot;
int mid=(l+r)>>1;
now = d;
nth_element(order+l, order+mid, order+r+1, cmp);
t[u].p = order[mid];
lc = build(l,mid-1,d^1); //奇偶轮转法。没有用例题hdu2966的一般轮转法
rc = build(mid+1,r,d^1);
update(u);
return u;
}
bool notbalance(int u){ //替罪羊树:判断子树u是否平衡
if(t[lc].size>alpha*t[u].size || t[rc].size>alpha*t[u].size)
return true; //不平衡了
return false; //还是平衡的
}
void Insert(int &u,Point now,int d){
if(!u) {
if(top) u=tree_stack[top--];
else u = ++tot;
lc = rc = 0,t[u].p = now;
update(u);
return;
}
if(now.dim[d] <= t[u].p.dim[d]) Insert(lc,now,d^1);//按第d维的坐标比较
else Insert(rc,now,d^1);
update(u);
if(notbalance(u)){ //不平衡
cnt = 0;
slap(u); //拍平
u = build(1,t[u].size,d); //重建
}
}
int query(int u,int x1,int y1,int x2,int y2){
if(!u) return 0;
int X1=t[u].mi[0], Y1=t[u].mi[1], X2=t[u].ma[0], Y2=t[u].ma[1];
if(x1<=X1 && x2>=X2 && y1<=Y1 && y2>=Y2) return t[u].sum;
//子树表示的矩形完全在询问矩形范围内
if(x1>X2 || x2<X1 || y1>Y2 || y2<Y1) return 0;
//子树表示的矩形完全在询问矩形范围外
int ans=0;
X1=t[u].p.dim[0], Y1=t[u].p.dim[1], X2=t[u].p.dim[0], Y2=t[u].p.dim[1];
if(x1<=X1 && x2>=X2 && y1<=Y1 && y2>=Y2) ans+=t[u].p.val;//根在询问矩形内
ans += query(lc,x1,y1,x2,y2) + query(rc,x1,y1,x2,y2); //递归左右子树
return ans;
}
int main(){
int n; cin >> n;
int ans=0;
while(1){
int opt;scanf("%d",&opt);
if(opt==1){
int x,y,val; scanf("%d%d%d",&x,&y,&val);
x^=ans,y^=ans,val^=ans;
Insert(root,Point(x,y,val),0);
}
if(opt==2){
int x1,y1,x2,y2; scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
x1^=ans,y1^=ans,x2^=ans,y2^=ans;
ans = query(root,x1,y1,x2,y2);
printf("%d\n",ans);
}
if(opt==3) break;
}
}
十八. 动态树与LCT
作用对象:动态规划的树和森林
//代码改写自:https://www.luogu.com.cn/blog/ecnerwaIa/dai-ma-jiang-xie
#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5;
struct node{ int fa,ch[2],sum,val,lazy; }t[N]; //lazy用来标记reverse()的左右翻转
#define lc t[x].ch[0] //左儿子
#define rc t[x].ch[1] //右儿子
bool isRoot(int x){ //判断是否是splay根节点
int g=t[x].fa;
return t[g].ch[0]!=x && t[g].ch[1]!=x;//若为根,则父结点不应该有这个儿子
}
void pushup(int x){ //本题的求路径异或和。上传信息
t[x].sum=t[x].val^t[lc].sum^t[rc].sum;
}
void reverse(int x){
if(!x)return;
swap(lc,rc); //翻转x的左右儿子
t[x].lazy^=1; //懒惰标记,先不翻转儿子的后代,后面再翻转
}
void pushdown(int x){ //递归翻转x的儿子的后代,并释放懒标记。
if(t[x].lazy){
reverse(lc);
reverse(rc);
t[x].lazy=0;
}
}
void push(int x){
if(!isRoot(x)) push(t[x].fa); //从根到x全部pushdown
pushdown(x);
}
void rotate(int x){
int y=t[x].fa;
int z=t[y].fa;
int k=t[y].ch[1]==x;
if(!isRoot(y)) t[z].ch[t[z].ch[1]==y]=x;
t[x].fa=z;
t[y].ch[k]=t[x].ch[k^1];
if(t[x].ch[k^1])t[t[x].ch[k^1]].fa=y;
t[y].fa=x;
t[x].ch[k^1]=y;
pushup(y);
}
void splay(int x){ //提根:把x旋转为它所在的Splay树的根
int y,z;
push(x); //先pushdown处理x的所有子孙的lazy标记
while(!isRoot(x)){
y=t[x].fa,z=t[y].fa;
if(!isRoot(y))
(t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
rotate(x);
}
pushup(x);
}
void access(int x){ //在原树上建一条实链,起点是根,终点是x
for(int child=0; x; child=x, x=t[x].fa){ //从x往上走,沿着虚边走到根
splay(x);
rc = child; //右孩子是child,建立了一条实边
pushup(x);
}
}
void makeroot(int x){ //把x在原树上旋转到根的位置
access(x); splay(x); reverse(x);
}
void split(int x,int y){ //把原树上以x为起点、y为终点的路径,生成一条实链
makeroot(x);
access(y);
splay(y);
}
void link(int x,int y){ //在结点x和y之间连接一条边
makeroot(x); t[x].fa=y;
}
void cut(int x,int y){ //将x,y的边切断
split(x,y);
if(t[y].ch[0]!=x||rc) return;
t[x].fa=t[y].ch[0]=0;
pushup(x);
}
int findroot(int x){ //查找x在原树上的根
access(x); splay(x);
while(lc) pushdown(x),x=lc; //找Splay树最左端的结点
return x;
}
int main(){
int n,m; scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i){ scanf("%d",&t[i].val); t[i].sum = t[i].val; }
while(m--){
int opt,a,b; scanf("%d%d%d",&opt,&a,&b);
switch(opt){
case 0: split(a,b); printf("%d\n",t[b].sum); break;
case 1: if(findroot(a) != findroot(b))link(a,b); break;
case 2: cut(a,b); break;
case 3: splay(a); t[a].val=b; break;
}
}
return 0;
}
应用
- 判断连通性:findroot(a)==findroot(b)
- 求两点之间距离 先执行split(x,y),然后累加这课Splay树的边权
- 求LCA
//洛谷P3379的部分代码
const int N=500000+5;
int query_lca(int x, int y) { //求LCA(x, y)
access(x);
int ans;
for (int child = 0; y; child = y, y = t[y].fa) { //模拟access(), y==0时退出
splay(y); //若y在从根出发的路径p上,splay(y)后y是Splay的根,y没有父结点,t[y].fa=0
t[y].ch[1] = child;
ans = y;
}
return ans;
}
int main() {
int n,m,rt; scanf("%d%d%d",&n,&m,&rt);
for (int i=1; i<n; ++i) { int x,y; scanf("%d%d",&x,&y); link(x, y);}
makeroot(rt); //定义根结点是rt
for (int i=1; i<=m; ++i){ int x,y; scanf("%d%d",&x,&y); printf("%d\n", query_lca(x, y));}
return 0;
}