洛谷P1001【A + B Problem】题解

题目背景

强烈推荐新用户必读帖

不熟悉算法竞赛的选手请看这里:

算法竞赛中要求的输出格式中,不能有多余的内容这也包括了“请输入整数 aa 和 bb” 这一类的提示用户输入信息的内容。若包含了这些内容,将会被认为是 Wrong Answer,即洛谷上的 WA。在对比代码输出和标准输出时,系统将忽略每一行结尾的空格,以及最后一行之后多余的换行符。

若因此类问题出现本机似乎输出了正确的结果,但是实际提交结果为错误的现象,请勿认为是洛谷评测机出了问题,而是你的代码中可能存在多余的输出信息。用户可以参考在题目末尾提供的代码。

此外,请善用应用中的在线 IDE 功能,以避免不同平台的评测产生差异。

最后,请不要在对应的题目讨论区中发布自己的题解,请发布到题解区域中,否则将处以删除或禁言的处罚。若发现无法提交题解则表明本题题解数量过多,仍不应发布讨论。若您的做法确实与其他所有题解均不一样,请联系管理员添加题解。

题目描述

输入两个整数 a,ba,b,输出它们的和(∣a∣,∣b∣≤109∣a∣,∣b∣≤109)。

注意

  1. Pascal 使用 integer 会爆掉哦!
  2. 有负数哦!
  3. C/C++ 的 main 函数必须是 int 类型,而且 C 最后要 return 0。这不仅对洛谷其他题目有效,而且也是 NOIP/CSP/NOI 比赛的要求!

好吧,同志们,我们就从这一题开始,向着大牛的路进发。

任何一个伟大的思想,都有一个微不足道的开始。

输入格式

两个以空格分开的整数。

输出格式

一个整数。

输入输出样例

输入 #1复制

20 30

输出 #1复制

50

       看到这篇文章,你们会不会骂死我?毕竟这么简单的问题一年级的小朋友都会。但是,今天我介绍的是一种特别的方法,请不要眨眼,题解马上开始:

我利用的是Link-Cut Tree算法,那么Link-Cut Tree是什么东西呐?

Link-Cut Tree用于解决动态树问题,又名实链剖分。

给定一棵树,要求以下操作:

1 x y 代表询问从 x xx 到 y yy 的路径上的点的权值的和。
2 x y 代表连接 x xx 到 y yy,若 x xx 到 y yy 已经联通则无需连接。
3 x y 代表删除边 ( x , y ) (x,y)(x,y),不保证边 ( x , y ) (x,y)(x,y) 存在。
4 x y 代表将点 x xx 上的权值变成 y yy。
像这类问题,存在加边和删边操作,被称为动态树问题。

概念:
实边:为平衡时间复杂度而定义,无实际意义,每个节点连0,1,2条实边连向儿子
实链:由多条实边组成,构成一个S p l a y SplaySplay结构
虚边:连接两个实链
辅助树是可以在满足中序遍历、Splay 的性质下任意换根的。
虚实链变换可以轻松在辅助树上完成,这也就是实现了动态维护树链剖分。

辅助树
辅助树是由多个Splay构成的,多个辅助树结构就构成了森林。

辅助树由多棵 Splay 组成,每棵 Splay 维护原树中的一条路径,且中序遍历这棵 Splay 得到的点序列,从前到后对应原树“从上到下”的一条路径。
原树每个节点与辅助树的 Splay 节点一一对应。
辅助树的各棵 Splay 之间并不是独立的。每棵 Splay 的根节点的父亲节点本应是空,但在 LCT 中每棵 Splay 的根节点的父亲节点指向原树中 这条链 的父亲节点(即链最顶端的点的父亲节点)。这类父亲链接与通常 Splay 的父亲链接区别在于儿子认父亲,而父亲不认儿子,对应原树的一条 虚边。因此,每个连通块恰好有一个点的父亲节点为空。
由于辅助树的以上性质,我们维护任何操作都不需要维护原树,辅助树可以在任何情况下拿出一个唯一的原树,我们只需要维护辅助树即可。
如图,现在我们有一棵原树:

 

它的辅助树如下:

 

函数声明
Splay系列函数
pushrev(x)反转函数,用于懒标记
PushUp(x)更新
PushDown(x)下传标记
rotate(x)旋转一次
splay(x)S p l a y SplaySplay核心操作
LCT操作
Access(x)将x xx与 根节点 的路径上所有边改为实边
IsRoot(x)判断x xx是否是所在Splay的根。
makeroot(x)将x xx变为原树的根。
findroot(x)查找原树根
split(x, y)使 x 与 y 在同一个 Splay 中
link(x, y)连接 x 与 y
cut(x, y)切断 x 与 y 之间的边
Splay
不多说,但有一点不一样,已在代码中标注:

void pushrev(int x) // 翻转一次x
{
	swap(tr[x].s[0], tr[x].s[1]);
	tr[x].rev ^= 1;
}

void pushup(int x)
{
	tr[x].sum = tr[tr[x].s[0]].sum ^ tr[x].v ^ tr[tr[x].s[1]].sum; // 这里以xor为例
}

void pushdown(int x)
{
	if (tr[x].rev)
	{
		pushrev(tr[x].s[0]), pushrev(tr[x].s[1]);
		tr[x].rev = 0;
	}
}

void rotate(int x)
{
	int y = tr[x].p, z = tr[y].p;
	int k = tr[y].s[1] == x;
	if (!isroot(y))tr[z].s[tr[z].s[1] == y] = x; // 判断是否为实边
	tr[x].p = z;
	tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y;
	tr[x].s[k ^ 1] = y, tr[y].p = x;
	pushup(y), pushup(x);
}

void splay(int x) 
{
	int top = 0, r = x;
	stk[++ top] = r; // 需要先下传标记
	while (!isroot(r)) stk[++ top] = r = tr[r].p;
	while (top)pushdown(stk[top -- ]);
	while (!isroot(x))
	{
		int y = tr[x].p, z = tr[y].p;
		if (!isroot(y))
			if ((tr[y].s[1] == x) ^ (tr[z].s[1] == y))rotate(x);
			else rotate(y);
		rotate(x);	
	}
}

 

Access(x)
目的是让x与根节点属于同一个Splay中

即让x到根节点的所有边都变为实边:

void access(int x) // 将 x 与 根节点 的路径上所有边改为实边
{
	int z = x;
	for (int y = 0; x; y = x, x = tr[x].p)
	{
		splay(x); // 将 x 转到所在Splay的根节点
		tr[x].s[1] = y; // 将 y 设为 x 的右儿子,所有路径上的点都为右儿子
		pushup(x); // 更新 x 节点
	}
	splay(z); // 使整棵树更平衡
}

IsRoot(x):

bool isroot(int x) // x是否是所在Splay的根节点
{
	return tr[tr[x].p].s[0] != x && tr[tr[x].p].s[1] != x;
}

makeroot(x)
在access(x) 中存在一个细节:所有路径上的点都为右儿子

由于辅助树上的节点按中序遍历标记深度,那么x为最深的点

想要成为根,就要反转这颗Splay:

void makeroot(int x) // 将 x 变为原树的根节点
{
	access(x); // 将 x 与 根节点 的路径上所有边改为实边 
	pushrev(x); // 反转 x 所在的 Splay
}

findroot(int x)
目的为找原树的根

原树的根位于辅助树根节点所在Splay中深度最低的点

从辅助树根出发,只要有实边就向左走,最终到达的点即为根:

int findroot(int x) // 查找原树根
{
	access(x); // 将 x 与 根节点 的路径上所有边改为实边 
	while (tr[x].s[0])pushdown(x), x = tr[x].s[0]; // 找到根节点
	splay(x); // 使整棵树更平衡
	return x; // 返回根节点
}

split(x, y)

将 x 变为原树的根节点

将 y 与 x 的路径上所有边改为实边:

void split(int x, int y) // 使 x 与 y 在同一个 Splay 中
{
	makeroot(x); // 将 x 变为原树的根节点
	access(y); // 将 y 与 x 的路径上所有边改为实边 
}

link/cut

有了前面的函数,这两个函数很好解决:

void link(int x, int y) // 连接 x 与 y
{
	makeroot(x); // 将 x 变为根节点
	if (findroot(y) != x)tr[x].p = y; // 连虚边
}

void cut(int x, int y) // 切断 x 与 y 之间的边
{
	makeroot(x); // 将 x 变为根节点
	if (findroot(y) == x && tr[y].p == x && !tr[y].s[0])
	{
		tr[x].s[1] = tr[y].p = 0; // 删边
		pushup(x); // 更新
	}
}

实现:

#include <bits/stdc++.h>
using namespace std;

const int N = 100010;

int n, m;
struct Node {
	int s[2], p, v;
	int sum, rev;
}tr[N];
int stk[N];

bool isroot(int x) // x是否是所在Splay的根节点
{
	return tr[tr[x].p].s[0] != x && tr[tr[x].p].s[1] != x;
}

void pushrev(int x) // 翻转一次x
{
	swap(tr[x].s[0], tr[x].s[1]);
	tr[x].rev ^= 1;
}

void pushup(int x)
{
	tr[x].sum = tr[tr[x].s[0]].sum ^ tr[x].v ^ tr[tr[x].s[1]].sum;
}

void pushdown(int x)
{
	if (tr[x].rev)
	{
		pushrev(tr[x].s[0]), pushrev(tr[x].s[1]);
		tr[x].rev = 0;
	}
}

void rotate(int x)
{
	int y = tr[x].p, z = tr[y].p;
	int k = tr[y].s[1] == x;
	if (!isroot(y))tr[z].s[tr[z].s[1] == y] = x; // 判断是否为实边
	tr[x].p = z;
	tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y;
	tr[x].s[k ^ 1] = y, tr[y].p = x;
	pushup(y), pushup(x);
}

void splay(int x) 
{
	int top = 0, r = x;
	stk[++ top] = r; // 先要下传标记
	while (!isroot(r)) stk[++ top] = r = tr[r].p;
	while (top)pushdown(stk[top -- ]);
	while (!isroot(x))
	{
		int y = tr[x].p, z = tr[y].p;
		if (!isroot(y))
			if ((tr[y].s[1] == x) ^ (tr[z].s[1] == y))rotate(x);
			else rotate(y);
		rotate(x);	
	}
}

void access(int x) // 将 x 与 根节点 的路径上所有边改为实边
{
	int z = x;
	for (int y = 0; x; y = x, x = tr[x].p)
	{
		splay(x); // 将 x 转到所在Splay的根节点
		tr[x].s[1] = y; // 将 y 设为 x 的右儿子,所有路径上的点都为右儿子
		pushup(x); // 更新 x 节点
	}
	splay(z); // 使整棵树更平衡
}

void makeroot(int x) // 将 x 变为原树的根节点
{
	access(x); // 将 x 与 根节点 的路径上所有边改为实边 
	pushrev(x); // 反转 x 所在的 Splay
}

int findroot(int x) // 查找原树根
{
	access(x); // 将 x 与 根节点 的路径上所有边改为实边 
	while (tr[x].s[0])pushdown(x), x = tr[x].s[0]; // 找到根节点
	splay(x); // 使整棵树更平衡
	return x; // 返回根节点
}

void split(int x, int y) // 使 x 与 y 在同一个 Splay 中
{
	makeroot(x); // 将 x 变为原树的根节点
	access(y); // 将 y 与 x 的路径上所有边改为实边 
}

void link(int x, int y) // 连接 x 与 y
{
	makeroot(x); // 将 x 变为根节点
	if (findroot(y) != x)tr[x].p = y; // 连虚边
}

void cut(int x, int y) // 切断 x 与 y 之间的边
{
	makeroot(x); // 将 x 变为根节点
	if (findroot(y) == x && tr[y].p == x && !tr[y].s[0])
	{
		tr[x].s[1] = tr[y].p = 0; // 删边
		pushup(x); // 更新
	}
}

int main()
{
 	scanf("%d%d", &n, &m);
 	for (int i = 1; i <= n; i ++ )scanf("%d", &tr[i].v);
 	while (m -- )
 	{
 		int t, x, y;
 		scanf("%d%d%d", &t, &x, &y);
 		if (t == 0)
 		{
 			split(x, y);
 			printf("%d\n", tr[y].sum);
		}
		if (t == 1)link(x, y);
		if (t == 2)cut(x, y);
		if (t == 3)
		{
			splay(x);
			tr[x].v = y;
			pushup(x);
		}
	}
 	return 0;
}

学会了动态树,接下来我们来打一下原问题的代码:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cstring>
using namespace std;
struct node 
{
    int data,rev,sum;
    node *son[2],*pre;
    bool judge();
    bool isroot();
    void pushdown();
    void update();
    void setson(node *child,int lr);
}lct[233];
int top,a,b;
node *getnew(int x)
{
    node *now=lct+ ++top;
    now->data=x;
    now->pre=now->son[1]=now->son[0]=lct;
    now->sum=0;
    now->rev=0;
    return now;
}
bool node::judge(){return pre->son[1]==this;}
bool node::isroot()
{
    if(pre==lct)return true;
    return !(pre->son[1]==this||pre->son[0]==this);
}
void node::pushdown()
{
    if(this==lct||!rev)return;
    swap(son[0],son[1]);
    son[0]->rev^=1;
    son[1]->rev^=1;
    rev=0;
}
void node::update(){sum=son[1]->sum+son[0]->sum+data;}
void node::setson(node *child,int lr)
{
    this->pushdown();
    child->pre=this;
    son[lr]=child;
    this->update();
}
void rotate(node *now)
{
    node *father=now->pre,*grandfa=father->pre;
    if(!father->isroot()) grandfa->pushdown();
    father->pushdown();now->pushdown();
    int lr=now->judge();
    father->setson(now->son[lr^1],lr);
    if(father->isroot()) now->pre=grandfa;
    else grandfa->setson(now,father->judge());
    now->setson(father,lr^1);
    father->update();now->update();
    if(grandfa!=lct) grandfa->update();
}
void splay(node *now)
{
    if(now->isroot())return;
    for(;!now->isroot();rotate(now))
    if(!now->pre->isroot())
    now->judge()==now->pre->judge()?rotate(now->pre):rotate(now);
}
node *access(node *now)
{
    node *last=lct;
    for(;now!=lct;last=now,now=now->pre)
    {
        splay(now);
        now->setson(last,1);
    }
    return last;
}
void changeroot(node *now)
{
    access(now)->rev^=1;
    splay(now);
}
void connect(node *x,node *y)
{
    changeroot(x);
    x->pre=y;
    access(x);
}
void cut(node *x,node *y)
{
    changeroot(x);
    access(y);
    splay(x);
    x->pushdown();
    x->son[1]=y->pre=lct;
    x->update();
}
int query(node *x,node *y)
{
    changeroot(x);
    node *now=access(y);
    return now->sum;
}
int main()
{
    scanf("%d%d",&a,&b);
    node *A=getnew(a);
    node *B=getnew(b);
    //连边 Link
        connect(A,B);
    //断边 Cut
        cut(A,B);
    //再连边orz Link again
        connect(A,B);
    printf("%d\n",query(A,B)); 
    return 0;
}

好的,以上是AC代码,本篇题解就这么结束啦!(大佬勿喷)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值